diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 5d5906c..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,11 +0,0 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file - -version: 2 -updates: - - package-ecosystem: "gomod" # See documentation for possible values - directory: "/" # Location of package manifests - schedule: - interval: "daily" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index d3e55a2..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Go Test - -on: - push: - tags: - - '*' - -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.24' - - - name: Run tests - run: go test -v ./... - - - name: Run tests race - run: go test -race -v ./... diff --git a/LICENSE b/LICENSE index dbdb0fa..7076bf8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,373 +1,199 @@ -Mozilla Public License Version 2.0 -================================== +WJQserver Studio 开源许可证 +版本 v2.0 -1. Definitions --------------- +版权所有 © WJQserver Studio 2025 +版权所有 © Infinite Iroha 2025 +版权所有 © WJQserver 2025 -1.1. "Contributor" - means each individual or legal entity that creates, contributes to - the creation of, or owns Covered Software. +定义 -1.2. "Contributor Version" - means the combination of the Contributions of others (if any) used - by a Contributor and that particular Contributor's Contribution. +* 许可 (License): 指的是在本许可证内定义的使用、复制、分发与修改软件的条款与要求。 +* 授权方 (Licensor): 指的是拥有版权的个人或组织,亦或是拥有版权的个人或组织所指派的实体,在本许可证中特指 WJQserver Studio。 +* 贡献者 (Contributor): 指的是授权方以及根据本许可证授予贡献代码或软件的个人或实体。 +* 您 (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 - 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. +本项目为开源软件,允许用户在遵循本许可证的前提下访问和使用源代码。 +本项目旨在向用户提供尽可能广泛的非商业使用自由,同时保障社区的共同发展和良性生态,并为商业创新提供清晰的路径。 +强调版权所有,所有权利由 WJQserver Studio 及贡献者共同保留。 -1.5. "Incompatible With Secondary Licenses" - means +许可证条款 - (a) that the initial Contributor has attached the notice described - in Exhibit B to the Covered Software; or +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.1 非营利性使用: 您被授予在非营利性使用场景下,为了任何目的,自由使用本软件的权限。 非营利性使用的具体场景包括但不限于定义部分所列举的各种情况。 -1.6. "Executable Form" - means any form of the work other than Source Code Form. +* 1.2 商业使用: 您可以在商业环境中使用本软件,无需获得额外授权,但您的商业使用行为必须遵守以下条款: -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.2.1 保持声明: 您在进行商业使用时,不得移除或修改软件中包含的原始版权声明、许可证声明以及来源声明。 + * 1.2.2 开源继承 (Copyleft) 与互惠共享: 如果您或您的组织希望将本软件或其衍生作品用于任何商业用途,包括但不限于: -1.8. "License" - means this document. + * 盈利性分发: 销售、出租、许可分发本软件或其衍生作品。 + * 盈利性服务: 基于本软件或其衍生作品提供商业服务,例如 SaaS 服务、咨询服务、定制开发服务、收费技术支持服务等。 + * 嵌入式商业应用: 将本软件或其衍生作品嵌入到商业产品或解决方案中进行销售。 + * 组织内部商业运营: 在营利性组织的内部运营中使用修改后的版本以直接支持其商业活动,例如定制化内部系统,通过例如但不限于在软件或相关服务中投放广告 (例如 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. + 您必须选择以下两种方式之一: -1.10. "Modifications" - means any of the following: + * i) 继承本许可证并开源: 您必须以本许可证或兼容的开源许可证分发您的衍生作品,并公开您的衍生作品的全部源代码,使得您的衍生作品的接收者也享有与您相同的权利,包括进一步修改和商业使用的权利。 本选项旨在促进社区的共同发展和知识共享,确保基于本软件的商业创新成果也能回馈社区。 + * ii) 获得授权方明确授权: 如果您不希望以开源方式发布您的衍生作品,或者希望使用其他许可证进行分发,或者您希望在商业运营中使用修改后的版本但不开源,您必须事先获得 WJQserver Studio 的明确书面授权。 授权的具体条款和条件将由 WJQserver Studio 另行协商确定。 - (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.2.2 条(开源继承与互惠共享)的约束。 -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.2.2 条(开源继承与互惠共享)以及第 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 -distribution or licensing of Covered Software under this License. -Notwithstanding Section 2.1(b) above, no patent license is granted by a -Contributor: - -(a) for any code that a Contributor has removed from Covered Software; - or - -(b) for infringements caused by: (i) Your and any other third party's - modifications of Covered Software, or (ii) the combination of its - Contributions with other software (except as part of its Contributor - Version); or - -(c) under Patent Claims infringed by Covered Software in the absence of - its Contributions. - -This License does not grant any rights in the trademarks, service marks, -or logos of any Contributor (except as may be necessary to comply with -the notice requirements in Section 3.4). - -2.4. Subsequent Licenses - -No Contributor makes additional grants as a result of Your choice to -distribute the Covered Software under a subsequent version of this -License (see Section 10.2) or under the terms of a Secondary License (if -permitted under the terms of Section 3.3). - -2.5. Representation - -Each Contributor represents that the Contributor believes its -Contributions are its original creation(s) or it has sufficient rights -to grant the rights to its Contributions conveyed by this License. - -2.6. Fair Use - -This License is not intended to limit any rights You have under -applicable copyright doctrines of fair use, fair dealing, or other -equivalents. - -2.7. Conditions - -Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted -in Section 2.1. - -3. Responsibilities -------------------- - -3.1. Distribution of Source Form - -All distribution of Covered Software in Source Code Form, including any -Modifications that You create or to which You contribute, must be under -the terms of this License. You must inform recipients that the Source -Code Form of the Covered Software is governed by the terms of this -License, and how they can obtain a copy of this License. You may not -attempt to alter or restrict the recipients' rights in the Source Code -Form. - -3.2. Distribution of Executable Form - -If You distribute Covered Software in Executable Form then: - -(a) such Covered Software must also be made available in Source Code - Form, as described in Section 3.1, and You must inform recipients of - the Executable Form how they can obtain a copy of such Source Code - Form by reasonable means in a timely manner, at a charge no more - than the cost of distribution to the recipient; and - -(b) You may distribute such Executable Form under the terms of this - License, or sublicense it under different terms, provided that the - license for the Executable Form does not attempt to limit or alter - the recipients' rights in the Source Code Form under this License. - -3.3. Distribution of a Larger Work - -You may create and distribute a Larger Work under terms of Your choice, -provided that You also comply with the requirements of this License for -the Covered Software. If the Larger Work is a combination of Covered -Software with a work governed by one or more Secondary Licenses, and the -Covered Software is not Incompatible With Secondary Licenses, this -License permits You to additionally distribute such Covered Software -under the terms of such Secondary License(s), so that the recipient of -the Larger Work may, at their option, further distribute the Covered -Software under the terms of either this License or such Secondary -License(s). - -3.4. Notices - -You may not remove or alter the substance of any license notices -(including copyright notices, patent notices, disclaimers of warranty, -or limitations of liability) contained within the Source Code Form of -the Covered Software, except that You may alter any license notices to -the extent required to remedy known factual inaccuracies. - -3.5. Application of Additional Terms - -You may choose to offer, and to charge a fee for, warranty, support, -indemnity or liability obligations to one or more recipients of Covered -Software. However, You may do so only on Your own behalf, and not on -behalf of any Contributor. You must make it absolutely clear that any -such warranty, support, indemnity, or liability obligation is offered by -You alone, and You hereby agree to indemnify every Contributor for any -liability incurred by such Contributor as a result of warranty, support, -indemnity or liability terms You offer. You may include additional -disclaimers of warranty and limitations of liability specific to any -jurisdiction. - -4. Inability to Comply Due to Statute or Regulation ---------------------------------------------------- - -If it is impossible for You to comply with any of the terms of this -License with respect to some or all of the Covered Software due to -statute, judicial order, or regulation then You must: (a) comply with -the terms of this License to the maximum extent possible; and (b) -describe the limitations and the code they affect. Such description must -be placed in a text file included with all distributions of the Covered -Software under this License. Except to the extent prohibited by statute -or regulation, such description must be sufficiently detailed for a -recipient of ordinary skill to be able to understand it. - -5. Termination --------------- - -5.1. The rights granted under this License will terminate automatically -if You fail to comply with any of its terms. However, if You become -compliant, then the rights granted under this License from a particular -Contributor are reinstated (a) provisionally, unless and until such -Contributor explicitly and finally terminates Your grants, and (b) on an -ongoing basis, if such Contributor fails to notify You of the -non-compliance by some reasonable means prior to 60 days after You have -come back into compliance. Moreover, Your grants from a particular -Contributor are reinstated on an ongoing basis if such Contributor -notifies You of the non-compliance by some reasonable means, this is the -first time You have received notice of non-compliance with this License -from such Contributor, and You become compliant prior to 30 days after -Your receipt of the notice. - -5.2. If You initiate litigation against any entity by asserting a patent -infringement claim (excluding declaratory judgment actions, -counter-claims, and cross-claims) alleging that a Contributor Version -directly or indirectly infringes any patent, then the rights granted to -You by any and all Contributors for the Covered Software under Section -2.1 of this License shall terminate. - -5.3. In the event of termination under Sections 5.1 or 5.2 above, all -end user license agreements (excluding distributors and resellers) which -have been validly granted by You or Your distributors under this License -prior to termination shall survive termination. - -************************************************************************ -* * -* 6. Disclaimer of Warranty * -* ------------------------- * -* * -* Covered Software is provided under this License on an "as is" * -* basis, without warranty of any kind, either expressed, implied, or * -* statutory, including, without limitation, warranties that the * -* Covered Software is free of defects, merchantable, fit for a * -* particular purpose or non-infringing. The entire risk as to the * -* quality and performance of the Covered Software is with You. * -* Should any Covered Software prove defective in any respect, You * -* (not any Contributor) assume the cost of any necessary servicing, * -* repair, or correction. This disclaimer of warranty constitutes an * -* essential part of this License. No use of any Covered Software is * -* authorized under this License except under this disclaimer. * -* * -************************************************************************ - -************************************************************************ -* * -* 7. Limitation of Liability * -* -------------------------- * -* * -* Under no circumstances and under no legal theory, whether tort * -* (including negligence), contract, or otherwise, shall any * -* Contributor, or anyone who distributes Covered Software as * -* permitted above, be liable to You for any direct, indirect, * -* special, incidental, or consequential damages of any character * -* including, without limitation, damages for lost profits, loss of * -* goodwill, work stoppage, computer failure or malfunction, or any * -* and all other commercial damages or losses, even if such party * -* shall have been informed of the possibility of such damages. This * -* limitation of liability shall not apply to liability for death or * -* personal injury resulting from such party's negligence to the * -* extent applicable law prohibits such limitation. Some * -* jurisdictions do not allow the exclusion or limitation of * -* incidental or consequential damages, so this exclusion and * -* limitation may not apply to You. * -* * -************************************************************************ - -8. Litigation -------------- - -Any litigation relating to this License may be brought only in the -courts of a jurisdiction where the defendant maintains its principal -place of business and such litigation shall be governed by laws of that -jurisdiction, without reference to its conflict-of-law provisions. -Nothing in this Section shall prevent a party's ability to bring -cross-claims or counter-claims. - -9. Miscellaneous ----------------- - -This License represents the complete agreement concerning the subject -matter hereof. If any provision of this License is held to be -unenforceable, such provision shall be reformed only to the extent -necessary to make it enforceable. Any law or regulation which provides -that the language of a contract shall be construed against the drafter -shall not be used to construe this License against a Contributor. - -10. Versions of the License ---------------------------- - -10.1. New Versions - -Mozilla Foundation is the license steward. Except as provided in Section -10.3, no one other than the license steward has the right to modify or -publish new versions of this License. Each version will be given a -distinguishing version number. - -10.2. Effect of New Versions - -You may distribute the Covered Software under the terms of the version -of the License under which You originally received the Covered Software, -or under the terms of any subsequent version published by the license -steward. - -10.3. Modified Versions - -If you create software not governed by this License, and you want to -create a new license for such software, you may create and use a -modified version of this License if you rename the license and remove -any references to the name of the license steward (except to note that -such modified license differs from this License). - -10.4. Distributing Source Code Form that is Incompatible With Secondary -Licenses - -If You choose to distribute Source Code Form that is Incompatible With -Secondary Licenses under the terms of this version of the License, the -notice described in Exhibit B of this License must be attached. - -Exhibit A - Source Code Form License Notice -------------------------------------------- - - This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at https://mozilla.org/MPL/2.0/. - -If it is not possible or desirable to put the notice in a particular -file, then You may include the notice in a location (such as a LICENSE -file in a relevant directory) where a recipient would be likely to look -for such a notice. - -You may add additional accurate notices of copyright ownership. - -Exhibit B - "Incompatible With Secondary Licenses" Notice ---------------------------------------------------------- - - This Source Code Form is "Incompatible With Secondary Licenses", as - defined by the Mozilla Public License, v. 2.0. \ No newline at end of file +* 5.3 用户法律责任: 用户需根据当地法律对待本项目,确保遵守所有适用法规。 + +6. 许可证期限与终止 + +* 6.1 许可证期限: 除版权所有人主动宣布放弃本软件版权外,本许可证无限期生效。 + +* 6.2 许可证终止: 如果您未能遵守本许可证的任何条款或条件,授权方有权终止本许可证。 您的许可证将在您违反本许可证条款时自动终止。 + +* 6.3 终止后的效力: 许可证终止后,您根据本许可证所享有的所有权利将立即终止,但您在许可证终止前已合法分发的软件副本,其接收者所获得的许可及权利将不受影响,继续有效。 免责声明(第 5 条)和责任限制(第 5.2 条)在本许可证终止后仍然有效。 + +7. 条款修订 + +* 7.1 修订权利保留: 授权方保留随时修改本许可证条款的权利,以便更好地适应法律、技术发展以及社区需求。 + +* 7.2 修订生效与接受: 修订后的条款将在发布时生效,除非另行声明,否则继续使用、复制、分发或修改本软件即表示您接受修订后的条款。授权方鼓励用户定期查阅本许可证的最新版本。 + +8. 其他 + +* 8.1 法定权利: 本许可证不影响您作为最终用户在适用法律下的法定权利。 + +* 8.2 条款可分割性: 若本许可证的某些条款被认定为不可执行,其余条款仍然完全有效。 + +* 8.3 版本更新: 授权方可能会发布本许可证的修订版本或新版本。您可以选择是继续使用本许可证的旧版本还是选择适用新版本。 + +WJQserver Studio Open Source License +Version v2.0 + +Copyright © WJQserver Studio 2024 + +Definitions + +* License: Refers to the terms and requirements for use, reproduction, distribution, and modification defined within this license. +* Licensor: Refers to the individual or organization that holds the copyright, or the entity designated by the copyright holder, specifically WJQserver Studio in this license. +* Contributor: Refers to the Licensor and individuals or entities who contribute code or software under this License. +* You: Refers to the individual or legal entity exercising permissions granted by this License. +* Derivative Works: Refers to works modified based on the Software or any part thereof, regardless of the extent of modification. This includes but is not limited to modifications, revisions, adaptations, translations, or other forms of creation based on the Software or any part thereof, as well as collective works containing the Software or parts thereof. +* Non-profit Use: Refers to uses not primarily intended for direct commercial profit, including but not limited to: + * Personal Use: Use by an individual for personal learning, research, experimentation, non-commercial projects, personal website development, graduation projects, home entertainment, and other non-directly commercial purposes. + * Educational Use: Use within educational institutions (such as schools, universities, training organizations) for activities such as teaching, research, and academic exchange. + * Scientific Research Use: Use within scientific research institutions, laboratories, and similar organizations for activities such as scientific research and experimental development. + * Charitable and Public Welfare Use: Use by charitable organizations, public welfare organizations, and similar non-profit entities for their public missions or internal operation of charitable activities, or to provide public services that do not directly generate commercial profit. + * Internal Operational Use (Non-profit Organizations): Use within the internal operations of non-profit organizations, such as for administrative management, membership management, internal communication, project management, and other non-directly profit-generating activities. + +Open Source and Free Software + +This project is open-source software, allowing users to access and use the source code under the premise of complying with this License. +This project aims to provide users with the broadest possible freedom for non-commercial use while ensuring the common development and healthy ecosystem of the community, and providing a clear path for commercial innovation. +Copyright is emphasized; all rights are jointly reserved by WJQserver Studio and Contributors. + +License Terms + +1. Permissions for Use + +* 1.1 Non-profit Use: You are granted permission to freely use the Software for any purpose in non-profit use scenarios. Specific non-profit use scenarios include but are not limited to the various situations listed in the Definition section. + +* 1.2 Commercial Use: You may use the Software in a commercial environment without additional authorization, but your commercial use must comply with the following terms: + + * 1.2.1 Maintain Statements: When conducting commercial use, you must not remove or modify the original copyright notices, license notices, and source statements contained in the Software. + * 1.2.2 Open Source Inheritance (Copyleft) and Reciprocal Sharing: If you or your organization wish to use the Software or its Derivative Works for any commercial purpose, including but not limited to: + + * Profit-generating Distribution: Selling, renting, licensing, or distributing the Software or its Derivative Works. + * Profit-generating Services: Providing commercial services based on the Software or its Derivative Works, such as SaaS services, consulting services, custom development services, and paid technical support services. + * Embedded Commercial Applications: Embedding the Software or its Derivative Works into commercial products or solutions for sale. + * Internal Commercial Operations: Using modified versions within the internal operations of for-profit organizations to directly support their commercial activities, such as customized internal systems, generating commercial revenue directly or indirectly through means including but not limited to placing advertisements in the software or related services (e.g., Google Ads), in-app purchases, membership subscriptions, and charging for value-added features. + + You must choose one of the following two options: + + * i) Inherit this License and Open Source: You must distribute your Derivative Works under this License or a compatible open-source license and publicly disclose the entire source code of your Derivative Works, so that recipients of your Derivative Works also enjoy the same rights as you, including the right to further modify and use commercially. This option aims to promote the common development and knowledge sharing of the community, ensuring that commercial innovation achievements based on this Software can also contribute back to the community. + * ii) Obtain Explicit Authorization from the Licensor: If you do not wish to release your Derivative Works in an open-source manner, or wish to distribute them under another license, or you wish to use a modified version in commercial operations without open-sourcing it, you must obtain explicit written authorization from WJQserver Studio in advance. The specific terms and conditions of authorization will be determined separately by WJQserver Studio through negotiation. + +2. Reproduction and Distribution + +* 2.1 Reproduction and Distribution of Original Version: You may reproduce and distribute the original version of the Software, provided that the following conditions are met: + + * Retain All Statements: Completely retain all original copyright notices, license notices, source statements, and other proprietary notices. + * Accompany with License: When distributing the Software, you must also include the full text of this License to ensure that recipients are aware of and understand all terms of this License. + +* 2.2 Reproduction and Distribution of Derivative Works: You may reproduce and distribute Derivative Works based on the Software. Your distribution of Derivative Works will be subject to the constraints of Clause 1.2.2 of this License (Open Source Inheritance and Reciprocal Sharing). + +3. Modification Permissions + +* 3.1 Free Modification: You are granted permission to freely modify the Software, regardless of whether the purpose of modification is for non-profit use or commercial use. + +* 3.2 Constraints on Use and Distribution after Modification: When you use a modified version for commercial purposes or distribute a modified version, you need to comply with the provisions of Clause 1.2.2 of this License (Open Source Inheritance and Reciprocal Sharing) and Clause 2 (Reproduction and Distribution). Even if you do not distribute the modified version, as long as you use it for commercial purposes, you also need to comply with the open-source inheritance clause or obtain authorization. + +* 3.3 Contribution Acceptance: WJQserver Studio encourages community contribution of code. If you contribute code to this project, you need to agree that your contributed code is licensed under the terms of this License. + +4. Patent Rights + +* 4.1 No Patent Warranty, Risk Self-Bearing: The software is provided “AS IS”, and the Licensor and Contributors explicitly declare that they do not provide any form of warranty regarding patent infringement issues of this software, nor do they assume any responsibility and consequences arising from patent infringement. Users understand and agree that the patent risk of using this software is entirely borne by the users themselves. + +* 4.2 Handling of Patent Disputes: If any patent infringement allegations, lawsuits, or claims arise due to the user's use of this Software, the user shall be solely responsible for handling and bear all legal liabilities. The Licensor and Contributors are under no obligation to participate in any related legal proceedings, nor do they bear any costs or compensation arising therefrom. + +5. Disclaimer of Warranty + +* 5.1 “AS IS” Provision, No Warranty: The software is provided “AS IS” without any express or implied warranties, including but not limited to warranties of merchantability, fitness for a particular purpose, and non-infringement. + +* 5.2 Limitation of Liability: To the maximum extent permitted by applicable law, in no event shall the Licensor or any Contributor be liable for any direct, indirect, incidental, special, punitive, or consequential damages (including but not limited to procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage. + +* 5.3 User Legal Responsibility: Users shall treat this project in accordance with local laws and regulations to ensure compliance with all applicable laws and regulations. + +6. License Term and Termination + +* 6.1 License Term: Unless the copyright holder proactively announces the abandonment of the copyright of this software, this License shall be effective indefinitely from the date of your acceptance. + +* 6.2 License Termination: If you fail to comply with any terms or conditions of this License, the Licensor has the right to terminate this License. Your License will automatically terminate upon your violation of the terms of this License. + +* 6.3 Effect after Termination: Upon termination of the License, all rights granted to you under this License will terminate immediately, but the licenses and rights obtained by recipients of software copies you have legally distributed before the termination of the License will not be affected and will remain valid. The Disclaimer of Warranty (Clause 5) and Limitation of Liability (Clause 5.2) shall remain in effect after the termination of this License. + +7. Revision of Terms + +* 7.1 Reservation of Revision Rights: The Licensor reserves the right to modify the terms of this License at any time to better adapt to legal, technological developments, and community needs. + +* 7.2 Effectiveness and Acceptance of Revisions: Revised terms will take effect upon publication, and unless otherwise stated, continued use, reproduction, distribution, or modification of the Software indicates your acceptance of the revised terms. The Licensor encourages users to periodically review the latest version of this License. + +8. Other + +* 8.1 Statutory Rights: This License does not affect your statutory rights as an end-user under applicable laws. + +* 8.2 Severability of Terms: If certain terms of this License are deemed unenforceable, the remaining terms shall remain in full force and effect. + +* 8.3 Version Updates: The Licensor may publish revised versions or new versions of this License. You may choose to continue using the old version of this License or choose to apply the new version. diff --git a/README.md b/README.md index 3ab971f..4e3fa5c 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,9 @@ -# Touka(灯花)框架 - -Touka(灯花) 是一个基于 Go 语言构建的多层次、高性能 Web 框架。其设计目标是为开发者提供**更直接的控制、有效的扩展能力,以及针对特定场景的行为优化**。 - -**想深入了解 Touka 吗?请阅读我们的 -> [深度指南 (about-touka.md)](about-touka.md)** - -这份深度指南包含了对框架设计哲学、核心功能(路由、上下文、中间件、错误处理等)的全面剖析,并提供了大量可直接使用的代码示例,帮助您快速上手并精通 Touka。 - -### 快速上手 - -```go -package main - -import ( - "fmt" - "io" - "log" - "net/http" - "os" - "time" - - "github.com/fenthope/reco" - "github.com/infinite-iroha/touka" -) - -func main() { - r := touka.Default() // 使用带 Recovery 中间件的默认引擎 - - // 配置日志记录器 (可选) - logConfig := reco.Config{ - Level: reco.LevelDebug, - Mode: reco.ModeText, - Output: os.Stdout, - Async: true, - } - r.SetLoggerCfg(logConfig) - - // 配置统一错误处理器 - r.SetErrorHandler(func(c *touka.Context, code int, err error) { - c.JSON(code, touka.H{"error_code": code, "message": http.StatusText(code)}) - c.GetLogger().Errorf("发生HTTP错误: %d, 路径: %s, 错误: %v", code, c.Request.URL.Path, err) - }) - - // 注册路由 - r.GET("/hello/:name", func(c *touka.Context) { - name := c.Param("name") - query := c.DefaultQuery("mood", "happy") - c.String(http.StatusOK, "Hello, %s! You seem %s.", name, query) - }) - - // 启动服务器 (支持优雅关闭) - log.Println("Touka Server starting on :8080...") - if err := r.RunShutdown(":8080", 10*time.Second); err != nil { - log.Fatalf("Touka server failed to start: %v", err) - } -} -``` - -## 中间件支持 - -### 内置 - -- **Recovery:** `r.Use(touka.Recovery())` (已包含在 `touka.Default()` 中) - -### 第三方 (fenthope) - -- [访问日志-record](https://github.com/fenthope/record) -- [Gzip](https://github.com/fenthope/gzip) -- [压缩-Compress(Deflate,Gzip,Zstd)](https://github.com/fenthope/compress) -- [请求速率限制-ikumi](https://github.com/fenthope/ikumi) -- [sessions](https://github.com/fenthope/sessions) -- [jwt](https://github.com/fenthope/jwt) -- [带宽限制](https://github.com/fenthope/toukautil/blob/main/bandwithlimiter.go) - -## 文档与贡献 - -* **深度指南:** **[about-touka.md](about-touka.md)** -* **API 文档:** 访问 [pkg.go.dev/github.com/infinite-iroha/touka](https://pkg.go.dev/github.com/infinite-iroha/touka) 查看完整的 API 参考。 -* **贡献:** 我们欢迎任何形式的贡献,无论是错误报告、功能建议还是代码提交。请遵循项目的贡献指南。 - -## 相关项目 - -- [gin](https://github.com/gin-gonic/gin): Touka 在路由和 API 设计上参考了 Gin。 -- [reco](https://github.com/fenthope/reco): Touka 框架的默认日志库。 -- [httpc](https://github.com/WJQSERVER-STUDIO/httpc): 一个现代化且易用的 HTTP Client,作为 Touka 框架 Context 携带的 HTTPC。 +# Touka 框架 ## 许可证 -本项目基于 [Mozilla Public License, v. 2.0](https://mozilla.org/MPL/2.0/) 许可。 +本项目在v0阶段使用WJQSERVER STUDIO LICENSE许可证, 后续进行调整 -`tree.go` 部分代码源自 [gin](https://github.com/gin-gonic/gin) 与 [httprouter](https://github.com/julienschmidt/httprouter),其原始许可为 BSD-style。 +tree部分来自[gin](https://github.com/gin-gonic/gin)与[httprouter](https://github.com/julienschmidt/httprouter) + +[WJQSERVER/httproute](https://github.com/WJQSERVER/httprouter)是本项目的前身(一个[httprouter](https://github.com/julienschmidt/httprouter)的fork版本) \ No newline at end of file diff --git a/about-touka.md b/about-touka.md deleted file mode 100644 index 86a056f..0000000 --- a/about-touka.md +++ /dev/null @@ -1,577 +0,0 @@ -# 关于 Touka (灯花) 框架:一份深度指南 - -Touka (灯花) 是一个基于 Go 语言构建的、功能丰富且高性能的 Web 框架。它的核心设计目标是为开发者提供一个既强大又灵活的工具集,允许对框架行为进行深度定制,同时通过精心设计的组件和机制,优化在真实业务场景中的开发体验和运行性能。 - -本文档旨在提供一份全面而深入的指南,帮助您理解 Touka 的核心概念、设计哲学以及如何利用其特性来构建健壮、高效的 Web 应用。 - ---- - -## 核心设计哲学 - -Touka 的设计哲学根植于以下几个核心原则: - -* **控制力与可扩展性:** 框架在提供强大默认功能的同时,也赋予开发者充分的控制权。我们相信开发者最了解自己的业务需求。因此,无论是路由行为、错误处理逻辑,还是服务器协议,都可以根据具体需求进行精细调整和扩展。 -* **明确性与可预测性:** API 设计力求直观和一致,使得框架的行为易于理解和预测,减少开发过程中的意外。我们避免使用过多的“魔法”,倾向于让代码的意图清晰可见。 -* **性能意识:** 在核心组件的设计中,性能是一个至关重要的考量因素。通过采用如对象池、优化的路由算法等技术,Touka 致力于在高并发场景下保持低延迟和高吞吐。 -* **开发者体验:** 框架内置了丰富的辅助工具和便捷的 API,例如与请求上下文绑定的日志记录器和 HTTP 客户端,旨在简化常见任务,提升开发效率。 - ---- - -## 核心功能深度剖析 - -### 1. 引擎 (Engine):框架的中央枢纽 - -`Engine` 是 Touka 框架的实例,也是所有功能的入口和协调者。它实现了 `http.Handler` 接口,可以无缝集成到 Go 的标准 HTTP 生态中。 - -#### 1.1. 初始化引擎 - -```go -// 创建一个“干净”的引擎,不包含任何默认中间件 -r := touka.New() - -// 创建一个带有默认中间件的引擎,目前仅包含 Recovery() -// 推荐在生产环境中使用,以防止 panic 导致整个服务崩溃 -r := touka.Default() -``` - -#### 1.2. 引擎配置 - -`Engine` 提供了丰富的配置选项,允许您定制其核心行为。 - -```go -func main() { - r := touka.New() - - // === 路由行为配置 === - - // 自动重定向尾部带斜杠的路径,默认为 true - // e.g., /foo/ 会被重定向到 /foo - r.SetRedirectTrailingSlash(true) - - // 自动修复路径的大小写,默认为 true - // e.g., /FOO 会被重定向到 /foo (如果 /foo 存在) - r.SetRedirectFixedPath(true) - - // 当路由存在但方法不匹配时,自动处理 405 Method Not Allowed,默认为 true - r.SetHandleMethodNotAllowed(true) - - // === IP 地址解析配置 === - - // 是否信任 X-Forwarded-For, X-Real-IP 等头部来获取客户端 IP,默认为 true - // 在反向代理环境下非常有用 - r.SetForwardByClientIP(true) - // 自定义用于解析 IP 的头部列表,按顺序查找 - r.SetRemoteIPHeaders([]string{"X-Forwarded-For", "X-App-Client-IP", "X-Real-IP"}) - - // === 请求体大小限制 === - - // 设置全局默认的请求体最大字节数,-1 表示不限制 - // 这有助于防止 DoS 攻击 - r.SetGlobalMaxRequestBodySize(10 * 1024 * 1024) // 10 MB - - // ... 其他配置 - r.Run(":8080") -} -``` - -#### 1.3. 服务器生命周期管理 - -Touka 提供了对底层 `*http.Server` 的完全控制,并内置了优雅关闭的逻辑。 - -```go -func main() { - r := touka.New() - - // 通过 ServerConfigurator 对 http.Server 进行自定义配置 - r.SetServerConfigurator(func(server *http.Server) { - // 设置自定义的读写超时时间 - server.ReadTimeout = 15 * time.Second - server.WriteTimeout = 15 * time.Second - fmt.Println("自定义的 HTTP 服务器配置已应用") - }) - - // 启动服务器,并支持优雅关闭 - // RunShutdown 会阻塞,直到收到 SIGINT 或 SIGTERM 信号 - // 第二个参数是优雅关闭的超时时间 - fmt.Println("服务器启动于 :8080") - if err := r.RunShutdown(":8080", 10*time.Second); err != nil { - log.Fatalf("服务器启动失败: %v", err) - } -} -``` - ---- - -### 2. 路由系统 (Routing):强大、灵活、高效 - -Touka 的路由系统基于一个经过优化的**基数树 (Radix Tree)**,它支持静态路径、路径参数和通配符,并能实现极高的查找性能。 - -#### 2.1. 基本路由 - -```go -// 精确匹配的静态路由 -r.GET("/ping", func(c *touka.Context) { - c.String(http.StatusOK, "pong") -}) - -// 注册多个 HTTP 方法 -r.HandleFunc([]string{"GET", "POST"}, "/data", func(c *touka.Context) { - c.String(http.StatusOK, "Data received via %s", c.Request.Method) -}) - -// 注册所有常见 HTTP 方法 -r.ANY("/any", func(c *touka.Context) { - c.String(http.StatusOK, "Handled with ANY for method %s", c.Request.Method) -}) -``` - -#### 2.2. 参数化路由 - -使用冒号 `:` 来定义路径参数。 - -```go -r.GET("/users/:id", func(c *touka.Context) { - // 通过 c.Param() 获取路径参数 - userID := c.Param("id") - c.String(http.StatusOK, "获取用户 ID: %s", userID) -}) - -r.GET("/articles/:category/:article_id", func(c *touka.Context) { - category := c.Param("category") - articleID := c.Param("article_id") - c.JSON(http.StatusOK, touka.H{ - "category": category, - "id": articleID, - }) -}) -``` - -#### 2.3. 通配符路由 (Catch-all) - -使用星号 `*` 来定义通配符路由,它会捕获该点之后的所有路径段。**通配符路由必须位于路径的末尾**。 - -```go -// 匹配如 /static/js/main.js, /static/css/style.css 等 -r.GET("/static/*filepath", func(c *touka.Context) { - // 捕获的路径可以通过参数名 "filepath" 获取 - filePath := c.Param("filepath") - c.String(http.StatusOK, "请求的文件路径是: %s", filePath) -}) -``` - -#### 2.4. 路由组 (RouterGroup) - -路由组是组织和管理路由的强大工具,特别适用于构建结构化的 API。 - -```go -func main() { - r := touka.New() - - // 所有 /api/v1 下的路由都需要经过 AuthMiddleware - v1 := r.Group("/api/v1") - v1.Use(AuthMiddleware()) // 应用组级别的中间件 - { - // 匹配 /api/v1/products - v1.GET("/products", getProducts) - // 匹配 /api/v1/products/:id - v1.GET("/products/:id", getProductByID) - - // 可以在组内再嵌套组 - ordersGroup := v1.Group("/orders") - ordersGroup.Use(OrderPermissionsMiddleware()) // 更具体的中间件 - { - // 匹配 /api/v1/orders - ordersGroup.GET("", getOrders) - // 匹配 /api/v1/orders/:id - ordersGroup.GET("/:id", getOrderByID) - } - } - - r.Run(":8080") -} - -func AuthMiddleware() touka.HandlerFunc { - return func(c *touka.Context) { - // 模拟认证逻辑 - fmt.Println("V1 Auth Middleware: Checking credentials...") - c.Next() - } -} -// ... 其他处理器 -``` - ---- - -### 3. 上下文 (Context):请求的灵魂 - -`touka.Context` 是框架中最为核心的结构,它作为每个 HTTP 请求的上下文,在中间件和最终处理器之间流转。它提供了海量的便捷 API 来简化开发。 - -#### 3.1. 请求数据解析 - -##### 获取查询参数 - -```go -// 请求 URL: /search?q=touka&lang=go&page=1 -r.GET("/search", func(c *touka.Context) { - // c.Query() 获取指定参数,不存在则返回空字符串 - query := c.Query("q") // "touka" - - // c.DefaultQuery() 获取参数,不存在则返回指定的默认值 - lang := c.DefaultQuery("lang", "en") // "go" - category := c.DefaultQuery("cat", "all") // "all" - - c.JSON(http.StatusOK, touka.H{ - "query": query, - "language": lang, - "category": category, - }) -}) -``` - -##### 获取 POST 表单数据 - -```go -// 使用 curl 测试: -// curl -X POST http://localhost:8080/register -d "username=test&email=test@example.com" -r.POST("/register", func(c *touka.Context) { - username := c.PostForm("username") - email := c.DefaultPostForm("email", "anonymous@example.com") - // 也可以获取所有表单数据 - // form, _ := c.Request.MultipartForm() - - c.String(http.StatusOK, "注册成功: 用户名=%s, 邮箱=%s", username, email) -}) -``` - -##### JSON 数据绑定 - -Touka 可以轻松地将请求体中的 JSON 数据绑定到 Go 结构体。 - -```go -type UserProfile struct { - Name string `json:"name" binding:"required"` - Age int `json:"age" binding:"gte=18"` - Tags []string `json:"tags"` - Address string `json:"address,omitempty"` -} - -// 使用 curl 测试: -// curl -X POST http://localhost:8080/profile -H "Content-Type: application/json" -d ''' -// { -// "name": "Alice", -// "age": 25, -// "tags": ["go", "web"] -// } -// ''' -r.POST("/profile", func(c *touka.Context) { - var profile UserProfile - - // c.ShouldBindJSON() 会解析 JSON 并填充到结构体中 - if err := c.ShouldBindJSON(&profile); err != nil { - // 如果 JSON 格式错误或不满足绑定标签,会返回错误 - c.JSON(http.StatusBadRequest, touka.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, touka.H{ - "status": "success", - "profile": profile, - }) -}) -``` - -#### 3.2. 响应构建 - -##### 发送 JSON, String, Text - -```go -r.GET("/responses", func(c *touka.Context) { - // c.JSON(http.StatusOK, touka.H{"framework": "Touka"}) - // c.String(http.StatusOK, "Hello, %s", "World") - c.Text(http.StatusOK, "This is plain text.") -}) -``` - -##### 渲染 HTML 模板 - -首先,需要为引擎配置一个模板渲染器。 - -```go -// main.go -import "html/template" - -func main() { - r := touka.New() - // 加载模板文件 - r.HTMLRender = template.Must(template.ParseGlob("templates/*.html")) - - r.GET("/index", func(c *touka.Context) { - // 渲染 index.html 模板,并传入数据 - c.HTML(http.StatusOK, "index.html", touka.H{ - "title": "Touka 模板渲染", - "user": "Guest", - }) - }) - - r.Run(":8080") -} - -// templates/index.html -//

{{ .title }}

-//

Welcome, {{ .user }}!

-``` - -##### 文件和流式响应 - -```go -// 直接发送一个文件 -r.GET("/download/report", func(c *touka.Context) { - // 浏览器会提示下载 - c.File("./reports/latest.pdf") -}) - -// 将文件内容作为响应体 -r.GET("/show/config", func(c *touka.Context) { - // 浏览器会直接显示文件内容(如果支持) - c.SetRespBodyFile(http.StatusOK, "./config.yaml") -}) - -// 流式响应,适用于大文件或实时数据 -r.GET("/stream", func(c *touka.Context) { - // 假设 getRealTimeDataStream() 返回一个 io.Reader - // dataStream := getRealTimeDataStream() - // c.WriteStream(dataStream) -}) -``` - -#### 3.3. Cookie 操作 - -Touka 提供了简单的 API 来管理 Cookie。 - -```go -r.GET("/login", func(c *touka.Context) { - // 设置一个有效期为 1 小时的 cookie - c.SetCookie("session_id", "user-12345", 3600, "/", "localhost", false, true) - c.String(http.StatusOK, "登录成功!") -}) - -r.GET("/me", func(c *touka.Context) { - sessionID, err := c.GetCookie("session_id") - if err != nil { - c.String(http.StatusUnauthorized, "请先登录") - return - } - c.String(http.StatusOK, "您的会话 ID 是: %s", sessionID) -}) - -r.GET("/logout", func(c *touka.Context) { - // 通过将 MaxAge 设置为 -1 来删除 cookie - c.DeleteCookie("session_id") - c.String(http.StatusOK, "已退出登录") -}) -``` - -#### 3.4. 中间件数据传递 - -使用 `c.Set()` 和 `c.Get()` 可以在处理链中传递数据。 - -```go -// 中间件:生成并设置请求 ID -func RequestIDMiddleware() touka.HandlerFunc { - return func(c *touka.Context) { - requestID := fmt.Sprintf("req-%d", time.Now().UnixNano()) - c.Set("RequestID", requestID) - c.Next() - } -} - -func main() { - r := touka.New() - r.Use(RequestIDMiddleware()) - - r.GET("/status", func(c *touka.Context) { - // 在处理器中获取由中间件设置的数据 - // c.MustGet() 在 key 不存在时会 panic,适用于确定存在的场景 - requestID := c.MustGet("RequestID").(string) - - // 或者使用安全的 Get - // requestID, exists := c.GetString("RequestID") - - c.JSON(http.StatusOK, touka.H{"status": "ok", "request_id": requestID}) - }) - - r.Run(":8080") -} -``` - -#### 3.5. 集成的工具 - -##### 日志记录 - -Touka 集成了 `reco` 日志库,可以直接在 `Context` 中使用。 - -```go -r.GET("/log-test", func(c *touka.Context) { - userID := "user-abc" - c.Infof("用户 %s 访问了 /log-test", userID) - - err := errors.New("一个模拟的错误") - if err != nil { - c.Errorf("处理请求时发生错误: %v, 用户: %s", err, userID) - } - - c.String(http.StatusOK, "日志已记录") -}) -``` - -##### HTTP 客户端 - -Touka 集成了 `httpc` 客户端,方便发起出站请求。 - -```go -r.GET("/fetch-data", func(c *touka.Context) { - // 使用 Context 携带的 httpc 客户端 - resp, err := c.GetHTTPC().Get("https://api.github.com/users/WJQSERVER-STUDIO", httpc.WithTimeout(5*time.Second)) - if err != nil { - c.ErrorUseHandle(http.StatusInternalServerError, err) - return - } - defer resp.Body.Close() - - // 将外部响应直接流式传输给客户端 - c.SetHeader("Content-Type", resp.Header.Get("Content-Type")) - c.WriteStream(resp.Body) -}) -``` - ---- - -### 4. 错误处理:统一且强大 - -Touka 的一个标志性特性是其统一的错误处理机制。 - -#### 4.1. 自定义全局错误处理器 - -```go -func main() { - r := touka.New() - - // 设置一个自定义的全局错误处理器 - r.SetErrorHandler(func(c *touka.Context, code int, err error) { - // 检查是否是客户端断开连接 - if errors.Is(err, context.Canceled) { - return // 不做任何事 - } - - // 记录详细错误 - c.GetLogger().Errorf("捕获到错误: code=%d, err=%v, path=%s", code, err, c.Request.URL.Path) - - // 根据错误码返回不同的响应 - switch code { - case http.StatusNotFound: - c.JSON(code, touka.H{"error": "您要找的页面去火星了"}) - case http.StatusMethodNotAllowed: - c.JSON(code, touka.H{"error": "不支持的请求方法"}) - default: - c.JSON(code, touka.H{"error": "服务器内部错误"}) - } - }) - - // 这个路由不存在,会触发 404 - // r.GET("/this-route-does-not-exist", ...) - - // 静态文件服务,如果文件不存在,也会被上面的 ErrorHandler 捕获 - r.StaticDir("/files", "./non-existent-dir") - - r.Run(":8080") -} -``` - -#### 4.2. `errorCapturingResponseWriter` 的魔力 - -Touka 如何捕获 `http.FileServer` 的错误?答案是 `errorCapturingResponseWriter`。 - -当您使用 `r.StaticDir` 或类似方法时,Touka 不会直接将 `http.FileServer` 作为处理器。相反,它会用一个自定义的 `ResponseWriter` 实现(即 `ecw`)来包装原始的 `ResponseWriter`,然后才调用 `http.FileServer.ServeHTTP`。 - -这个包装器会: -1. **拦截 `WriteHeader(statusCode)` 调用:** 当 `http.FileServer` 内部决定要写入一个例如 `404 Not Found` 的状态码时,`ecw` 会捕获这个 `statusCode`。 -2. **判断是否为错误:** 如果 `statusCode >= 400`,`ecw` 会将此视为一个错误信号。 -3. **阻止原始响应:** `ecw` 会阻止 `http.FileServer` 继续向客户端写入任何内容(包括响应体)。 -4. **调用全局 `ErrorHandler`:** 最后,`ecw` 会调用您通过 `r.SetErrorHandler` 设置的全局错误处理器,并将捕获到的 `statusCode` 和一个通用错误传递给它。 - -这个机制确保了无论是动态 API 的错误还是静态文件服务的错误,都能被统一、优雅地处理,从而提供一致的用户体验。 - ---- - -### 5. 静态文件服务与嵌入式资源 - -#### 5.1. 服务本地文件 - -```go -// 将 URL /assets/ 映射到本地的 ./static 目录 -r.StaticDir("/assets", "./static") - -// 将 URL /favicon.ico 映射到本地的 ./static/img/favicon.ico 文件 -r.StaticFile("/favicon.ico", "./static/img/favicon.ico") -``` - -#### 5.2. 服务嵌入式资源 (Go 1.16+) - -使用 `go:embed` 可以将静态资源直接编译到二进制文件中,实现真正的单体应用部署。 - -```go -// main.go -package main - -import ( - "embed" - "io/fs" - "net/http" - "github.com/infinite-iroha/touka" -) - -//go:embed frontend/dist -var embeddedFS embed.FS - -func main() { - r := touka.New() - - // 创建一个子文件系统,根目录为 embeddedFS 中的 frontend/dist - subFS, err := fs.Sub(embeddedFS, "frontend/dist") - if err != nil { - panic(err) - } - - // 使用 StaticFS 来服务这个嵌入式文件系统 - // 所有对 / 的访问都会映射到嵌入的 frontend/dist 目录 - r.StaticFS("/", http.FS(subFS)) - - r.Run(":8080") -} -``` - ---- - -### 6. 与标准库的无缝集成 - -Touka 提供了适配器,可以轻松使用任何实现了标准 `http.Handler` 或 `http.HandlerFunc` 接口的组件。 - -```go -import "net/http/pprof" - -// 适配一个标准的 http.HandlerFunc -r.GET("/legacy-handler", touka.AdapterStdFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("这是一个标准的 http.HandlerFunc")) -})) - -// 适配一个标准的 http.Handler,例如 pprof -debugGroup := r.Group("/debug/pprof") -{ - debugGroup.GET("/", touka.AdapterStdFunc(pprof.Index)) - debugGroup.GET("/cmdline", touka.AdapterStdFunc(pprof.Cmdline)) - debugGroup.GET("/profile", touka.AdapterStdFunc(pprof.Profile)) - // ... 其他 pprof 路由 -} -``` - -这使得您可以方便地利用 Go 生态中大量现有的、遵循标准接口的第三方中间件和工具。 diff --git a/adapter.go b/adapter.go deleted file mode 100644 index 88166ee..0000000 --- a/adapter.go +++ /dev/null @@ -1,56 +0,0 @@ -// 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/. -// Copyright 2024 WJQSERVER. All rights reserved. -// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization. -package touka - -import ( - "net/http" -) - -// AdapterStdFunc 将一个标准的 http.HandlerFunc (func(http.ResponseWriter, *http.Request)) -// 适配成一个 Touka 框架的 HandlerFunc (func(*Context)) -// 这使得标准的 HTTP 处理器可以轻松地在 Touka 路由中使用 -// -// 示例: -// -// stdHandlerFunc := func(w http.ResponseWriter, r *http.Request) { -// w.Write([]byte("Hello from a standard handler function!")) -// } -// r.GET("/std-func", touka.AdapterStdFunc(stdHandlerFunc)) -// -// 注意: 被适配的处理器执行完毕后,Touka 的处理链会被中止 (c.Abort()), -// 因为我们假设标准处理器已经完成了对请求的响应 -func AdapterStdFunc(f http.HandlerFunc) HandlerFunc { - return func(c *Context) { - // 从 Touka Context 中提取标准的 ResponseWriter 和 Request - // 并将它们传递给原始的 http.HandlerFunc - f(c.Writer, c.Request) - - // 中止 Touka 的处理链,防止执行后续的处理器 - c.Abort() - } -} - -// AdapterStdHandle 将一个实现了 http.Handler 接口的对象 -// 适配成一个 Touka 框架的 HandlerFunc (func(*Context)) -// 这使得像 http.FileServer, http.StripPrefix 或其他第三方库的 Handler -// 可以直接在 Touka 路由中使用 -// -// 示例: -// -// // 创建一个 http.FileServer -// fileServer := http.FileServer(http.Dir("./static")) -// // 将 FileServer 适配后用于 Touka 路由 -// r.GET("/static/*filepath", touka.AdapterStdHandle(http.StripPrefix("/static", fileServer))) -// -// 注意: 被适配的处理器执行完毕后,Touka 的处理链会被中止 (c.Abort()) -func AdapterStdHandle(h http.Handler) HandlerFunc { - return func(c *Context) { - // 调用 Handler 接口的 ServeHTTP 方法 - h.ServeHTTP(c.Writer, c.Request) - - // 中止 Touka 的处理链 - c.Abort() - } -} diff --git a/context.go b/context.go index 8c52b1f..4efb57d 100644 --- a/context.go +++ b/context.go @@ -1,33 +1,22 @@ -// 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/. -// Copyright 2024 WJQSERVER. All rights reserved. -// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization. package touka import ( - "bytes" "context" - "encoding/gob" "errors" "fmt" "html/template" "io" "math" - "mime" + "net" "net/http" "net/netip" "net/url" - "os" - "path/filepath" "strings" "sync" - "time" - "github.com/WJQSERVER/wanf" - "github.com/fenthope/reco" "github.com/go-json-experiment/json" - "github.com/WJQSERVER-STUDIO/go-utils/iox" + "github.com/WJQSERVER-STUDIO/go-utils/copyb" "github.com/WJQSERVER-STUDIO/httpc" ) @@ -43,7 +32,7 @@ type Context struct { index int8 // 当前执行到处理链的哪个位置 mu sync.RWMutex - Keys map[string]any // 用于在中间件之间传递数据 + Keys map[string]interface{} // 用于在中间件之间传递数据 Errors []error // 用于收集处理过程中的错误 @@ -54,62 +43,44 @@ type Context struct { // 携带ctx以实现关闭逻辑 ctx context.Context - // HTTPClient 用于在此上下文中执行出站 HTTP 请求 - // 它由 Engine 提供 + // HTTPClient 用于在此上下文中执行出站 HTTP 请求。 + // 它由 Engine 提供。 HTTPClient *httpc.Client // 引用所属的 Engine 实例,方便访问 Engine 的配置(如 HTMLRender) engine *Engine - - sameSite http.SameSite - - // 请求体Body大小限制 - MaxRequestBodySize int64 - - // skippedNodes 用于记录跳过的节点信息,以便回溯 - // 通常在处理嵌套路由时使用 - SkippedNodes []skippedNode } // --- Context 相关方法实现 --- -// reset 重置 Context 对象以供复用 -// 每次从 sync.Pool 中获取 Context 后,都需要调用此方法进行初始化 +// reset 重置 Context 对象以供复用。 +// 每次从 sync.Pool 中获取 Context 后,都需要调用此方法进行初始化。 func (c *Context) reset(w http.ResponseWriter, req *http.Request) { - - if rw, ok := c.Writer.(*responseWriterImpl); ok && !rw.IsHijacked() { - rw.reset(w) + // 每次重置时,确保 Writer 包装的是最新的 http.ResponseWriter + // 并重置其内部状态 + if rw, ok := c.Writer.(*responseWriterImpl); ok { + rw.ResponseWriter = w + rw.status = 0 + rw.size = 0 } else { + // 如果 c.Writer 不是 responseWriterImpl,重新创建 c.Writer = newResponseWriter(w) } c.Request = req - //c.Params = c.Params[:0] // 清空 Params 切片,而不是重新分配,以复用底层数组 - //避免params长度为0 - if cap(c.Params) > 0 { - c.Params = c.Params[:0] - } else { - c.Params = make(Params, 0, c.engine.maxParams) - } + c.Params = c.Params[:0] // 清空 Params 切片,而不是重新分配,以复用底层数组 c.handlers = nil c.index = -1 // 初始为 -1,`Next()` 将其设置为 0 - c.Keys = make(map[string]any) // 每次请求重新创建 map,避免数据污染 + c.Keys = make(map[string]interface{}) // 每次请求重新创建 map,避免数据污染 c.Errors = c.Errors[:0] // 清空 Errors 切片 c.queryCache = nil // 清空查询参数缓存 c.formCache = nil // 清空表单数据缓存 c.ctx = req.Context() // 使用请求的上下文,继承其取消信号和值 - c.sameSite = http.SameSiteDefaultMode // 默认 SameSite 模式 - c.MaxRequestBodySize = c.engine.GlobalMaxRequestBodySize - - if cap(c.SkippedNodes) > 0 { - c.SkippedNodes = c.SkippedNodes[:0] - } else { - c.SkippedNodes = make([]skippedNode, 0, 256) - } + // c.HTTPClient 和 c.engine 保持不变,它们引用 Engine 实例的成员 } -// Next 在处理链中执行下一个处理函数 -// 这是中间件模式的核心,允许请求依次经过多个处理函数 +// Next 在处理链中执行下一个处理函数。 +// 这是中间件模式的核心,允许请求依次经过多个处理函数。 func (c *Context) Next() { c.index++ for c.index < int8(len(c.handlers)) { @@ -118,125 +89,54 @@ func (c *Context) Next() { } } -// Abort 停止处理链的后续执行 -// 通常在中间件中,当遇到错误或需要提前终止请求时调用 +// Abort 停止处理链的后续执行。 +// 通常在中间件中,当遇到错误或需要提前终止请求时调用。 func (c *Context) Abort() { c.index = abortIndex // 将 index 设置为一个很大的值,使后续 Next() 调用跳过所有处理函数 } -// IsAborted 返回处理链是否已被中止 +// IsAborted 返回处理链是否已被中止。 func (c *Context) IsAborted() bool { return c.index >= abortIndex } -// AbortWithStatus 中止处理链并设置 HTTP 状态码 +// AbortWithStatus 中止处理链并设置 HTTP 状态码。 func (c *Context) AbortWithStatus(code int) { c.Writer.WriteHeader(code) // 设置响应状态码 c.Abort() // 中止处理链 } -// Set 将一个键值对存储到 Context 中 -// 这是一个线程安全的操作,用于在中间件之间传递数据 -func (c *Context) Set(key string, value any) { +// Set 将一个键值对存储到 Context 中。 +// 这是一个线程安全的操作,用于在中间件之间传递数据。 +func (c *Context) Set(key string, value interface{}) { c.mu.Lock() // 加写锁 if c.Keys == nil { - c.Keys = make(map[string]any) + c.Keys = make(map[string]interface{}) } c.Keys[key] = value c.mu.Unlock() // 解写锁 } -// Get 从 Context 中获取一个值 -// 这是一个线程安全的操作 -func (c *Context) Get(key string) (value any, exists bool) { +// Get 从 Context 中获取一个值。 +// 这是一个线程安全的操作。 +func (c *Context) Get(key string) (value interface{}, exists bool) { c.mu.RLock() // 加读锁 value, exists = c.Keys[key] c.mu.RUnlock() // 解读锁 return } -// GetString 从 Context 中获取一个字符串值 -// 这是一个线程安全的操作 -func (c *Context) GetString(key string) (value string, exists bool) { - if val, exists := c.Get(key); exists { - if str, ok := val.(string); ok { - return str, true - } - } - return "", false -} - -// GetInt 从 Context 中获取一个 int 值 -// 这是一个线程安全的操作 -func (c *Context) GetInt(key string) (value int, exists bool) { - if val, exists := c.Get(key); exists { - if i, ok := val.(int); ok { - return i, true - } - } - return 0, false -} - -// GetBool 从 Context 中获取一个 bool 值 -// 这是一个线程安全的操作 -func (c *Context) GetBool(key string) (value bool, exists bool) { - if val, exists := c.Get(key); exists { - if b, ok := val.(bool); ok { - return b, true - } - } - return false, false -} - -// GetFloat64 从 Context 中获取一个 float64 值 -// 这是一个线程安全的操作 -func (c *Context) GetFloat64(key string) (value float64, exists bool) { - if val, exists := c.Get(key); exists { - if f, ok := val.(float64); ok { - return f, true - } - } - return 0.0, false -} - -// GetTime 从 Context 中获取一个 time.Time 值 -// 这是一个线程安全的操作 -func (c *Context) GetTime(key string) (value time.Time, exists bool) { - if val, exists := c.Get(key); exists { - if t, ok := val.(time.Time); ok { - return t, true - } - } - return time.Time{}, false -} - -// GetDuration 从 Context 中获取一个 time.Duration 值 -// 这是一个线程安全的操作 -func (c *Context) GetDuration(key string) (value time.Duration, exists bool) { - if val, exists := c.Get(key); exists { - if d, ok := val.(time.Duration); ok { - return d, true - } - } - return 0, false -} - -// MustGet 从 Context 中获取一个值,如果不存在则 panic -// 适用于确定值一定存在的场景 -func (c *Context) MustGet(key string) any { +// MustGet 从 Context 中获取一个值,如果不存在则 panic。 +// 适用于确定值一定存在的场景。 +func (c *Context) MustGet(key string) interface{} { if value, exists := c.Get(key); exists { return value } panic("Key \"" + key + "\" does not exist in context.") } -// SetMaxRequestBodySize -func (c *Context) SetMaxRequestBodySize(size int64) { - c.MaxRequestBodySize = size -} - -// Query 从 URL 查询参数中获取值 -// 懒加载解析查询参数,并进行缓存 +// Query 从 URL 查询参数中获取值。 +// 懒加载解析查询参数,并进行缓存。 func (c *Context) Query(key string) string { if c.queryCache == nil { c.queryCache = c.Request.URL.Query() // 首次访问时解析并缓存 @@ -244,7 +144,7 @@ func (c *Context) Query(key string) string { return c.queryCache.Get(key) } -// DefaultQuery 从 URL 查询参数中获取值,如果不存在则返回默认值 +// DefaultQuery 从 URL 查询参数中获取值,如果不存在则返回默认值。 func (c *Context) DefaultQuery(key, defaultValue string) string { if value := c.Query(key); value != "" { return value @@ -252,8 +152,8 @@ func (c *Context) DefaultQuery(key, defaultValue string) string { return defaultValue } -// PostForm 从 POST 请求体中获取表单值 -// 懒加载解析表单数据,并进行缓存 +// PostForm 从 POST 请求体中获取表单值。 +// 懒加载解析表单数据,并进行缓存。 func (c *Context) PostForm(key string) string { if c.formCache == nil { c.Request.ParseMultipartForm(defaultMemory) // 解析 multipart/form-data 或 application/x-www-form-urlencoded @@ -262,7 +162,7 @@ func (c *Context) PostForm(key string) string { return c.formCache.Get(key) } -// DefaultPostForm 从 POST 请求体中获取表单值,如果不存在则返回默认值 +// DefaultPostForm 从 POST 请求体中获取表单值,如果不存在则返回默认值。 func (c *Context) DefaultPostForm(key, defaultValue string) string { if value := c.PostForm(key); value != "" { return value @@ -270,185 +170,38 @@ func (c *Context) DefaultPostForm(key, defaultValue string) string { return defaultValue } -// Param 从 URL 路径参数中获取值 -// 例如,对于路由 /users/:id,c.Param("id") 可以获取 id 的值 +// Param 从 URL 路径参数中获取值。 +// 例如,对于路由 /users/:id,c.Param("id") 可以获取 id 的值。 func (c *Context) Param(key string) string { return c.Params.ByName(key) } -// Raw 向响应写入bytes -func (c *Context) Raw(code int, contentType string, data []byte) { - c.Writer.Header().Set("Content-Type", contentType) - c.Writer.WriteHeader(code) - c.Writer.Write(data) -} - -// String 向响应写入格式化的字符串 -func (c *Context) String(code int, format string, values ...any) { +// String 向响应写入格式化的字符串。 +func (c *Context) String(code int, format string, values ...interface{}) { c.Writer.WriteHeader(code) c.Writer.Write([]byte(fmt.Sprintf(format, values...))) } -// Text 向响应写入无需格式化的string -func (c *Context) Text(code int, text string) { - c.Writer.Header().Set("Content-Type", "text/plain; charset=utf-8") - c.Writer.WriteHeader(code) - c.Writer.Write([]byte(text)) -} - -// FileText -func (c *Context) FileText(code int, filePath string) { - // 清理path - cleanPath := filepath.Clean(filePath) - if !filepath.IsAbs(cleanPath) { - c.AddError(fmt.Errorf("relative path not allowed: %s", cleanPath)) - c.ErrorUseHandle(http.StatusBadRequest, fmt.Errorf("relative path not allowed")) - return - } - // 检查文件是否存在 - if _, err := os.Stat(cleanPath); os.IsNotExist(err) { - c.AddError(fmt.Errorf("file not found: %s", cleanPath)) - c.ErrorUseHandle(http.StatusNotFound, fmt.Errorf("file not found")) - return - } - - // 打开文件 - file, err := os.Open(cleanPath) - if err != nil { - c.AddError(fmt.Errorf("failed to open file %s: %w", cleanPath, err)) - c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to open file: %w", err)) - return - } - defer file.Close() - - // 获取文件信息以获取文件大小 - fileInfo, err := file.Stat() - if err != nil { - c.AddError(fmt.Errorf("failed to get file info for %s: %w", cleanPath, err)) - c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to get file info: %w", err)) - return - } - // 判断是否是dir - if fileInfo.IsDir() { - c.AddError(fmt.Errorf("path is a directory, not a file: %s", cleanPath)) - c.ErrorUseHandle(http.StatusBadRequest, fmt.Errorf("path is a directory")) - return - } - - c.SetHeader("Content-Type", "text/plain; charset=utf-8") - - c.SetBodyStream(file, int(fileInfo.Size())) -} - -/* -// not fot work -// FileTextSafeDir -func (c *Context) FileTextSafeDir(code int, filePath string, safeDir string) { - - // 清理path - cleanPath := path.Clean(filePath) - if !filepath.IsAbs(cleanPath) { - c.AddError(fmt.Errorf("relative path not allowed: %s", cleanPath)) - c.ErrorUseHandle(http.StatusBadRequest, fmt.Errorf("relative path not allowed")) - return - } - if strings.Contains(cleanPath, "..") { - c.AddError(fmt.Errorf("path traversal attempt detected: %s", cleanPath)) - c.ErrorUseHandle(http.StatusBadRequest, fmt.Errorf("path traversal attempt detected")) - return - } - - // 判断filePath是否包含在safeDir内, 防止路径穿越 - relPath, err := filepath.Rel(safeDir, cleanPath) - if err != nil { - c.AddError(fmt.Errorf("failed to get relative path: %w", err)) - c.ErrorUseHandle(http.StatusBadRequest, fmt.Errorf("failed to get relative path: %w", err)) - return - } - cleanPath = filepath.Join(safeDir, relPath) - - // 检查文件是否存在 - if _, err := os.Stat(cleanPath); os.IsNotExist(err) { - c.AddError(fmt.Errorf("file not found: %s", cleanPath)) - c.ErrorUseHandle(http.StatusNotFound, fmt.Errorf("file not found")) - return - } - - // 打开文件 - file, err := os.Open(cleanPath) - if err != nil { - c.AddError(fmt.Errorf("failed to open file %s: %w", cleanPath, err)) - c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to open file: %w", err)) - return - } - defer file.Close() - - // 获取文件信息以获取文件大小 - fileInfo, err := file.Stat() - if err != nil { - c.AddError(fmt.Errorf("failed to get file info for %s: %w", cleanPath, err)) - c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to get file info: %w", err)) - return - } - // 判断是否是dir - if fileInfo.IsDir() { - c.AddError(fmt.Errorf("path is a directory, not a file: %s", cleanPath)) - c.ErrorUseHandle(http.StatusBadRequest, fmt.Errorf("path is a directory")) - return - } - - c.SetHeader("Content-Type", "text/plain; charset=utf-8") - - c.SetBodyStream(file, int(fileInfo.Size())) -} -*/ - -// JSON 向响应写入 JSON 数据 -// 设置 Content-Type 为 application/json -func (c *Context) JSON(code int, obj any) { +// JSON 向响应写入 JSON 数据。 +// 设置 Content-Type 为 application/json。 +func (c *Context) JSON(code int, obj interface{}) { c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8") c.Writer.WriteHeader(code) - if err := json.MarshalWrite(c.Writer, obj); err != nil { + // 实际 JSON 编码 + jsonBytes, err := json.Marshal(obj) + if err != nil { c.AddError(fmt.Errorf("failed to marshal JSON: %w", err)) - c.Errorf("failed to marshal JSON: %s", err) - c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to marshal JSON: %w", err)) + c.String(http.StatusInternalServerError, "Internal Server Error: Failed to marshal JSON") return } + c.Writer.Write(jsonBytes) } -// GOB 向响应写入GOB数据 -// 设置 Content-Type 为 application/octet-stream -func (c *Context) GOB(code int, obj any) { - c.Writer.Header().Set("Content-Type", "application/octet-stream") // 设置合适的 Content-Type - c.Writer.WriteHeader(code) - // GOB 编码 - encoder := gob.NewEncoder(c.Writer) - if err := encoder.Encode(obj); err != nil { - c.AddError(fmt.Errorf("failed to encode GOB: %w", err)) - c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to encode GOB: %w", err)) - return - } -} - -// WANF向响应写入WANF数据 -// 设置 application/vnd.wjqserver.wanf; charset=utf-8 -func (c *Context) WANF(code int, obj any) { - c.Writer.Header().Set("Content-Type", "application/vnd.wjqserver.wanf; charset=utf-8") - c.Writer.WriteHeader(code) - // WANF 编码 - encoder := wanf.NewStreamEncoder(c.Writer) - if err := encoder.Encode(obj); err != nil { - c.AddError(fmt.Errorf("failed to encode WANF: %w", err)) - c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to encode WANF: %w", err)) - return - } -} - -// HTML 渲染 HTML 模板 -// 如果 Engine 配置了 HTMLRender,则使用它进行渲染 -// 否则,会进行简单的字符串输出 -// 预留接口,可以扩展为支持多种模板引擎 -func (c *Context) HTML(code int, name string, obj any) { +// HTML 渲染 HTML 模板。 +// 如果 Engine 配置了 HTMLRender,则使用它进行渲染。 +// 否则,会进行简单的字符串输出。 +// 预留接口,可以扩展为支持多种模板引擎。 +func (c *Context) HTML(code int, name string, obj interface{}) { c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8") c.Writer.WriteHeader(code) @@ -458,7 +211,7 @@ func (c *Context) HTML(code int, name string, obj any) { err := tpl.ExecuteTemplate(c.Writer, name, obj) if err != nil { c.AddError(fmt.Errorf("failed to render HTML template '%s': %w", name, err)) - c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to render HTML template '%s': %w", name, err)) + c.String(http.StatusInternalServerError, "Internal Server Error: Failed to render HTML template") } return } @@ -468,8 +221,8 @@ func (c *Context) HTML(code int, name string, obj any) { c.Writer.Write([]byte(fmt.Sprintf("\n
%v
", name, obj))) } -// Redirect 执行 HTTP 重定向 -// code 应为 3xx 状态码 (如 http.StatusMovedPermanently, http.StatusFound) +// Redirect 执行 HTTP 重定向。 +// code 应为 3xx 状态码 (如 http.StatusMovedPermanently, http.StatusFound)。 func (c *Context) Redirect(code int, location string) { http.Redirect(c.Writer, c.Request, location, code) c.Abort() @@ -478,11 +231,17 @@ func (c *Context) Redirect(code int, location string) { } } -// ShouldBindJSON 尝试将请求体绑定到 JSON 对象 -func (c *Context) ShouldBindJSON(obj any) error { +// ShouldBindJSON 尝试将请求体绑定到 JSON 对象。 +func (c *Context) ShouldBindJSON(obj interface{}) error { if c.Request.Body == nil { return errors.New("request body is empty") } + /* + decoder := json.NewDecoder(c.Request.Body) + if err := decoder.Decode(obj); err != nil { + return fmt.Errorf("json binding error: %w", err) + } + */ err := json.UnmarshalRead(c.Request.Body, obj) if err != nil { return fmt.Errorf("json binding error: %w", err) @@ -490,28 +249,10 @@ func (c *Context) ShouldBindJSON(obj any) error { return nil } -// ShouldBindWANF -func (c *Context) ShouldBindWANF(obj any) error { - if c.Request.Body == nil { - return errors.New("request body is empty") - } - decoder, err := wanf.NewStreamDecoder(c.Request.Body) - if err != nil { - return fmt.Errorf("failed to create WANF decoder: %w", err) - } - - if err := decoder.Decode(obj); err != nil { - return fmt.Errorf("WANF binding error: %w", err) - } - return nil -} - -// Deprecated: This function is a reserved placeholder for future API extensions -// and is not yet implemented. It will either be properly defined or removed in v2.0.0. Do not use. -// ShouldBind 尝试将请求体绑定到各种类型(JSON, Form, XML 等) -// 这是一个复杂的通用绑定接口,通常根据 Content-Type 或其他头部来判断绑定方式 -// 预留接口,可根据项目需求进行扩展 -func (c *Context) ShouldBind(obj any) error { +// ShouldBind 尝试将请求体绑定到各种类型(JSON, Form, XML 等)。 +// 这是一个复杂的通用绑定接口,通常根据 Content-Type 或其他头部来判断绑定方式。 +// 预留接口,可根据项目需求进行扩展。 +func (c *Context) ShouldBind(obj interface{}) error { // TODO: 完整的通用绑定逻辑 // 可以根据 c.Request.Header.Get("Content-Type") 来判断是 JSON, Form, XML 等 // 例如: @@ -525,46 +266,46 @@ func (c *Context) ShouldBind(obj any) error { return errors.New("generic binding not fully implemented yet, implement based on Content-Type") } -// AddError 添加一个错误到 Context -// 允许在处理请求过程中收集多个错误 +// AddError 添加一个错误到 Context。 +// 允许在处理请求过程中收集多个错误。 func (c *Context) AddError(err error) { c.Errors = append(c.Errors, err) } -// Errors 返回 Context 中收集的所有错误 +// Errors 返回 Context 中收集的所有错误。 func (c *Context) GetErrors() []error { return c.Errors } -// Client 返回 Engine 提供的 HTTPClient -// 方便在请求处理函数中进行出站 HTTP 请求 +// Client 返回 Engine 提供的 HTTPClient。 +// 方便在请求处理函数中进行出站 HTTP 请求。 func (c *Context) Client() *httpc.Client { return c.HTTPClient } -// Context() 返回请求的上下文,用于取消操作 -// 这是 Go 标准库的 `context.Context`,用于请求的取消和超时管理 +// Context() 返回请求的上下文,用于取消操作。 +// 这是 Go 标准库的 `context.Context`,用于请求的取消和超时管理。 func (c *Context) Context() context.Context { return c.ctx } // Done returns a channel that is closed when the request context is cancelled or times out. -// 继承自 `context.Context` +// 继承自 `context.Context`。 func (c *Context) Done() <-chan struct{} { return c.ctx.Done() } // Err returns the error, if any, that caused the context to be canceled or to // time out. -// 继承自 `context.Context` +// 继承自 `context.Context`。 func (c *Context) Err() error { return c.ctx.Err() } // Value returns the value associated with this context for key, or nil if no // value is associated with key. -// 可以用于从 Context 中获取与特定键关联的值,包括 Go 原生 Context 的值和 Touka Context 的 Keys -func (c *Context) Value(key any) any { +// 可以用于从 Context 中获取与特定键关联的值,包括 Go 原生 Context 的值和 Touka Context 的 Keys。 +func (c *Context) Value(key interface{}) interface{} { if keyAsString, ok := key.(string); ok { if val, exists := c.Get(keyAsString); exists { return val @@ -573,23 +314,23 @@ func (c *Context) Value(key any) any { return c.ctx.Value(key) // 尝试从 Go 原生 Context 中获取值 } -// GetWriter 获得一个 io.Writer 接口,可以直接向响应体写入数据 -// 这对于需要自定义流式写入或与其他需要 io.Writer 的库集成非常有用 +// GetWriter 获得一个 io.Writer 接口,可以直接向响应体写入数据。 +// 这对于需要自定义流式写入或与其他需要 io.Writer 的库集成非常有用。 func (c *Context) GetWriter() io.Writer { return c.Writer // ResponseWriter 接口嵌入了 http.ResponseWriter,而 http.ResponseWriter 实现了 io.Writer } -// WriteStream 接受一个 io.Reader 并将其内容流式传输到响应体 -// 返回写入的字节数和可能遇到的错误 -// 该方法在开始写入之前,会确保设置 HTTP 状态码为 200 OK +// WriteStream 接受一个 io.Reader 并将其内容流式传输到响应体。 +// 返回写入的字节数和可能遇到的错误。 +// 该方法在开始写入之前,会确保设置 HTTP 状态码为 200 OK。 func (c *Context) WriteStream(reader io.Reader) (written int64, err error) { - // 确保在写入数据前设置状态码 - // WriteHeader 会在第一次写入时被 Write 方法隐式调用,但显式调用可以确保状态码的预期 + // 确保在写入数据前设置状态码。 + // WriteHeader 会在第一次写入时被 Write 方法隐式调用,但显式调用可以确保状态码的预期。 if !c.Writer.Written() { c.Writer.WriteHeader(http.StatusOK) // 默认 200 OK } - written, err = iox.Copy(c.Writer, reader) // 从 reader 读取并写入 ResponseWriter + written, err = copyb.Copy(c.Writer, reader) // 从 reader 读取并写入 ResponseWriter if err != nil { c.AddError(fmt.Errorf("failed to write stream: %w", err)) } @@ -597,408 +338,102 @@ func (c *Context) WriteStream(reader io.Reader) (written int64, err error) { } // GetReqBody 以获取一个 io.ReadCloser 接口,用于读取请求体 -// 注意:请求体只能读取一次 +// 注意:请求体只能读取一次。 func (c *Context) GetReqBody() io.ReadCloser { return c.Request.Body } -// GetReqBodyFull 读取并返回请求体的所有内容 -// 注意:请求体只能读取一次 -func (c *Context) GetReqBodyFull() ([]byte, error) { - if c.Request.Body == nil { - return nil, nil - } - - var limitBytesReader io.ReadCloser - - if c.MaxRequestBodySize > 0 { - limitBytesReader = NewMaxBytesReader(c.Request.Body, c.MaxRequestBodySize) - defer func() { - err := limitBytesReader.Close() - if err != nil { - c.AddError(fmt.Errorf("failed to close request body: %w", err)) - } - }() - } else { - limitBytesReader = c.Request.Body - defer func() { - err := limitBytesReader.Close() - if err != nil { - c.AddError(fmt.Errorf("failed to close request body: %w", err)) - } - }() - } - - data, err := iox.ReadAll(limitBytesReader) - if err != nil { - c.AddError(fmt.Errorf("failed to read request body: %w", err)) - return nil, fmt.Errorf("failed to read request body: %w", err) - } - return data, nil -} - -// 类似 GetReqBodyFull, 返回 *bytes.Buffer -func (c *Context) GetReqBodyBuffer() (*bytes.Buffer, error) { - if c.Request.Body == nil { - return nil, nil - } - - var limitBytesReader io.ReadCloser - - if c.MaxRequestBodySize > 0 { - limitBytesReader = NewMaxBytesReader(c.Request.Body, c.MaxRequestBodySize) - defer func() { - err := limitBytesReader.Close() - if err != nil { - c.AddError(fmt.Errorf("failed to close request body: %w", err)) - } - }() - } else { - limitBytesReader = c.Request.Body - defer func() { - err := limitBytesReader.Close() - if err != nil { - c.AddError(fmt.Errorf("failed to close request body: %w", err)) - } - }() - } - - data, err := iox.ReadAll(limitBytesReader) - if err != nil { - c.AddError(fmt.Errorf("failed to read request body: %w", err)) - return nil, fmt.Errorf("failed to read request body: %w", err) - } - return bytes.NewBuffer(data), nil -} - -// RequestIP 返回客户端的 IP 地址 +// RequestIP 返回客户端的 IP 地址。 // 它会根据 Engine 的配置 (ForwardByClientIP) 尝试从 X-Forwarded-For 或 X-Real-IP 等头部获取, -// 否则回退到 Request.RemoteAddr +// 否则回退到 Request.RemoteAddr。 func (c *Context) RequestIP() string { if c.engine.ForwardByClientIP { for _, headerName := range c.engine.RemoteIPHeaders { - ipValue := c.Request.Header.Get(headerName) - if ipValue == "" { - continue // 头部为空, 继续检查下一个 - } - - // 使用索引高效遍历逗号分隔的 IP 列表, 避免 strings.Split 的内存分配 - currentPos := 0 - for currentPos < len(ipValue) { - nextComma := strings.IndexByte(ipValue[currentPos:], ',') - - var ipSegment string - if nextComma == -1 { - // 这是列表中的最后一个 IP - ipSegment = ipValue[currentPos:] - currentPos = len(ipValue) // 结束循环 - } else { - // 截取当前 IP 段 - ipSegment = ipValue[currentPos : currentPos+nextComma] - currentPos += nextComma + 1 // 移动到下一个 IP 段的起始位置 - } - - // 去除空格并检查是否为空 (例如 "ip1,,ip2") - trimmedIP := strings.TrimSpace(ipSegment) - if trimmedIP == "" { - continue - } - - // 使用 netip.ParseAddr 进行 IP 地址的解析和验证 - addr, err := netip.ParseAddr(trimmedIP) - if err == nil { - // 成功解析到合法的 IP, 立即返回 - return addr.String() + if ipValue := c.Request.Header.Get(headerName); ipValue != "" { + // X-Forwarded-For 可能包含多个 IP,约定第一个(最左边)是客户端 IP + // 其他头部(如 X-Real-IP)通常只有一个 + ips := strings.Split(ipValue, ",") + for _, singleIP := range ips { + trimmedIP := strings.TrimSpace(singleIP) + // 使用 netip.ParseAddr 进行 IP 地址的解析和格式验证 + addr, err := netip.ParseAddr(trimmedIP) + if err == nil { + // 成功解析到合法的 IP 地址格式,立即返回 + return addr.String() + } + // 如果当前 singleIP 无效,继续检查列表中的下一个 } } } } - // 回退到 Request.RemoteAddr 的处理 - // 优先使用 netip.ParseAddrPort, 它比 net.SplitHostPort 更高效且分配更少 - addrp, err := netip.ParseAddrPort(c.Request.RemoteAddr) - if err == nil { - // 成功从 "ip:port" 格式中解析出 IP - return addrp.Addr().String() + // 如果没有启用 ForwardByClientIP 或头部中没有找到有效 IP,回退到 Request.RemoteAddr + // RemoteAddr 通常是 "host:port" 格式,但也可能直接就是 IP 地址 + remoteAddrStr := c.Request.RemoteAddr + ip, _, err := net.SplitHostPort(remoteAddrStr) // 尝试分离 host 和 port + if err != nil { + // 如果分离失败,意味着 remoteAddrStr 可能直接就是 IP 地址(或畸形) + ip = remoteAddrStr // 此时将整个 remoteAddrStr 作为候选 IP } - // 如果上面的解析失败 (例如 RemoteAddr 只有 IP, 没有端口), - // 则尝试将整个字符串作为 IP 地址进行解析 - addr, err := netip.ParseAddr(c.Request.RemoteAddr) - if err == nil { - return addr.String() + // 对从 RemoteAddr 中提取/使用的 IP 进行最终的合法性验证 + addr, parseErr := netip.ParseAddr(ip) + if parseErr == nil { + return addr.String() // 成功解析并返回合法 IP } - // 所有方法都失败, 返回空字符串 return "" } -// ClientIP 返回客户端的 IP 地址 -// 这是一个别名,与 RequestIP 功能相同 +// ClientIP 返回客户端的 IP 地址。 +// 这是一个别名,与 RequestIP 功能相同。 func (c *Context) ClientIP() string { return c.RequestIP() } -// ContentType 返回请求的 Content-Type 头部 +// ContentType 返回请求的 Content-Type 头部。 func (c *Context) ContentType() string { return c.GetReqHeader("Content-Type") } -// UserAgent 返回请求的 User-Agent 头部 +// UserAgent 返回请求的 User-Agent 头部。 func (c *Context) UserAgent() string { return c.GetReqHeader("User-Agent") } -// Status 设置响应状态码 +// Status 设置响应状态码。 func (c *Context) Status(code int) { c.Writer.WriteHeader(code) } -// File 将指定路径的文件作为响应发送 -// 它会设置 Content-Type 和 Content-Disposition 头部 +// File 将指定路径的文件作为响应发送。 +// 它会设置 Content-Type 和 Content-Disposition 头部。 func (c *Context) File(filepath string) { http.ServeFile(c.Writer, c.Request, filepath) c.Abort() // 发送文件后中止后续处理 } -// SetHeader 设置响应头部 +// SetHeader 设置响应头部。 func (c *Context) SetHeader(key, value string) { c.Writer.Header().Set(key, value) } -// AddHeader 添加响应头部 +// AddHeader 添加响应头部。 func (c *Context) AddHeader(key, value string) { c.Writer.Header().Add(key, value) } -// Header 作为SetHeader的别名 -func (c *Context) Header(key, value string) { - c.SetHeader(key, value) -} - -// DelHeader 删除响应头部 +// DelHeader 删除响应头部。 func (c *Context) DelHeader(key string) { c.Writer.Header().Del(key) } -// GetReqHeader 获取请求头部的值 +// GetReqHeader 获取请求头部的值。 func (c *Context) GetReqHeader(key string) string { return c.Request.Header.Get(key) } -// SetHeaders 接受headers列表 -func (c *Context) SetHeaders(headers map[string][]string) { - for key, values := range headers { - for _, value := range values { - c.Writer.Header().Add(key, value) - } - } -} - -// 获取所有resp Headers -func (c *Context) GetAllRespHeader() http.Header { - return c.Writer.Header() -} - -// GetAllReqHeader 获取所有请求头部 +// GetAllReqHeader 获取所有请求头部。 func (c *Context) GetAllReqHeader() http.Header { return c.Request.Header } - -// 使用定义的errorHandle来处理error并结束当前handle -func (c *Context) ErrorUseHandle(code int, err error) { - if c.engine != nil && c.engine.errorHandle.handler != nil { - c.engine.errorHandle.handler(c, code, err) - c.Abort() - return - } else { - c.String(code, "%s", http.StatusText(code)) - c.Abort() - } -} - -// GetProtocol 获取当前连接版本 -func (c *Context) GetProtocol() string { - return c.Request.Proto -} - -// GetHTTPC 获取框架自带传递的httpc -func (c *Context) GetHTTPC() *httpc.Client { - return c.HTTPClient -} - -// GetLogger 获取engine的Logger -func (c *Context) GetLogger() *reco.Logger { - return c.engine.LogReco -} - -// GetReqQueryString -// GetReqQueryString 返回请求的原始查询字符串 -func (c *Context) GetReqQueryString() string { - return c.Request.URL.RawQuery -} - -// SetBodyStream 设置响应体为一个 io.Reader,并指定内容长度 -// 如果 contentSize 为 -1,则表示内容长度未知,将使用 Transfer-Encoding: chunked -func (c *Context) SetBodyStream(reader io.Reader, contentSize int) { - // 如果指定了内容长度且大于等于 0,则设置 Content-Length 头部 - if contentSize >= 0 { - c.Writer.Header().Set("Content-Length", fmt.Sprintf("%d", contentSize)) - } else { - // 如果内容长度未知,移除 Content-Length 头部,通常会使用 Transfer-Encoding: chunked - c.Writer.Header().Del("Content-Length") - } - - // 确保在写入数据前设置状态码 - if !c.Writer.Written() { - c.Writer.WriteHeader(http.StatusOK) // 默认 200 OK - } - - // 将 reader 的内容直接复制到 ResponseWriter - // ResponseWriter 实现了 io.Writer 接口 - _, err := iox.Copy(c.Writer, reader) - if err != nil { - c.AddError(fmt.Errorf("failed to write stream: %w", err)) - // 注意:这里可能无法设置错误状态码,因为头部可能已经发送 - // 可以在调用 SetBodyStream 之前检查错误,或者在中间件中处理 Context.Errors - } -} - -// GetRequestURI 返回请求的原始 URI -func (c *Context) GetRequestURI() string { - return c.Request.RequestURI -} - -// GetRequestURIPath 返回请求的原始 URI 的路径部分 -func (c *Context) GetRequestURIPath() string { - return c.Request.URL.Path -} - -// === 文件操作 === - -// 将文件内容作为响应body -func (c *Context) SetRespBodyFile(code int, filePath string) { - // 清理path - cleanPath := filepath.Clean(filePath) - - // 打开文件 - file, err := os.Open(cleanPath) - if err != nil { - c.AddError(fmt.Errorf("failed to open file %s: %w", cleanPath, err)) - c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to open file: %w", err)) - return - } - defer file.Close() - - // 获取文件信息以获取文件大小和MIME类型 - fileInfo, err := file.Stat() - if err != nil { - c.AddError(fmt.Errorf("failed to get file info for %s: %w", cleanPath, err)) - c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to get file info: %w", err)) - return - } - - // 尝试根据文件扩展名猜测 Content-Type - contentType := mime.TypeByExtension(filepath.Ext(cleanPath)) - if contentType == "" { - // 如果无法猜测,则使用默认的二进制流类型 - contentType = "application/octet-stream" - } - - // 设置响应头 - c.Writer.Header().Set("Content-Type", contentType) - c.Writer.Header().Set("Content-Length", fmt.Sprintf("%d", fileInfo.Size())) - // 还可以设置 Content-Disposition 来控制浏览器是下载还是直接显示 - // c.Writer.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, path.Base(cleanPath))) - - // 设置状态码 - c.Writer.WriteHeader(code) - - // 将文件内容写入响应体 - _, err = iox.Copy(c.Writer, file) - if err != nil { - c.AddError(fmt.Errorf("failed to write file %s to response: %w", cleanPath, err)) - // 注意:这里可能无法设置错误状态码,因为头部可能已经发送 - // 可以在调用 SetRespBodyFile 之前检查错误,或者在中间件中处理 Context.Errors - } - c.Abort() // 文件发送后中止后续处理 -} - -// == cookie === - -// SetSameSite 设置响应的 SameSite cookie 属性 -func (c *Context) SetSameSite(samesite http.SameSite) { - c.sameSite = samesite -} - -// SetCookie 设置一个 HTTP cookie -func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool) { - if path == "" { - path = "/" - } - http.SetCookie(c.Writer, &http.Cookie{ - Name: name, - Value: url.QueryEscape(value), - MaxAge: maxAge, - Path: path, - Domain: domain, - SameSite: c.sameSite, - Secure: secure, - HttpOnly: httpOnly, - }) -} - -func (c *Context) SetCookieData(cookie *http.Cookie) { - if cookie.Path == "" { - cookie.Path = "/" - } - if cookie.SameSite == http.SameSiteDefaultMode { - cookie.SameSite = c.sameSite - } - http.SetCookie(c.Writer, cookie) -} - -// GetCookie 获取指定名称的 cookie 值 -func (c *Context) GetCookie(name string) (string, error) { - cookie, err := c.Request.Cookie(name) - if err != nil { - return "", err - } - // 对 cookie 值进行 URL 解码 - value, err := url.QueryUnescape(cookie.Value) - if err != nil { - return "", fmt.Errorf("failed to unescape cookie value: %w", err) - } - return value, nil -} - -// DeleteCookie 删除指定名称的 cookie -// 通过设置 MaxAge 为 -1 来删除 cookie -func (c *Context) DeleteCookie(name string) { - c.SetCookie(name, "", -1, "/", "", false, false) // 设置 MaxAge 为 -1 删除 cookie -} - -// === 日志记录 === -func (c *Context) Debugf(format string, args ...any) { - c.engine.LogReco.Debugf(format, args...) -} - -func (c *Context) Infof(format string, args ...any) { - c.engine.LogReco.Infof(format, args...) -} - -func (c *Context) Warnf(format string, args ...any) { - c.engine.LogReco.Warnf(format, args...) -} - -func (c *Context) Errorf(format string, args ...any) { - c.engine.LogReco.Errorf(format, args...) -} - -func (c *Context) Fatalf(format string, args ...any) { - c.engine.LogReco.Fatalf(format, args...) -} - -func (c *Context) Panicf(format string, args ...any) { - c.engine.LogReco.Panicf(format, args...) -} diff --git a/ecw.go b/ecw.go index c87be28..8f1417a 100644 --- a/ecw.go +++ b/ecw.go @@ -1,13 +1,6 @@ -// 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/. -// Copyright 2024 WJQSERVER. All rights reserved. -// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization. package touka import ( - "bufio" - "errors" - "net" "net/http" "sync" ) @@ -51,9 +44,9 @@ func (ecw *errorCapturingResponseWriter) reset(w http.ResponseWriter, r *http.Re // AcquireErrorCapturingResponseWriter 从对象池获取一个 errorCapturingResponseWriter 实例 // 必须在处理完成后调用 ReleaseErrorCapturingResponseWriter -func AcquireErrorCapturingResponseWriter(c *Context) *errorCapturingResponseWriter { +func AcquireErrorCapturingResponseWriter(c *Context, eh ErrorHandler) *errorCapturingResponseWriter { ecw := errorResponseWriterPool.Get().(*errorCapturingResponseWriter) - ecw.reset(c.Writer, c.Request, c, c.engine.errorHandle.handler) // 传入 Touka Context 的 Writer + ecw.reset(c.Writer, c.Request, c, eh) // 传入 Touka Context 的 Writer return ecw } @@ -81,9 +74,9 @@ func (ecw *errorCapturingResponseWriter) WriteHeader(statusCode int) { if ecw.responseStarted { return // 响应已开始, 忽略后续的 WriteHeader 调用 } - ecw.statusCode = statusCode + ecw.statusCode = statusCode // 总是记录 FileServer 意图的状态码 - if ecw.Status() >= 400 { + if statusCode >= http.StatusBadRequest { ecw.capturedErrorSignal = true // 是一个错误状态码 (>=400), 激活错误信号 // 不会将这个 WriteHeader 传递给原始的 w, 等待 processAfterFileServer 处理 @@ -115,7 +108,7 @@ func (ecw *errorCapturingResponseWriter) Write(data []byte) (int, error) { for k, v := range ecw.headerSnapshot { ecw.w.Header()[k] = v // 直接赋值 []string, 保留所有值 } - ecw.w.WriteHeader(ecw.Status()) // 发送实际的状态码 (可能是 200 或之前设置的 2xx) + ecw.w.WriteHeader(ecw.statusCode) // 发送实际的状态码 (可能是 200 或之前设置的 2xx) ecw.responseStarted = true } return ecw.w.Write(data) // 写入数据到原始 ResponseWriter @@ -140,7 +133,7 @@ func (ecw *errorCapturingResponseWriter) processAfterFileServer() { ecw.ctx.Next() } else { // 调用用户自定义的 ErrorHandlerFunc, 由它负责完整的错误响应 - ecw.errorHandlerFunc(ecw.ctx, ecw.Status(), errors.New("file server error")) + ecw.errorHandlerFunc(ecw.ctx, ecw.statusCode) ecw.ctx.Abort() } } @@ -148,57 +141,3 @@ func (ecw *errorCapturingResponseWriter) processAfterFileServer() { // 如果 ecw.capturedErrorSignal && ecw.responseStarted, 表示在捕获错误信号之前, // 成功路径的响应已经开始, 此时无法再进行错误处理覆盖 } - -// Status 返回当前记录的状态码 -func (ecw *errorCapturingResponseWriter) Status() int { - if ecw.statusCode == 0 && !ecw.responseStarted { - // 如果还没有显式设置状态码, 并且响应尚未开始, - // 则尝试从底层 ResponseWriter 获取状态码 (如果它实现了 Statuser) - if tw, ok := ecw.w.(ResponseWriter); ok { - return tw.Status() - } - // 否则, 默认返回 200 OK (Go HTTP server 的默认行为) - return http.StatusOK - } - return ecw.statusCode -} - -// Size 返回已写入响应体的字节数 -func (ecw *errorCapturingResponseWriter) Size() int { - // ecw 在捕获错误信号时会丢弃 FileServer 写入的数据, 所以 Size 应返回 0 - if ecw.capturedErrorSignal { - return 0 - } - // 否则, 尝试从底层 ResponseWriter 获取已写入的字节数 - if tw, ok := ecw.w.(ResponseWriter); ok { - return tw.Size() - } - // 对于其他类型的 ResponseWriter, 无法可靠获取, 只能返回 0 - return 0 -} - -// Written方式 -func (ecw *errorCapturingResponseWriter) Written() bool { - // 如果响应已经通过这个包装器开始写入 (WriteHeader 或 Write 成功调用) - // 或者如果原始 ResponseWriter 已经标记为 Written (例如, 如果它是 touka.ResponseWriterImpl) - // 则认为响应已开始 - if ecw.responseStarted { - return true - } - // 检查原始 ResponseWriter 是否已经写入 - if tw, ok := ecw.w.(ResponseWriter); ok { - return tw.Written() - } - // 对于其他类型的 ResponseWriter, 无法可靠判断是否已写入, 只能依赖 responseStarted 标记 - return false -} - -// Hijack 实现 http.Hijacker 接口 -// 它将 Hijack 调用委托给底层的 ResponseWriter -func (ecw *errorCapturingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { - hijacker, ok := ecw.w.(http.Hijacker) - if !ok { - return nil, nil, errors.New("the underlying ResponseWriter does not support the Hijacker interface") - } - return hijacker.Hijack() -} diff --git a/engine.go b/engine.go index 0a95765..8f5d406 100644 --- a/engine.go +++ b/engine.go @@ -1,26 +1,21 @@ -// 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/. -// Copyright 2024 WJQSERVER. All rights reserved. -// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization. package touka import ( "context" - "errors" "reflect" "runtime" "strings" "net/http" + "path" "sync" "github.com/WJQSERVER-STUDIO/httpc" - "github.com/fenthope/reco" ) -// Last 返回链中的最后一个处理函数 -// 如果链为空,则返回 nil +// Last 返回链中的最后一个处理函数。 +// 如果链为空,则返回 nil。 func (c HandlersChain) Last() HandlerFunc { if len(c) > 0 { return c[len(c)-1] @@ -28,82 +23,40 @@ func (c HandlersChain) Last() HandlerFunc { return nil } -// Engine 是 Touka 框架的核心,负责路由注册、中间件管理和请求分发 -// 它实现了 http.Handler 接口,可以直接用于 http.ListenAndServe +// Engine 是 Touka 框架的核心,负责路由注册、中间件管理和请求分发。 +// 它实现了 http.Handler 接口,可以直接用于 http.ListenAndServe。 type Engine struct { methodTrees methodTrees // 存储所有HTTP方法的路由树 - pool sync.Pool // Context Pool 用于复用 Context 对象,提高性能 + pool sync.Pool // Context Pool 用于复用 Context 对象,提高性能。 - globalHandlers HandlersChain // 全局中间件,应用于所有路由 + globalHandlers HandlersChain // 全局中间件,应用于所有路由。 - maxParams uint16 // 记录所有路由中最大的参数数量,用于优化 Params 切片的分配 + maxParams uint16 // 记录所有路由中最大的参数数量,用于优化 Params 切片的分配。 - // 可配置项,用于控制框架行为,参考 Gin + // 可配置项,用于控制框架行为,参考 Gin RedirectTrailingSlash bool // 是否自动重定向带尾部斜杠的路径到不带尾部斜杠的路径 (e.g. /foo/ -> /foo) RedirectFixedPath bool // 是否自动修复路径中的大小写错误 (e.g. /Foo -> /foo) HandleMethodNotAllowed bool // 是否启用 MethodNotAllowed 处理器 ForwardByClientIP bool // 是否信任 X-Forwarded-For 等头部获取客户端 IP - RemoteIPHeaders []string // 用于获取客户端 IP 的头部列表,例如 {"X-Forwarded-For", "X-Real-IP"} - // TrustedProxies []string // 可信代理 IP 列表,用于判断是否使用 X-Forwarded-For 等头部 (预留接口) + RemoteIPHeaders []string // 用于获取客户端 IP 的头部列表,例如 {"X-Forwarded-For", "X-Real-IP"} + // TrustedProxies []string // 可信代理 IP 列表,用于判断是否使用 X-Forwarded-For 等头部 (预留接口) - HTTPClient *httpc.Client // 用于在此上下文中执行出站 HTTP 请求 + HTTPClient *httpc.Client // 用于在此上下文中执行出站 HTTP 请求。 - LogReco *reco.Logger - - HTMLRender interface{} // 用于 HTML 模板渲染,可以设置为 *template.Template 或自定义渲染器接口 + HTMLRender interface{} // 用于 HTML 模板渲染,可以设置为 *template.Template 或自定义渲染器接口 routesInfo []RouteInfo // 存储所有注册的路由信息 errorHandle ErrorHandle // 错误处理 - noRoute HandlerFunc // NoRoute 处理器 - noRoutes HandlersChain // NoRoutes 处理器链 (如果 noRoute 未设置,则使用此链) + noRoute HandlerFunc - unMatchFS UnMatchFS // 未匹配下的处理 - UnMatchFSRoutes HandlersChain // UnMatch 处理器链, 用于扩展自由度, 在此局部链上, unMatchFS相关处理会在最后 + unMatchFS UnMatchFS // 未匹配下的处理 serverProtocols *http.Protocols //服务协议 Protocols ProtocolsConfig //协议版本配置 useDefaultProtocols bool //是否使用默认协议 - - // ServerConfigurator 允许在服务器启动前对其进行自定义配置 - // 例如,设置 ReadTimeout, WriteTimeout 等 - ServerConfigurator func(*http.Server) - - // TLSServerConfigurator 允许在 HTTPS 服务器启动前进行自定义配置 - // 如果设置了此回调,它将优先于 ServerConfigurator 被用于 HTTPS 服务器 - // 如果未设置,HTTPS 服务器将回退使用 ServerConfigurator (如果已设置) - TLSServerConfigurator func(*http.Server) - - // GlobalMaxRequestBodySize 全局请求体Body大小限制 - GlobalMaxRequestBodySize int64 -} - -// HandleFunc 注册一个或多个 HTTP 方法的路由 -// methods 参数是一个字符串切片,包含要注册的 HTTP 方法(例如 []string{"GET", "POST"}) -// relativePath 是相对于当前组或 Engine 的路径 -// handlers 是处理函数链 -func (engine *Engine) HandleFunc(methods []string, relativePath string, handlers ...HandlerFunc) { - for _, method := range methods { - if _, ok := MethodsSet[method]; !ok { - panic("invalid method: " + method) - } - engine.Handle(method, relativePath, handlers...) - } -} - -// HandleFunc 注册一个或多个 HTTP 方法的路由 -// methods 参数是一个字符串切片,包含要注册的 HTTP 方法(例如 []string{"GET", "POST"}) -// relativePath 是相对于当前组或 Engine 的路径 -// handlers 是处理函数链 -func (group *RouterGroup) HandleFunc(methods []string, relativePath string, handlers ...HandlerFunc) { - for _, method := range methods { - if _, ok := MethodsSet[method]; !ok { - panic("invalid method: " + method) - } - group.Handle(method, relativePath, handlers...) - } } type ErrorHandle struct { @@ -111,27 +64,19 @@ type ErrorHandle struct { handler ErrorHandler } -type ErrorHandler func(c *Context, code int, err error) +type ErrorHandler func(c *Context, code int) // defaultErrorHandle 默认错误处理 -func defaultErrorHandle(c *Context, code int, err error) { // 检查客户端是否已断开连接 +func defaultErrorHandle(c *Context, code int) { // 检查客户端是否已断开连接 select { case <-c.Request.Context().Done(): return default: - if c.Writer.Written() { - return - } // 输出json 状态码与状态码对应描述 - var errMsg string - if err != nil { - errMsg = err.Error() - } c.JSON(code, H{ "code": code, "message": http.StatusText(code), - "error": errMsg, }) c.Writer.Flush() c.Abort() @@ -139,37 +84,6 @@ func defaultErrorHandle(c *Context, code int, err error) { // 检查客户端是 } } -// 默认errorhandle包装 避免竞争意外问题, 保证稳定性 -func defaultErrorWarp(handler ErrorHandler) ErrorHandler { - return func(c *Context, code int, err error) { - select { - case <-c.Request.Context().Done(): - return - default: - if c.Writer.Written() { - c.Debugf("errpage: response already started for status %d, skipping error page rendering, err: %v", code, err) - return - } - } - // 查看context内有没有收集到error - if len(c.Errors) > 0 { - c.Errorf("errpage: context errors: %v, current error: %v", errors.Join(c.Errors...), err) - if err == nil { - err = errors.Join(c.Errors...) - } - } - // 如果客户端已经断开连接,则不尝试写入响应 - // 避免在客户端已关闭连接后写入响应导致的问题 - // 检查 context.Context 是否已取消 - if errors.Is(c.Request.Context().Err(), context.Canceled) { - c.Debugf("errpage: client disconnected, skipping error page rendering for status %d, err: %v", code, err) - return - } - - handler(c, code, err) - } -} - type UnMatchFS struct { FSForUnmatched http.FileSystem ServeUnmatchedAsFS bool @@ -182,7 +96,7 @@ type ProtocolsConfig struct { Http2_Cleartext bool // 是否启用 H2C } -// New 创建并返回一个 Engine 实例 +// New 创建并返回一个 Engine 实例。 func New() *Engine { engine := &Engine{ methodTrees: make(methodTrees, 0, 9), // 常见的HTTP方法有9个 (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, CONNECT, TRACE) @@ -201,25 +115,19 @@ func New() *Engine { unMatchFS: UnMatchFS{ ServeUnmatchedAsFS: false, }, - noRoute: nil, - noRoutes: make(HandlersChain, 0), - ServerConfigurator: nil, - TLSServerConfigurator: nil, - GlobalMaxRequestBodySize: -1, } //engine.SetProtocols(GetDefaultProtocolsConfig()) engine.SetDefaultProtocols() - engine.SetLoggerCfg(defaultLogRecoConfig) - // 初始化 Context Pool,为每个新 Context 实例提供一个构造函数 + // 初始化 Context Pool,为每个新 Context 实例提供一个构造函数 engine.pool.New = func() interface{} { return &Context{ - Writer: newResponseWriter(nil), // 初始时可以传入nil,在ServeHTTP中会重新设置实际的 http.ResponseWriter + Writer: newResponseWriter(nil), // 初始时可以传入nil,在ServeHTTP中会重新设置实际的 http.ResponseWriter Params: make(Params, 0, engine.maxParams), // 预分配 Params 切片以减少内存分配 Keys: make(map[string]interface{}), Errors: make([]error, 0), - ctx: context.Background(), // 初始上下文,后续会被请求的 Context 覆盖 + ctx: context.Background(), // 初始上下文,后续会被请求的 Context 覆盖 HTTPClient: engine.HTTPClient, - engine: engine, // Context 持有 Engine 引用,方便访问 Engine 的配置 + engine: engine, // Context 持有 Engine 引用,方便访问 Engine 的配置 } } @@ -235,48 +143,10 @@ func Default() *Engine { // === 外部操作方法 === -// SetServerConfigurator 设置一个函数,该函数将在任何 HTTP 或 HTTPS 服务器 -// (通过 RunShutdown, RunTLS, RunTLSRedir) 启动前被调用, -// 允许用户对底层的 *http.Server 实例进行自定义配置 -func (engine *Engine) SetServerConfigurator(fn func(*http.Server)) { - engine.ServerConfigurator = fn -} - -// SetTLSServerConfigurator 设置一个函数,该函数将专门用于配置 HTTPS 服务器 -// 如果设置了此函数,它将覆盖通用的 ServerConfigurator -func (engine *Engine) SetTLSServerConfigurator(fn func(*http.Server)) { - engine.TLSServerConfigurator = fn -} - -// 是否开启末尾slash重定向 -func (engine *Engine) SetRedirectTrailingSlash(enable bool) { - engine.RedirectTrailingSlash = enable -} - -// 是否开启固定路径重定向 -func (engine *Engine) SetRedirectFixedPath(enable bool) { - engine.RedirectFixedPath = enable -} - -// 是否开启MethodNotAllowed -func (engine *Engine) SetHandleMethodNotAllowed(enable bool) { - engine.HandleMethodNotAllowed = enable -} - -// SetLogger传入实例 -func (engine *Engine) SetLogger(logger *reco.Logger) { - engine.LogReco = logger -} - -// 配置日志LoggerCfg -func (engine *Engine) SetLoggerCfg(logcfg reco.Config) { - engine.LogReco = NewLogger(logcfg) -} - // 设置自定义错误处理 func (engine *Engine) SetErrorHandler(handler ErrorHandler) { engine.errorHandle.useDefault = false - engine.errorHandle.handler = defaultErrorWarp(handler) + engine.errorHandle.handler = handler } // 获取一个默认错误处理handle @@ -284,22 +154,13 @@ func (engine *Engine) GetDefaultErrHandler() ErrorHandler { return defaultErrorHandle } -func (engine *Engine) SetUnMatchFS(fs http.FileSystem, handlers ...HandlerFunc) { - engine.SetUnMatchFSChain(fs, handlers...) -} - -func (engine *Engine) SetUnMatchFSChain(fs http.FileSystem, handlers ...HandlerFunc) { +// 传入并配置unMatchFS +func (engine *Engine) SetUnMatchFS(fs http.FileSystem) { if fs != nil { engine.unMatchFS.FSForUnmatched = fs engine.unMatchFS.ServeUnmatchedAsFS = true - unMatchFileServer := GetStaticFSHandleFunc(http.FileServer(fs)) - combinedChain := make(HandlersChain, len(handlers)+1) - copy(combinedChain, handlers) - combinedChain[len(handlers)] = unMatchFileServer - engine.UnMatchFSRoutes = combinedChain } else { engine.unMatchFS.ServeUnmatchedAsFS = false - engine.UnMatchFSRoutes = nil } } @@ -332,37 +193,32 @@ func (engine *Engine) SetProtocols(config *ProtocolsConfig) { engine.useDefaultProtocols = false } -// 配置全局Req Body大小限制 -func (engine *Engine) SetGlobalMaxRequestBodySize(size int64) { - engine.GlobalMaxRequestBodySize = size -} - // 配置Req IP来源 Headers func (engine *Engine) SetRemoteIPHeaders(headers []string) { engine.RemoteIPHeaders = headers } -// SetForwardByClientIP 设置是否信任 X-Forwarded-For 等头部获取客户端 IP +// SetForwardByClientIP 设置是否信任 X-Forwarded-For 等头部获取客户端 IP。 func (engine *Engine) SetForwardByClientIP(enable bool) { engine.ForwardByClientIP = enable } -// SetHTTPClient 设置 Engine 使用的 httpc.Client +// SetHTTPClient 设置 Engine 使用的 httpc.Client。 func (engine *Engine) SetHTTPClient(client *httpc.Client) { if client != nil { engine.HTTPClient = client } } -// registerMethodTree 内部方法,用于获取或注册对应 HTTP 方法的路由树根节点 -// 如果该方法没有对应的树,则创建一个新的树 +// registerMethodTree 内部方法,用于获取或注册对应 HTTP 方法的路由树根节点。 +// 如果该方法没有对应的树,则创建一个新的树。 func (engine *Engine) registerMethodTree(method string) *node { for _, tree := range engine.methodTrees { if tree.method == method { return tree.root } } - // 如果没有找到,则创建一个新的方法树并添加到列表中 + // 如果没有找到,则创建一个新的方法树并添加到列表中 root := &node{ nType: root, // 根节点类型 fullPath: "/", // 根路径 @@ -371,9 +227,9 @@ func (engine *Engine) registerMethodTree(method string) *node { return root } -// addRoute 将一个路由及处理函数链添加到路由树中 -// 这是框架内部路由注册的核心逻辑 -// groupPath 用于记录路由所属的分组路径 +// addRoute 将一个路由及处理函数链添加到路由树中。 +// 这是框架内部路由注册的核心逻辑。 +// groupPath 用于记录路由所属的分组路径。 func (engine *Engine) addRoute(method, absolutePath, groupPath string, handlers HandlersChain) { // relativePath 更名为 absolutePath if absolutePath == "" { panic("absolute path must not be empty") @@ -382,7 +238,7 @@ func (engine *Engine) addRoute(method, absolutePath, groupPath string, handlers panic("handlers must not be empty") } - // 检查并更新 maxParams,使用 absolutePath + // 检查并更新 maxParams,使用 absolutePath if n := countParams(absolutePath); n > engine.maxParams { engine.maxParams = n } @@ -403,10 +259,10 @@ func (engine *Engine) addRoute(method, absolutePath, groupPath string, handlers }) } -// getHandlerName 辅助函数,用于获取 HandlerFunc 的名称 -// 注意:这只是一个简单的反射实现,对于匿名函数或闭包,可能返回不可读的名称 +// getHandlerName 辅助函数,用于获取 HandlerFunc 的名称。 +// 注意:这只是一个简单的反射实现,对于匿名函数或闭包,可能返回不可读的名称。 func getHandlerName(h HandlerFunc) string { - //return reflect.TypeOf(h).Name() // 对于具名函数,返回函数名对于匿名函数,可能返回空字符串或类似 func123 这样的名称 + //return reflect.TypeOf(h).Name() // 对于具名函数,返回函数名。对于匿名函数,可能返回空字符串或类似 func123 这样的名称。 // 更精确的获取函数名需要 import "runtime" // pc := reflect.ValueOf(h).Pointer() // f := runtime.FuncForPC(pc) @@ -421,39 +277,198 @@ func getHandlerName(h HandlerFunc) string { } -const MaxSkippedNodesCap = 256 +// ServeHTTP 实现了 http.Handler 接口,是 Engine 处理所有 HTTP 请求的入口。 +// 每个传入的 HTTP 请求都会调用此方法。 +func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { + // 从 Context Pool 中获取一个 Context 对象进行复用 + c := engine.pool.Get().(*Context) + c.reset(w, req) // 重置 Context 对象的状态以适应当前请求 -// TempSkippedNodesPool 存储 *[]skippedNode 以复用内存 -var TempSkippedNodesPool = sync.Pool{ - New: func() any { - // 返回一个指向容量为 256 的新切片的指针 - s := make([]skippedNode, 0, MaxSkippedNodesCap) - return &s - }, + // 执行请求处理 + engine.handleRequest(c) + + // 将 Context 对象放回 Context Pool,以供下次复用 + engine.pool.Put(c) } -// GetTempSkippedNodes 从 Pool 中获取一个 *[]skippedNode 指针 -func GetTempSkippedNodes() *[]skippedNode { - // 直接返回 Pool 中存储的指针 - return TempSkippedNodesPool.Get().(*[]skippedNode) +// handleRequest 负责根据请求查找路由并执行相应的处理函数链。 +// 这是路由查找和执行的核心逻辑。 +func (engine *Engine) handleRequest(c *Context) { + httpMethod := c.Request.Method + requestPath := c.Request.URL.Path + + // 查找对应的路由树的根节点 + rootNode := engine.methodTrees.get(httpMethod) // 这里获取到的 rootNode 已经是 *node 类型 + if rootNode != nil { + // 查找匹配的节点和处理函数 + // 这里传递 &c.Params 而不是重新创建,以利用 Context 中预分配的容量 + // skippedNodes 内部使用,因此无需从外部传入已分配的 slice + var skippedNodes []skippedNode // 用于回溯的跳过节点 + // 直接在 rootNode 上调用 getValue 方法 + value := rootNode.getValue(requestPath, &c.Params, &skippedNodes, true) // unescape=true 对路径参数进行 URL 解码 + + if value.handlers != nil { + //c.handlers = engine.combineHandlers(engine.globalHandlers, value.handlers) // 组合全局中间件和路由处理函数 + c.handlers = value.handlers + c.Next() // 执行处理函数链 + c.Writer.Flush() // 确保所有缓冲的响应数据被发送 + return + } + + // 如果没有找到处理函数,检查是否需要重定向(尾部斜杠或大小写修复) + if httpMethod != http.MethodConnect && requestPath != "/" { // CONNECT 方法和根路径不进行重定向 + if value.tsr && engine.RedirectTrailingSlash { + // 尾部斜杠重定向:/foo/ -> /foo 或 /foo -> /foo/ + redirectPath := requestPath + if len(requestPath) > 0 && requestPath[len(requestPath)-1] == '/' { + redirectPath = requestPath[:len(requestPath)-1] + } else { + redirectPath = requestPath + "/" + } + c.Redirect(http.StatusMovedPermanently, redirectPath) // 301 永久重定向 + return + } + // 尝试不区分大小写的查找 + // 直接在 rootNode 上调用 findCaseInsensitivePath 方法 + ciPath, found := rootNode.findCaseInsensitivePath(requestPath, engine.RedirectTrailingSlash) + if found && engine.RedirectFixedPath { + c.Redirect(http.StatusMovedPermanently, BytesToString(ciPath)) // 301 永久重定向到修正后的路径 + return + } + } + } + /* + // 如果没有找到路由,且启用了 MethodNotAllowed 处理 + if engine.HandleMethodNotAllowed { + // 是否是OPTIONS方式 + if httpMethod == http.MethodOptions { + // 如果是 OPTIONS 请求,尝试查找所有允许的方法 + allowedMethods := []string{} + for _, treeIter := range engine.methodTrees { + var tempSkippedNodes []skippedNode + // 注意这里 treeIter.root 才是正确的,因为 treeIter 是 methodTree 类型 + value := treeIter.root.getValue(requestPath, nil, &tempSkippedNodes, false) + if value.handlers != nil { + allowedMethods = append(allowedMethods, treeIter.method) + } + } + if len(allowedMethods) > 0 { + // 如果找到了允许的方法,返回 200 OK 并设置 Allow 头部 + c.Writer.Header().Set("Allow", strings.Join(allowedMethods, ", ")) + c.Status(http.StatusOK) + return + } + } + // 尝试遍历所有方法树,看是否有其他方法可以匹配当前路径 + for _, treeIter := range engine.methodTrees { + if treeIter.method == httpMethod { // 已经处理过当前方法,跳过 + continue + } + var tempSkippedNodes []skippedNode // 用于临时查找,不影响主 Context + // 注意这里 treeIter.root 才是正确的,因为 treeIter 是 methodTree 类型 + value := treeIter.root.getValue(requestPath, nil, &tempSkippedNodes, false) // 只查找是否存在,不需要参数 + if value.handlers != nil { + // 使用定义的ErrorHandle处理 + engine.errorHandle.handler(c, http.StatusMethodNotAllowed) + return + } + } + } + + // 是否开启了UnMatchFS + if engine.unMatchFS.ServeUnmatchedAsFS { + // 若不是GET HEAD OPTIONS则返回405 + if c.Request.Method == http.MethodGet || c.Request.Method == http.MethodHead { + // 使用 http.FileServer 处理未匹配的请求 + fileServer := http.FileServer(engine.unMatchFS.FSForUnmatched) + //ecw := newErrorCapturingResponseWriter(c, c.engine.errorHandle.handler) + ecw := AcquireErrorCapturingResponseWriter(c, c.engine.errorHandle.handler) + defer ReleaseErrorCapturingResponseWriter(ecw) + fileServer.ServeHTTP(ecw, c.Request) + ecw.processAfterFileServer() + return + } else { + log.Printf("Not Allowed Method: %s", c.Request.Method) + // 若为OPTIONS + if c.Request.Method == http.MethodOptions { + //返回allow get + c.Writer.Header().Set("Allow", "GET") + c.Status(http.StatusOK) + c.Abort() + return + } else { + engine.errorHandle.handler(c, http.StatusMethodNotAllowed) + return + } + } + + } else { + engine.errorHandle.handler(c, http.StatusNotFound) + return + } + */ + + // 构建处理链 + // 组合全局中间件和路由处理函数 + handlers := engine.globalHandlers + + // 如果启用了 MethodNotAllowed 处理,并且没有找到精确匹配的路由 + // 则在全局中间件之后添加 MethodNotAllowed 处理器 + if engine.HandleMethodNotAllowed { + handlers = append(handlers, MethodNotAllowed()) + } + + // 如果启用了 UnMatchFS 处理,并且没有找到精确匹配的路由和 MethodNotAllowed + // 则在处理链的最后添加 UnMatchFS 处理器 + if engine.unMatchFS.ServeUnmatchedAsFS { + handlers = append(handlers, unMatchFSHandle()) + } + + // 如果用户设置了 NoRoute 处理器,且没有匹配到任何路由、MethodNotAllowed 或 UnMatchFS + // 则在处理链的最后添加 NoRoute 处理器 + if engine.noRoute != nil { + handlers = append(handlers, engine.noRoute) + } + + handlers = append(handlers, NotFound()) + + c.handlers = handlers + c.Next() // 执行处理函数链 + c.Writer.Flush() // 确保所有缓冲的响应数据被发送 + } -// PutTempSkippedNodes 将用完的 *[]skippedNode 指针放回 Pool -func PutTempSkippedNodes(skippedNodes *[]skippedNode) { - if skippedNodes == nil || *skippedNodes == nil { - return +// UnMatchFS HandleFunc +func unMatchFSHandle() HandlerFunc { + return func(c *Context) { + engine := c.engine + if c.Request.Method == http.MethodGet || c.Request.Method == http.MethodHead { + // 使用 http.FileServer 处理未匹配的请求 + fileServer := http.FileServer(engine.unMatchFS.FSForUnmatched) + //ecw := newErrorCapturingResponseWriter(c, c.engine.errorHandle.handler) + ecw := AcquireErrorCapturingResponseWriter(c, c.engine.errorHandle.handler) + defer ReleaseErrorCapturingResponseWriter(ecw) + fileServer.ServeHTTP(ecw, c.Request) + ecw.processAfterFileServer() + return + } else { + if engine.noRoute == nil { + // 若为OPTIONS + if c.Request.Method == http.MethodOptions { + //返回allow get + c.Writer.Header().Set("Allow", "GET") + c.Status(http.StatusOK) + c.Abort() + return + } else { + engine.errorHandle.handler(c, http.StatusMethodNotAllowed) + return + } + } else { + c.Next() + } + } } - - // 检查容量是否符合预期。如果容量不足,则丢弃,不放回 Pool。 - if cap(*skippedNodes) < MaxSkippedNodesCap { - return // 丢弃该对象,让 Pool 在下次 Get 时通过 New 重新分配 - } - - // 长度重置为 0,保留容量,实现复用 - *skippedNodes = (*skippedNodes)[:0] - - // 将指针存回 Pool - TempSkippedNodesPool.Put(skippedNodes) } // 405中间件 @@ -464,36 +479,34 @@ func MethodNotAllowed() HandlerFunc { engine := c.engine // 是否是OPTIONS方式 if httpMethod == http.MethodOptions { - // 如果是 OPTIONS 请求,尝试查找所有允许的方法 + // 如果是 OPTIONS 请求,尝试查找所有允许的方法 allowedMethods := []string{} for _, treeIter := range engine.methodTrees { - // 注意这里 treeIter.root 才是正确的,因为 treeIter 是 methodTree 类型 - tempSkippedNodes := GetTempSkippedNodes() - value := treeIter.root.getValue(requestPath, nil, tempSkippedNodes, false) - PutTempSkippedNodes(tempSkippedNodes) + var tempSkippedNodes []skippedNode + // 注意这里 treeIter.root 才是正确的,因为 treeIter 是 methodTree 类型 + value := treeIter.root.getValue(requestPath, nil, &tempSkippedNodes, false) if value.handlers != nil { allowedMethods = append(allowedMethods, treeIter.method) } } if len(allowedMethods) > 0 { - // 如果找到了允许的方法,返回 200 OK 并设置 Allow 头部 + // 如果找到了允许的方法,返回 200 OK 并设置 Allow 头部 c.Writer.Header().Set("Allow", strings.Join(allowedMethods, ", ")) c.Status(http.StatusOK) return } } - // 尝试遍历所有方法树,看是否有其他方法可以匹配当前路径 + // 尝试遍历所有方法树,看是否有其他方法可以匹配当前路径 for _, treeIter := range engine.methodTrees { - if treeIter.method == httpMethod { // 已经处理过当前方法,跳过 + if treeIter.method == httpMethod { // 已经处理过当前方法,跳过 continue } - // 注意这里 treeIter.root 才是正确的,因为 treeIter 是 methodTree 类型 - tempSkippedNodes := GetTempSkippedNodes() - value := treeIter.root.getValue(requestPath, nil, tempSkippedNodes, false) // 只查找是否存在,不需要参数 - PutTempSkippedNodes(tempSkippedNodes) + var tempSkippedNodes []skippedNode // 用于临时查找,不影响主 Context + // 注意这里 treeIter.root 才是正确的,因为 treeIter 是 methodTree 类型 + value := treeIter.root.getValue(requestPath, nil, &tempSkippedNodes, false) // 只查找是否存在,不需要参数 if value.handlers != nil { // 使用定义的ErrorHandle处理 - engine.errorHandle.handler(c, http.StatusMethodNotAllowed, errors.New("method not allowed")) + engine.errorHandle.handler(c, http.StatusMethodNotAllowed) return } } @@ -504,24 +517,18 @@ func MethodNotAllowed() HandlerFunc { func NotFound() HandlerFunc { return func(c *Context) { engine := c.engine - engine.errorHandle.handler(c, http.StatusNotFound, errors.New("not found")) + engine.errorHandle.handler(c, http.StatusNotFound) + return } } // 传入并设置NoRoute (这不是最后一个处理, 你仍可以next到默认的404处理) func (Engine *Engine) NoRoute(handler HandlerFunc) { Engine.noRoute = handler - Engine.noRoutes = nil } -// 传入并设置NoRoutes (这不是最后一个处理, 你仍可以next到默认的404处理) -func (Engine *Engine) NoRoutes(handlerFuncs ...HandlerFunc) { - Engine.noRoute = nil - Engine.noRoutes = handlerFuncs -} - -// combineHandlers 组合多个处理函数链为一个 -// 这是构建完整处理链(全局中间件 + 组中间件 + 路由处理函数)的关键 +// combineHandlers 组合多个处理函数链为一个。 +// 这是构建完整处理链(全局中间件 + 组中间件 + 路由处理函数)的关键。 func (engine *Engine) combineHandlers(h1 HandlersChain, h2 HandlersChain) HandlersChain { finalSize := len(h1) + len(h2) mergedHandlers := make(HandlersChain, finalSize) @@ -530,59 +537,58 @@ func (engine *Engine) combineHandlers(h1 HandlersChain, h2 HandlersChain) Handle return mergedHandlers } -// Use 将全局中间件添加到 Engine -// 这些中间件将应用于所有注册的路由 +// Use 将全局中间件添加到 Engine。 +// 这些中间件将应用于所有注册的路由。 func (engine *Engine) Use(middleware ...HandlerFunc) IRouter { engine.globalHandlers = append(engine.globalHandlers, middleware...) return engine } -// Handle 注册通用 HTTP 方法的路由 -// 这是所有具体 HTTP 方法注册的基础方法 +// Handle 注册通用 HTTP 方法的路由。 +// 这是所有具体 HTTP 方法注册的基础方法。 func (engine *Engine) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) { - //absolutePath := path.Join("/", relativePath) // 修正:统一使用 path.Join 进行路径拼接 - absolutePath := resolveRoutePath("/", relativePath) + absolutePath := path.Join("/", relativePath) // 修正:统一使用 path.Join 进行路径拼接 // 修正:将全局中间件与此路由的处理函数合并 fullHandlers := engine.combineHandlers(engine.globalHandlers, handlers) engine.addRoute(httpMethod, absolutePath, "/", fullHandlers) } -// GET 注册 GET 方法的路由 +// GET 注册 GET 方法的路由。 func (engine *Engine) GET(relativePath string, handlers ...HandlerFunc) { engine.Handle(http.MethodGet, relativePath, handlers...) } -// POST 注册 POST 方法的路由 +// POST 注册 POST 方法的路由。 func (engine *Engine) POST(relativePath string, handlers ...HandlerFunc) { engine.Handle(http.MethodPost, relativePath, handlers...) } -// PUT 注册 PUT 方法的路由 +// PUT 注册 PUT 方法的路由。 func (engine *Engine) PUT(relativePath string, handlers ...HandlerFunc) { engine.Handle(http.MethodPut, relativePath, handlers...) } -// DELETE 注册 DELETE 方法的路由 +// DELETE 注册 DELETE 方法的路由。 func (engine *Engine) DELETE(relativePath string, handlers ...HandlerFunc) { engine.Handle(http.MethodDelete, relativePath, handlers...) } -// PATCH 注册 PATCH 方法的路由 +// PATCH 注册 PATCH 方法的路由。 func (engine *Engine) PATCH(relativePath string, handlers ...HandlerFunc) { engine.Handle(http.MethodPatch, relativePath, handlers...) } -// HEAD 注册 HEAD 方法的路由 +// HEAD 注册 HEAD 方法的路由。 func (engine *Engine) HEAD(relativePath string, handlers ...HandlerFunc) { engine.Handle(http.MethodHead, relativePath, handlers...) } -// OPTIONS 注册 OPTIONS 方法的路由 +// OPTIONS 注册 OPTIONS 方法的路由。 func (engine *Engine) OPTIONS(relativePath string, handlers ...HandlerFunc) { engine.Handle(http.MethodOptions, relativePath, handlers...) } -// ANY 注册所有常见 HTTP 方法的路由 +// ANY 注册所有常见 HTTP 方法的路由。 func (engine *Engine) ANY(relativePath string, handlers ...HandlerFunc) { engine.Handle(http.MethodGet, relativePath, handlers...) engine.Handle(http.MethodPost, relativePath, handlers...) @@ -593,45 +599,45 @@ func (engine *Engine) ANY(relativePath string, handlers ...HandlerFunc) { engine.Handle(http.MethodOptions, relativePath, handlers...) } -// GetRouterInfo 返回所有已注册的路由信息 +// GetRouterInfo 返回所有已注册的路由信息。 func (engine *Engine) GetRouterInfo() []RouteInfo { return engine.routesInfo } -// Group 创建一个新的路由组 -// 路由组允许将具有相同前缀路径和/或共享中间件的路由组织在一起 +// Group 创建一个新的路由组。 +// 路由组允许将具有相同前缀路径和/或共享中间件的路由组织在一起。 func (engine *Engine) Group(relativePath string, handlers ...HandlerFunc) IRouter { return &RouterGroup{ Handlers: engine.combineHandlers(engine.globalHandlers, handlers), // 继承全局中间件 - basePath: resolveRoutePath("/", relativePath), + basePath: path.Join("/", relativePath), engine: engine, // 指向 Engine 实例 } } -// RouterGroup 表示一个路由分组,可以添加组特定的中间件和路由 -// 它也实现了 IRouter 接口,允许嵌套分组 +// RouterGroup 表示一个路由分组,可以添加组特定的中间件和路由。 +// 它也实现了 IRouter 接口,允许嵌套分组。 type RouterGroup struct { - Handlers HandlersChain // 组中间件,仅应用于当前组及其子组的路由 + Handlers HandlersChain // 组中间件,仅应用于当前组及其子组的路由 basePath string // 组路径前缀 - engine *Engine // 指向 Engine 实例,用于注册路由到全局路由树 + engine *Engine // 指向 Engine 实例,用于注册路由到全局路由树 } -// Use 将中间件应用于当前路由组 -// 这些中间件将应用于当前组及其子组的所有路由 +// Use 将中间件应用于当前路由组。 +// 这些中间件将应用于当前组及其子组的所有路由。 func (group *RouterGroup) Use(middleware ...HandlerFunc) IRouter { group.Handlers = append(group.Handlers, middleware...) return group } -// Handle 注册通用 HTTP 方法的路由到当前组 -// 路径是相对于当前组的 basePath +// Handle 注册通用 HTTP 方法的路由到当前组。 +// 路径是相对于当前组的 basePath。 func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) { - absolutePath := resolveRoutePath(group.basePath, relativePath) + absolutePath := path.Join(group.basePath, relativePath) fullHandlers := group.engine.combineHandlers(group.Handlers, handlers) group.engine.addRoute(httpMethod, absolutePath, group.basePath, fullHandlers) } -// GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, ANY 方法与 Engine 类似,只是通过 Group 的 Handle 方法注册 +// GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, ANY 方法与 Engine 类似,只是通过 Group 的 Handle 方法注册。 func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) { group.Handle(http.MethodGet, relativePath, handlers...) } @@ -663,106 +669,11 @@ func (group *RouterGroup) ANY(relativePath string, handlers ...HandlerFunc) { group.Handle(http.MethodOptions, relativePath, handlers...) } -// Group 为当前组创建一个新的子组 +// Group 为当前组创建一个新的子组。 func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) IRouter { return &RouterGroup{ Handlers: group.engine.combineHandlers(group.Handlers, handlers), - basePath: resolveRoutePath(group.basePath, relativePath), + basePath: path.Join(group.basePath, relativePath), engine: group.engine, // 指向 Engine 实例 } } - -// ServeHTTP 实现了 http.Handler 接口,是 Engine 处理所有 HTTP 请求的入口 -// 每个传入的 HTTP 请求都会调用此方法 -func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { - // 从 Context Pool 中获取一个 Context 对象进行复用 - c := engine.pool.Get().(*Context) - c.reset(w, req) // 重置 Context 对象的状态以适应当前请求 - - // 执行请求处理 - engine.handleRequest(c) - - // 将 Context 对象放回 Context Pool,以供下次复用 - engine.pool.Put(c) -} - -// handleRequest 负责根据请求查找路由并执行相应的处理函数链 -// 这是路由查找和执行的核心逻辑 -func (engine *Engine) handleRequest(c *Context) { - httpMethod := c.Request.Method - requestPath := c.Request.URL.Path - - // 查找对应的路由树的根节点 - rootNode := engine.methodTrees.get(httpMethod) // 这里获取到的 rootNode 已经是 *node 类型 - if rootNode != nil { - // 查找匹配的节点和处理函数 - // 这里传递 &c.Params 而不是重新创建,以利用 Context 中预分配的容量 - // skippedNodes 内部使用,因此无需从外部传入已分配的 slice - // 直接在 rootNode 上调用 getValue 方法 - value := rootNode.getValue(requestPath, &c.Params, &c.SkippedNodes, true) // unescape=true 对路径参数进行 URL 解码 - - if value.handlers != nil { - //c.handlers = engine.combineHandlers(engine.globalHandlers, value.handlers) // 组合全局中间件和路由处理函数 - c.handlers = value.handlers - c.Next() // 执行处理函数链 - //c.Writer.Flush() // 确保所有缓冲的响应数据被发送 - return - } - - // 如果没有找到处理函数,检查是否需要重定向(尾部斜杠或大小写修复) - if httpMethod != http.MethodConnect && requestPath != "/" { // CONNECT 方法和根路径不进行重定向 - if value.tsr && engine.RedirectTrailingSlash { - // 尾部斜杠重定向:/foo/ -> /foo 或 /foo -> /foo/ - redirectPath := requestPath - if len(requestPath) > 0 && requestPath[len(requestPath)-1] == '/' { - redirectPath = requestPath[:len(requestPath)-1] - } else { - redirectPath = requestPath + "/" - } - c.Redirect(http.StatusMovedPermanently, redirectPath) // 301 永久重定向 - return - } - // 尝试不区分大小写的查找 - // 直接在 rootNode 上调用 findCaseInsensitivePath 方法 - ciPath, found := rootNode.findCaseInsensitivePath(requestPath, engine.RedirectTrailingSlash) - if found && engine.RedirectFixedPath { - c.Redirect(http.StatusMovedPermanently, BytesToString(ciPath)) // 301 永久重定向到修正后的路径 - return - } - } - } - - // 构建处理链 - // 组合全局中间件和路由处理函数 - handlers := engine.globalHandlers - - // 如果启用了 MethodNotAllowed 处理,并且没有找到精确匹配的路由 - // 则在全局中间件之后添加 MethodNotAllowed 处理器 - if engine.HandleMethodNotAllowed { - handlers = append(handlers, MethodNotAllowed()) - } - - // 如果启用了 UnMatchFS 处理,并且没有找到精确匹配的路由和 MethodNotAllowed - // 则在处理链的最后添加 UnMatchFS 处理器 - if engine.unMatchFS.ServeUnmatchedAsFS { - /* - var unMatchFSHandle = c.engine.unMatchFileServer - handlers = append(handlers, unMatchFSHandle) - */ - handlers = append(handlers, engine.UnMatchFSRoutes...) - } - - // 如果用户设置了 NoRoute 处理器,且没有匹配到任何路由、MethodNotAllowed 或 UnMatchFS - // 则在处理链的最后添加 NoRoute 处理器 - if engine.noRoute != nil { - handlers = append(handlers, engine.noRoute) - } else if len(engine.noRoutes) > 0 { - handlers = append(handlers, engine.noRoutes...) - } - - handlers = append(handlers, NotFound()) - - c.handlers = handlers - c.Next() // 执行处理函数链 - //c.Writer.Flush() // 确保所有缓冲的响应数据被发送 -} diff --git a/fileserver.go b/fileserver.go deleted file mode 100644 index 1aa1aaf..0000000 --- a/fileserver.go +++ /dev/null @@ -1,286 +0,0 @@ -// 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/. -// Copyright 2024 WJQSERVER. All rights reserved. -// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization. -package touka - -import ( - "errors" - "net/http" - "path" - "strings" -) - -// === FileServer相关 === - -var allowedFileServerMethods = map[string]struct{}{ - http.MethodGet: {}, - http.MethodHead: {}, -} - -var ( - ErrInputFSisNil = errors.New("input FS is nil") - ErrMethodNotAllowed = errors.New("method not allowed") -) - -// FileServer方式, 返回一个HandleFunc, 统一化处理 -func FileServer(fs http.FileSystem) HandlerFunc { - if fs == nil { - return func(c *Context) { - c.ErrorUseHandle(http.StatusInternalServerError, ErrInputFSisNil) - } - } - - fileServerInstance := http.FileServer(fs) - return func(c *Context) { - FileServerHandleServe(c, fileServerInstance) - - // 中止处理链,因为 FileServer 已经处理了响应 - c.Abort() - } -} - -func FileServerHandleServe(c *Context, fsHandle http.Handler) { - if fsHandle == nil { - c.AddError(ErrInputFSisNil) - // 500 - c.ErrorUseHandle(http.StatusInternalServerError, ErrInputFSisNil) - return - } - - // 检查是否是 GET 或 HEAD 方法 - if _, ok := allowedFileServerMethods[c.Request.Method]; !ok { - // 如果不是,且启用了 MethodNotAllowed 处理,则继续到 MethodNotAllowed 中间件 - if c.engine.HandleMethodNotAllowed { - c.Next() - } else { - if c.engine.noRoute == nil { - if c.Request.Method == http.MethodOptions { - //返回allow get - c.Writer.Header().Set("Allow", "GET, HEAD") - c.Status(http.StatusOK) - c.Abort() - return - } else { - // 否则,返回 405 Method Not Allowed - c.engine.errorHandle.handler(c, http.StatusMethodNotAllowed, ErrMethodNotAllowed) - } - } else { - c.Next() - } - } - return - } - - // 使用自定义的 ResponseWriter 包装器来捕获 FileServer 可能返回的错误状态码 - ecw := AcquireErrorCapturingResponseWriter(c) - defer ReleaseErrorCapturingResponseWriter(ecw) - - // 调用 http.FileServer 处理请求 - fsHandle.ServeHTTP(ecw, c.Request) - - // 在 FileServer 处理完成后,检查是否捕获到错误状态码,并调用 ErrorHandler - ecw.processAfterFileServer() -} - -// StaticDir 传入一个文件夹路径, 使用FileServer进行处理 -// r.StaticDir("/test/*filepath", "/var/www/test") -func (engine *Engine) StaticDir(relativePath, rootPath string) { - // 清理路径 - relativePath = path.Clean(relativePath) - rootPath = path.Clean(rootPath) - - // 确保相对路径以 '/' 结尾,以便 FileServer 正确处理子路径 - if !strings.HasSuffix(relativePath, "/") { - relativePath += "/" - } - - // 创建一个文件系统处理器 - fileServer := http.FileServer(http.Dir(rootPath)) - - // 注册一个捕获所有路径的路由,使用自定义处理器 - // 注意:这里使用 ANY 方法,但 FileServer 通常只处理 GET 和 HEAD - // 我们可以通过在处理函数内部检查方法来限制 - engine.ANY(relativePath+"*filepath", GetStaticDirHandleFunc(fileServer)) -} - -// Group的StaticDir方式 -func (group *RouterGroup) StaticDir(relativePath, rootPath string) { - // 清理路径 - relativePath = path.Clean(relativePath) - rootPath = path.Clean(rootPath) - - // 确保相对路径以 '/' 结尾,以便 FileServer 正确处理子路径 - if !strings.HasSuffix(relativePath, "/") { - relativePath += "/" - } - - // 创建一个文件系统处理器 - fileServer := http.FileServer(http.Dir(rootPath)) - - // 注册一个捕获所有路径的路由,使用自定义处理器 - // 注意:这里使用 ANY 方法,但 FileServer 通常只处理 GET 和 HEAD - // 我们可以通过在处理函数内部检查方法来限制 - group.ANY(relativePath+"*filepath", GetStaticDirHandleFunc(fileServer)) -} - -// GetStaticDirHandleFunc -func (engine *Engine) GetStaticDirHandle(rootPath string) HandlerFunc { - // 清理路径 - rootPath = path.Clean(rootPath) - - // 创建一个文件系统处理器 - fileServer := http.FileServer(http.Dir(rootPath)) - - return GetStaticDirHandleFunc(fileServer) -} - -// GetStaticDirHandleFunc -func (group *RouterGroup) GetStaticDirHandle(rootPath string) HandlerFunc { // 清理路径 - return group.engine.GetStaticDirHandle(rootPath) -} - -// GetStaticDirHandle -func GetStaticDirHandleFunc(fsHandle http.Handler) HandlerFunc { - return func(c *Context) { - requestPath := c.Request.URL.Path - - // 获取捕获到的文件路径参数 - filepath := c.Param("filepath") - - // 构造文件服务器需要处理的请求路径 - c.Request.URL.Path = filepath - - FileServerHandleServe(c, fsHandle) - - // 恢复原始请求路径,以便后续中间件或日志记录使用 - c.Request.URL.Path = requestPath - - // 中止处理链,因为 FileServer 已经处理了响应 - c.Abort() - } -} - -// Static File 传入一个文件路径, 使用FileServer进行处理 -func (engine *Engine) StaticFile(relativePath, filePath string) { - // 清理路径 - relativePath = path.Clean(relativePath) - filePath = path.Clean(filePath) - - FileHandle := engine.GetStaticFileHandle(filePath) - - // 注册一个精确匹配的路由 - engine.GET(relativePath, FileHandle) - engine.HEAD(relativePath, FileHandle) - engine.OPTIONS(relativePath, FileHandle) - -} - -// Group的StaticFile -func (group *RouterGroup) StaticFile(relativePath, filePath string) { - // 清理路径 - relativePath = path.Clean(relativePath) - filePath = path.Clean(filePath) - - FileHandle := group.GetStaticFileHandle(filePath) - - // 注册一个精确匹配的路由 - group.GET(relativePath, FileHandle) - group.HEAD(relativePath, FileHandle) - group.OPTIONS(relativePath, FileHandle) -} - -// GetStaticFileHandleFunc -func (engine *Engine) GetStaticFileHandle(filePath string) HandlerFunc { - // 清理路径 - filePath = path.Clean(filePath) - - // 创建一个文件系统处理器,指向包含目标文件的目录 - dir := path.Dir(filePath) - fileName := path.Base(filePath) - fileServer := http.FileServer(http.Dir(dir)) - - return GetStaticFileHandleFunc(fileServer, fileName) -} - -// GetStaticFileHandleFunc -func (group *RouterGroup) GetStaticFileHandle(filePath string) HandlerFunc { - // 清理路径 - filePath = path.Clean(filePath) - - // 创建一个文件系统处理器,指向包含目标文件的目录 - dir := path.Dir(filePath) - fileName := path.Base(filePath) - fileServer := http.FileServer(http.Dir(dir)) - - return GetStaticFileHandleFunc(fileServer, fileName) -} - -// GetStaticFileHandleFunc -func GetStaticFileHandleFunc(fsHandle http.Handler, fileName string) HandlerFunc { - return func(c *Context) { - requestPath := c.Request.URL.Path - - // 构造文件服务器需要处理的请求路径 - c.Request.URL.Path = "/" + fileName - - FileServerHandleServe(c, fsHandle) - - // 恢复原始请求路径 - c.Request.URL.Path = requestPath - - // 中止处理链,因为 FileServer 已经处理了响应 - c.Abort() - } -} - -// StaticFS -func (engine *Engine) StaticFS(relativePath string, fs http.FileSystem) { - // 清理路径 - relativePath = path.Clean(relativePath) - - // 确保相对路径以 '/' 结尾,以便 FileServer 正确处理子路径 - if !strings.HasSuffix(relativePath, "/") { - relativePath += "/" - } - - fileServer := http.StripPrefix(relativePath, http.FileServer(fs)) - engine.ANY(relativePath+"*filepath", GetStaticFSHandleFunc(fileServer)) -} - -// Group的StaticFS -func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) { - // 清理路径 - relativePath = path.Clean(relativePath) - - // 确保相对路径以 '/' 结尾,以便 FileServer 正确处理子路径 - if !strings.HasSuffix(relativePath, "/") { - relativePath += "/" - } - - fileServer := http.StripPrefix(relativePath, http.FileServer(fs)) - group.ANY(relativePath+"*filepath", GetStaticFSHandleFunc(fileServer)) -} - -// GetStaticFSHandleFunc -func GetStaticFSHandleFunc(fsHandle http.Handler) HandlerFunc { - return func(c *Context) { - - FileServerHandleServe(c, fsHandle) - - // 中止处理链,因为 FileServer 已经处理了响应 - c.Abort() - } -} - -// GetStaticFSHandleFunc -func (engine *Engine) GetStaticFSHandle(fs http.FileSystem) HandlerFunc { - fileServer := http.FileServer(fs) - return GetStaticFSHandleFunc(fileServer) -} - -// GetStaticFSHandleFunc -func (group *RouterGroup) GetStaticFSHandle(fs http.FileSystem) HandlerFunc { - fileServer := http.FileServer(fs) - return GetStaticFSHandleFunc(fileServer) -} diff --git a/go.mod b/go.mod index f9d10a9..42be0b7 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,11 @@ -module github.com/infinite-iroha/touka +module github.com/WJQSERVER/touka -go 1.25.1 +go 1.24.3 require ( - github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2 - github.com/WJQSERVER-STUDIO/httpc v0.8.2 - github.com/WJQSERVER/wanf v0.0.3 - github.com/fenthope/reco v0.0.4 - github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e + github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 + github.com/WJQSERVER-STUDIO/httpc v0.5.1 + github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 ) -require ( - github.com/valyala/bytebufferpool v1.0.0 // indirect - golang.org/x/net v0.49.0 // indirect -) +require github.com/valyala/bytebufferpool v1.0.0 // indirect diff --git a/go.sum b/go.sum index b75fec4..6dfccbb 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,8 @@ -github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2 h1:AiIHXP21LpK7pFfqUlUstgQEWzjbekZgxOuvVwiMfyM= -github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2/go.mod h1:mCLqYU32bTmEE6dpj37MKKiZgz70Jh/xyK9vVbq6pok= -github.com/WJQSERVER-STUDIO/httpc v0.8.2 h1:PFPLodV0QAfGEP6915J57vIqoKu9cGuuiXG/7C9TNUk= -github.com/WJQSERVER-STUDIO/httpc v0.8.2/go.mod h1:8WhHVRO+olDFBSvL5PC/bdMkb6U3vRdPJ4p4pnguV5Y= -github.com/WJQSERVER/wanf v0.0.3 h1:OqhG7ETiR5Knqr0lmbb+iUMw9O7re2vEogjVf06QevM= -github.com/WJQSERVER/wanf v0.0.3/go.mod h1:q2Pyg+G+s1acMWxrbI4CwS/Yk76/BzLREEdZ8iFwUNE= -github.com/fenthope/reco v0.0.4 h1:yo2g3aWwdoMpaZWZX4SdZOW7mCK82RQIU/YI8ZUQThM= -github.com/fenthope/reco v0.0.4/go.mod h1:eMyS8HpdMVdJ/2WJt6Cvt8P1EH9Igzj5lSJrgc+0jeg= -github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU= -github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok= +github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 h1:JLtFd00AdFg/TP+dtvIzLkdHwKUGPOAijN1sMtEYoFg= +github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4/go.mod h1:FZ6XE+4TKy4MOfX1xWKe6Rwsg0ucYFCdNh1KLvyKTfc= +github.com/WJQSERVER-STUDIO/httpc v0.5.1 h1:+TKCPYBuj7PAHuiduGCGAqsHAa4QtsUfoVwRN777q64= +github.com/WJQSERVER-STUDIO/httpc v0.5.1/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= +github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 h1:o8UqXPI6SVwQt04RGsqKp3qqmbOfTNMqDrWsc4O47kk= +github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= diff --git a/licenses/httprouter-license b/licenses/httprouter-license deleted file mode 100644 index 3ab5aa6..0000000 --- a/licenses/httprouter-license +++ /dev/null @@ -1,29 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2013, Julien Schmidt -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 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. \ No newline at end of file diff --git a/logreco.go b/logreco.go deleted file mode 100644 index 4bda8d3..0000000 --- a/logreco.go +++ /dev/null @@ -1,46 +0,0 @@ -// 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/. -// Copyright 2024 WJQSERVER. All rights reserved. -// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization. -package touka - -import ( - "log" - "os" - "time" - - "github.com/fenthope/reco" -) - -// 默认LogReco配置 -var defaultLogRecoConfig = reco.Config{ - Level: reco.LevelInfo, - Mode: reco.ModeText, - TimeFormat: time.RFC3339, - Output: os.Stdout, - Async: true, - DefaultFields: nil, -} - -func NewLogger(logcfg reco.Config) *reco.Logger { - logger, err := reco.New(logcfg) - if err != nil { - log.Printf("New Logreco Error: %s", err) - return nil - } - return logger -} - -func CloseLogger(logger *reco.Logger) { - err := logger.Close() - if err != nil { - log.Printf("Close Logreco Error: %s", err) - return - } -} - -func (engine *Engine) CloseLogger() { - if engine.LogReco != nil { - CloseLogger(engine.LogReco) - } -} diff --git a/maxreader.go b/maxreader.go deleted file mode 100644 index c6201e6..0000000 --- a/maxreader.go +++ /dev/null @@ -1,96 +0,0 @@ -// 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/. -// Copyright 2024 WJQSERVER. All rights reserved. -// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization. -package touka - -import ( - "fmt" - "io" - "sync/atomic" -) - -// ErrBodyTooLarge 是当读取的字节数超过 MaxBytesReader 设置的限制时返回的错误. -// 将其定义为可导出的变量, 方便调用方使用 errors.Is 进行判断. -var ErrBodyTooLarge = fmt.Errorf("body too large") - -// maxBytesReader 是一个实现了 io.ReadCloser 接口的结构体. -// 它包装了另一个 io.ReadCloser, 并限制了从其中读取的最大字节数. -type maxBytesReader struct { - // r 是底层的 io.ReadCloser. - r io.ReadCloser - // n 是允许读取的最大字节数. - n int64 - // read 是一个原子计数器, 用于安全地在多个 goroutine 之间跟踪已读取的字节数. - read atomic.Int64 -} - -// NewMaxBytesReader 创建并返回一个 io.ReadCloser, 它从 r 读取数据, -// 但在读取的字节数超过 n 后会返回 ErrBodyTooLarge 错误. -// -// 如果 r 为 nil, 会 panic. -// 如果 n 小于 0, 则读取不受限制, 直接返回原始的 r. -func NewMaxBytesReader(r io.ReadCloser, n int64) io.ReadCloser { - if r == nil { - panic("NewMaxBytesReader called with a nil reader") - } - // 如果限制为负数, 意味着不限制, 直接返回原始的 ReadCloser. - if n < 0 { - return r - } - return &maxBytesReader{ - r: r, - n: n, - } -} - -// Read 方法从底层的 ReadCloser 读取数据, 同时检查是否超过了字节限制. -func (mbr *maxBytesReader) Read(p []byte) (int, error) { - // 在函数开始时只加载一次原子变量, 减少后续的原子操作开销. - readSoFar := mbr.read.Load() - - // 快速失败路径: 如果在读取之前就已经达到了限制, 立即返回错误. - if readSoFar >= mbr.n { - return 0, ErrBodyTooLarge - } - - // 计算当前还可以读取多少字节. - remaining := mbr.n - readSoFar - - // 如果请求读取的长度大于剩余可读长度, 我们需要限制本次读取的长度. - // 这样可以保证即使 p 很大, 我们也只读取到恰好达到 maxBytes 的字节数. - if int64(len(p)) > remaining { - p = p[:remaining] - } - - // 从底层 Reader 读取数据. - n, err := mbr.r.Read(p) - - // 如果实际读取到了数据, 更新原子计数器. - if n > 0 { - readSoFar = mbr.read.Add(int64(n)) - } - - // 如果底层 Read 返回错误 (例如 io.EOF). - if err != nil { - // 如果是 EOF, 并且我们还没有读满 n 个字节, 这是一个正常的结束. - // 如果已经读满了 n 个字节, 即使是 EOF, 也可以认为成功了. - return n, err - } - - // 读后检查: 如果这次读取使得总字节数超过了限制, 返回超限错误. - // 这是处理"跨越"限制情况的关键. - if readSoFar > mbr.n { - // 返回实际读取的字节数 n, 并附上超限错误. - // 上层调用者知道已经有 n 字节被读入了缓冲区 p, 但流已因超限而关闭. - return n, ErrBodyTooLarge - } - - // 一切正常, 返回读取的字节数和 nil 错误. - return n, nil -} - -// Close 方法关闭底层的 ReadCloser, 保证资源释放. -func (mbr *maxBytesReader) Close() error { - return mbr.r.Close() -} diff --git a/mergectx.go b/mergectx.go deleted file mode 100644 index 7ce2031..0000000 --- a/mergectx.go +++ /dev/null @@ -1,122 +0,0 @@ -// 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/. -// Copyright 2024 WJQSERVER. All rights reserved. -// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization. -package touka - -import ( - "context" - "sync" - "time" -) - -// mergedContext 实现了 context.Context 接口, 是 Merge 函数返回的实际类型. -type mergedContext struct { - // 嵌入一个基础 context, 它持有最早的 deadline 和取消信号. - context.Context - // 保存了所有的父 context, 用于 Value() 方法的查找. - parents []context.Context - // 用于手动取消此 mergedContext 的函数. - cancel context.CancelFunc -} - -// MergeCtx 创建并返回一个新的 context.Context. -// 这个新的 context 会在任何一个传入的父 contexts 被取消时, 或者当返回的 CancelFunc 被调用时, -// 自动被取消 (逻辑或关系). -// -// 新的 context 会继承: -// - Deadline: 所有父 context 中最早的截止时间. -// - Value: 按传入顺序从第一个能找到值的父 context 中获取值. -func MergeCtx(parents ...context.Context) (ctx context.Context, cancel context.CancelFunc) { - if len(parents) == 0 { - return context.WithCancel(context.Background()) - } - if len(parents) == 1 { - return context.WithCancel(parents[0]) - } - - var earliestDeadline time.Time - for _, p := range parents { - if deadline, ok := p.Deadline(); ok { - if earliestDeadline.IsZero() || deadline.Before(earliestDeadline) { - earliestDeadline = deadline - } - } - } - - var baseCtx context.Context - var baseCancel context.CancelFunc - if !earliestDeadline.IsZero() { - baseCtx, baseCancel = context.WithDeadline(context.Background(), earliestDeadline) - } else { - baseCtx, baseCancel = context.WithCancel(context.Background()) - } - - mc := &mergedContext{ - Context: baseCtx, - parents: parents, - cancel: baseCancel, - } - - // 启动一个监控 goroutine. - go func() { - defer mc.cancel() - - // orDone 会返回一个 channel, 当任何一个父 context 被取消时, 这个 channel 就会关闭. - // 同时监听 baseCtx.Done() 以便支持手动取消. - select { - case <-orDone(mc.parents...): - case <-mc.Context.Done(): - } - }() - - return mc, mc.cancel -} - -// Value 返回当前Ctx Value -func (mc *mergedContext) Value(key any) any { - return mc.Context.Value(key) -} - -// Deadline 实现了 context.Context 的 Deadline 方法. -func (mc *mergedContext) Deadline() (deadline time.Time, ok bool) { - return mc.Context.Deadline() -} - -// Done 实现了 context.Context 的 Done 方法. -func (mc *mergedContext) Done() <-chan struct{} { - return mc.Context.Done() -} - -// Err 实现了 context.Context 的 Err 方法. -func (mc *mergedContext) Err() error { - return mc.Context.Err() -} - -// orDone 是一个辅助函数, 返回一个 channel. -// 当任意一个输入 context 的 Done() channel 关闭时, orDone 返回的 channel 也会关闭. -// 这是一个非阻塞的、不会泄漏 goroutine 的实现. -func orDone(contexts ...context.Context) <-chan struct{} { - done := make(chan struct{}) - - var once sync.Once - closeDone := func() { - once.Do(func() { - close(done) - }) - } - - // 为每个父 context 启动一个 goroutine. - for _, ctx := range contexts { - go func(c context.Context) { - select { - case <-c.Done(): - closeDone() - case <-done: - // orDone 已经被其他 goroutine 关闭了, 当前 goroutine 可以安全退出. - } - }(ctx) - } - - return done -} diff --git a/midware_x.go b/midware_x.go deleted file mode 100644 index 3e21329..0000000 --- a/midware_x.go +++ /dev/null @@ -1,80 +0,0 @@ -// 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/. -// Copyright 2024 WJQSERVER. All rights reserved. -// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization. -package touka - -type MiddlewareXFunc func() HandlerFunc - -// UseChainIf 是一个条件中间件包装器,用于一组中间件 -// 如果 `condition` 为 true,它将按顺序创建并执行提供的 `factories` 生成的中间件链 -// 否则,它会直接跳过整个链 -func (engine *Engine) UseChainIf(condition bool, factories ...MiddlewareXFunc) HandlerFunc { - // 如果条件不满足或没有提供任何工厂函数,返回一个“穿透”中间件 - if !condition || len(factories) == 0 { - return func(c *Context) { - c.Next() - } - } - - // 在配置路由时就创建好所有中间件实例 - middlewares := make(HandlersChain, 0, len(factories)) - for _, factory := range factories { - if factory != nil { - middlewares = append(middlewares, factory()) - } - } - - // 返回一个处理器,该处理器负责执行这个子链 - // 这个实现通过临时替换 Context 的处理器链来注入子链,是健壮的 - return func(c *Context) { - // 将当前的处理链和索引位置保存下来 - originalHandlers := c.handlers - originalIndex := c.index - - // 创建一个新的临时处理链 - // 它由我们预先创建的 `middlewares` 和一个特殊的“恢复”处理器组成 - subChain := make(HandlersChain, len(middlewares)+1) - copy(subChain, middlewares) - - // 在子链的末尾添加“恢复”处理器 - // 当所有 `middlewares` 都执行完毕并调用了 Next() 后,这个函数会被执行 - subChain[len(middlewares)] = func(ctx *Context) { - // 恢复原始的处理链状态 - ctx.handlers = originalHandlers - ctx.index = originalIndex - // 继续执行原始处理链中 `UseChainIf` 之后的下一个处理器 - ctx.Next() - } - - // 将 Context 的处理器链临时替换新的的子链,并重置索引以从头开始 - c.handlers = subChain - c.index = -1 - - c.Next() - } -} - -// UseIf 是一个条件中间件包装器 -func (engine *Engine) UseIf(condition bool, middlewareX MiddlewareXFunc) HandlerFunc { - if !condition { - return func(c *Context) { - c.Next() - } - } - - // 只有当条件为 true 时,才调用工厂函数创建中间件实例 - // 注意:这会导致每次请求都创建一个新的中间件实例(如果中间件本身有状态) - // 如果中间件是无状态的,可以进行优化 - - // 优化:只创建一次 - - return func(c *Context) { - middleware := middlewareX() - if middleware != nil { - middleware(c) - } else { - c.Next() - } - } -} diff --git a/path.go b/path.go deleted file mode 100644 index 6c5cc4c..0000000 --- a/path.go +++ /dev/null @@ -1,53 +0,0 @@ -// 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/. -// Copyright 2024 WJQSERVER. All rights reserved. -// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization. -package touka - -import ( - "path" - "strings" -) - -// resolveRoutePath 安全地拼接基础路径和相对路径,并正确处理尾部斜杠。 -// 这是一个为高性能路由注册优化的版本。 -func resolveRoutePath(basePath, relativePath string) string { - // 如果相对路径为空,直接返回基础路径 - if relativePath == "" { - return basePath - } - // 如果基础路径为空,直接返回相对路径(确保以/开头) - if basePath == "" { - return relativePath - } - - // 使用 strings.Builder 来高效构建路径,避免多次字符串分配 - var b strings.Builder - // 估算一个合理的容量以减少扩容 - b.Grow(len(basePath) + len(relativePath) + 1) - b.WriteString(basePath) - - // 检查 basePath 是否以斜杠结尾 - if basePath[len(basePath)-1] != '/' { - b.WriteByte('/') // 如果没有,则添加 - } - - // 检查 relativePath 是否以斜杠开头,如果是,则移除 - if relativePath[0] == '/' { - b.WriteString(relativePath[1:]) - } else { - b.WriteString(relativePath) - } - - // path.Clean 仍然是处理 '..' 和 '//' 等复杂情况最可靠的方式。 - // 我们可以只在最终结果上调用一次,而不是在拼接过程中。 - finalPath := path.Clean(b.String()) - - // 关键:如果原始 relativePath 有尾部斜杠,但 Clean 把它移除了,我们要加回来。 - // 只有当最终路径不是根路径 "/" 时才需要加回。 - if strings.HasSuffix(relativePath, "/") && finalPath != "/" { - return finalPath + "/" - } - - return finalPath -} diff --git a/path_test.go b/path_test.go deleted file mode 100644 index 64ff185..0000000 --- a/path_test.go +++ /dev/null @@ -1,99 +0,0 @@ -// touka/path_test.go -package touka - -import ( - "fmt" - "path" - "strings" - "testing" -) - -func TestResolveRoutePath(t *testing.T) { - // 定义一组测试用例 - testCases := []struct { - basePath string - relativePath string - expected string - }{ - // --- 基本情况 --- - {basePath: "/api", relativePath: "/v1", expected: "/api/v1"}, - {basePath: "/api/", relativePath: "v1", expected: "/api/v1"}, - {basePath: "/api", relativePath: "v1", expected: "/api/v1"}, - {basePath: "/api/", relativePath: "/v1", expected: "/api/v1"}, - - // --- 尾部斜杠处理 --- - {basePath: "/api", relativePath: "/v1/", expected: "/api/v1/"}, - {basePath: "/api/", relativePath: "v1/", expected: "/api/v1/"}, - {basePath: "", relativePath: "/v1/", expected: "/v1/"}, - {basePath: "/", relativePath: "/v1/", expected: "/v1/"}, - - // --- 根路径和空路径 --- - {basePath: "/", relativePath: "/", expected: "/"}, - {basePath: "/api", relativePath: "/", expected: "/api/"}, - {basePath: "/api/", relativePath: "/", expected: "/api/"}, - {basePath: "/", relativePath: "/users", expected: "/users"}, - {basePath: "/users", relativePath: "", expected: "/users"}, - {basePath: "", relativePath: "/users", expected: "/users"}, - - // --- 路径清理测试 (由 path.Clean 处理) --- - {basePath: "/api/v1", relativePath: "../v2", expected: "/api/v2"}, - {basePath: "/api/v1/", relativePath: "../v2/", expected: "/api/v2/"}, - {basePath: "/api//v1", relativePath: "/users", expected: "/api/v1/users"}, - {basePath: "/api/./v1", relativePath: "/users", expected: "/api/v1/users"}, - } - - for _, tc := range testCases { - // 使用 t.Run 为每个测试用例创建一个子测试,方便定位问题 - testName := fmt.Sprintf("base:'%s', rel:'%s'", tc.basePath, tc.relativePath) - t.Run(testName, func(t *testing.T) { - result := resolveRoutePath(tc.basePath, tc.relativePath) - if result != tc.expected { - t.Errorf("resolveRoutePath('%s', '%s') = '%s'; want '%s'", - tc.basePath, tc.relativePath, result, tc.expected) - } - }) - } -} - -// 性能基准测试,用于观测优化效果 -func BenchmarkResolveRoutePath(b *testing.B) { - basePath := "/api/v1/some/long/path" - relativePath := "/users/profile/details/" - - // b.N 是由 testing 包提供的循环次数 - for i := 0; i < b.N; i++ { - // 在循环内调用被测试的函数 - resolveRoutePath(basePath, relativePath) - } -} - -// (可选)可以保留旧的实现,进行性能对比 -func resolveRoutePath_Old(basePath, relativePath string) string { - if relativePath == "/" { - if basePath != "" && basePath != "/" && !strings.HasSuffix(basePath, "/") { - return basePath + "/" - } - return basePath - } - finalPath := path.Clean(basePath + "/" + relativePath) - if strings.HasSuffix(relativePath, "/") && !strings.HasSuffix(finalPath, "/") { - return finalPath + "/" - } - return finalPath -} - -func BenchmarkResolveRoutePath_Old(b *testing.B) { - basePath := "/api/v1/some/long/path" - relativePath := "/users/profile/details/" - for i := 0; i < b.N; i++ { - resolveRoutePath_Old(basePath, relativePath) - } -} - -func BenchmarkJoinStd(b *testing.B) { - basePath := "/api/v1/some/long/path" - relativePath := "/users/profile/details/" - for i := 0; i < b.N; i++ { - path.Join(basePath, relativePath) - } -} diff --git a/recovery.go b/recovery.go index 5dfb837..5597e53 100644 --- a/recovery.go +++ b/recovery.go @@ -1,144 +1,38 @@ -// 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/. -// Copyright 2024 WJQSERVER. All rights reserved. -// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization. package touka import ( - "errors" - "io" + "fmt" "log" - "net" "net/http" - "net/http/httputil" // 用于 DumpRequest - "os" "runtime/debug" - "strings" ) -// PanicHandlerFunc 定义了用户自定义的 panic 处理函数类型 -// 它接收当前的 Context 和 panic 的值 -type PanicHandlerFunc func(c *Context, panicInfo interface{}) - -// RecoveryWithOptions 返回一个可配置的 panic 恢复中间件 -// -// 参数: -// - handler (PanicHandlerFunc): 一个可选的回调函数 如果提供了,当 panic 发生时, -// 它将被调用,允许用户进行自定义的日志记录、错误上报或响应 -// 如果为 nil,将使用默认的 panic 处理逻辑 -func RecoveryWithOptions(handler PanicHandlerFunc) HandlerFunc { - // 如果未提供 handler,则使用默认的 panic 处理器 - if handler == nil { - handler = defaultPanicHandler - } - +// Recovery 返回一个 Touka 的 HandlerFunc,用于捕获处理链中的 panic。 +func Recovery() HandlerFunc { return func(c *Context) { + // 使用 defer 和 recover() 来捕获 panic defer func() { if r := recover(); r != nil { - // 捕获到 panic,调用配置的处理器 - handler(c, r) + // 记录 panic 信息和堆栈追踪 + err := fmt.Errorf("panic occurred: %v", r) + log.Printf("[Recovery] %s\n%s", err, debug.Stack()) // 记录错误和堆栈 + + // 检查客户端是否已断开连接,如果已断开则不再尝试写入响应 + select { + case <-c.Request.Context().Done(): + log.Printf("[Recovery] Client disconnected, skipping response for panic: %v", r) + return // 客户端已断开,直接返回 + default: + // 客户端未断开,返回 500 Internal Server Error + // 使用统一的错误处理机制 + c.engine.errorHandle.handler(c, http.StatusInternalServerError) + // Abort() 确保后续的处理函数不再执行 + c.Abort() + } } }() - c.Next() // 执行后续的处理链 + + // 继续执行处理链中的下一个处理函数 + c.Next() } } - -// Recovery 返回一个使用默认配置的 panic 恢复中间件 -// 它是 RecoveryWithOptions(nil) 的一个便捷包装 -func Recovery() HandlerFunc { - return RecoveryWithOptions(nil) // 使用默认处理器 -} - -// defaultPanicHandler 是默认的 panic 处理逻辑 -func defaultPanicHandler(c *Context, r interface{}) { - // 检查连接是否已由客户端关闭 - // 常见的错误类型包括 net.OpError (其内部错误可能是 os.SyscallError), - // 以及在 HTTP/2 中可能出现的特定 stream 错误 - // isBrokenPipeError 是一个辅助函数,用于检查这些情况 - if isBrokenPipeError(r) { - // 如果是客户端断开连接导致的 panic,我们不应再尝试写入响应 - // 只需要记录一个信息级别的日志,然后中止处理 - log.Printf("[Recovery] Client connection closed for request %s %s. Panic: %v. No response sent.", - c.Request.Method, c.Request.URL.Path, r) - c.Abort() // 仅设置中止标志 - return - } - - // 对于其他类型的 panic,我们认为是服务器端内部错误 - // 记录详细的错误日志,包括请求信息和堆栈跟踪 - // 使用 httputil.DumpRequest 来获取请求的快照,但注意不要读取 Body - httpRequest, _ := httputil.DumpRequest(c.Request, false) - // 隐藏敏感头部信息,例如 Authorization - headers := strings.Split(string(httpRequest), "\r\n") - for idx, header := range headers { - current := strings.SplitN(header, ":", 2) - if len(current) > 1 && strings.EqualFold(current[0], "Authorization") { - headers[idx] = current[0] + ": [REDACTED]" // 替换为脱敏信息 - } - } - redactedRequest := strings.Join(headers, "\r\n") - // 使用英文记录日志 - log.Printf("[Recovery] Panic recovered:\nPanic: %v\nRequest:\n%s\nStack:\n%s", - r, redactedRequest, string(debug.Stack())) - - // 在发送 500 错误响应之前,检查响应是否已经开始写入 - // 如果 c.Writer.Written() 返回 true,说明响应头已经发送, - // 此时再尝试写入状态码或响应体会导致错误或 panic,所以应该直接中止 - if c.Writer.Written() { - // 使用英文记录日志 - log.Println("[Recovery] Response headers already sent. Cannot write 500 error.") - c.Abort() - return - } - - // 尝试发送 500 Internal Server Error 响应 - // 使用框架提供的统一错误处理器(如果可用) - if c.engine != nil && c.engine.errorHandle.handler != nil { - c.engine.errorHandle.handler(c, http.StatusInternalServerError, errors.New("Internal Panic Error")) - } else { - // 如果框架错误处理器不可用,提供一个备用的简单响应 - // 返回英文错误信息 - http.Error(c.Writer, "Internal Server Error", http.StatusInternalServerError) - } - // 确保 Touka 的处理链被中止 - // errorHandle.handler 通常会调用 Abort,但在这里再次调用是安全的 - c.Abort() -} - -// isBrokenPipeError 检查 recover() 捕获的值是否表示一个由客户端断开连接引起的网络错误 -// 这对于防止在已关闭的连接上写入响应至关重要 -func isBrokenPipeError(r interface{}) bool { - // 将 recover() 的结果转换为 error 类型 - err, ok := r.(error) - if !ok { - return false // 如果 panic 的不是一个 error,则不认为是 broken pipe - } - - var opErr *net.OpError - // 检查错误链中是否存在 net.OpError - if errors.As(err, &opErr) { - var syscallErr *os.SyscallError - // 检查 net.OpError 的内部错误是否是 os.SyscallError - if errors.As(opErr.Err, &syscallErr) { - // 将系统调用错误转换为小写字符串进行检查 - errMsg := strings.ToLower(syscallErr.Error()) - // 常见的由客户端断开引起的错误消息 - if strings.Contains(errMsg, "broken pipe") || strings.Contains(errMsg, "connection reset by peer") { - return true - } - } - } - - // 还需要处理 HTTP/2 中的 stream closed 错误 - // 在 Go 1.16+ 中,当写入已关闭的 HTTP/2 流时,可能会返回 io.EOF - if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) { - // 在流式写入的上下文中,io.EOF 或 net.ErrClosed 也常常表示连接已关闭 - return true - } - - if errors.Is(err, http.ErrAbortHandler) { - return true - } - - return false -} diff --git a/respw.go b/respw.go index 2cf6700..ea8aa85 100644 --- a/respw.go +++ b/respw.go @@ -1,21 +1,15 @@ -// 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/. -// Copyright 2024 WJQSERVER. All rights reserved. -// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization. package touka import ( "bufio" "errors" - "log" "net" "net/http" - "runtime/debug" ) // --- ResponseWriter 包装 --- -// ResponseWriter 接口扩展了 http.ResponseWriter 以提供对响应状态和大小的访问 +// ResponseWriter 接口扩展了 http.ResponseWriter 以提供对响应状态和大小的访问。 type ResponseWriter interface { http.ResponseWriter http.Hijacker // 支持 WebSocket 等 @@ -24,38 +18,26 @@ type ResponseWriter interface { Status() int // 返回写入的 HTTP 状态码,如果未写入则为 0 Size() int // 返回已写入响应体的字节数 Written() bool // 返回 WriteHeader 是否已被调用 - IsHijacked() bool } -// responseWriterImpl 是 ResponseWriter 的具体实现 +// responseWriterImpl 是 ResponseWriter 的具体实现。 type responseWriterImpl struct { http.ResponseWriter - size int - status int // 0 表示尚未写入状态码 - hijacked bool + size int + status int // 0 表示尚未写入状态码 } -// NewResponseWriter 创建并返回一个 responseWriterImpl 实例 +// NewResponseWriter 创建并返回一个 responseWriterImpl 实例。 func newResponseWriter(w http.ResponseWriter) ResponseWriter { - return &responseWriterImpl{ + rw := &responseWriterImpl{ ResponseWriter: w, status: 0, // 明确初始状态 size: 0, - hijacked: false, } -} - -func (rw *responseWriterImpl) reset(w http.ResponseWriter) { - rw.ResponseWriter = w - rw.status = 0 - rw.size = 0 - rw.hijacked = false + return rw } func (rw *responseWriterImpl) WriteHeader(statusCode int) { - if rw.hijacked { - return - } if rw.status == 0 { // 确保只设置一次 rw.status = statusCode rw.ResponseWriter.WriteHeader(statusCode) @@ -63,12 +45,9 @@ func (rw *responseWriterImpl) WriteHeader(statusCode int) { } func (rw *responseWriterImpl) Write(b []byte) (int, error) { - if rw.hijacked { - return 0, errors.New("http: response already hijacked") - } if rw.status == 0 { // 如果 WriteHeader 没被显式调用,Go 的 http server 会默认为 200 - // 我们在这里也将其标记为 200,因为即将写入数据 + // 我们在这里也将其标记为 200,因为即将写入数据。 rw.status = http.StatusOK // ResponseWriter.Write 会在第一次写入时自动调用 WriteHeader(http.StatusOK) // 所以不需要在这里显式调用 rw.ResponseWriter.WriteHeader(http.StatusOK) @@ -90,51 +69,17 @@ func (rw *responseWriterImpl) Written() bool { return rw.status != 0 } -// Hijack 实现 http.Hijacker 接口 +// Hijack 实现 http.Hijacker 接口。 func (rw *responseWriterImpl) Hijack() (net.Conn, *bufio.ReadWriter, error) { - // 检查是否已劫持 - if rw.hijacked { - return nil, nil, errors.New("http: connection already hijacked") + if hj, ok := rw.ResponseWriter.(http.Hijacker); ok { + return hj.Hijack() } - - // 尝试从底层 ResponseWriter 获取 Hijacker 接口 - hj, ok := rw.ResponseWriter.(http.Hijacker) - if !ok { - return nil, nil, errors.New("http.Hijacker interface not supported") - } - - // 调用底层的 Hijack 方法 - conn, brw, err := hj.Hijack() - if err != nil { - // 如果劫持失败,返回错误 - return nil, nil, err - } - - // 如果劫持成功,更新内部状态 - rw.hijacked = true - - return conn, brw, nil + return nil, nil, errors.New("http.Hijacker interface not supported") } -// Flush 实现 http.Flusher 接口 +// Flush 实现 http.Flusher 接口。 func (rw *responseWriterImpl) Flush() { - defer func() { - if r := recover(); r != nil { - // 记录捕获到的 panic 信息,这表明底层连接可能已经关闭或失效 - // 使用 log.Printf 记录,并包含堆栈信息,便于调试 - log.Printf("Recovered from panic during responseWriterImpl.Flush for request: %v\nStack: %s", r, debug.Stack()) - // 捕获后,不继续传播 panic,允许请求的 goroutine 优雅退出 - } - }() - if rw.hijacked { - return - } if fl, ok := rw.ResponseWriter.(http.Flusher); ok { fl.Flush() } } - -// IsHijacked 方法返回连接是否已被劫持 -func (rw *responseWriterImpl) IsHijacked() bool { - return rw.hijacked -} diff --git a/serve.go b/serve.go index 7e05b8c..ecda82b 100644 --- a/serve.go +++ b/serve.go @@ -1,7 +1,3 @@ -// 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/. -// Copyright 2024 WJQSERVER. All rights reserved. -// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization. package touka import ( @@ -17,340 +13,219 @@ import ( "sync" "syscall" "time" - - "github.com/fenthope/reco" ) -// defaultShutdownTimeout 定义了在强制关闭前等待优雅关闭的最长时间 -const defaultShutdownTimeout = 5 * time.Second +const defaultShutdownTimeout = 5 * time.Second // 定义默认的优雅关闭超时时间 -// --- 内部辅助函数 --- - -// resolveAddress 解析传入的地址参数,如果没有则返回默认的 ":8080" +// resolveAddress 辅助函数,处理传入的地址参数。 func resolveAddress(addr []string) string { switch len(addr) { case 0: - return ":8080" + return ":8080" // 默认端口 case 1: return addr[0] default: - panic("too many parameters provided for server address") + panic("too many parameters for Run method") // 参数过多则报错 } } -// getShutdownTimeout 解析可选的超时参数,如果无效或未提供则返回默认值 +// Run 启动 HTTP 服务器。 +// 接受一个可选的地址参数,如果未提供则默认为 ":8080"。 +func (engine *Engine) Run(addr ...string) (err error) { + address := resolveAddress(addr) // 解析服务器地址 + log.Printf("Touka server listening on %s\n", address) + err = http.ListenAndServe(address, engine) // 启动 HTTP 服务器 + return +} + +// getShutdownTimeout 解析可选的超时参数,如果未提供或无效,则返回默认超时。 func getShutdownTimeout(timeouts []time.Duration) time.Duration { - if len(timeouts) > 0 && timeouts[0] > 0 { - return timeouts[0] + var timeout time.Duration + if len(timeouts) > 0 { + timeout = timeouts[0] + if timeout <= 0 { + log.Printf("Warning: Provided shutdown timeout (%v) is non-positive. Using default timeout %v.\n", timeout, defaultShutdownTimeout) + timeout = defaultShutdownTimeout + } + } else { + timeout = defaultShutdownTimeout } - return defaultShutdownTimeout + return timeout } -// runServer 是一个内部辅助函数,负责在一个新的 goroutine 中启动一个 http.Server, -// 并处理其启动失败的致命错误 -// serverType 用于在日志中标识服务器类型 (例如 "HTTP", "HTTPS") -func runServer(serverType string, srv *http.Server) { - go func() { - var err error - protocol := "http" - if srv.TLSConfig != nil { - protocol = "https" - } - - log.Printf("Touka %s server listening on %s://%s", serverType, protocol, srv.Addr) - - if srv.TLSConfig != nil { - // 对于 HTTPS 服务器,如果 srv.TLSConfig.Certificates 已配置, - // ListenAndServeTLS 的前两个参数可以为空字符串 - err = srv.ListenAndServeTLS("", "") - } else { - err = srv.ListenAndServe() - } - - // 如果服务器停止不是因为被优雅关闭 (http.ErrServerClosed), - // 则认为是一个严重错误,并终止程序 - if err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Fatalf("Touka %s server failed: %v", serverType, err) - } - }() -} - -// handleGracefulShutdown 监听系统信号 (SIGINT, SIGTERM) 并优雅地关闭所有提供的服务器 -// 这是所有支持优雅关闭的 RunXXX 方法的最终归宿 -func handleGracefulShutdown(servers []*http.Server, timeout time.Duration, logger *reco.Logger) error { - // 创建一个 channel 来接收操作系统信号 +// handleGracefulShutdown 处理一个或多个 http.Server 实例的优雅关闭。 +// 它监听操作系统信号,并在指定超时时间内尝试关闭所有服务器。 +func handleGracefulShutdown(servers []*http.Server, timeout time.Duration) error { quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 监听中断和终止信号 - <-quit // 阻塞,直到接收到上述信号之一 + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit log.Println("Shutting down Touka server(s)...") - // 关闭日志记录器 - if logger != nil { - go func() { - log.Println("Closing Touka logger...") - CloseLogger(logger) - }() - } - - // 创建一个带超时的上下文,用于 Shutdown ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() var wg sync.WaitGroup - errChan := make(chan error, len(servers)) // 用于收集关闭错误的 channel + var errs []error + var errsMutex sync.Mutex // 保护 errs 切片 - // 并发地关闭所有服务器 for _, srv := range servers { + srv := srv // capture loop variable wg.Add(1) - go func(s *http.Server) { - defer wg.Done() - if err := s.Shutdown(ctx); err != nil { - // 将错误发送到 channel - errChan <- fmt.Errorf("server on %s shutdown failed: %w", s.Addr, err) - } - }(srv) - } - - wg.Wait() // 等待所有服务器的关闭 goroutine 完成 - close(errChan) // 关闭 channel,以便可以安全地遍历它 - - // 收集所有关闭过程中发生的错误 - var shutdownErrors []error - for err := range errChan { - shutdownErrors = append(shutdownErrors, err) - log.Printf("Shutdown error: %v", err) - } - - if len(shutdownErrors) > 0 { - return errors.Join(shutdownErrors...) // Go 1.20+ 的 errors.Join,用于合并多个错误 - } - log.Println("Touka server(s) exited gracefully.") - return nil -} - -func handleGracefulShutdownWithContext(servers []*http.Server, ctx context.Context, timeout time.Duration, logger *reco.Logger) error { - // 创建一个 channel 来接收操作系统信号 - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 监听中断和终止信号 - - // 启动服务器 - serverStopped := make(chan error, 1) - for _, srv := range servers { - go func(s *http.Server) { - serverStopped <- s.ListenAndServe() - }(srv) - } - - select { - case <-ctx.Done(): - // Context 被取消 (例如,通过外部取消函数) - log.Println("Context cancelled, shutting down Touka server(s)...") - case err := <-serverStopped: - // 服务器自身停止 (例如,端口被占用,或 ListenAndServe 返回错误) - if err != nil && !errors.Is(err, http.ErrServerClosed) { - return fmt.Errorf("Touka HTTP server failed: %w", err) - } - log.Println("Touka HTTP server stopped gracefully.") - return nil // 服务器已自行优雅关闭,无需进一步处理 - case <-quit: - // 接收到操作系统信号 - log.Println("Shutting down Touka server(s) due to OS signal...") - } - - // 关闭日志记录器 - if logger != nil { go func() { - log.Println("Closing Touka logger...") - CloseLogger(logger) + defer wg.Done() + if err := srv.Shutdown(ctx); err != nil { + errsMutex.Lock() + if err == context.DeadlineExceeded { + log.Printf("Server %s shutdown timed out after %v.\n", srv.Addr, timeout) + errs = append(errs, fmt.Errorf("server %s shutdown timed out", srv.Addr)) + } else { + log.Printf("Server %s forced to shutdown: %v\n", srv.Addr, err) + errs = append(errs, fmt.Errorf("server %s forced to shutdown: %w", srv.Addr, err)) + } + errsMutex.Unlock() + } }() } + wg.Wait() // 等待所有服务器的关闭 Goroutine 完成 - // 创建一个带超时的上下文,用于 Shutdown - shutdownCtx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - var wg sync.WaitGroup - errChan := make(chan error, len(servers)) // 用于收集关闭错误的 channel - - // 并发地关闭所有服务器 - for _, srv := range servers { - wg.Add(1) - go func(s *http.Server) { - defer wg.Done() - if err := s.Shutdown(shutdownCtx); err != nil { - // 将错误发送到 channel - errChan <- fmt.Errorf("server on %s shutdown failed: %w", s.Addr, err) - } - }(srv) + if len(errs) > 0 { + return errors.Join(errs...) // 返回所有收集到的错误 } - wg.Wait() - close(errChan) // 关闭 channel,以便可以安全地遍历它 - - // 收集所有关闭过程中发生的错误 - var shutdownErrors []error - for err := range errChan { - shutdownErrors = append(shutdownErrors, err) - log.Printf("Shutdown error: %v", err) - } - - if len(shutdownErrors) > 0 { - return errors.Join(shutdownErrors...) // Go 1.20+ 的 errors.Join,用于合并多个错误 - } log.Println("Touka server(s) exited gracefully.") return nil } -// --- 公共 Run 方法 --- - -// Run 启动一个不支持优雅关闭的 HTTP 服务器 -// 这是一个阻塞调用,主要用于简单的场景或快速测试 -// 建议在生产环境中使用 RunShutdown 或其他支持优雅关闭的方法 -func (engine *Engine) Run(addr ...string) error { - address := resolveAddress(addr) - srv := &http.Server{Addr: address, Handler: engine} - - // 即使是不支持优雅关闭的 Run,也应用默认和用户配置,以保持行为一致性 - //engine.applyDefaultServerConfig(srv) - if engine.ServerConfigurator != nil { - engine.ServerConfigurator(srv) - } - log.Printf("Starting Touka HTTP server on %s (no graceful shutdown)", address) - return srv.ListenAndServe() -} - -// RunShutdown 启动一个支持优雅关闭的 HTTP 服务器 +// RunShutdown 启动 HTTP 服务器并支持优雅关闭。 +// 它监听操作系统信号 (SIGINT, SIGTERM),并在指定超时时间内优雅地关闭服务器。 +// addr: 服务器监听的地址,例如 ":8080"。 +// timeouts: 可选的超时时间,如果未提供,则默认为 5 秒。 func (engine *Engine) RunShutdown(addr string, timeouts ...time.Duration) error { + timeout := getShutdownTimeout(timeouts) + srv := &http.Server{ Addr: addr, - Handler: engine, + Handler: engine, // Engine 实现了 http.Handler 接口 } - // 应用框架的默认配置和用户提供的自定义配置 - //engine.applyDefaultServerConfig(srv) - if engine.ServerConfigurator != nil { - engine.ServerConfigurator(srv) - } + // 启动服务器在单独的 Goroutine 中运行,以便主 Goroutine 可以监听信号 + go func() { + log.Printf("Touka HTTP server listening on %s\n", addr) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Touka HTTP server listen error: %s\n", err) + } + }() - runServer("HTTP", srv) - return handleGracefulShutdown([]*http.Server{srv}, getShutdownTimeout(timeouts), engine.LogReco) + return handleGracefulShutdown([]*http.Server{srv}, timeout) } -// RunShutdown 启动一个支持优雅关闭的 HTTP 服务器 -func (engine *Engine) RunShutdownWithContext(addr string, ctx context.Context, timeouts ...time.Duration) error { - srv := &http.Server{ - Addr: addr, - Handler: engine, - } - - // 应用框架的默认配置和用户提供的自定义配置 - //engine.applyDefaultServerConfig(srv) - if engine.ServerConfigurator != nil { - engine.ServerConfigurator(srv) - } - - return handleGracefulShutdownWithContext([]*http.Server{srv}, ctx, getShutdownTimeout(timeouts), engine.LogReco) -} - -// RunTLS 启动一个支持优雅关闭的 HTTPS 服务器 -func (engine *Engine) RunTLS(addr string, tlsConfig *tls.Config, timeouts ...time.Duration) error { +// RunWithTLS 启动 HTTPS 服务器并支持优雅关闭。 +// 用户需自行创建并传入 *tls.Config 实例,以提供完整的 TLS 配置自由度。 +// addr: 服务器监听的地址,例如 ":8443"。 +// tlsConfig: 包含 TLS 证书、密钥及其他配置的 tls.Config 实例。 +// timeouts: 可选的超时时间,如果未提供,则默认为 5 秒。 +func (engine *Engine) RunWithTLS(addr string, tlsConfig *tls.Config, timeouts ...time.Duration) error { if tlsConfig == nil { - return errors.New("tls.Config must not be nil for RunTLS") - } - - // 配置 HTTP/2 支持 (如果使用默认配置) - if engine.useDefaultProtocols { - engine.SetProtocols(&ProtocolsConfig{ - Http1: true, - Http2: true, // 默认在 TLS 上启用 HTTP/2 - }) + return errors.New("tls.Config must not be nil for RunWithTLS") } + timeout := getShutdownTimeout(timeouts) srv := &http.Server{ Addr: addr, Handler: engine, - TLSConfig: tlsConfig, + TLSConfig: tlsConfig, // 使用用户传入的 tls.Config } - // 应用框架的默认配置和用户提供的自定义配置 - // 优先使用 TLSServerConfigurator,如果未设置,则回退到通用的 ServerConfigurator - //engine.applyDefaultServerConfig(srv) - if engine.TLSServerConfigurator != nil { - engine.TLSServerConfigurator(srv) - } else if engine.ServerConfigurator != nil { - engine.ServerConfigurator(srv) - } - - runServer("HTTPS", srv) - return handleGracefulShutdown([]*http.Server{srv}, getShutdownTimeout(timeouts), engine.LogReco) -} - -// RunWithTLS 是 RunTLS 的别名,为了保持向后兼容性或更直观的命名 -func (engine *Engine) RunWithTLS(addr string, tlsConfig *tls.Config, timeouts ...time.Duration) error { - return engine.RunTLS(addr, tlsConfig, timeouts...) -} - -// RunTLSRedir 启动 HTTP 重定向服务器和 HTTPS 应用服务器,两者都支持优雅关闭 -func (engine *Engine) RunTLSRedir(httpAddr, httpsAddr string, tlsConfig *tls.Config, timeouts ...time.Duration) error { - if tlsConfig == nil { - return errors.New("tls.Config must not be nil for RunTLSRedir") - } - - // --- HTTPS 服务器 --- if engine.useDefaultProtocols { - engine.SetProtocols(&ProtocolsConfig{Http1: true, Http2: true}) + //加入HTTP2支持 + engine.SetProtocols(&ProtocolsConfig{ + Http1: true, + Http2: true, // 默认启用 HTTP/2 + Http2_Cleartext: false, + }) } + + go func() { + log.Printf("Touka HTTPS server listening on %s\n", addr) + if err := srv.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { + log.Fatalf("Touka HTTPS server listen error: %s\n", err) + } + }() + + return handleGracefulShutdown([]*http.Server{srv}, timeout) +} + +// RunWithTLSRedir 启动 HTTP 和 HTTPS 服务器,并将所有 HTTP 请求重定向到 HTTPS。 +// httpAddr: HTTP 服务器监听的地址,例如 ":80"。 +// httpsAddr: HTTPS 服务器监听的地址,例如 ":443"。 +// tlsConfig: 包含 TLS 证书、密钥及其他配置的 tls.Config 实例,用于 HTTPS 服务器。 +// timeouts: 可选的超时时间,如果未提供,则默认为 5 秒。 +func (engine *Engine) RunWithTLSRedir(httpAddr, httpsAddr string, tlsConfig *tls.Config, timeouts ...time.Duration) error { + if tlsConfig == nil { + return errors.New("tls.Config must not be nil for RunWithTLSRedir") + } + timeout := getShutdownTimeout(timeouts) + + // HTTPS Server httpsSrv := &http.Server{ Addr: httpsAddr, Handler: engine, - TLSConfig: tlsConfig, - } - //engine.applyDefaultServerConfig(httpsSrv) - if engine.TLSServerConfigurator != nil { - engine.TLSServerConfigurator(httpsSrv) - } else if engine.ServerConfigurator != nil { - engine.ServerConfigurator(httpsSrv) + TLSConfig: tlsConfig, // 使用用户传入的 tls.Config } - // --- HTTP 重定向服务器 --- - redirectHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - host, _, err := net.SplitHostPort(r.Host) - if err != nil { - host = r.Host - } + if engine.useDefaultProtocols { + //加入HTTP2支持 + engine.SetProtocols(&ProtocolsConfig{ + Http1: true, + Http2: true, // 默认启用 HTTP/2 + Http2_Cleartext: false, + }) + } - _, httpsPort, err := net.SplitHostPort(httpsAddr) - if err != nil { - // 如果 httpsAddr 没有端口,这是一个配置错误 - - log.Fatalf("Invalid HTTPS address for redirection '%s': must include a port.", httpsAddr) - } - - targetURL := "https://" + host - // 只有在非标准 HTTPS 端口 (443) 时才附加端口号 - if httpsPort != "443" { - targetURL = "https://" + net.JoinHostPort(host, httpsPort) - } - targetURL += r.URL.RequestURI() - - http.Redirect(w, r, targetURL, http.StatusMovedPermanently) - }) + // HTTP Server for redirection httpSrv := &http.Server{ - Addr: httpAddr, - Handler: redirectHandler, - } - //engine.applyDefaultServerConfig(httpSrv) - if engine.ServerConfigurator != nil { - engine.ServerConfigurator(httpSrv) + Addr: httpAddr, + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // 从 r.Host 提取 hostname,例如 "localhost:8080" -> "localhost" + hostOnly, _, err := net.SplitHostPort(r.Host) + if err != nil { // r.Host 可能没有端口,例如 "example.com" + hostOnly = r.Host + } + + // 从 httpsAddr 提取目标 HTTPS 端口,例如 ":443" -> "443" + _, targetHttpsPort, err := net.SplitHostPort(httpsAddr) + if err != nil { // httpsAddr 必须包含一个有效的端口 + log.Fatalf("Error: Invalid HTTPS address '%s' for redirection. Must specify a port (e.g., ':443').", httpsAddr) + } + + var redirectHost string + if targetHttpsPort == "443" { + redirectHost = hostOnly // 如果是默认 HTTPS 端口,则无需在 URL 中显式指定端口 + } else { + redirectHost = net.JoinHostPort(hostOnly, targetHttpsPort) // 否则,显式指定端口 + } + + // 构建目标 HTTPS URL + targetURL := "https://" + redirectHost + r.URL.RequestURI() + http.Redirect(w, r, targetURL, http.StatusMovedPermanently) // 301 Permanent Redirect + }), } - // --- 启动服务器和优雅关闭 --- - runServer("HTTPS", httpsSrv) - runServer("HTTP Redirect", httpSrv) - return handleGracefulShutdown([]*http.Server{httpsSrv, httpSrv}, getShutdownTimeout(timeouts), engine.LogReco) -} + // Start HTTPS server + go func() { + log.Printf("Touka HTTPS server listening on %s\n", httpsAddr) + if err := httpsSrv.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { // 同样,传入空字符串 + log.Fatalf("Touka HTTPS server listen error: %s\n", err) + } + }() -// RunWithTLSRedir 是 RunTLSRedir 的别名,为了保持向后兼容性 -func (engine *Engine) RunWithTLSRedir(httpAddr, httpsAddr string, tlsConfig *tls.Config, timeouts ...time.Duration) error { - return engine.RunTLSRedir(httpAddr, httpsAddr, tlsConfig, timeouts...) + // Start HTTP redirect server + go func() { + log.Printf("Touka HTTP redirect server listening on %s\n", httpAddr) + if err := httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Touka HTTP redirect server listen error: %s\n", err) + } + }() + + return handleGracefulShutdown([]*http.Server{httpsSrv, httpSrv}, timeout) } diff --git a/sse.go b/sse.go deleted file mode 100644 index 3b98800..0000000 --- a/sse.go +++ /dev/null @@ -1,184 +0,0 @@ -// 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/. -// Copyright 2025 WJQSERVER. All rights reserved. -// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization. -package touka - -import ( - "bytes" - "io" - "net/http" - "strings" -) - -// Event 代表一个服务器发送事件(SSE). -type Event struct { - // Event 是事件的名称. - Event string - // Data 是事件的内容, 可以是多行文本. - Data string - // Id 是事件的唯一标识符. - Id string - // Retry 是指定客户端在连接丢失后应等待多少毫秒后尝试重新连接. - Retry string -} - -// Render 将事件格式化并写入给定的 writer. -// 通过逐行处理数据, 此方法可防止因数据中包含换行符而导致的CRLF注入问题. -// 为了性能, 它使用 bytes.Buffer 并通过 WriteTo 直接写入, 以避免不必要的内存分配. -func (e *Event) Render(w io.Writer) error { - var buf bytes.Buffer - - if len(e.Id) > 0 { - buf.WriteString("id: ") - buf.WriteString(e.Id) - buf.WriteString("\n") - } - if len(e.Event) > 0 { - buf.WriteString("event: ") - buf.WriteString(e.Event) - buf.WriteString("\n") - } - if len(e.Data) > 0 { - lines := strings.Split(e.Data, "\n") - for _, line := range lines { - buf.WriteString("data: ") - buf.WriteString(line) - buf.WriteString("\n") - } - } - if len(e.Retry) > 0 { - buf.WriteString("retry: ") - buf.WriteString(e.Retry) - buf.WriteString("\n") - } - - // 每个事件都以一个额外的换行符结尾. - buf.WriteString("\n") - - // 直接将 buffer 的内容写入 writer, 避免生成中间字符串. - _, err := buf.WriteTo(w) - return err -} - -// EventStream 启动一个 SSE 事件流. -// 这是推荐的、更简单安全的方式, 采用阻塞和回调的设计, 框架负责管理连接生命周期. -// -// 详细用法: -// -// r.GET("/sse/callback", func(c *touka.Context) { -// // streamer 回调函数会在一个循环中被调用. -// c.EventStream(func(w io.Writer) bool { -// event := touka.Event{ -// Event: "time-tick", -// Data: time.Now().Format(time.RFC1123), -// } -// -// if err := event.Render(w); err != nil { -// // 发生写入错误, 停止发送. -// return false // 返回 false 结束事件流. -// } -// -// time.Sleep(2 * time.Second) -// return true // 返回 true 继续事件流. -// }) -// // 当事件流结束后(例如客户端关闭页面), 这行代码会被执行. -// fmt.Println("Client disconnected from /sse/callback") -// }) -func (c *Context) EventStream(streamer func(w io.Writer) bool) { - // 为现代网络协议优化头部. - c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8") - c.Writer.Header().Set("Cache-Control", "no-cache, no-transform") - c.Writer.Header().Del("Connection") - c.Writer.Header().Del("Transfer-Encoding") - - c.Writer.WriteHeader(http.StatusOK) - c.Writer.Flush() // 直接调用, ResponseWriter 接口保证了 Flush 方法的存在. - - for { - select { - case <-c.Request.Context().Done(): - return - default: - if !streamer(c.Writer) { - return - } - c.Writer.Flush() - } - } -} - -// EventStreamChan 返回用于 SSE 事件流的 channel. -// 这是为高级并发场景设计的、更灵活的API. -// -// 重要: -// - 调用者必须 close(eventChan) 来结束事件流. -// - 调用者必须在独立的 goroutine 中消费 errChan 来处理错误和连接断开. -// - 为防止 goroutine 泄漏, 建议发送方在 select 中同时监听 c.Request.Context().Done(). -// -// 详细用法: -// -// r.GET("/sse/channel", func(c *touka.Context) { -// eventChan, errChan := c.EventStreamChan() -// -// // 必须在独立的goroutine中处理错误和连接断开. -// go func() { -// if err := <-errChan; err != nil { -// c.Errorf("SSE channel error: %v", err) -// } -// }() -// -// // 在另一个goroutine中异步发送事件. -// go func() { -// // 重要: 必须在逻辑结束时关闭channel, 以通知框架. -// defer close(eventChan) -// -// for i := 1; i <= 5; i++ { -// select { -// case <-c.Request.Context().Done(): -// return // 客户端已断开, 退出 goroutine. -// default: -// eventChan <- touka.Event{ -// Id: fmt.Sprintf("%d", i), -// Data: "hello from channel", -// } -// time.Sleep(2 * time.Second) -// } -// } -// }() -// }) -func (c *Context) EventStreamChan() (chan<- Event, <-chan error) { - eventChan := make(chan Event) - errChan := make(chan error, 1) - - c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8") - c.Writer.Header().Set("Cache-Control", "no-cache, no-transform") - c.Writer.Header().Del("Connection") - c.Writer.Header().Del("Transfer-Encoding") - - c.Writer.WriteHeader(http.StatusOK) - c.Writer.Flush() - - go func() { - defer close(errChan) - - for { - select { - case event, ok := <-eventChan: - if !ok { - return - } - if err := event.Render(c.Writer); err != nil { - errChan <- err - return - } - c.Writer.Flush() - case <-c.Request.Context().Done(): - errChan <- c.Request.Context().Err() - return - } - } - }() - - return eventChan, errChan -} diff --git a/testutil.go b/testutil.go deleted file mode 100644 index 3511320..0000000 --- a/testutil.go +++ /dev/null @@ -1,144 +0,0 @@ -// 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/. -// Copyright 2024 WJQSERVER. All rights reserved. -// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization. -package touka - -import ( - "fmt" - "io" - "net/http" - "net/http/httptest" -) - -// CreateTestContext 为测试创建一个 *Context 和一个关联的 *Engine。 -// 它使用 httptest.NewRecorder() (如果传入的 w 为 nil) 来捕获响应。 -// 返回的 Context 已经过初始化,其 Writer 指向提供的 ResponseWriter (或新创建的 Recorder), -// 其 Request 是一个默认的 "GET /" 请求,并且其 engine 字段指向返回的 Engine 实例。 -// -// 参数: -// - w (http.ResponseWriter): 可选的。如果为 nil,函数内部会创建一个 httptest.ResponseRecorder。 -// 通常在测试中,你会传入一个 httptest.ResponseRecorder 来检查响应。 -// -// 返回: -// - c (*Context): 一个初始化的 Touka Context。 -// - r (*Engine): 一个新的 Touka Engine 实例,与 c 相关联。 -func CreateTestContext(w http.ResponseWriter) (c *Context, r *Engine) { - // 1. 如果未提供 ResponseWriter,则创建一个测试用的 Recorder - // ResponseRecorder 实现了 http.ResponseWriter 接口 - var testResponseWriter http.ResponseWriter = w - if testResponseWriter == nil { - testResponseWriter = httptest.NewRecorder() - } - - // 2. 创建一个新的 Engine 实例 - // 使用 New() 而不是 Default() 以获得一个“干净”的引擎,不带默认中间件 (如 Recovery) - // 如果你的测试依赖于 Default() 的中间件,可以改为 r = Default() - r = New() - - // 3. 从 Engine 的池中获取一个 Context 对象 - // 这是模拟真实请求处理的最佳方式 - c = r.pool.Get().(*Context) - - // 4. 创建一个默认的 HTTP 请求 - // 测试时可以根据需要修改这个请求的 Method, URL, Body, Headers 等 - // http.NewRequest 的 body 可以是 nil (对于GET) 或 bytes.NewBufferString("body content") 等 - req, err := http.NewRequest(http.MethodGet, "/", nil) - if err != nil { - // NewRequest 对于 "GET" 和 "/" 以及 nil body 通常不会失败 - // 但作为健壮性考虑,可以 panic 或返回错误 - panic("touka.CreateTestContext: Failed to create dummy request: " + err.Error()) // 英文 panic - } - // 确保请求有关联的 Context (尽管 c.reset 也会设置) - // req = req.WithContext(context.Background()) // 通常 reset 会处理这个 - - // 5. 重置/初始化 Context - // c.reset() 方法期望一个 http.ResponseWriter 和 *http.Request - // 它会将 c.Writer 包装成 touka.ResponseWriter (responseWriterImpl) - // 并设置 c.Request, c.Params (清空), c.handlers (nil), c.index (-1), - // c.Keys (新map), c.Errors (清空), c.ctx (来自 req.Context()), - // 以及 c.engine (在 c.pool.New 中已经设置,但 reset 会确保其他关联正确)。 - c.reset(testResponseWriter, req) - - // 确保 Context 中的 engine 字段指向我们创建的这个 Engine 实例 - // 虽然 c.pool.New 应该已经做了,但显式确认或设置无害,尤其是如果我们不完全依赖 pool.New 的细节。 - // 在当前的 Context.reset 实现中,c.engine 是在从池中 New() 时由 Engine 自身设置的, - // reset 方法不会改变它。所以只要 c 是从 r.pool 获取的,c.engine 就应该是 r。 - - return c, r -} - -// CreateTestContextWithRequest 功能与 CreateTestContext 类似,但允许传入自定义的 *http.Request。 -// 这对于测试需要特定请求方法、URL、头部或Body的处理器非常有用。 -// -// 参数: -// - w (http.ResponseWriter): 可选。如果为 nil,创建一个 httptest.ResponseRecorder。 -// - req (*http.Request): 用户提供的 HTTP 请求。如果为 nil,则内部创建一个默认的 "GET /"。 -// -// 返回: -// - c (*Context): 一个初始化的 Touka Context。 -// - r (*Engine): 一个新的 Touka Engine 实例,与 c 相关联。 -func CreateTestContextWithRequest(w http.ResponseWriter, req *http.Request) (c *Context, r *Engine) { - var testResponseWriter http.ResponseWriter = w - if testResponseWriter == nil { - testResponseWriter = httptest.NewRecorder() - } - - r = New() // 创建 Engine - c = r.pool.Get().(*Context) // 从池获取 Context - - var finalReq *http.Request = req - if finalReq == nil { // 如果未提供请求,创建默认请求 - var err error - finalReq, err = http.NewRequest(http.MethodGet, "/", nil) - if err != nil { - panic("touka.CreateTestContextWithRequest: Failed to create dummy request: " + err.Error()) // 英文 panic - } - } - - c.reset(testResponseWriter, finalReq) // 使用提供的或默认的请求重置 Context - - // c.engine 已由 r.pool.New 设置为 r - - return c, r -} - -// PerformRequest 在给定的 Engine 上执行一个模拟的 HTTP 请求,并返回响应记录器。 -// 这是一个更高级别的测试辅助函数,封装了创建请求、Context 和执行引擎的 ServeHTTP 方法。 -// -// 参数: -// - engine (*Engine): 要测试的 Touka 引擎实例。 -// - method (string): HTTP 请求方法 (例如 "GET", "POST")。 -// - path (string): 请求的路径 (例如 "/", "/users/123?name=test")。 -// - body (io.Reader): 可选的请求体。对于 GET, HEAD 等通常为 nil。 -// - headers (http.Header): 可选的请求头部。 -// -// 返回: -// - *httptest.ResponseRecorder: 包含响应状态、头部和主体的记录器。 -// -// 示例: -// -// rr := touka.PerformRequest(myEngine, "GET", "/ping", nil, nil) -// assert.Equal(t, http.StatusOK, rr.Code) -// assert.Equal(t, "pong", rr.Body.String()) -func PerformRequest(engine *Engine, method, path string, body io.Reader, headers http.Header) *httptest.ResponseRecorder { - req, err := http.NewRequest(method, path, body) - if err != nil { - // 通常 NewRequest 对于合法的方法和路径不会失败(除非路径解析错误) - panic(fmt.Sprintf("touka.PerformRequest: Failed to create request %s %s: %v", method, path, err)) // 英文 panic - } - - // 设置请求头部 (如果提供) - if headers != nil { - req.Header = headers - } - - // 创建一个 ResponseRecorder 来捕获响应 - rr := httptest.NewRecorder() - - // 直接调用 Engine 的 ServeHTTP 方法来处理请求 - // Engine 会负责创建和管理 Context - engine.ServeHTTP(rr, req) - - return rr -} diff --git a/touka.go b/touka.go index 837d62d..ba8400d 100644 --- a/touka.go +++ b/touka.go @@ -1,7 +1,3 @@ -// 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/. -// Copyright 2024 WJQSERVER. All rights reserved. -// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization. package touka import ( @@ -46,28 +42,3 @@ type RouteInfo struct { Handler string // 处理函数名称 Group string // 路由分组 } - -// 维护一个Methods列表 -var ( - MethodGet = "GET" - MethodHead = "HEAD" - MethodPost = "POST" - MethodPut = "PUT" - MethodPatch = "PATCH" - MethodDelete = "DELETE" - MethodConnect = "CONNECT" - MethodOptions = "OPTIONS" - MethodTrace = "TRACE" -) - -var MethodsSet = map[string]struct{}{ - MethodGet: {}, - MethodHead: {}, - MethodPost: {}, - MethodPut: {}, - MethodPatch: {}, - MethodDelete: {}, - MethodConnect: {}, - MethodOptions: {}, - MethodTrace: {}, -} diff --git a/tree.go b/tree.go index 31246a5..6f99223 100644 --- a/tree.go +++ b/tree.go @@ -2,43 +2,51 @@ // Use of this source code is governed by a BSD-style license that can be found // at https://github.com/julienschmidt/httprouter/blob/master/LICENSE // This tree.go is gin's fork, you can see https://github.com/gin-gonic/gin/blob/master/tree.go -package touka + +package touka // 定义包名为 touka,该包可能是一个路由或Web框架的核心组件 import ( - "net/url" - "strings" - "unicode" - "unicode/utf8" - "unsafe" + "bytes" // 导入 bytes 包,用于操作字节切片 + "net/url" // 导入 net/url 包,用于 URL 解析和转义 + "strings" // 导入 strings 包,用于字符串操作 + "unicode" // 导入 unicode 包,用于处理 Unicode 字符 + "unicode/utf8" // 导入 unicode/utf8 包,用于 UTF-8 编码和解码 + "unsafe" // 导入 unsafe 包,用于不安全的类型转换,以避免内存分配 ) -// StringToBytes 将字符串转换为字节切片, 不进行内存分配. -// 更多详情, 请参见 https://github.com/golang/go/issues/53003#issuecomment-1140276077. -// 注意: 此函数使用 unsafe 包, 应谨慎使用, 因为它可能导致内存不安全. +// StringToBytes 将字符串转换为字节切片,不进行内存分配。 +// 更多详情,请参见 https://github.com/golang/go/issues/53003#issuecomment-1140276077。 +// 注意:此函数使用 unsafe 包,应谨慎使用,因为它可能导致内存不安全。 func StringToBytes(s string) []byte { return unsafe.Slice(unsafe.StringData(s), len(s)) } -// BytesToString 将字节切片转换为字符串, 不进行内存分配. -// 更多详情, 请参见 https://github.com/golang/go/issues/53003#issuecomment-1140276077. -// 注意: 此函数使用 unsafe 包, 应谨慎使用, 因为它可能导致内存不安全. +// BytesToString 将字节切片转换为字符串,不进行内存分配。 +// 更多详情,请参见 https://github.com/golang/go/issues/53003#issuecomment-1140276077。 +// 注意:此函数使用 unsafe 包,应谨慎使用,因为它可能导致内存不安全。 func BytesToString(b []byte) string { return unsafe.String(unsafe.SliceData(b), len(b)) } -// Param 是单个 URL 参数, 由键和值组成. +var ( + strColon = []byte(":") // 定义字节切片常量,表示冒号,用于路径参数识别 + strStar = []byte("*") // 定义字节切片常量,表示星号,用于捕获所有路径识别 + strSlash = []byte("/") // 定义字节切片常量,表示斜杠,用于路径分隔符识别 +) + +// Param 是单个 URL 参数,由键和值组成。 type Param struct { Key string // 参数的键名 Value string // 参数的值 } -// Params 是 Param 类型的切片, 由路由器返回. -// 该切片是有序的, 第一个 URL 参数也是切片中的第一个值. -// 因此, 按索引读取值是安全的. +// Params 是 Param 类型的切片,由路由器返回。 +// 该切片是有序的,第一个 URL 参数也是切片中的第一个值。 +// 因此,按索引读取值是安全的。 type Params []Param -// Get 返回键名与给定名称匹配的第一个 Param 的值, 并返回一个布尔值 true. -// 如果未找到匹配的 Param, 则返回空字符串和布尔值 false. +// Get 返回键名与给定名称匹配的第一个 Param 的值,并返回一个布尔值 true。 +// 如果未找到匹配的 Param,则返回空字符串和布尔值 false。 func (ps Params) Get(name string) (string, bool) { for _, entry := range ps { if entry.Key == name { @@ -48,24 +56,24 @@ func (ps Params) Get(name string) (string, bool) { return "", false } -// ByName 返回键名与给定名称匹配的第一个 Param 的值. -// 如果未找到匹配的 Param, 则返回空字符串. +// ByName 返回键名与给定名称匹配的第一个 Param 的值。 +// 如果未找到匹配的 Param,则返回空字符串。 func (ps Params) ByName(name string) (va string) { - va, _ = ps.Get(name) // 调用 Get 方法获取值, 忽略第二个返回值 + va, _ = ps.Get(name) // 调用 Get 方法获取值,忽略第二个返回值 return } -// methodTree 表示特定 HTTP 方法的路由树. +// methodTree 表示特定 HTTP 方法的路由树。 type methodTree struct { - method string // HTTP 方法(例如 "GET", "POST") + method string // HTTP 方法(例如 "GET", "POST") root *node // 该方法的根节点 } -// methodTrees 是 methodTree 的切片. +// methodTrees 是 methodTree 的切片。 type methodTrees []methodTree -// get 根据给定的 HTTP 方法查找并返回对应的根节点. -// 如果找不到, 则返回 nil. +// get 根据给定的 HTTP 方法查找并返回对应的根节点。 +// 如果找不到,则返回 nil。 func (trees methodTrees) get(method string) *node { for _, tree := range trees { if tree.method == method { @@ -75,7 +83,7 @@ func (trees methodTrees) get(method string) *node { return nil } -// longestCommonPrefix 计算两个字符串的最长公共前缀的长度. +// longestCommonPrefix 计算两个字符串的最长公共前缀的长度。 func longestCommonPrefix(a, b string) int { i := 0 max_ := min(len(a), len(b)) // 找出两个字符串中较短的长度 @@ -85,61 +93,64 @@ func longestCommonPrefix(a, b string) int { return i // 返回公共前缀的长度 } -// addChild 添加一个子节点, 并将通配符子节点(如果存在)保持在数组的末尾. +// addChild 添加一个子节点,并将通配符子节点(如果存在)保持在数组的末尾。 func (n *node) addChild(child *node) { if n.wildChild && len(n.children) > 0 { - // 如果当前节点有通配符子节点, 且已有子节点, 则将通配符子节点移到末尾 + // 如果当前节点有通配符子节点,且已有子节点,则将通配符子节点移到末尾 wildcardChild := n.children[len(n.children)-1] n.children = append(n.children[:len(n.children)-1], child, wildcardChild) } else { - // 否则, 直接添加子节点 + // 否则,直接添加子节点 n.children = append(n.children, child) } } -// countParams 计算路径中参数(冒号)和捕获所有(星号)的数量. +// countParams 计算路径中参数(冒号)和捕获所有(星号)的数量。 func countParams(path string) uint16 { - colons := strings.Count(path, ":") - stars := strings.Count(path, "*") - return uint16(colons + stars) + var n uint16 + s := StringToBytes(path) // 将路径字符串转换为字节切片 + n += uint16(bytes.Count(s, strColon)) // 统计冒号的数量 + n += uint16(bytes.Count(s, strStar)) // 统计星号的数量 + return n } -// countSections 计算路径中斜杠('/')的数量, 即路径段的数量. +// countSections 计算路径中斜杠('/')的数量,即路径段的数量。 func countSections(path string) uint16 { - return uint16(strings.Count(path, "/")) + s := StringToBytes(path) // 将路径字符串转换为字节切片 + return uint16(bytes.Count(s, strSlash)) // 统计斜杠的数量 } -// nodeType 定义了节点的类型. +// nodeType 定义了节点的类型。 type nodeType uint8 const ( - static nodeType = iota // 静态节点, 路径中不包含参数或通配符 + static nodeType = iota // 静态节点,路径中不包含参数或通配符 root // 根节点 - param // 参数节点(例如:name) - catchAll // 捕获所有节点(例如*path) + param // 参数节点(例如:name) + catchAll // 捕获所有节点(例如*path) ) -// node 表示路由树中的一个节点. +// node 表示路由树中的一个节点。 type node struct { path string // 当前节点的路径段 - indices string // 子节点第一个字符的索引字符串, 用于快速查找子节点 - wildChild bool // 是否包含通配符子节点(:param 或 *catchAll) - nType nodeType // 节点的类型(静态, 根, 参数, 捕获所有) - priority uint32 // 节点的优先级, 用于查找时优先匹配 - children []*node // 子节点切片, 最多有一个 :param 风格的节点位于数组末尾 + indices string // 子节点第一个字符的索引字符串,用于快速查找子节点 + wildChild bool // 是否包含通配符子节点(:param 或 *catchAll) + nType nodeType // 节点的类型(静态、根、参数、捕获所有) + priority uint32 // 节点的优先级,用于查找时优先匹配 + children []*node // 子节点切片,最多有一个 :param 风格的节点位于数组末尾 handlers HandlersChain // 绑定到此节点的处理函数链 - fullPath string // 完整路径, 用于调试和错误信息 + fullPath string // 完整路径,用于调试和错误信息 } -// incrementChildPrio 增加给定子节点的优先级并在必要时重新排序. +// incrementChildPrio 增加给定子节点的优先级并在必要时重新排序。 func (n *node) incrementChildPrio(pos int) int { cs := n.children // 获取子节点切片 cs[pos].priority++ // 增加指定位置子节点的优先级 prio := cs[pos].priority // 获取新的优先级 - // 调整位置(向前移动) + // 调整位置(向前移动) newPos := pos - // 从当前位置向前遍历, 如果前一个子节点的优先级小于当前子节点, 则交换位置 + // 从当前位置向前遍历,如果前一个子节点的优先级小于当前子节点,则交换位置 for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- { // 交换节点位置 cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1] @@ -147,9 +158,9 @@ func (n *node) incrementChildPrio(pos int) int { // 构建新的索引字符字符串 if newPos != pos { - // 如果位置发生变化, 则重新构建 indices 字符串 + // 如果位置发生变化,则重新构建 indices 字符串 // 前缀部分 + 移动的索引字符 + 剩余部分 - n.indices = n.indices[:newPos] + // 未改变的前缀, 可能为空 + n.indices = n.indices[:newPos] + // 未改变的前缀,可能为空 n.indices[pos:pos+1] + // 被移动的索引字符 n.indices[newPos:pos] + n.indices[pos+1:] // 除去原位置字符的其余部分 } @@ -157,13 +168,13 @@ func (n *node) incrementChildPrio(pos int) int { return newPos // 返回新的位置 } -// addRoute 为给定路径添加一个带有处理函数的节点. -// 非并发安全! +// addRoute 为给定路径添加一个带有处理函数的节点。 +// 非并发安全! func (n *node) addRoute(path string, handlers HandlersChain) { fullPath := path // 记录完整的路径 n.priority++ // 增加当前节点的优先级 - // 如果是空树(根节点) + // 如果是空树(根节点) if len(n.path) == 0 && len(n.children) == 0 { n.insertChild(path, fullPath, handlers) // 直接插入子节点 n.nType = root // 设置为根节点类型 @@ -174,12 +185,12 @@ func (n *node) addRoute(path string, handlers HandlersChain) { walk: // 外部循环用于遍历和构建路由树 for { - // 找到最长公共前缀. - // 这也意味着公共前缀不包含 ':' 或 '*',因为现有键不能包含这些字符. + // 找到最长公共前缀。 + // 这也意味着公共前缀不包含 ':' 或 '*',因为现有键不能包含这些字符。 i := longestCommonPrefix(path, n.path) // 分裂边 (Split edge) - // 如果公共前缀小于当前节点的路径长度, 说明当前节点需要被分裂 + // 如果公共前缀小于当前节点的路径长度,说明当前节点需要被分裂 if i < len(n.path) { child := node{ path: n.path[i:], // 子节点路径是当前节点路径的剩余部分 @@ -188,27 +199,27 @@ walk: // 外部循环用于遍历和构建路由树 indices: n.indices, // 继承索引 children: n.children, // 继承子节点 handlers: n.handlers, // 继承处理函数 - priority: n.priority - 1, // 优先级减1, 因为分裂会降低优先级 + priority: n.priority - 1, // 优先级减1,因为分裂会降低优先级 fullPath: n.fullPath, // 继承完整路径 } - n.children = []*node{&child} // 当前节点现在只有一个子节点: 新分裂出的子节点 + n.children = []*node{&child} // 当前节点现在只有一个子节点:新分裂出的子节点 // 将当前节点的 indices 设置为新子节点路径的第一个字符 n.indices = BytesToString([]byte{n.path[i]}) // []byte 用于正确的 Unicode 字符转换 n.path = path[:i] // 当前节点的路径更新为公共前缀 - n.handlers = nil // 当前节点不再有处理函数(因为它被分裂了) + n.handlers = nil // 当前节点不再有处理函数(因为它被分裂了) n.wildChild = false // 当前节点不再是通配符子节点 n.fullPath = fullPath[:parentFullPathIndex+i] // 更新完整路径 } // 将新节点作为当前节点的子节点 - // 如果路径仍然有剩余部分(即未完全匹配) + // 如果路径仍然有剩余部分(即未完全匹配) if i < len(path) { path = path[i:] // 移除已匹配的前缀 c := path[0] // 获取剩余路径的第一个字符 // '/' 在参数之后 - // 如果当前节点是参数类型, 且剩余路径以 '/' 开头, 并且只有一个子节点 + // 如果当前节点是参数类型,且剩余路径以 '/' 开头,并且只有一个子节点 // 则继续遍历其唯一的子节点 if n.nType == param && c == '/' && len(n.children) == 1 { parentFullPathIndex += len(n.path) // 更新父节点完整路径索引 @@ -227,8 +238,8 @@ walk: // 外部循环用于遍历和构建路由树 } } - // 否则, 插入新节点 - // 如果第一个字符不是 ':' 也不是 '*', 且当前节点不是 catchAll 类型 + // 否则,插入新节点 + // 如果第一个字符不是 ':' 也不是 '*',且当前节点不是 catchAll 类型 if c != ':' && c != '*' && n.nType != catchAll { // 将新字符添加到索引字符串 n.indices += BytesToString([]byte{c}) // []byte 用于正确的 Unicode 字符转换 @@ -239,18 +250,18 @@ walk: // 外部循环用于遍历和构建路由树 n.incrementChildPrio(len(n.indices) - 1) // 增加新子节点的优先级并重新排序 n = child // 移动到新子节点 } else if n.wildChild { - // 正在插入一个通配符节点, 需要检查是否与现有通配符冲突 + // 正在插入一个通配符节点,需要检查是否与现有通配符冲突 n = n.children[len(n.children)-1] // 移动到现有的通配符子节点 n.priority++ // 增加其优先级 // 检查通配符是否匹配 - // 如果剩余路径长度大于等于通配符节点的路径长度, 且通配符节点路径是剩余路径的前缀 - // 并且不是 catchAll 类型(不能有子路由), + // 如果剩余路径长度大于等于通配符节点的路径长度,且通配符节点路径是剩余路径的前缀 + // 并且不是 catchAll 类型(不能有子路由), // 并且通配符之后没有更多字符或紧跟着 '/' if len(path) >= len(n.path) && n.path == path[:len(n.path)] && // 不能向 catchAll 添加子节点 n.nType != catchAll && - // 检查更长的通配符, 例如 :name 和 :names + // 检查更长的通配符,例如 :name 和 :names (len(n.path) >= len(path) || path[len(n.path)] == '/') { continue walk // 继续外部循环 } @@ -258,7 +269,7 @@ walk: // 外部循环用于遍历和构建路由树 // 通配符冲突 pathSeg := path if n.nType != catchAll { - pathSeg, _, _ = strings.Cut(pathSeg, "/") // 如果不是 catchAll, 则截取到下一个 '/' + pathSeg, _, _ = strings.Cut(pathSeg, "/") // 如果不是 catchAll,则截取到下一个 '/' } prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path // 构造冲突前缀 panic("'" + pathSeg + // 抛出 panic 表示通配符冲突 @@ -268,13 +279,13 @@ walk: // 外部循环用于遍历和构建路由树 "'") } - n.insertChild(path, fullPath, handlers) // 插入子节点(可能包含通配符) + n.insertChild(path, fullPath, handlers) // 插入子节点(可能包含通配符) return // 完成添加路由 } - // 否则, 将处理函数添加到当前节点 + // 否则,将处理函数添加到当前节点 if n.handlers != nil { - panic("handlers are already registered for path '" + fullPath + "'") // 如果已注册处理函数, 则报错 + panic("handlers are already registered for path '" + fullPath + "'") // 如果已注册处理函数,则报错 } n.handlers = handlers // 设置处理函数 n.fullPath = fullPath // 设置完整路径 @@ -282,20 +293,20 @@ walk: // 外部循环用于遍历和构建路由树 } } -// findWildcard 搜索通配符段并检查名称是否包含无效字符. -// 如果未找到通配符, 则返回 -1 作为索引. +// findWildcard 搜索通配符段并检查名称是否包含无效字符。 +// 如果未找到通配符,则返回 -1 作为索引。 func findWildcard(path string) (wildcard string, i int, valid bool) { // 查找开始位置 escapeColon := false // 是否正在处理转义字符 for start, c := range []byte(path) { if escapeColon { escapeColon = false - if c == ':' { // 如果转义字符是 ':', 则跳过 + if c == ':' { // 如果转义字符是 ':',则跳过 continue } panic("invalid escape string in path '" + path + "'") // 无效的转义字符串 } - if c == '\\' { // 如果是反斜杠, 则设置转义标志 + if c == '\\' { // 如果是反斜杠,则设置转义标志 escapeColon = true continue } @@ -308,36 +319,36 @@ func findWildcard(path string) (wildcard string, i int, valid bool) { valid = true // 默认为有效 for end, c := range []byte(path[start+1:]) { switch c { - case '/': // 如果遇到斜杠, 说明通配符段结束 + case '/': // 如果遇到斜杠,说明通配符段结束 return path[start : start+1+end], start, valid - case ':', '*': // 如果在通配符段中再次遇到 ':' 或 '*', 则无效 + case ':', '*': // 如果在通配符段中再次遇到 ':' 或 '*',则无效 valid = false } } - return path[start:], start, valid // 返回找到的通配符, 起始索引和有效性 + return path[start:], start, valid // 返回找到的通配符、起始索引和有效性 } return "", -1, false // 未找到通配符 } -// insertChild 插入一个带有处理函数的节点. -// 此函数处理包含通配符的路径插入逻辑. +// insertChild 插入一个带有处理函数的节点。 +// 此函数处理包含通配符的路径插入逻辑。 func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) { for { // 找到第一个通配符之前的前缀 wildcard, i, valid := findWildcard(path) - if i < 0 { // 未找到通配符, 结束循环 + if i < 0 { // 未找到通配符,结束循环 break } // 通配符名称只能包含一个 ':' 或 '*' 字符 if !valid { panic("only one wildcard per path segment is allowed, has: '" + - wildcard + "' in path '" + fullPath + "'") // 报错: 每个路径段只允许一个通配符 + wildcard + "' in path '" + fullPath + "'") // 报错:每个路径段只允许一个通配符 } // 检查通配符是否有名称 if len(wildcard) < 2 { - panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") // 报错: 通配符必须有非空名称 + panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") // 报错:通配符必须有非空名称 } if wildcard[0] == ':' { // 如果是参数节点 (param) @@ -357,7 +368,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) n = child // 移动到新创建的参数节点 n.priority++ // 增加优先级 - // 如果路径不以通配符结束, 则会有一个以 '/' 开头的子路径 + // 如果路径不以通配符结束,则会有一个以 '/' 开头的子路径 if len(wildcard) < len(path) { path = path[len(wildcard):] // 剩余路径去除通配符部分 @@ -365,19 +376,19 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) priority: 1, // 新子节点优先级 fullPath: fullPath, // 设置子节点的完整路径 } - n.addChild(child) // 添加子节点(通常是斜杠后的静态部分) + n.addChild(child) // 添加子节点(通常是斜杠后的静态部分) n = child // 移动到这个新子节点 - continue // 继续循环, 查找下一个通配符或结束 + continue // 继续循环,查找下一个通配符或结束 } - // 否则, 我们已经完成. 将处理函数插入到新叶节点中 + // 否则,我们已经完成。将处理函数插入到新叶节点中 n.handlers = handlers // 设置处理函数 return // 完成 } // 如果是捕获所有节点 (catchAll) if i+len(wildcard) != len(path) { - panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") // 报错: 捕获所有路由只能在路径末尾 + panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") // 报错:捕获所有路由只能在路径末尾 } // 检查路径段冲突 @@ -386,34 +397,34 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) if len(n.children) != 0 { pathSeg, _, _ = strings.Cut(n.children[0].path, "/") } - panic("catch-all wildcard '" + path + // 报错: 捕获所有通配符与现有路径段冲突 + panic("catch-all wildcard '" + path + // 报错:捕获所有通配符与现有路径段冲突 "' in new path '" + fullPath + "' conflicts with existing path segment '" + pathSeg + "' in existing prefix '" + n.path + pathSeg + "'") } - // 当前固定宽度为 1, 用于 '/' + // 当前固定宽度为 1,用于 '/' i-- if i < 0 || path[i] != '/' { - panic("no / before catch-all in path '" + fullPath + "'") // 报错: 捕获所有之前没有 '/' + panic("no / before catch-all in path '" + fullPath + "'") // 报错:捕获所有之前没有 '/' } n.path = path[:i] // 当前节点路径更新为 catchAll 之前的部分 - // 第一个节点: 路径为空的 catchAll 节点 + // 第一个节点:路径为空的 catchAll 节点 child := &node{ wildChild: true, // 标记为有通配符子节点 nType: catchAll, // 类型为 catchAll fullPath: fullPath, // 设置完整路径 } - n.addChild(child) // 添加子节点 - n.indices = "/" // 索引设置为 '/' - n = child // 移动到新创建的 catchAll 节点 - n.priority++ // 增加优先级 + n.addChild(child) // 添加子节点 + n.indices = string('/') // 索引设置为 '/' + n = child // 移动到新创建的 catchAll 节点 + n.priority++ // 增加优先级 - // 第二个节点: 包含变量的节点 + // 第二个节点:包含变量的节点 child = &node{ path: path[i:], // 路径为 catchAll 的实际路径段 nType: catchAll, // 类型为 catchAll @@ -426,7 +437,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) return // 完成 } - // 如果没有找到通配符, 简单地插入路径和处理函数 + // 如果没有找到通配符,简单地插入路径和处理函数 n.path = path // 设置当前节点路径 n.handlers = handlers // 设置处理函数 n.fullPath = fullPath // 设置完整路径 @@ -440,16 +451,16 @@ type nodeValue struct { fullPath string // 匹配到的完整路径 } -// skippedNode 结构体用于在 getValue 查找过程中记录跳过的节点信息, 以便回溯. +// skippedNode 结构体用于在 getValue 查找过程中记录跳过的节点信息,以便回溯。 type skippedNode struct { path string // 跳过时的当前路径 node *node // 跳过的节点 paramsCount int16 // 跳过时已收集的参数数量 } -// getValue 返回注册到给定路径(key)的处理函数. 通配符的值会保存到 map 中. -// 如果找不到处理函数, 则在存在一个带有额外(或不带)尾部斜杠的处理函数时, -// 建议进行 TSR(尾部斜杠重定向). +// getValue 返回注册到给定路径(key)的处理函数。通配符的值会保存到 map 中。 +// 如果找不到处理函数,则在存在一个带有额外(或不带)尾部斜杠的处理函数时, +// 建议进行 TSR(尾部斜杠重定向)。 func (n *node) getValue(path string, params *Params, skippedNodes *[]skippedNode, unescape bool) (value nodeValue) { var globalParamsCount int16 // 全局参数计数 @@ -460,16 +471,11 @@ walk: // 外部循环用于遍历路由树 if path[:len(prefix)] == prefix { // 如果路径以当前节点的前缀开头 path = path[len(prefix):] // 移除已匹配的前缀 - // 在访问 path[0] 之前进行安全检查 - if len(path) == 0 { - continue walk - } - - // 优先尝试所有非通配符子节点, 通过匹配索引字符 + // 优先尝试所有非通配符子节点,通过匹配索引字符 idxc := path[0] // 剩余路径的第一个字符 for i, c := range []byte(n.indices) { if c == idxc { // 如果找到匹配的索引字符 - // 如果当前节点有通配符子节点, 则将当前节点添加到 skippedNodes, 以便回溯 + // 如果当前节点有通配符子节点,则将当前节点添加到 skippedNodes,以便回溯 if n.wildChild { index := len(*skippedNodes) *skippedNodes = (*skippedNodes)[:index+1] @@ -512,20 +518,20 @@ walk: // 外部循环用于遍历路由树 } } - // 未找到. - // 如果存在一个带有额外(或不带)尾部斜杠的处理函数, - // 我们可以建议重定向到相同 URL, 不带尾部斜杠. - value.tsr = path == "/" && n.handlers != nil // 如果路径是 "/" 且当前节点有处理函数, 则建议 TSR + // 未找到。 + // 如果存在一个带有额外(或不带)尾部斜杠的处理函数, + // 我们可以建议重定向到相同 URL,不带尾部斜杠。 + value.tsr = path == "/" && n.handlers != nil // 如果路径是 "/" 且当前节点有处理函数,则建议 TSR return value } - // 处理通配符子节点, 它总是位于数组的末尾 + // 处理通配符子节点,它总是位于数组的末尾 n = n.children[len(n.children)-1] // 移动到通配符子节点 globalParamsCount++ // 增加全局参数计数 switch n.nType { case param: // 参数节点 - // 查找参数结束位置('/' 或路径末尾) + // 查找参数结束位置('/' 或路径末尾) end := 0 for end < len(path) && path[end] != '/' { end++ @@ -533,7 +539,7 @@ walk: // 外部循环用于遍历路由树 // 保存参数值 if params != nil { - // 如果需要, 预分配容量 + // 如果需要,预分配容量 if cap(*params) < int(globalParamsCount) { newParams := make(Params, len(*params), globalParamsCount) copy(newParams, *params) @@ -553,12 +559,12 @@ walk: // 外部循环用于遍历路由树 } } (*value.params)[i] = Param{ // 存储参数 - Key: n.path[1:], // 参数键名(去除冒号) + Key: n.path[1:], // 参数键名(去除冒号) Value: val, // 参数值 } } - // 我们需要继续深入! + // 我们需要继续深入! if end < len(path) { if len(n.children) > 0 { path = path[end:] // 移除已提取的参数部分 @@ -567,16 +573,16 @@ walk: // 外部循环用于遍历路由树 } // ... 但我们无法继续 - value.tsr = len(path) == end+1 // 如果路径只剩下斜杠, 则建议 TSR + value.tsr = len(path) == end+1 // 如果路径只剩下斜杠,则建议 TSR return value } if value.handlers = n.handlers; value.handlers != nil { value.fullPath = n.fullPath - return value // 如果当前节点有处理函数, 则返回 + return value // 如果当前节点有处理函数,则返回 } if len(n.children) == 1 { - // 未找到处理函数. 检查是否存在此路径加尾部斜杠的处理函数, 以进行 TSR 建议 + // 未找到处理函数。检查是否存在此路径加尾部斜杠的处理函数,以进行 TSR 建议 n = n.children[0] value.tsr = (n.path == "/" && n.handlers != nil) || (n.path == "" && n.indices == "/") } @@ -585,7 +591,7 @@ walk: // 外部循环用于遍历路由树 case catchAll: // 捕获所有节点 // 保存参数值 if params != nil { - // 如果需要, 预分配容量 + // 如果需要,预分配容量 if cap(*params) < int(globalParamsCount) { newParams := make(Params, len(*params), globalParamsCount) copy(newParams, *params) @@ -605,7 +611,7 @@ walk: // 外部循环用于遍历路由树 } } (*value.params)[i] = Param{ // 存储参数 - Key: n.path[2:], // 参数键名(去除星号) + Key: n.path[2:], // 参数键名(去除星号) Value: val, // 参数值 } } @@ -621,7 +627,7 @@ walk: // 外部循环用于遍历路由树 } if path == prefix { // 如果路径完全匹配当前节点的前缀 - // 如果当前路径不等于 '/' 且节点没有注册的处理函数, 且最近匹配的节点有子节点 + // 如果当前路径不等于 '/' 且节点没有注册的处理函数,且最近匹配的节点有子节点 // 当前节点需要回溯到最后一个有效的 skippedNode if n.handlers == nil && path != "/" { for length := len(*skippedNodes); length > 0; length-- { @@ -638,26 +644,26 @@ walk: // 外部循环用于遍历路由树 } } } - // 我们应该已经到达包含处理函数的节点. - // 检查此节点是否注册了处理函数. + // 我们应该已经到达包含处理函数的节点。 + // 检查此节点是否注册了处理函数。 if value.handlers = n.handlers; value.handlers != nil { value.fullPath = n.fullPath - return value // 如果有处理函数, 则返回 + return value // 如果有处理函数,则返回 } - // 如果此路由没有处理函数, 但此路由有通配符子节点, - // 则此路径必须有一个带有额外尾部斜杠的处理函数. + // 如果此路由没有处理函数,但此路由有通配符子节点, + // 则此路径必须有一个带有额外尾部斜杠的处理函数。 if path == "/" && n.wildChild && n.nType != root { value.tsr = true // 建议 TSR return value } if path == "/" && n.nType == static { - value.tsr = true // 如果是静态节点且路径是根, 则建议 TSR + value.tsr = true // 如果是静态节点且路径是根,则建议 TSR return value } - // 未找到处理函数. 检查此路径加尾部斜杠是否存在处理函数, 以进行尾部斜杠重定向建议 + // 未找到处理函数。检查此路径加尾部斜杠是否存在处理函数,以进行尾部斜杠重定向建议 for i, c := range []byte(n.indices) { if c == '/' { // 如果索引中包含 '/' n = n.children[i] // 移动到对应的子节点 @@ -670,11 +676,11 @@ walk: // 外部循环用于遍历路由树 return value } - // 未找到. 我们可以建议重定向到相同 URL, 添加一个额外的尾部斜杠, - // 如果该路径的叶节点存在. + // 未找到。我们可以建议重定向到相同 URL,添加一个额外的尾部斜杠, + // 如果该路径的叶节点存在。 value.tsr = path == "/" || // 如果路径是根路径 (len(prefix) == len(path)+1 && prefix[len(path)] == '/' && // 或者前缀比路径多一个斜杠 - path == prefix[:len(prefix)-1] && n.handlers != nil) // 且路径是前缀去掉最后一个斜杠, 且有处理函数 + path == prefix[:len(prefix)-1] && n.handlers != nil) // 且路径是前缀去掉最后一个斜杠,且有处理函数 // 回溯到最后一个有效的 skippedNode if !value.tsr && path != "/" { @@ -697,17 +703,17 @@ walk: // 外部循环用于遍历路由树 } } -// findCaseInsensitivePath 对给定路径进行不区分大小写的查找, 并尝试找到处理函数. -// 它还可以选择修复尾部斜杠. -// 它返回大小写校正后的路径和一个布尔值, 指示查找是否成功. +// findCaseInsensitivePath 对给定路径进行不区分大小写的查找,并尝试找到处理函数。 +// 它还可以选择修复尾部斜杠。 +// 它返回大小写校正后的路径和一个布尔值,指示查找是否成功。 func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]byte, bool) { const stackBufSize = 128 // 栈上缓冲区的默认大小 - // 在常见情况下使用栈上静态大小的缓冲区. - // 如果路径太长, 则在堆上分配缓冲区. + // 在常见情况下使用栈上静态大小的缓冲区。 + // 如果路径太长,则在堆上分配缓冲区。 buf := make([]byte, 0, stackBufSize) if length := len(path) + 1; length > stackBufSize { - buf = make([]byte, 0, length) // 如果路径太长, 则分配更大的缓冲区 + buf = make([]byte, 0, length) // 如果路径太长,则分配更大的缓冲区 } ciPath := n.findCaseInsensitivePathRec( @@ -720,7 +726,7 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]by return ciPath, ciPath != nil // 返回校正后的路径和是否成功找到 } -// shiftNRuneBytes 将字节数组中的字节向左移动 n 个字节. +// shiftNRuneBytes 将字节数组中的字节向左移动 n 个字节。 func shiftNRuneBytes(rb [4]byte, n int) [4]byte { switch n { case 0: @@ -736,12 +742,12 @@ func shiftNRuneBytes(rb [4]byte, n int) [4]byte { } } -// findCaseInsensitivePathRec 由 n.findCaseInsensitivePath 使用的递归不区分大小写查找函数. +// findCaseInsensitivePathRec 由 n.findCaseInsensitivePath 使用的递归不区分大小写查找函数。 func (n *node) findCaseInsensitivePathRec(path string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) []byte { npLen := len(n.path) // 当前节点的路径长度 walk: // 外部循环用于遍历路由树 - // 只要剩余路径长度大于等于当前节点路径长度, 且当前节点路径(除第一个字符外)不区分大小写匹配剩余路径 + // 只要剩余路径长度大于等于当前节点路径长度,且当前节点路径(除第一个字符外)不区分大小写匹配剩余路径 for len(path) >= npLen && (npLen == 0 || strings.EqualFold(path[1:npLen], n.path[1:])) { // 将公共前缀添加到结果中 oldPath := path // 保存原始路径 @@ -749,13 +755,13 @@ walk: // 外部循环用于遍历路由树 ciPath = append(ciPath, n.path...) // 将当前节点的路径添加到不区分大小写路径中 if len(path) == 0 { // 如果路径已完全匹配 - // 我们应该已经到达包含处理函数的节点. - // 检查此节点是否注册了处理函数. + // 我们应该已经到达包含处理函数的节点。 + // 检查此节点是否注册了处理函数。 if n.handlers != nil { - return ciPath // 如果有处理函数, 则返回校正后的路径 + return ciPath // 如果有处理函数,则返回校正后的路径 } - // 未找到处理函数. + // 未找到处理函数。 // 尝试通过添加尾部斜杠来修复路径 if fixTrailingSlash { for i, c := range []byte(n.indices) { @@ -769,11 +775,11 @@ walk: // 外部循环用于遍历路由树 } } } - return nil // 未找到, 返回 nil + return nil // 未找到,返回 nil } - // 如果此节点没有通配符(参数或捕获所有)子节点, - // 我们可以直接查找下一个子节点并继续遍历树. + // 如果此节点没有通配符(参数或捕获所有)子节点, + // 我们可以直接查找下一个子节点并继续遍历树。 if !n.wildChild { // 跳过已处理的 rune 字节 rb = shiftNRuneBytes(rb, npLen) @@ -793,9 +799,9 @@ walk: // 外部循环用于遍历路由树 // 处理一个新的 rune var rv rune - // 查找 rune 的开始位置. - // Runes 最长为 4 字节. - // -4 肯定会是另一个 rune. + // 查找 rune 的开始位置。 + // Runes 最长为 4 字节。 + // -4 肯定会是另一个 rune。 var off int for max_ := min(npLen, 3); off < max_; off++ { if i := npLen - off; utf8.RuneStart(oldPath[i]) { @@ -816,17 +822,17 @@ walk: // 外部循环用于遍历路由树 for i, c := range []byte(n.indices) { // 小写匹配 if c == idxc { - // 必须使用递归方法, 因为大写字节和小写字节都可能作为索引存在 + // 必须使用递归方法,因为大写字节和小写字节都可能作为索引存在 if out := n.children[i].findCaseInsensitivePathRec( path, ciPath, rb, fixTrailingSlash, ); out != nil { - return out // 如果找到, 则返回 + return out // 如果找到,则返回 } break } } - // 如果未找到匹配项, 则对大写 rune 执行相同操作(如果它不同) + // 如果未找到匹配项,则对大写 rune 执行相同操作(如果它不同) if up := unicode.ToUpper(rv); up != lo { utf8.EncodeRune(rb[:], up) // 将大写 rune 编码到缓冲区 rb = shiftNRuneBytes(rb, off) @@ -844,18 +850,18 @@ walk: // 外部循环用于遍历路由树 } } - // 未找到. 我们可以建议重定向到相同 URL, 不带尾部斜杠, - // 如果该路径的叶节点存在. + // 未找到。我们可以建议重定向到相同 URL,不带尾部斜杠, + // 如果该路径的叶节点存在。 if fixTrailingSlash && path == "/" && n.handlers != nil { - return ciPath // 如果可以修复尾部斜杠且有处理函数, 则返回 + return ciPath // 如果可以修复尾部斜杠且有处理函数,则返回 } - return nil // 未找到, 返回 nil + return nil // 未找到,返回 nil } - n = n.children[0] // 移动到通配符子节点(通常是唯一一个) + n = n.children[0] // 移动到通配符子节点(通常是唯一一个) switch n.nType { case param: // 参数节点 - // 查找参数结束位置('/' 或路径末尾) + // 查找参数结束位置('/' 或路径末尾) end := 0 for end < len(path) && path[end] != '/' { end++ @@ -864,7 +870,7 @@ walk: // 外部循环用于遍历路由树 // 将参数值添加到不区分大小写路径中 ciPath = append(ciPath, path[:end]...) - // 我们需要继续深入! + // 我们需要继续深入! if end < len(path) { if len(n.children) > 0 { // 继续处理子节点 @@ -876,45 +882,45 @@ walk: // 外部循环用于遍历路由树 // ... 但我们无法继续 if fixTrailingSlash && len(path) == end+1 { - return ciPath // 如果可以修复尾部斜杠且路径只剩下斜杠, 则返回 + return ciPath // 如果可以修复尾部斜杠且路径只剩下斜杠,则返回 } - return nil // 未找到, 返回 nil + return nil // 未找到,返回 nil } if n.handlers != nil { - return ciPath // 如果有处理函数, 则返回 + return ciPath // 如果有处理函数,则返回 } if fixTrailingSlash && len(n.children) == 1 { - // 未找到处理函数. 检查此路径加尾部斜杠是否存在处理函数 + // 未找到处理函数。检查此路径加尾部斜杠是否存在处理函数 n = n.children[0] if n.path == "/" && n.handlers != nil { return append(ciPath, '/') // 返回添加斜杠后的路径 } } - return nil // 未找到, 返回 nil + return nil // 未找到,返回 nil case catchAll: // 捕获所有节点 - return append(ciPath, path...) // 返回添加剩余路径后的路径(捕获所有) + return append(ciPath, path...) // 返回添加剩余路径后的路径(捕获所有) default: panic("invalid node type") // 无效的节点类型 } } - // 未找到. + // 未找到。 // 尝试通过添加/删除尾部斜杠来修复路径 if fixTrailingSlash { if path == "/" { - return ciPath // 如果路径是根路径, 则返回 + return ciPath // 如果路径是根路径,则返回 } - // 如果路径长度比当前节点路径少一个斜杠, 且末尾是斜杠, - // 且不区分大小写匹配, 且当前节点有处理函数 + // 如果路径长度比当前节点路径少一个斜杠,且末尾是斜杠, + // 且不区分大小写匹配,且当前节点有处理函数 if len(path)+1 == npLen && n.path[len(path)] == '/' && strings.EqualFold(path[1:], n.path[1:len(path)]) && n.handlers != nil { return append(ciPath, n.path...) // 返回添加当前节点路径后的路径 } } - return nil // 未找到, 返回 nil + return nil // 未找到,返回 nil } diff --git a/tree_test.go b/tree_test.go deleted file mode 100644 index d3ffdfa..0000000 --- a/tree_test.go +++ /dev/null @@ -1,1078 +0,0 @@ -// Copyright 2013 Julien Schmidt. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be found -// at https://github.com/julienschmidt/httprouter/blob/master/LICENSE -// This tree_test.go is gin's fork, you can see https://github.com/gin-gonic/gin/blob/master/tree_test.go - -package touka - -import ( - "fmt" - "reflect" - "regexp" - "strings" - "testing" -) - -// Used as a workaround since we can't compare functions or their addresses -var fakeHandlerValue string - -func fakeHandler(val string) HandlersChain { - return HandlersChain{func(c *Context) { - fakeHandlerValue = val - }} -} - -type testRequests []struct { - path string - nilHandler bool - route string - ps Params -} - -func getParams() *Params { - ps := make(Params, 0, 20) - return &ps -} - -func getSkippedNodes() *[]skippedNode { - ps := make([]skippedNode, 0, 20) - return &ps -} - -func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ...bool) { - unescape := false - if len(unescapes) >= 1 { - unescape = unescapes[0] - } - - for _, request := range requests { - value := tree.getValue(request.path, getParams(), getSkippedNodes(), unescape) - - if value.handlers == nil { - if !request.nilHandler { - t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path) - } - } else if request.nilHandler { - t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path) - } else { - value.handlers[0](nil) - if fakeHandlerValue != request.route { - t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route) - } - } - - if value.params != nil { - if !reflect.DeepEqual(*value.params, request.ps) { - t.Errorf("Params mismatch for route '%s'", request.path) - } - } - - } -} - -func checkPriorities(t *testing.T, n *node) uint32 { - var prio uint32 - for i := range n.children { - prio += checkPriorities(t, n.children[i]) - } - - if n.handlers != nil { - prio++ - } - - if n.priority != prio { - t.Errorf( - "priority mismatch for node '%s': is %d, should be %d", - n.path, n.priority, prio, - ) - } - - return prio -} - -func TestCountParams(t *testing.T) { - if countParams("/path/:param1/static/*catch-all") != 2 { - t.Fail() - } - if countParams(strings.Repeat("/:param", 256)) != 256 { - t.Fail() - } -} - -func TestTreeAddAndGet(t *testing.T) { - tree := &node{} - - routes := [...]string{ - "/hi", - "/contact", - "/co", - "/c", - "/a", - "/ab", - "/doc/", - "/doc/go_faq.html", - "/doc/go1.html", - "/α", - "/β", - } - for _, route := range routes { - tree.addRoute(route, fakeHandler(route)) - } - - checkRequests(t, tree, testRequests{ - {"/a", false, "/a", nil}, - {"/", true, "", nil}, - {"/hi", false, "/hi", nil}, - {"/contact", false, "/contact", nil}, - {"/co", false, "/co", nil}, - {"/con", true, "", nil}, // key mismatch - {"/cona", true, "", nil}, // key mismatch - {"/no", true, "", nil}, // no matching child - {"/ab", false, "/ab", nil}, - {"/α", false, "/α", nil}, - {"/β", false, "/β", nil}, - }) - - checkPriorities(t, tree) -} - -func TestTreeWildcard(t *testing.T) { - tree := &node{} - - routes := [...]string{ - "/", - "/cmd/:tool/", - "/cmd/:tool/:sub", - "/cmd/whoami", - "/cmd/whoami/root", - "/cmd/whoami/root/", - "/src/*filepath", - "/search/", - "/search/:query", - "/search/gin-gonic", - "/search/google", - "/user_:name", - "/user_:name/about", - "/files/:dir/*filepath", - "/doc/", - "/doc/go_faq.html", - "/doc/go1.html", - "/info/:user/public", - "/info/:user/project/:project", - "/info/:user/project/:project/*filepath", - "/info/:user/project/golang", - "/aa/*xx", - "/ab/*xx", - "/:cc", - "/c1/:dd/e", - "/c1/:dd/e1", - "/:cc/cc", - "/:cc/:dd/ee", - "/:cc/:dd/:ee/ff", - "/:cc/:dd/:ee/:ff/gg", - "/:cc/:dd/:ee/:ff/:gg/hh", - "/get/test/abc/", - "/get/:param/abc/", - "/something/:paramname/thirdthing", - "/something/secondthing/test", - "/get/abc", - "/get/:param", - "/get/abc/123abc", - "/get/abc/:param", - "/get/abc/123abc/xxx8", - "/get/abc/123abc/:param", - "/get/abc/123abc/xxx8/1234", - "/get/abc/123abc/xxx8/:param", - "/get/abc/123abc/xxx8/1234/ffas", - "/get/abc/123abc/xxx8/1234/:param", - "/get/abc/123abc/xxx8/1234/kkdd/12c", - "/get/abc/123abc/xxx8/1234/kkdd/:param", - "/get/abc/:param/test", - "/get/abc/123abd/:param", - "/get/abc/123abddd/:param", - "/get/abc/123/:param", - "/get/abc/123abg/:param", - "/get/abc/123abf/:param", - "/get/abc/123abfff/:param", - "/get/abc/escaped_colon/test\\:param", - } - for _, route := range routes { - tree.addRoute(route, fakeHandler(route)) - } - - checkRequests(t, tree, testRequests{ - {"/", false, "/", nil}, - {"/cmd/test", true, "/cmd/:tool/", Params{Param{"tool", "test"}}}, - {"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}}, - {"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "test"}, Param{Key: "sub", Value: "3"}}}, - {"/cmd/who", true, "/cmd/:tool/", Params{Param{"tool", "who"}}}, - {"/cmd/who/", false, "/cmd/:tool/", Params{Param{"tool", "who"}}}, - {"/cmd/whoami", false, "/cmd/whoami", nil}, - {"/cmd/whoami/", true, "/cmd/whoami", nil}, - {"/cmd/whoami/r", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "whoami"}, Param{Key: "sub", Value: "r"}}}, - {"/cmd/whoami/r/", true, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "whoami"}, Param{Key: "sub", Value: "r"}}}, - {"/cmd/whoami/root", false, "/cmd/whoami/root", nil}, - {"/cmd/whoami/root/", false, "/cmd/whoami/root/", nil}, - {"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}}, - {"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}}, - {"/search/", false, "/search/", nil}, - {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}}, - {"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}}, - {"/search/gin", false, "/search/:query", Params{Param{"query", "gin"}}}, - {"/search/gin-gonic", false, "/search/gin-gonic", nil}, - {"/search/google", false, "/search/google", nil}, - {"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}}, - {"/user_gopher/about", false, "/user_:name/about", Params{Param{Key: "name", Value: "gopher"}}}, - {"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{Key: "dir", Value: "js"}, Param{Key: "filepath", Value: "/inc/framework.js"}}}, - {"/info/gordon/public", false, "/info/:user/public", Params{Param{Key: "user", Value: "gordon"}}}, - {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}}, - {"/info/gordon/project/golang", false, "/info/:user/project/golang", Params{Param{Key: "user", Value: "gordon"}}}, - {"/info/gordon/project/go/src/file.go", false, "/info/:user/project/:project/*filepath", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}, Param{Key: "filepath", Value: "/src/file.go"}}}, - {"/aa/aa", false, "/aa/*xx", Params{Param{Key: "xx", Value: "/aa"}}}, - {"/ab/ab", false, "/ab/*xx", Params{Param{Key: "xx", Value: "/ab"}}}, - {"/a", false, "/:cc", Params{Param{Key: "cc", Value: "a"}}}, - // * Error with argument being intercepted - // new PR handle (/all /all/cc /a/cc) - // fix PR: https://github.com/gin-gonic/gin/pull/2796 - {"/all", false, "/:cc", Params{Param{Key: "cc", Value: "all"}}}, - {"/d", false, "/:cc", Params{Param{Key: "cc", Value: "d"}}}, - {"/ad", false, "/:cc", Params{Param{Key: "cc", Value: "ad"}}}, - {"/dd", false, "/:cc", Params{Param{Key: "cc", Value: "dd"}}}, - {"/dddaa", false, "/:cc", Params{Param{Key: "cc", Value: "dddaa"}}}, - {"/aa", false, "/:cc", Params{Param{Key: "cc", Value: "aa"}}}, - {"/aaa", false, "/:cc", Params{Param{Key: "cc", Value: "aaa"}}}, - {"/aaa/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "aaa"}}}, - {"/ab", false, "/:cc", Params{Param{Key: "cc", Value: "ab"}}}, - {"/abb", false, "/:cc", Params{Param{Key: "cc", Value: "abb"}}}, - {"/abb/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "abb"}}}, - {"/allxxxx", false, "/:cc", Params{Param{Key: "cc", Value: "allxxxx"}}}, - {"/alldd", false, "/:cc", Params{Param{Key: "cc", Value: "alldd"}}}, - {"/all/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "all"}}}, - {"/a/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "a"}}}, - {"/c1/d/e", false, "/c1/:dd/e", Params{Param{Key: "dd", Value: "d"}}}, - {"/c1/d/e1", false, "/c1/:dd/e1", Params{Param{Key: "dd", Value: "d"}}}, - {"/c1/d/ee", false, "/:cc/:dd/ee", Params{Param{Key: "cc", Value: "c1"}, Param{Key: "dd", Value: "d"}}}, - {"/cc/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "cc"}}}, - {"/ccc/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "ccc"}}}, - {"/deedwjfs/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "deedwjfs"}}}, - {"/acllcc/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "acllcc"}}}, - {"/get/test/abc/", false, "/get/test/abc/", nil}, - {"/get/te/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "te"}}}, - {"/get/testaa/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "testaa"}}}, - {"/get/xx/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "xx"}}}, - {"/get/tt/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "tt"}}}, - {"/get/a/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "a"}}}, - {"/get/t/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "t"}}}, - {"/get/aa/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "aa"}}}, - {"/get/abas/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "abas"}}}, - {"/something/secondthing/test", false, "/something/secondthing/test", nil}, - {"/something/abcdad/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "abcdad"}}}, - {"/something/secondthingaaaa/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "secondthingaaaa"}}}, - {"/something/se/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "se"}}}, - {"/something/s/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "s"}}}, - {"/c/d/ee", false, "/:cc/:dd/ee", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}}}, - {"/c/d/e/ff", false, "/:cc/:dd/:ee/ff", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}}}, - {"/c/d/e/f/gg", false, "/:cc/:dd/:ee/:ff/gg", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}, Param{Key: "ff", Value: "f"}}}, - {"/c/d/e/f/g/hh", false, "/:cc/:dd/:ee/:ff/:gg/hh", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}, Param{Key: "ff", Value: "f"}, Param{Key: "gg", Value: "g"}}}, - {"/cc/dd/ee/ff/gg/hh", false, "/:cc/:dd/:ee/:ff/:gg/hh", Params{Param{Key: "cc", Value: "cc"}, Param{Key: "dd", Value: "dd"}, Param{Key: "ee", Value: "ee"}, Param{Key: "ff", Value: "ff"}, Param{Key: "gg", Value: "gg"}}}, - {"/get/abc", false, "/get/abc", nil}, - {"/get/a", false, "/get/:param", Params{Param{Key: "param", Value: "a"}}}, - {"/get/abz", false, "/get/:param", Params{Param{Key: "param", Value: "abz"}}}, - {"/get/12a", false, "/get/:param", Params{Param{Key: "param", Value: "12a"}}}, - {"/get/abcd", false, "/get/:param", Params{Param{Key: "param", Value: "abcd"}}}, - {"/get/abc/123abc", false, "/get/abc/123abc", nil}, - {"/get/abc/12", false, "/get/abc/:param", Params{Param{Key: "param", Value: "12"}}}, - {"/get/abc/123ab", false, "/get/abc/:param", Params{Param{Key: "param", Value: "123ab"}}}, - {"/get/abc/xyz", false, "/get/abc/:param", Params{Param{Key: "param", Value: "xyz"}}}, - {"/get/abc/123abcddxx", false, "/get/abc/:param", Params{Param{Key: "param", Value: "123abcddxx"}}}, - {"/get/abc/123abc/xxx8", false, "/get/abc/123abc/xxx8", nil}, - {"/get/abc/123abc/x", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "x"}}}, - {"/get/abc/123abc/xxx", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "xxx"}}}, - {"/get/abc/123abc/abc", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "abc"}}}, - {"/get/abc/123abc/xxx8xxas", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "xxx8xxas"}}}, - {"/get/abc/123abc/xxx8/1234", false, "/get/abc/123abc/xxx8/1234", nil}, - {"/get/abc/123abc/xxx8/1", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "1"}}}, - {"/get/abc/123abc/xxx8/123", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "123"}}}, - {"/get/abc/123abc/xxx8/78k", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "78k"}}}, - {"/get/abc/123abc/xxx8/1234xxxd", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "1234xxxd"}}}, - {"/get/abc/123abc/xxx8/1234/ffas", false, "/get/abc/123abc/xxx8/1234/ffas", nil}, - {"/get/abc/123abc/xxx8/1234/f", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "f"}}}, - {"/get/abc/123abc/xxx8/1234/ffa", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "ffa"}}}, - {"/get/abc/123abc/xxx8/1234/kka", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "kka"}}}, - {"/get/abc/123abc/xxx8/1234/ffas321", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "ffas321"}}}, - {"/get/abc/123abc/xxx8/1234/kkdd/12c", false, "/get/abc/123abc/xxx8/1234/kkdd/12c", nil}, - {"/get/abc/123abc/xxx8/1234/kkdd/1", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "1"}}}, - {"/get/abc/123abc/xxx8/1234/kkdd/12", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "12"}}}, - {"/get/abc/123abc/xxx8/1234/kkdd/12b", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "12b"}}}, - {"/get/abc/123abc/xxx8/1234/kkdd/34", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "34"}}}, - {"/get/abc/123abc/xxx8/1234/kkdd/12c2e3", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "12c2e3"}}}, - {"/get/abc/12/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "12"}}}, - {"/get/abc/123abdd/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abdd"}}}, - {"/get/abc/123abdddf/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abdddf"}}}, - {"/get/abc/123ab/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123ab"}}}, - {"/get/abc/123abgg/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abgg"}}}, - {"/get/abc/123abff/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abff"}}}, - {"/get/abc/123abffff/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abffff"}}}, - {"/get/abc/123abd/test", false, "/get/abc/123abd/:param", Params{Param{Key: "param", Value: "test"}}}, - {"/get/abc/123abddd/test", false, "/get/abc/123abddd/:param", Params{Param{Key: "param", Value: "test"}}}, - {"/get/abc/123/test22", false, "/get/abc/123/:param", Params{Param{Key: "param", Value: "test22"}}}, - {"/get/abc/123abg/test", false, "/get/abc/123abg/:param", Params{Param{Key: "param", Value: "test"}}}, - {"/get/abc/123abf/testss", false, "/get/abc/123abf/:param", Params{Param{Key: "param", Value: "testss"}}}, - {"/get/abc/123abfff/te", false, "/get/abc/123abfff/:param", Params{Param{Key: "param", Value: "te"}}}, - {"/get/abc/escaped_colon/test\\:param", false, "/get/abc/escaped_colon/test\\:param", nil}, - }) - - checkPriorities(t, tree) -} - -func TestUnescapeParameters(t *testing.T) { - tree := &node{} - - routes := [...]string{ - "/", - "/cmd/:tool/:sub", - "/cmd/:tool/", - "/src/*filepath", - "/search/:query", - "/files/:dir/*filepath", - "/info/:user/project/:project", - "/info/:user", - } - for _, route := range routes { - tree.addRoute(route, fakeHandler(route)) - } - - unescape := true - checkRequests(t, tree, testRequests{ - {"/", false, "/", nil}, - {"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}}, - {"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}}, - {"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}}, - {"/src/some/file+test.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file test.png"}}}, - {"/src/some/file++++%%%%test.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file++++%%%%test.png"}}}, - {"/src/some/file%2Ftest.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file/test.png"}}}, - {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng in ünìcodé"}}}, - {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}}, - {"/info/slash%2Fgordon", false, "/info/:user", Params{Param{Key: "user", Value: "slash/gordon"}}}, - {"/info/slash%2Fgordon/project/Project%20%231", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "slash/gordon"}, Param{Key: "project", Value: "Project #1"}}}, - {"/info/slash%%%%", false, "/info/:user", Params{Param{Key: "user", Value: "slash%%%%"}}}, - {"/info/slash%%%%2Fgordon/project/Project%%%%20%231", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "slash%%%%2Fgordon"}, Param{Key: "project", Value: "Project%%%%20%231"}}}, - }, unescape) - - checkPriorities(t, tree) -} - -func catchPanic(testFunc func()) (recv any) { - defer func() { - recv = recover() - }() - - testFunc() - return -} - -type testRoute struct { - path string - conflict bool -} - -func testRoutes(t *testing.T, routes []testRoute) { - tree := &node{} - - for _, route := range routes { - recv := catchPanic(func() { - tree.addRoute(route.path, nil) - }) - - if route.conflict { - if recv == nil { - t.Errorf("no panic for conflicting route '%s'", route.path) - } - } else if recv != nil { - t.Errorf("unexpected panic for route '%s': %v", route.path, recv) - } - } -} - -func TestTreeWildcardConflict(t *testing.T) { - routes := []testRoute{ - {"/cmd/:tool/:sub", false}, - {"/cmd/vet", false}, - {"/foo/bar", false}, - {"/foo/:name", false}, - {"/foo/:names", true}, - {"/cmd/*path", true}, - {"/cmd/:badvar", true}, - {"/cmd/:tool/names", false}, - {"/cmd/:tool/:badsub/details", true}, - {"/src/*filepath", false}, - {"/src/:file", true}, - {"/src/static.json", true}, - {"/src/*filepathx", true}, - {"/src/", true}, - {"/src/foo/bar", true}, - {"/src1/", false}, - {"/src1/*filepath", true}, - {"/src2*filepath", true}, - {"/src2/*filepath", false}, - {"/search/:query", false}, - {"/search/valid", false}, - {"/user_:name", false}, - {"/user_x", false}, - {"/user_:name", false}, - {"/id:id", false}, - {"/id/:id", false}, - {"/static/*file", false}, - {"/static/", true}, - {"/escape/test\\:d1", false}, - {"/escape/test\\:d2", false}, - {"/escape/test:param", false}, - } - testRoutes(t, routes) -} - -func TestCatchAllAfterSlash(t *testing.T) { - routes := []testRoute{ - {"/non-leading-*catchall", true}, - } - testRoutes(t, routes) -} - -func TestTreeChildConflict(t *testing.T) { - routes := []testRoute{ - {"/cmd/vet", false}, - {"/cmd/:tool", false}, - {"/cmd/:tool/:sub", false}, - {"/cmd/:tool/misc", false}, - {"/cmd/:tool/:othersub", true}, - {"/src/AUTHORS", false}, - {"/src/*filepath", true}, - {"/user_x", false}, - {"/user_:name", false}, - {"/id/:id", false}, - {"/id:id", false}, - {"/:id", false}, - {"/*filepath", true}, - } - testRoutes(t, routes) -} - -func TestTreeDuplicatePath(t *testing.T) { - tree := &node{} - - routes := [...]string{ - "/", - "/doc/", - "/src/*filepath", - "/search/:query", - "/user_:name", - } - for _, route := range routes { - recv := catchPanic(func() { - tree.addRoute(route, fakeHandler(route)) - }) - if recv != nil { - t.Fatalf("panic inserting route '%s': %v", route, recv) - } - - // Add again - recv = catchPanic(func() { - tree.addRoute(route, nil) - }) - if recv == nil { - t.Fatalf("no panic while inserting duplicate route '%s", route) - } - } - - // printChildren(tree, "") - - checkRequests(t, tree, testRequests{ - {"/", false, "/", nil}, - {"/doc/", false, "/doc/", nil}, - {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}}, - {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, - {"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}}, - }) -} - -func TestEmptyWildcardName(t *testing.T) { - tree := &node{} - - routes := [...]string{ - "/user:", - "/user:/", - "/cmd/:/", - "/src/*", - } - for _, route := range routes { - recv := catchPanic(func() { - tree.addRoute(route, nil) - }) - if recv == nil { - t.Fatalf("no panic while inserting route with empty wildcard name '%s", route) - } - } -} - -func TestTreeCatchAllConflict(t *testing.T) { - routes := []testRoute{ - {"/src/*filepath/x", true}, - {"/src2/", false}, - {"/src2/*filepath/x", true}, - {"/src3/*filepath", false}, - {"/src3/*filepath/x", true}, - } - testRoutes(t, routes) -} - -func TestTreeCatchAllConflictRoot(t *testing.T) { - routes := []testRoute{ - {"/", false}, - {"/*filepath", true}, - } - testRoutes(t, routes) -} - -func TestTreeCatchMaxParams(t *testing.T) { - tree := &node{} - route := "/cmd/*filepath" - tree.addRoute(route, fakeHandler(route)) -} - -func TestTreeDoubleWildcard(t *testing.T) { - const panicMsg = "only one wildcard per path segment is allowed" - - routes := [...]string{ - "/:foo:bar", - "/:foo:bar/", - "/:foo*bar", - } - - for _, route := range routes { - tree := &node{} - recv := catchPanic(func() { - tree.addRoute(route, nil) - }) - - if rs, ok := recv.(string); !ok || !strings.HasPrefix(rs, panicMsg) { - t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsg, route, recv) - } - } -} - -/*func TestTreeDuplicateWildcard(t *testing.T) { - tree := &node{} - routes := [...]string{ - "/:id/:name/:id", - } - for _, route := range routes { - ... - } -}*/ - -func TestTreeTrailingSlashRedirect(t *testing.T) { - tree := &node{} - - routes := [...]string{ - "/hi", - "/b/", - "/search/:query", - "/cmd/:tool/", - "/src/*filepath", - "/x", - "/x/y", - "/y/", - "/y/z", - "/0/:id", - "/0/:id/1", - "/1/:id/", - "/1/:id/2", - "/aa", - "/a/", - "/admin", - "/admin/:category", - "/admin/:category/:page", - "/doc", - "/doc/go_faq.html", - "/doc/go1.html", - "/no/a", - "/no/b", - "/api/:page/:name", - "/api/hello/:name/bar/", - "/api/bar/:name", - "/api/baz/foo", - "/api/baz/foo/bar", - "/blog/:p", - "/posts/:b/:c", - "/posts/b/:c/d/", - "/vendor/:x/*y", - } - for _, route := range routes { - recv := catchPanic(func() { - tree.addRoute(route, fakeHandler(route)) - }) - if recv != nil { - t.Fatalf("panic inserting route '%s': %v", route, recv) - } - } - - tsrRoutes := [...]string{ - "/hi/", - "/b", - "/search/gopher/", - "/cmd/vet", - "/src", - "/x/", - "/y", - "/0/go/", - "/1/go", - "/a", - "/admin/", - "/admin/config/", - "/admin/config/permissions/", - "/doc/", - "/admin/static/", - "/admin/cfg/", - "/admin/cfg/users/", - "/api/hello/x/bar", - "/api/baz/foo/", - "/api/baz/bax/", - "/api/bar/huh/", - "/api/baz/foo/bar/", - "/api/world/abc/", - "/blog/pp/", - "/posts/b/c/d", - "/vendor/x", - } - - for _, route := range tsrRoutes { - value := tree.getValue(route, nil, getSkippedNodes(), false) - if value.handlers != nil { - t.Fatalf("non-nil handler for TSR route '%s", route) - } else if !value.tsr { - t.Errorf("expected TSR recommendation for route '%s'", route) - } - } - - noTsrRoutes := [...]string{ - "/", - "/no", - "/no/", - "/_", - "/_/", - "/api", - "/api/", - "/api/hello/x/foo", - "/api/baz/foo/bad", - "/foo/p/p", - } - for _, route := range noTsrRoutes { - value := tree.getValue(route, nil, getSkippedNodes(), false) - if value.handlers != nil { - t.Fatalf("non-nil handler for No-TSR route '%s", route) - } else if value.tsr { - t.Errorf("expected no TSR recommendation for route '%s'", route) - } - } -} - -func TestTreeRootTrailingSlashRedirect(t *testing.T) { - tree := &node{} - - recv := catchPanic(func() { - tree.addRoute("/:test", fakeHandler("/:test")) - }) - if recv != nil { - t.Fatalf("panic inserting test route: %v", recv) - } - - value := tree.getValue("/", nil, getSkippedNodes(), false) - if value.handlers != nil { - t.Fatalf("non-nil handler") - } else if value.tsr { - t.Errorf("expected no TSR recommendation") - } -} - -func TestRedirectTrailingSlash(t *testing.T) { - data := []struct { - path string - }{ - {"/hello/:name"}, - {"/hello/:name/123"}, - {"/hello/:name/234"}, - } - - node := &node{} - for _, item := range data { - node.addRoute(item.path, fakeHandler("test")) - } - - value := node.getValue("/hello/abx/", nil, getSkippedNodes(), false) - if value.tsr != true { - t.Fatalf("want true, is false") - } -} - -func TestTreeFindCaseInsensitivePath(t *testing.T) { - tree := &node{} - - longPath := "/l" + strings.Repeat("o", 128) + "ng" - lOngPath := "/l" + strings.Repeat("O", 128) + "ng/" - - routes := [...]string{ - "/hi", - "/b/", - "/ABC/", - "/search/:query", - "/cmd/:tool/", - "/src/*filepath", - "/x", - "/x/y", - "/y/", - "/y/z", - "/0/:id", - "/0/:id/1", - "/1/:id/", - "/1/:id/2", - "/aa", - "/a/", - "/doc", - "/doc/go_faq.html", - "/doc/go1.html", - "/doc/go/away", - "/no/a", - "/no/b", - "/Π", - "/u/apfêl/", - "/u/äpfêl/", - "/u/öpfêl", - "/v/Äpfêl/", - "/v/Öpfêl", - "/w/♬", // 3 byte - "/w/♭/", // 3 byte, last byte differs - "/w/𠜎", // 4 byte - "/w/𠜏/", // 4 byte - longPath, - } - - for _, route := range routes { - recv := catchPanic(func() { - tree.addRoute(route, fakeHandler(route)) - }) - if recv != nil { - t.Fatalf("panic inserting route '%s': %v", route, recv) - } - } - - // Check out == in for all registered routes - // With fixTrailingSlash = true - for _, route := range routes { - out, found := tree.findCaseInsensitivePath(route, true) - if !found { - t.Errorf("Route '%s' not found!", route) - } else if string(out) != route { - t.Errorf("Wrong result for route '%s': %s", route, string(out)) - } - } - // With fixTrailingSlash = false - for _, route := range routes { - out, found := tree.findCaseInsensitivePath(route, false) - if !found { - t.Errorf("Route '%s' not found!", route) - } else if string(out) != route { - t.Errorf("Wrong result for route '%s': %s", route, string(out)) - } - } - - tests := []struct { - in string - out string - found bool - slash bool - }{ - {"/HI", "/hi", true, false}, - {"/HI/", "/hi", true, true}, - {"/B", "/b/", true, true}, - {"/B/", "/b/", true, false}, - {"/abc", "/ABC/", true, true}, - {"/abc/", "/ABC/", true, false}, - {"/aBc", "/ABC/", true, true}, - {"/aBc/", "/ABC/", true, false}, - {"/abC", "/ABC/", true, true}, - {"/abC/", "/ABC/", true, false}, - {"/SEARCH/QUERY", "/search/QUERY", true, false}, - {"/SEARCH/QUERY/", "/search/QUERY", true, true}, - {"/CMD/TOOL/", "/cmd/TOOL/", true, false}, - {"/CMD/TOOL", "/cmd/TOOL/", true, true}, - {"/SRC/FILE/PATH", "/src/FILE/PATH", true, false}, - {"/x/Y", "/x/y", true, false}, - {"/x/Y/", "/x/y", true, true}, - {"/X/y", "/x/y", true, false}, - {"/X/y/", "/x/y", true, true}, - {"/X/Y", "/x/y", true, false}, - {"/X/Y/", "/x/y", true, true}, - {"/Y/", "/y/", true, false}, - {"/Y", "/y/", true, true}, - {"/Y/z", "/y/z", true, false}, - {"/Y/z/", "/y/z", true, true}, - {"/Y/Z", "/y/z", true, false}, - {"/Y/Z/", "/y/z", true, true}, - {"/y/Z", "/y/z", true, false}, - {"/y/Z/", "/y/z", true, true}, - {"/Aa", "/aa", true, false}, - {"/Aa/", "/aa", true, true}, - {"/AA", "/aa", true, false}, - {"/AA/", "/aa", true, true}, - {"/aA", "/aa", true, false}, - {"/aA/", "/aa", true, true}, - {"/A/", "/a/", true, false}, - {"/A", "/a/", true, true}, - {"/DOC", "/doc", true, false}, - {"/DOC/", "/doc", true, true}, - {"/NO", "", false, true}, - {"/DOC/GO", "", false, true}, - {"/π", "/Π", true, false}, - {"/π/", "/Π", true, true}, - {"/u/ÄPFÊL/", "/u/äpfêl/", true, false}, - {"/u/ÄPFÊL", "/u/äpfêl/", true, true}, - {"/u/ÖPFÊL/", "/u/öpfêl", true, true}, - {"/u/ÖPFÊL", "/u/öpfêl", true, false}, - {"/v/äpfêL/", "/v/Äpfêl/", true, false}, - {"/v/äpfêL", "/v/Äpfêl/", true, true}, - {"/v/öpfêL/", "/v/Öpfêl", true, true}, - {"/v/öpfêL", "/v/Öpfêl", true, false}, - {"/w/♬/", "/w/♬", true, true}, - {"/w/♭", "/w/♭/", true, true}, - {"/w/𠜎/", "/w/𠜎", true, true}, - {"/w/𠜏", "/w/𠜏/", true, true}, - {lOngPath, longPath, true, true}, - } - // With fixTrailingSlash = true - for _, test := range tests { - out, found := tree.findCaseInsensitivePath(test.in, true) - if found != test.found || (found && (string(out) != test.out)) { - t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t", - test.in, string(out), found, test.out, test.found) - return - } - } - // With fixTrailingSlash = false - for _, test := range tests { - out, found := tree.findCaseInsensitivePath(test.in, false) - if test.slash { - if found { // test needs a trailingSlash fix. It must not be found! - t.Errorf("Found without fixTrailingSlash: %s; got %s", test.in, string(out)) - } - } else { - if found != test.found || (found && (string(out) != test.out)) { - t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t", - test.in, string(out), found, test.out, test.found) - return - } - } - } -} - -func TestTreeInvalidNodeType(t *testing.T) { - const panicMsg = "invalid node type" - - tree := &node{} - tree.addRoute("/", fakeHandler("/")) - tree.addRoute("/:page", fakeHandler("/:page")) - - // set invalid node type - tree.children[0].nType = 42 - - // normal lookup - recv := catchPanic(func() { - tree.getValue("/test", nil, getSkippedNodes(), false) - }) - if rs, ok := recv.(string); !ok || rs != panicMsg { - t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv) - } - - // case-insensitive lookup - recv = catchPanic(func() { - tree.findCaseInsensitivePath("/test", true) - }) - if rs, ok := recv.(string); !ok || rs != panicMsg { - t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv) - } -} - -func TestTreeInvalidParamsType(t *testing.T) { - tree := &node{} - // add a child with wildcard - route := "/:path" - tree.addRoute(route, fakeHandler(route)) - - // set invalid Params type - params := make(Params, 0) - - // try to trigger slice bounds out of range with capacity 0 - tree.getValue("/test", ¶ms, getSkippedNodes(), false) -} - -func TestTreeExpandParamsCapacity(t *testing.T) { - data := []struct { - path string - }{ - {"/:path"}, - {"/*path"}, - } - - for _, item := range data { - tree := &node{} - tree.addRoute(item.path, fakeHandler(item.path)) - params := make(Params, 0) - - value := tree.getValue("/test", ¶ms, getSkippedNodes(), false) - - if value.params == nil { - t.Errorf("Expected %s params to be set, but they weren't", item.path) - continue - } - - if len(*value.params) != 1 { - t.Errorf("Wrong number of %s params: got %d, want %d", - item.path, len(*value.params), 1) - continue - } - } -} - -func TestTreeWildcardConflictEx(t *testing.T) { - conflicts := [...]struct { - route string - segPath string - existPath string - existSegPath string - }{ - {"/who/are/foo", "/foo", `/who/are/\*you`, `/\*you`}, - {"/who/are/foo/", "/foo/", `/who/are/\*you`, `/\*you`}, - {"/who/are/foo/bar", "/foo/bar", `/who/are/\*you`, `/\*you`}, - {"/con:nection", ":nection", `/con:tact`, `:tact`}, - } - - for _, conflict := range conflicts { - // I have to re-create a 'tree', because the 'tree' will be - // in an inconsistent state when the loop recovers from the - // panic which threw by 'addRoute' function. - tree := &node{} - routes := [...]string{ - "/con:tact", - "/who/are/*you", - "/who/foo/hello", - } - - for _, route := range routes { - tree.addRoute(route, fakeHandler(route)) - } - - recv := catchPanic(func() { - tree.addRoute(conflict.route, fakeHandler(conflict.route)) - }) - - if !regexp.MustCompile(fmt.Sprintf("'%s' in new path .* conflicts with existing wildcard '%s' in existing prefix '%s'", conflict.segPath, conflict.existSegPath, conflict.existPath)).MatchString(fmt.Sprint(recv)) { - t.Fatalf("invalid wildcard conflict error (%v)", recv) - } - } -} - -func TestTreeInvalidEscape(t *testing.T) { - routes := map[string]bool{ - "/r1/r": true, - "/r2/:r": true, - "/r3/\\:r": true, - } - tree := &node{} - for route, valid := range routes { - recv := catchPanic(func() { - tree.addRoute(route, fakeHandler(route)) - }) - if recv == nil != valid { - t.Fatalf("%s should be %t but got %v", route, valid, recv) - } - } -} - -func TestWildcardInvalidSlash(t *testing.T) { - const panicMsgPrefix = "no / before catch-all in path" - - routes := map[string]bool{ - "/foo/bar": true, - "/foo/x*zy": false, - "/foo/b*r": false, - } - - for route, valid := range routes { - tree := &node{} - recv := catchPanic(func() { - tree.addRoute(route, nil) - }) - - if recv == nil != valid { - t.Fatalf("%s should be %t but got %v", route, valid, recv) - } - - if rs, ok := recv.(string); recv != nil && (!ok || !strings.HasPrefix(rs, panicMsgPrefix)) { - t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsgPrefix, route, recv) - } - } -} - -// TestComplexBacktrackingWithCatchAll 是一个更复杂的回归测试. -// 它确保在静态路径匹配失败后, 路由器能够正确地回溯并成功匹配一个 -// 包含多个命名参数、静态部分和捕获所有参数的复杂路由. -// 这个测试对于验证在禁用 RedirectTrailingSlash 时的算法健壮性至关重要. -func TestComplexBacktrackingWithCatchAll(t *testing.T) { - // 1. Arrange: 初始化路由树并设置复杂的路由结构 - tree := &node{} - routes := [...]string{ - "/abc/b", // 静态诱饵路由 - "/abc/:p1/cde", // 一个不相关的、不会被匹配到的干扰路由 - "/abc/:p1/:p2/def/*filepath", // 最终应该匹配到的复杂目标路由 - } - for _, route := range routes { - tree.addRoute(route, fakeHandler(route)) - } - - // 2. Act: 执行一个会触发深度回溯的请求 - // 这个路径会首先尝试匹配静态的 /abc/b, 但因为后续路径不匹配而失败, - // 从而强制回溯到 /abc/ 节点, 并重新尝试匹配通配符路径. - reqPath := "/abc/b/d/def/some/file.txt" - wantRoute := "/abc/:p1/:p2/def/*filepath" - wantParams := Params{ - {Key: "p1", Value: "b"}, - {Key: "p2", Value: "d"}, - {Key: "filepath", Value: "/some/file.txt"}, // 注意: catch-all 会包含前导斜杠 - } - - // 使用 defer/recover 来断言整个过程不会发生 panic - defer func() { - if r := recover(); r != nil { - t.Fatalf("预期不应发生 panic, 但在处理路径 '%s' 时捕获到了: %v", reqPath, r) - } - }() - - // 执行查找操作 - value := tree.getValue(reqPath, getParams(), getSkippedNodes(), false) - - // 3. Assert: 验证回溯后的结果是否正确 - // 断言找到了一个有效的句柄 - if value.handlers == nil { - t.Fatalf("处理路径 '%s' 时句柄不匹配: 期望得到非空的句柄, 但实际为 nil", reqPath) - } - - // 断言匹配到了正确的路由 - value.handlers[0](nil) - if fakeHandlerValue != wantRoute { - t.Errorf("处理路径 '%s' 时句柄不匹配: \n 得到: %s\n 想要: %s", reqPath, fakeHandlerValue, wantRoute) - } - - // 断言URL参数被正确地解析和提取 - if value.params == nil || !reflect.DeepEqual(*value.params, wantParams) { - t.Errorf("处理路径 '%s' 时参数不匹配: \n 得到: %v\n 想要: %v", reqPath, *value.params, wantParams) - } -}