├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon ├── docs ├── benchmark │ ├── memory_ccx63_24.csv │ ├── peak_memory_ccx63_24.png │ ├── req_per_sec_ccx63_24.png │ └── request_ccx63_24.csv ├── getting_started.md ├── https.md └── img │ └── zzz.png ├── examples ├── basic │ └── main.zig ├── cookies │ └── main.zig ├── form │ └── main.zig ├── fs │ ├── main.zig │ └── static │ │ └── index.html ├── middleware │ └── main.zig ├── sse │ ├── index.html │ └── main.zig ├── tls │ ├── certs │ │ ├── cert.pem │ │ └── key.pem │ ├── embed │ │ └── pico.min.css │ └── main.zig └── unix │ └── main.zig ├── flake.lock ├── flake.nix └── src ├── core ├── any_case_string_map.zig ├── lib.zig ├── pseudoslice.zig ├── typed_storage.zig └── wrapping.zig ├── http ├── context.zig ├── cookie.zig ├── date.zig ├── encoding.zig ├── form.zig ├── lib.zig ├── method.zig ├── middlewares │ ├── compression.zig │ ├── lib.zig │ └── rate_limit.zig ├── mime.zig ├── request.zig ├── response.zig ├── router.zig ├── router │ ├── fs_dir.zig │ ├── middleware.zig │ ├── route.zig │ └── routing_trie.zig ├── server.zig ├── sse.zig └── status.zig ├── lib.zig └── tests.zig /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | pull_request: 4 | branches: [ main ] 5 | push: 6 | branches: [ main ] 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | build-examples: 15 | name: Build Examples 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, macos-latest, windows-latest] 19 | runs-on: ${{ matrix.os }} 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - uses: goto-bus-stop/setup-zig@v2 24 | with: 25 | version: 0.14.0 26 | - name: Build all examples 27 | run: zig build 28 | 29 | run-tests: 30 | name: Run Tests 31 | needs: build-examples 32 | strategy: 33 | matrix: 34 | os: [ubuntu-latest, macos-latest, windows-latest] 35 | runs-on: ${{ matrix.os }} 36 | 37 | steps: 38 | - uses: actions/checkout@v2 39 | - uses: goto-bus-stop/setup-zig@v2 40 | with: 41 | version: 0.14.0 42 | - name: Build all examples 43 | run: zig build test --summary all 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | zig-out/ 2 | .zig-cache/ 3 | perf*.data* 4 | heaptrack* 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | 375 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zzz 2 | ![zzz logo](./docs/img/zzz.png) 3 | 4 | 5 | ## Installing 6 | Compatible Zig Version: `0.14.0` 7 | 8 | Compatible [tardy](https://github.com/tardy-org/tardy) Version: `v0.3.0` 9 | 10 | Latest Release: `0.3.0` 11 | ``` 12 | zig fetch --save git+https://github.com/tardy-org/zzz#v0.3.0 13 | ``` 14 | 15 | You can then add the dependency in your `build.zig` file: 16 | ```zig 17 | const zzz = b.dependency("zzz", .{ 18 | .target = target, 19 | .optimize = optimize, 20 | }).module("zzz"); 21 | 22 | exe.root_module.addImport(zzz); 23 | ``` 24 | 25 | ## zzz? 26 | zzz is a framework for writing performant and reliable networked services in Zig. It supports both HTTP and HTTPS. 27 | 28 | zzz currently supports Linux, Mac and Windows. Linux is currently the recommended target for deployments. 29 | 30 | > [!IMPORTANT] 31 | > zzz is currently **alpha** software and there is still a lot changing at a fairly quick pace and certain places where things are less polished. 32 | 33 | It focuses on modularity and portability, allowing you to swap in your own implementations for various things. Consumers can provide an async implementation, allowing for maximum flexibility. This allows for use in standard servers as well as embedded/bare metal domains. 34 | 35 | For more information, look here: 36 | 1. [Getting Started](./docs/getting_started.md) 37 | 2. [HTTPS](./docs/https.md) 38 | 39 | ## Optimization 40 | zzz is **very** fast. Through a combination of methods, such as allocation at start up and avoiding thread contention, we are able to extract tons of performance out of a fairly simple implementation. zzz is quite robust currently but is still early stage software. It's currently been running in production, serving my [site](https://muki.gg). 41 | 42 | With the recent migration to [tardy](https://github.com/tardy-org/tardy), zzz is about as fast as gnet, the fastest plaintext HTTP server according to [TechEmpower](https://www.techempower.com/benchmarks/#hw=ph&test=plaintext§ion=data-r22), while consuming only ~22% of the memory that gnet requires. 43 | 44 | ![benchmark (request per sec)](./docs/benchmark/req_per_sec_ccx63_24.png) 45 | 46 | [Raw Data](./docs/benchmark/request_ccx63_24.csv) 47 | 48 | ![benchmark (peak memory)](./docs/benchmark/peak_memory_ccx63_24.png) 49 | 50 | [Raw Data](./docs/benchmark/memory_ccx63_24.csv) 51 | 52 | On the CCX63 instance on Hetzner with 2000 max connections, we are 70.9% faster than [zap](https://github.com/zigzap/zap) and 83.8% faster than [http.zig](https://github.com/karlseguin/http.zig). We also utilize less memory, using only ~3% of the memory used by zap and ~1.6% of the memory used by http.zig. 53 | 54 | zzz can be configured to utilize minimal memory while remaining performant. The provided `minram` example only uses 256 kB! 55 | 56 | ## Features 57 | - Built on top of [Tardy](https://github.com/tardy-org/tardy), an asynchronous runtime. 58 | - [Modular Asynchronous Implementation](https://muki.gg/post/modular-async) 59 | - `io_uring` for Linux (>= 5.1.0). 60 | - `epoll` for Linux (>= 2.5.45). 61 | - `kqueue` for BSD & Mac. 62 | - `poll` for Linux, Mac and Windows. 63 | - Layered Router, including Middleware 64 | - Single and Multithreaded Support 65 | - TLS using [secsock](https://github.com/tardy-org/secsock) 66 | - Memory Pooling for minimal allocations 67 | 68 | ## Contribution 69 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in zzz by you, shall be licensed as MPL2.0, without any additional terms or conditions. 70 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) void { 4 | const target = b.standardTargetOptions(.{}); 5 | const optimize = b.standardOptimizeOption(.{}); 6 | 7 | const zzz = b.addModule("zzz", .{ 8 | .root_source_file = b.path("src/lib.zig"), 9 | .target = target, 10 | .optimize = optimize, 11 | }); 12 | 13 | const tardy = b.dependency("tardy", .{ 14 | .target = target, 15 | .optimize = optimize, 16 | }).module("tardy"); 17 | 18 | zzz.addImport("tardy", tardy); 19 | 20 | const secsock = b.dependency("secsock", .{ 21 | .target = target, 22 | .optimize = optimize, 23 | }).module("secsock"); 24 | 25 | zzz.addImport("secsock", secsock); 26 | 27 | add_example(b, "basic", false, target, optimize, zzz); 28 | add_example(b, "cookies", false, target, optimize, zzz); 29 | add_example(b, "form", false, target, optimize, zzz); 30 | add_example(b, "fs", false, target, optimize, zzz); 31 | add_example(b, "middleware", false, target, optimize, zzz); 32 | add_example(b, "sse", false, target, optimize, zzz); 33 | add_example(b, "tls", true, target, optimize, zzz); 34 | 35 | if (target.result.os.tag != .windows) { 36 | add_example(b, "unix", false, target, optimize, zzz); 37 | } 38 | 39 | const tests = b.addTest(.{ 40 | .name = "tests", 41 | .root_source_file = b.path("./src/tests.zig"), 42 | }); 43 | tests.root_module.addImport("tardy", tardy); 44 | tests.root_module.addImport("secsock", secsock); 45 | 46 | const run_test = b.addRunArtifact(tests); 47 | run_test.step.dependOn(&tests.step); 48 | 49 | const test_step = b.step("test", "Run general unit tests"); 50 | test_step.dependOn(&run_test.step); 51 | } 52 | 53 | fn add_example( 54 | b: *std.Build, 55 | name: []const u8, 56 | link_libc: bool, 57 | target: std.Build.ResolvedTarget, 58 | optimize: std.builtin.Mode, 59 | zzz_module: *std.Build.Module, 60 | ) void { 61 | const example = b.addExecutable(.{ 62 | .name = name, 63 | .root_source_file = b.path(b.fmt("./examples/{s}/main.zig", .{name})), 64 | .target = target, 65 | .optimize = optimize, 66 | .strip = false, 67 | }); 68 | 69 | if (link_libc) { 70 | example.linkLibC(); 71 | } 72 | 73 | example.root_module.addImport("zzz", zzz_module); 74 | 75 | const install_artifact = b.addInstallArtifact(example, .{}); 76 | b.getInstallStep().dependOn(&install_artifact.step); 77 | 78 | const build_step = b.step(b.fmt("{s}", .{name}), b.fmt("Build zzz example ({s})", .{name})); 79 | build_step.dependOn(&install_artifact.step); 80 | 81 | const run_artifact = b.addRunArtifact(example); 82 | run_artifact.step.dependOn(&install_artifact.step); 83 | 84 | const run_step = b.step(b.fmt("run_{s}", .{name}), b.fmt("Run zzz example ({s})", .{name})); 85 | run_step.dependOn(&install_artifact.step); 86 | run_step.dependOn(&run_artifact.step); 87 | } 88 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .zzz, 3 | .fingerprint = 0xc3273dca261a7ae0, 4 | .version = "0.3.0", 5 | .minimum_zig_version = "0.14.0", 6 | .dependencies = .{ 7 | .tardy = .{ 8 | .url = "git+https://github.com/tardy-org/tardy?ref=v0.3.0#cd454060f3b6006368d53c05ab96cd16c73c34de", 9 | .hash = "tardy-0.3.0-69wrgi7PAwDFhO7m0aXae6N15s2b28VIOrnRrSHHake6", 10 | }, 11 | .secsock = .{ 12 | .url = "git+https://github.com/tardy-org/secsock?ref=v0.1.0#263dcd630e32c7a5c7a0522a8d1fd04e39b75c24", 13 | .hash = "secsock-0.0.0-p0qurf09AQD95s1NQF2MGpBqMmFz7cKZWibsgv_SQBAr", 14 | }, 15 | }, 16 | 17 | .paths = .{ 18 | "README.md", 19 | "LICENSE", 20 | "build.zig", 21 | "build.zig.zon", 22 | "src", 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /docs/benchmark/memory_ccx63_24.csv: -------------------------------------------------------------------------------- 1 | memory,server 2 | 60500,axum 3 | 51424,fasthttp 4 | 33396,gnet 5 | 82132,go 6 | 420256,httpz 7 | 197132,zap 8 | 7508,zzz_busyloop 9 | 7964,zzz_epoll 10 | 6564,zzz_iouring 11 | -------------------------------------------------------------------------------- /docs/benchmark/peak_memory_ccx63_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tardy-org/zzz/18ec7f1129ce4d0573b7c67f011b4d05c7b195d4/docs/benchmark/peak_memory_ccx63_24.png -------------------------------------------------------------------------------- /docs/benchmark/req_per_sec_ccx63_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tardy-org/zzz/18ec7f1129ce4d0573b7c67f011b4d05c7b195d4/docs/benchmark/req_per_sec_ccx63_24.png -------------------------------------------------------------------------------- /docs/benchmark/request_ccx63_24.csv: -------------------------------------------------------------------------------- 1 | connections,axum,fasthttp,gnet,go,httpz,zap,zzz_busyloop,zzz_epoll,zzz_iouring 2 | 100,267496.84,435813.75,1401345.65,287534.80,409479.02,464186.69,1064605.65,1195639.12,1152033.56 3 | 200,565707.49,454950.18,1688677.11,404877.20,983395.99,716983.92,1579955.57,1584684.42,1669862.27 4 | 300,745605.30,482680.97,1707583.25,466592.25,1017200.69,898091.14,1705219.70,1671168.39,1690530.64 5 | 400,881640.23,471810.31,1717315.26,515391.64,1024448.49,920625.88,1732361.41,1701504.28,1694987.96 6 | 500,995920.41,467399.23,1729100.10,543648.69,1021311.63,1004248.21,1751833.25,1695993.93,1687793.81 7 | 600,1116532.55,502652.92,1737437.41,570309.69,1008877.05,1011316.72,1763541.43,1698977.01,1694080.55 8 | 700,1182709.80,492771.09,1729354.87,589651.59,1005491.08,1011480.77,1752768.81,1702239.49,1683523.31 9 | 800,1238889.96,488044.44,1725939.53,600118.24,987834.47,1013245.52,1751190.65,1695428.51,1679581.43 10 | 900,1273687.74,479560.69,1713798.07,608887.39,967036.71,1011112.18,1750451.68,1697316.47,1671914.41 11 | 1000,1285327.20,515983.11,1731837.19,626961.94,946262.28,1017552.49,1739323.74,1702958.75,1672819.93 12 | 1100,1303873.21,510129.80,1737969.50,632749.76,948971.66,1017518.41,1735886.10,1696360.53,1666567.29 13 | 1300,1321792.47,508129.80,1735830.19,646153.60,927015.65,1035485.67,1736794.60,1691069.88,1670636.09 14 | 1500,1309933.66,505966.94,1727213.81,656414.41,926055.24,1053097.54,1734233.25,1678980.02,1664279.76 15 | 1800,1306196.60,495952.00,1725636.07,656978.44,923122.61,1052801.40,1722700.51,1678253.02,1661296.65 16 | -------------------------------------------------------------------------------- /docs/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | zzz is a networking framework (HTTP) that allows for modularity and flexibility in design. For most use cases, this flexibility is not a requirement and so various defaults are provided. 3 | 4 | For this guide, we will assume that you are running on a supported platform. 5 | This is the current latest release. 6 | 7 | `zig fetch --save git+https://github.com/mookums/zzz#v0.3.0` 8 | 9 | ## Hello, World! 10 | We can write a quick example that serves out "Hello, World" responses to any client that connects to the server. This example is derived from the one that is provided within the `examples/basic` directory. 11 | 12 | ```zig 13 | const std = @import("std"); 14 | const log = std.log.scoped(.@"examples/basic"); 15 | 16 | const zzz = @import("zzz"); 17 | const http = zzz.HTTP; 18 | 19 | const tardy = zzz.tardy; 20 | const Tardy = tardy.Tardy(.auto); 21 | const Runtime = tardy.Runtime; 22 | const Socket = tardy.Socket; 23 | 24 | const Server = http.Server; 25 | const Router = http.Router; 26 | const Context = http.Context; 27 | const Route = http.Route; 28 | const Respond = http.Respond; 29 | 30 | fn base_handler(ctx: *const Context, _: void) !Respond { 31 | return ctx.response.apply(.{ 32 | .status = .OK, 33 | .mime = http.Mime.HTML, 34 | .body = "Hello, world!", 35 | }); 36 | } 37 | 38 | pub fn main() !void { 39 | const host: []const u8 = "0.0.0.0"; 40 | const port: u16 = 9862; 41 | 42 | var gpa = std.heap.GeneralPurposeAllocator(.{ .thread_safe = true }){}; 43 | const allocator = gpa.allocator(); 44 | defer _ = gpa.deinit(); 45 | 46 | var t = try Tardy.init(allocator, .{ .threading = .auto }); 47 | defer t.deinit(); 48 | 49 | var router = try Router.init(allocator, &.{ 50 | Route.init("/").get({}, base_handler).layer(), 51 | }, .{}); 52 | defer router.deinit(allocator); 53 | 54 | var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); 55 | defer socket.close_blocking(); 56 | try socket.bind(); 57 | try socket.listen(4096); 58 | 59 | const EntryParams = struct { 60 | router: *const Router, 61 | socket: Socket, 62 | }; 63 | 64 | try t.entry( 65 | EntryParams{ .router = &router, .socket = socket }, 66 | struct { 67 | fn entry(rt: *Runtime, p: EntryParams) !void { 68 | var server = Server.init(.{ 69 | .stack_size = 1024 * 1024 * 4, 70 | .socket_buffer_bytes = 1024 * 2, 71 | .keepalive_count_max = null, 72 | .connection_count_max = 1024, 73 | }); 74 | try server.serve(rt, p.router, .{ .normal = p.socket }); 75 | } 76 | }.entry, 77 | ); 78 | } 79 | ``` 80 | 81 | The snippet above handles all of the basic tasks involved with serving a plaintext route using zzz's HTTP implementation. 82 | -------------------------------------------------------------------------------- /docs/https.md: -------------------------------------------------------------------------------- 1 | # HTTPS 2 | zzz utilizes [secsock](https://github.com/tardy-org/secsock) to provide a safe and performant TLS implementation. This TLS functionality is entirely separated from the I/O for maximum portability. 3 | 4 | *Note: TLS Support is not **entirely** complete yet. It's a very rough area that will be getting cleaned up in a future development cycle* 5 | 6 | ## TLS Example 7 | This is derived from the example at `examples/tls` and utilizes some certificates that are present within the repository. 8 | ```zig 9 | const std = @import("std"); 10 | const log = std.log.scoped(.@"examples/tls"); 11 | 12 | const zzz = @import("zzz"); 13 | const http = zzz.HTTP; 14 | 15 | const tardy = zzz.tardy; 16 | const Tardy = tardy.Tardy(.auto); 17 | const Runtime = tardy.Runtime; 18 | const Socket = tardy.Socket; 19 | 20 | const Server = http.Server; 21 | const Context = http.Context; 22 | const Route = http.Route; 23 | const Router = http.Router; 24 | const Respond = http.Respond; 25 | 26 | const secsock = zzz.secsock; 27 | const SecureSocket = secsock.SecureSocket; 28 | 29 | fn root_handler(ctx: *const Context, _: void) !Respond { 30 | const body = 31 | \\ 32 | \\ 33 | \\ 34 | \\ 35 | \\ 36 | \\ 37 | \\

Hello, World!

38 | \\ 39 | \\ 40 | ; 41 | 42 | return ctx.response.apply(.{ 43 | .status = .OK, 44 | .mime = http.Mime.HTML, 45 | .body = body[0..], 46 | }); 47 | } 48 | 49 | pub fn main() !void { 50 | const host: []const u8 = "0.0.0.0"; 51 | const port: u16 = 9862; 52 | 53 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 54 | const allocator = gpa.allocator(); 55 | defer _ = gpa.deinit(); 56 | 57 | var t = try Tardy.init(allocator, .{ .threading = .auto }); 58 | defer t.deinit(); 59 | 60 | var router = try Router.init(allocator, &.{ 61 | Route.init("/").get({}, root_handler).layer(), 62 | }, .{}); 63 | defer router.deinit(allocator); 64 | 65 | // create socket for tardy 66 | var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); 67 | defer socket.close_blocking(); 68 | try socket.bind(); 69 | try socket.listen(1024); 70 | 71 | var bearssl = secsock.BearSSL.init(allocator); 72 | defer bearssl.deinit(); 73 | try bearssl.add_cert_chain( 74 | "CERTIFICATE", 75 | @embedFile("certs/cert.pem"), 76 | "EC PRIVATE KEY", 77 | @embedFile("certs/key.pem"), 78 | ); 79 | const secure = try bearssl.to_secure_socket(socket, .server); 80 | 81 | const EntryParams = struct { 82 | router: *const Router, 83 | socket: SecureSocket, 84 | }; 85 | 86 | try t.entry( 87 | EntryParams{ .router = &router, .socket = secure }, 88 | struct { 89 | fn entry(rt: *Runtime, p: EntryParams) !void { 90 | var server = Server.init(.{ .stack_size = 1024 * 1024 * 8 }); 91 | try server.serve(rt, p.router, .{ .secure = p.socket }); 92 | } 93 | }.entry, 94 | ); 95 | } 96 | ``` 97 | This example above passes the `.tls` variant of the enum to the HTTP Server and provides the location of the certificate and key to be used. It also has the functionality to pass in a buffer containing the cert and key data if that is preferable. You must also provide the certificate and key name as the PEM format allows for multiple items to be placed within the same file. 98 | -------------------------------------------------------------------------------- /docs/img/zzz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tardy-org/zzz/18ec7f1129ce4d0573b7c67f011b4d05c7b195d4/docs/img/zzz.png -------------------------------------------------------------------------------- /examples/basic/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const log = std.log.scoped(.@"examples/basic"); 3 | 4 | const zzz = @import("zzz"); 5 | const http = zzz.HTTP; 6 | 7 | const tardy = zzz.tardy; 8 | const Tardy = tardy.Tardy(.auto); 9 | const Runtime = tardy.Runtime; 10 | const Socket = tardy.Socket; 11 | 12 | const Server = http.Server; 13 | const Router = http.Router; 14 | const Context = http.Context; 15 | const Route = http.Route; 16 | const Respond = http.Respond; 17 | 18 | fn base_handler(ctx: *const Context, _: void) !Respond { 19 | return ctx.response.apply(.{ 20 | .status = .OK, 21 | .mime = http.Mime.HTML, 22 | .body = "Hello, world!", 23 | }); 24 | } 25 | 26 | pub fn main() !void { 27 | const host: []const u8 = "0.0.0.0"; 28 | const port: u16 = 9862; 29 | 30 | var gpa = std.heap.GeneralPurposeAllocator(.{ .thread_safe = true }){}; 31 | const allocator = gpa.allocator(); 32 | defer _ = gpa.deinit(); 33 | 34 | var t = try Tardy.init(allocator, .{ .threading = .auto }); 35 | defer t.deinit(); 36 | 37 | var router = try Router.init(allocator, &.{ 38 | Route.init("/").get({}, base_handler).layer(), 39 | }, .{}); 40 | defer router.deinit(allocator); 41 | 42 | // create socket for tardy 43 | var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); 44 | defer socket.close_blocking(); 45 | try socket.bind(); 46 | try socket.listen(4096); 47 | 48 | const EntryParams = struct { 49 | router: *const Router, 50 | socket: Socket, 51 | }; 52 | 53 | try t.entry( 54 | EntryParams{ .router = &router, .socket = socket }, 55 | struct { 56 | fn entry(rt: *Runtime, p: EntryParams) !void { 57 | var server = Server.init(.{ 58 | .stack_size = 1024 * 1024 * 4, 59 | .socket_buffer_bytes = 1024 * 2, 60 | .keepalive_count_max = null, 61 | .connection_count_max = 1024, 62 | }); 63 | try server.serve(rt, p.router, .{ .normal = p.socket }); 64 | } 65 | }.entry, 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /examples/cookies/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const log = std.log.scoped(.@"examples/cookies"); 3 | 4 | const zzz = @import("zzz"); 5 | const http = zzz.HTTP; 6 | 7 | const tardy = zzz.tardy; 8 | const Tardy = tardy.Tardy(.auto); 9 | const Runtime = tardy.Runtime; 10 | const Socket = tardy.Socket; 11 | 12 | const Server = http.Server; 13 | const Router = http.Router; 14 | const Context = http.Context; 15 | const Route = http.Route; 16 | const Middleware = http.Middleware; 17 | const Respond = http.Respond; 18 | const Cookie = http.Cookie; 19 | 20 | fn base_handler(ctx: *const Context, _: void) !Respond { 21 | var iter = ctx.request.cookies.iterator(); 22 | while (iter.next()) |kv| log.debug("cookie: k={s} v={s}", .{ kv.key_ptr.*, kv.value_ptr.* }); 23 | 24 | const cookie = Cookie.init("example_cookie", "abcdef123"); 25 | return ctx.response.apply(.{ 26 | .status = .OK, 27 | .mime = http.Mime.HTML, 28 | .body = "Hello, world!", 29 | .headers = &.{ 30 | .{ "Set-Cookie", try cookie.to_string_alloc(ctx.allocator) }, 31 | }, 32 | }); 33 | } 34 | 35 | pub fn main() !void { 36 | const host: []const u8 = "0.0.0.0"; 37 | const port: u16 = 9862; 38 | 39 | var gpa = std.heap.GeneralPurposeAllocator(.{ .thread_safe = true }){}; 40 | const allocator = gpa.allocator(); 41 | defer _ = gpa.deinit(); 42 | 43 | var t = try Tardy.init(allocator, .{ .threading = .single }); 44 | defer t.deinit(); 45 | 46 | var router = try Router.init(allocator, &.{ 47 | Route.init("/").get({}, base_handler).layer(), 48 | }, .{}); 49 | defer router.deinit(allocator); 50 | 51 | // create socket for tardy 52 | var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); 53 | defer socket.close_blocking(); 54 | try socket.bind(); 55 | try socket.listen(4096); 56 | 57 | const EntryParams = struct { 58 | router: *const Router, 59 | socket: Socket, 60 | }; 61 | 62 | try t.entry( 63 | EntryParams{ .router = &router, .socket = socket }, 64 | struct { 65 | fn entry(rt: *Runtime, p: EntryParams) !void { 66 | var server = Server.init(.{ 67 | .stack_size = 1024 * 1024 * 4, 68 | .socket_buffer_bytes = 1024 * 2, 69 | .keepalive_count_max = null, 70 | .connection_count_max = 10, 71 | }); 72 | try server.serve(rt, p.router, .{ .normal = p.socket }); 73 | } 74 | }.entry, 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /examples/form/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const log = std.log.scoped(.@"examples/form"); 3 | 4 | const zzz = @import("zzz"); 5 | const http = zzz.HTTP; 6 | 7 | const tardy = zzz.tardy; 8 | const Tardy = tardy.Tardy(.auto); 9 | const Runtime = tardy.Runtime; 10 | const Socket = tardy.Socket; 11 | 12 | const Server = http.Server; 13 | const Router = http.Router; 14 | const Context = http.Context; 15 | const Route = http.Route; 16 | const Form = http.Form; 17 | const Query = http.Query; 18 | const Respond = http.Respond; 19 | 20 | fn base_handler(ctx: *const Context, _: void) !Respond { 21 | const body = 22 | \\
23 | \\ 24 | \\

25 | \\ 26 | \\

27 | \\ 28 | \\

29 | \\ 30 | \\

31 | \\ 32 | \\ 33 | \\
34 | ; 35 | 36 | return ctx.response.apply(.{ 37 | .status = .OK, 38 | .mime = http.Mime.HTML, 39 | .body = body, 40 | }); 41 | } 42 | 43 | const UserInfo = struct { 44 | fname: []const u8, 45 | mname: []const u8 = "Middle", 46 | lname: []const u8, 47 | age: u8, 48 | height: f32, 49 | weight: ?[]const u8, 50 | }; 51 | 52 | fn generate_handler(ctx: *const Context, _: void) !Respond { 53 | const info = switch (ctx.request.method.?) { 54 | .GET => try Query(UserInfo).parse(ctx.allocator, ctx), 55 | .POST => try Form(UserInfo).parse(ctx.allocator, ctx), 56 | else => return error.UnexpectedMethod, 57 | }; 58 | 59 | const body = try std.fmt.allocPrint( 60 | ctx.allocator, 61 | "First: {s} | Middle: {s} | Last: {s} | Age: {d} | Height: {d} | Weight: {s}", 62 | .{ 63 | info.fname, 64 | info.mname, 65 | info.lname, 66 | info.age, 67 | info.height, 68 | info.weight orelse "none", 69 | }, 70 | ); 71 | 72 | return ctx.response.apply(.{ 73 | .status = .OK, 74 | .mime = http.Mime.TEXT, 75 | .body = body, 76 | }); 77 | } 78 | 79 | pub fn main() !void { 80 | const host: []const u8 = "0.0.0.0"; 81 | const port: u16 = 9862; 82 | 83 | var gpa = std.heap.GeneralPurposeAllocator(.{ .thread_safe = true }){}; 84 | const allocator = gpa.allocator(); 85 | defer _ = gpa.deinit(); 86 | 87 | var t = try Tardy.init(allocator, .{ .threading = .auto }); 88 | defer t.deinit(); 89 | 90 | var router = try Router.init(allocator, &.{ 91 | Route.init("/").get({}, base_handler).layer(), 92 | Route.init("/generate").get({}, generate_handler).post({}, generate_handler).layer(), 93 | }, .{}); 94 | defer router.deinit(allocator); 95 | 96 | var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); 97 | defer socket.close_blocking(); 98 | try socket.bind(); 99 | try socket.listen(4096); 100 | 101 | const EntryParams = struct { 102 | router: *const Router, 103 | socket: Socket, 104 | }; 105 | 106 | try t.entry( 107 | EntryParams{ .router = &router, .socket = socket }, 108 | struct { 109 | fn entry(rt: *Runtime, p: EntryParams) !void { 110 | var server = Server.init(.{ 111 | .stack_size = 1024 * 1024 * 4, 112 | .socket_buffer_bytes = 1024 * 2, 113 | }); 114 | try server.serve(rt, p.router, .{ .normal = p.socket }); 115 | } 116 | }.entry, 117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /examples/fs/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const log = std.log.scoped(.@"examples/fs"); 3 | 4 | const zzz = @import("zzz"); 5 | const http = zzz.HTTP; 6 | 7 | const tardy = zzz.tardy; 8 | const Tardy = tardy.Tardy(.auto); 9 | const Runtime = tardy.Runtime; 10 | const Socket = tardy.Socket; 11 | const Dir = tardy.Dir; 12 | 13 | const Server = http.Server; 14 | const Router = http.Router; 15 | const Context = http.Context; 16 | const Route = http.Route; 17 | const Respond = http.Respond; 18 | const FsDir = http.FsDir; 19 | 20 | const Compression = http.Middlewares.Compression; 21 | 22 | fn base_handler(ctx: *const Context, _: void) !Respond { 23 | const body = 24 | \\ 25 | \\ 26 | \\ 27 | \\

Hello, World!

28 | \\ 29 | \\ 30 | ; 31 | 32 | return try ctx.response.apply(.{ 33 | .status = .OK, 34 | .mime = http.Mime.HTML, 35 | .body = body[0..], 36 | }); 37 | } 38 | 39 | pub fn main() !void { 40 | const host: []const u8 = "0.0.0.0"; 41 | const port: u16 = 9862; 42 | 43 | var gpa = std.heap.GeneralPurposeAllocator( 44 | .{ .thread_safe = true }, 45 | ){ .backing_allocator = std.heap.c_allocator }; 46 | const allocator = gpa.allocator(); 47 | defer _ = gpa.deinit(); 48 | 49 | var t = try Tardy.init(allocator, .{ .threading = .auto }); 50 | defer t.deinit(); 51 | 52 | const static_dir = Dir.from_std(try std.fs.cwd().openDir("examples/fs/static", .{})); 53 | 54 | var router = try Router.init(allocator, &.{ 55 | Compression(.{ .gzip = .{} }), 56 | Route.init("/").get({}, base_handler).layer(), 57 | FsDir.serve("/", static_dir), 58 | }, .{}); 59 | defer router.deinit(allocator); 60 | 61 | const EntryParams = struct { 62 | router: *const Router, 63 | socket: Socket, 64 | }; 65 | 66 | var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); 67 | defer socket.close_blocking(); 68 | try socket.bind(); 69 | try socket.listen(256); 70 | 71 | try t.entry( 72 | EntryParams{ .router = &router, .socket = socket }, 73 | struct { 74 | fn entry(rt: *Runtime, p: EntryParams) !void { 75 | var server = Server.init(.{ 76 | .stack_size = 1024 * 1024 * 4, 77 | .socket_buffer_bytes = 1024 * 4, 78 | }); 79 | try server.serve(rt, p.router, .{ .normal = p.socket }); 80 | } 81 | }.entry, 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /examples/fs/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | zzz (z3) sample page 5 | 6 | 7 |

test html serving from the filesystem!

8 |

free-range organic gmo-free hypertext from zzz

9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/middleware/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const log = std.log.scoped(.@"examples/middleware"); 3 | 4 | const zzz = @import("zzz"); 5 | const http = zzz.HTTP; 6 | 7 | const tardy = zzz.tardy; 8 | const Tardy = tardy.Tardy(.auto); 9 | const Runtime = tardy.Runtime; 10 | const Socket = tardy.Socket; 11 | 12 | const Server = http.Server; 13 | const Router = http.Router; 14 | const Context = http.Context; 15 | const Route = http.Route; 16 | const Next = http.Next; 17 | const Respond = http.Respond; 18 | const Middleware = http.Middleware; 19 | 20 | fn root_handler(ctx: *const Context, id: i8) !Respond { 21 | const body_fmt = 22 | \\ 23 | \\ 24 | \\ 25 | \\

Hello, World!

26 | \\

id: {d}

27 | \\

stored: {d}

28 | \\ 29 | \\ 30 | ; 31 | const body = try std.fmt.allocPrint( 32 | ctx.allocator, 33 | body_fmt, 34 | .{ id, ctx.storage.get(usize).? }, 35 | ); 36 | 37 | // This is the standard response and what you 38 | // will usually be using. This will send to the 39 | // client and then continue to await more requests. 40 | return ctx.response.apply(.{ 41 | .status = .OK, 42 | .mime = http.Mime.HTML, 43 | .body = body[0..], 44 | }); 45 | } 46 | 47 | fn passing_middleware(next: *Next, _: void) !Respond { 48 | log.info("pass middleware: {s}", .{next.context.request.uri.?}); 49 | try next.context.storage.put(usize, 100); 50 | return try next.run(); 51 | } 52 | 53 | fn failing_middleware(next: *Next, _: void) !Respond { 54 | log.info("fail middleware: {s}", .{next.context.request.uri.?}); 55 | return error.FailingMiddleware; 56 | } 57 | 58 | pub fn main() !void { 59 | const host: []const u8 = "0.0.0.0"; 60 | const port: u16 = 9862; 61 | 62 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 63 | const allocator = gpa.allocator(); 64 | defer _ = gpa.deinit(); 65 | 66 | var t = try Tardy.init(allocator, .{ .threading = .single }); 67 | defer t.deinit(); 68 | 69 | const num: i8 = 12; 70 | 71 | var router = try Router.init(allocator, &.{ 72 | Middleware.init({}, passing_middleware).layer(), 73 | Route.init("/").get(num, root_handler).layer(), 74 | Middleware.init({}, failing_middleware).layer(), 75 | Route.init("/").post(num, root_handler).layer(), 76 | Route.init("/fail").get(num, root_handler).layer(), 77 | }, .{}); 78 | defer router.deinit(allocator); 79 | 80 | const EntryParams = struct { 81 | router: *const Router, 82 | socket: Socket, 83 | }; 84 | 85 | var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); 86 | defer socket.close_blocking(); 87 | try socket.bind(); 88 | try socket.listen(256); 89 | 90 | try t.entry( 91 | EntryParams{ .router = &router, .socket = socket }, 92 | struct { 93 | fn entry(rt: *Runtime, p: EntryParams) !void { 94 | var server = Server.init(.{}); 95 | try server.serve(rt, p.router, .{ .normal = p.socket }); 96 | } 97 | }.entry, 98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /examples/sse/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | SSE Example 7 | 8 | 9 |

Server-Sent Events Example

10 | 11 |
12 | 13 |
14 |
15 | 16 | 71 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /examples/sse/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const log = std.log.scoped(.@"examples/sse"); 3 | 4 | const zzz = @import("zzz"); 5 | const http = zzz.HTTP; 6 | 7 | const tardy = zzz.tardy; 8 | const Tardy = tardy.Tardy(.auto); 9 | const Runtime = tardy.Runtime; 10 | const Socket = tardy.Socket; 11 | const Timer = tardy.Timer; 12 | 13 | const Server = http.Server; 14 | const Router = http.Router; 15 | const Context = http.Context; 16 | const Route = http.Route; 17 | const Respond = http.Respond; 18 | const SSE = http.SSE; 19 | 20 | fn sse_handler(ctx: *const Context, _: void) !Respond { 21 | var sse = try SSE.init(ctx); 22 | 23 | while (true) { 24 | sse.send(.{ .data = "hello from handler!" }) catch break; 25 | try Timer.delay(ctx.runtime, .{ .seconds = 1 }); 26 | } 27 | 28 | return .responded; 29 | } 30 | 31 | pub fn main() !void { 32 | const host: []const u8 = "0.0.0.0"; 33 | const port: u16 = 9862; 34 | 35 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 36 | const allocator = gpa.allocator(); 37 | defer _ = gpa.deinit(); 38 | 39 | var t = try Tardy.init(allocator, .{ .threading = .single }); 40 | defer t.deinit(); 41 | 42 | const router = try Router.init(allocator, &.{ 43 | Route.init("/").embed_file(.{ .mime = http.Mime.HTML }, @embedFile("./index.html")).layer(), 44 | Route.init("/stream").get({}, sse_handler).layer(), 45 | }, .{}); 46 | 47 | // create socket for tardy 48 | var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); 49 | defer socket.close_blocking(); 50 | try socket.bind(); 51 | try socket.listen(256); 52 | 53 | const EntryParams = struct { 54 | router: *const Router, 55 | socket: Socket, 56 | }; 57 | 58 | try t.entry( 59 | EntryParams{ .router = &router, .socket = socket }, 60 | struct { 61 | fn entry(rt: *Runtime, p: EntryParams) !void { 62 | var server = Server.init(.{ 63 | .stack_size = 1024 * 1024 * 4, 64 | .socket_buffer_bytes = 1024 * 2, 65 | }); 66 | try server.serve(rt, p.router, .{ .normal = p.socket }); 67 | } 68 | }.entry, 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /examples/tls/certs/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBmDCCAT+gAwIBAgIUXq+kgTxiu8vfVGXGnXmoXfZYeBwwCgYIKoZIzj0EAwIw 3 | FDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDMwOTAzMTkzNVoXDTI2MDMwOTAz 4 | MTkzNVowFDESMBAGA1UEAwwJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0D 5 | AQcDQgAExBVte4Wu3SsBhfD+3uvXE5u3hExgKZGryIAXu1BgVPPuQQDcObS6QwWx 6 | +wJHVD9P/SZYjmSHKtwh7/7tn11QI6NvMG0wHQYDVR0OBBYEFC81CrnuWJdxpV9e 7 | J0aneKk2SGB4MB8GA1UdIwQYMBaAFC81CrnuWJdxpV9eJ0aneKk2SGB4MA8GA1Ud 8 | EwEB/wQFMAMBAf8wGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMAoGCCqGSM49 9 | BAMCA0cAMEQCIGq9siIGaIfclJRYjsjfGpheWeVV8XZhrIFvQ9EaZz36AiAI/Wen 10 | 178H1CbdcwjpkENgfejbOdZv/E5O2aNVJwt/2A== 11 | -----END CERTIFICATE----- 12 | -------------------------------------------------------------------------------- /examples/tls/certs/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEILClMa6ufhTsG8tGqw4N7MkjkkXthPVVfwYObQMbAvyKoAoGCCqGSM49 3 | AwEHoUQDQgAExBVte4Wu3SsBhfD+3uvXE5u3hExgKZGryIAXu1BgVPPuQQDcObS6 4 | QwWx+wJHVD9P/SZYjmSHKtwh7/7tn11QIw== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /examples/tls/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const log = std.log.scoped(.@"examples/tls"); 3 | 4 | const zzz = @import("zzz"); 5 | const http = zzz.HTTP; 6 | 7 | const tardy = zzz.tardy; 8 | const Tardy = tardy.Tardy(.auto); 9 | const Runtime = tardy.Runtime; 10 | const Socket = tardy.Socket; 11 | 12 | const Server = http.Server; 13 | const Context = http.Context; 14 | const Route = http.Route; 15 | const Router = http.Router; 16 | const Respond = http.Respond; 17 | 18 | const secsock = zzz.secsock; 19 | const SecureSocket = secsock.SecureSocket; 20 | const Compression = http.Middlewares.Compression; 21 | 22 | fn root_handler(ctx: *const Context, _: void) !Respond { 23 | const body = 24 | \\ 25 | \\ 26 | \\ 27 | \\ 28 | \\ 29 | \\ 30 | \\

Hello, World!

31 | \\ 32 | \\ 33 | ; 34 | 35 | return ctx.response.apply(.{ 36 | .status = .OK, 37 | .mime = http.Mime.HTML, 38 | .body = body[0..], 39 | }); 40 | } 41 | 42 | pub fn main() !void { 43 | const host: []const u8 = "0.0.0.0"; 44 | const port: u16 = 9862; 45 | 46 | var gpa = std.heap.GeneralPurposeAllocator( 47 | .{ .thread_safe = true }, 48 | ){ .backing_allocator = std.heap.c_allocator }; 49 | const allocator = gpa.allocator(); 50 | defer _ = gpa.deinit(); 51 | 52 | var t = try Tardy.init(allocator, .{ .threading = .auto }); 53 | defer t.deinit(); 54 | 55 | var router = try Router.init(allocator, &.{ 56 | Route.init("/").get({}, root_handler).layer(), 57 | Compression(.{ .gzip = .{} }), 58 | Route.init("/embed/pico.min.css").embed_file( 59 | .{ .mime = http.Mime.CSS }, 60 | @embedFile("embed/pico.min.css"), 61 | ).layer(), 62 | }, .{}); 63 | defer router.deinit(allocator); 64 | 65 | // create socket for tardy 66 | var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); 67 | defer socket.close_blocking(); 68 | try socket.bind(); 69 | try socket.listen(1024); 70 | 71 | var bearssl = secsock.BearSSL.init(allocator); 72 | defer bearssl.deinit(); 73 | try bearssl.add_cert_chain( 74 | "CERTIFICATE", 75 | @embedFile("certs/cert.pem"), 76 | "EC PRIVATE KEY", 77 | @embedFile("certs/key.pem"), 78 | ); 79 | const secure = try bearssl.to_secure_socket(socket, .server); 80 | 81 | const EntryParams = struct { 82 | router: *const Router, 83 | socket: SecureSocket, 84 | }; 85 | 86 | try t.entry( 87 | EntryParams{ .router = &router, .socket = secure }, 88 | struct { 89 | fn entry(rt: *Runtime, p: EntryParams) !void { 90 | var server = Server.init(.{ .stack_size = 1024 * 1024 * 8 }); 91 | try server.serve(rt, p.router, .{ .secure = p.socket }); 92 | } 93 | }.entry, 94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /examples/unix/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const log = std.log.scoped(.@"examples/benchmark"); 3 | 4 | const zzz = @import("zzz"); 5 | const http = zzz.HTTP; 6 | 7 | const tardy = zzz.tardy; 8 | const Tardy = tardy.Tardy(.auto); 9 | const Runtime = tardy.Runtime; 10 | const Socket = tardy.Socket; 11 | 12 | const Server = http.Server; 13 | const Context = http.Context; 14 | const Route = http.Route; 15 | const Router = http.Router; 16 | const Respond = http.Respond; 17 | 18 | pub const std_options: std.Options = .{ .log_level = .err }; 19 | 20 | pub fn root_handler(ctx: *const Context, _: void) !Respond { 21 | return ctx.response.apply(.{ 22 | .status = .OK, 23 | .mime = http.Mime.HTML, 24 | .body = "This is an HTTP benchmark", 25 | }); 26 | } 27 | 28 | pub fn main() !void { 29 | var gpa = std.heap.GeneralPurposeAllocator(.{ .thread_safe = true }){}; 30 | const allocator = gpa.allocator(); 31 | defer _ = gpa.deinit(); 32 | 33 | var t = try Tardy.init(allocator, .{ .threading = .auto }); 34 | defer t.deinit(); 35 | 36 | var router = try Router.init(allocator, &.{ 37 | Route.init("/").get({}, root_handler).layer(), 38 | }, .{}); 39 | defer router.deinit(allocator); 40 | 41 | const EntryParams = struct { 42 | router: *const Router, 43 | socket: Socket, 44 | }; 45 | 46 | var socket = try Socket.init(.{ .unix = "/tmp/zzz.sock" }); 47 | defer std.fs.deleteFileAbsolute("/tmp/zzz.sock") catch unreachable; 48 | defer socket.close_blocking(); 49 | try socket.bind(); 50 | try socket.listen(256); 51 | 52 | try t.entry( 53 | EntryParams{ .router = &router, .socket = socket }, 54 | struct { 55 | fn entry(rt: *Runtime, p: EntryParams) !void { 56 | var server = Server.init(.{}); 57 | try server.serve(rt, p.router, .{ .normal = p.socket }); 58 | } 59 | }.entry, 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-compat": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1696426674, 7 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 8 | "owner": "edolstra", 9 | "repo": "flake-compat", 10 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "edolstra", 15 | "repo": "flake-compat", 16 | "type": "github" 17 | } 18 | }, 19 | "flake-compat_2": { 20 | "flake": false, 21 | "locked": { 22 | "lastModified": 1696426674, 23 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 24 | "owner": "edolstra", 25 | "repo": "flake-compat", 26 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 27 | "type": "github" 28 | }, 29 | "original": { 30 | "owner": "edolstra", 31 | "repo": "flake-compat", 32 | "type": "github" 33 | } 34 | }, 35 | "flake-compat_3": { 36 | "flake": false, 37 | "locked": { 38 | "lastModified": 1696426674, 39 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 40 | "owner": "edolstra", 41 | "repo": "flake-compat", 42 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 43 | "type": "github" 44 | }, 45 | "original": { 46 | "owner": "edolstra", 47 | "repo": "flake-compat", 48 | "type": "github" 49 | } 50 | }, 51 | "flake-compat_4": { 52 | "flake": false, 53 | "locked": { 54 | "lastModified": 1696426674, 55 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 56 | "owner": "edolstra", 57 | "repo": "flake-compat", 58 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 59 | "type": "github" 60 | }, 61 | "original": { 62 | "owner": "edolstra", 63 | "repo": "flake-compat", 64 | "type": "github" 65 | } 66 | }, 67 | "flake-utils": { 68 | "inputs": { 69 | "systems": "systems" 70 | }, 71 | "locked": { 72 | "lastModified": 1731533236, 73 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 74 | "owner": "numtide", 75 | "repo": "flake-utils", 76 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 77 | "type": "github" 78 | }, 79 | "original": { 80 | "owner": "numtide", 81 | "repo": "flake-utils", 82 | "type": "github" 83 | } 84 | }, 85 | "flake-utils_2": { 86 | "inputs": { 87 | "systems": "systems_2" 88 | }, 89 | "locked": { 90 | "lastModified": 1731533236, 91 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 92 | "owner": "numtide", 93 | "repo": "flake-utils", 94 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 95 | "type": "github" 96 | }, 97 | "original": { 98 | "owner": "numtide", 99 | "repo": "flake-utils", 100 | "type": "github" 101 | } 102 | }, 103 | "flake-utils_3": { 104 | "inputs": { 105 | "systems": "systems_3" 106 | }, 107 | "locked": { 108 | "lastModified": 1705309234, 109 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 110 | "owner": "numtide", 111 | "repo": "flake-utils", 112 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 113 | "type": "github" 114 | }, 115 | "original": { 116 | "owner": "numtide", 117 | "repo": "flake-utils", 118 | "type": "github" 119 | } 120 | }, 121 | "flake-utils_4": { 122 | "inputs": { 123 | "systems": "systems_4" 124 | }, 125 | "locked": { 126 | "lastModified": 1710146030, 127 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 128 | "owner": "numtide", 129 | "repo": "flake-utils", 130 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 131 | "type": "github" 132 | }, 133 | "original": { 134 | "owner": "numtide", 135 | "repo": "flake-utils", 136 | "type": "github" 137 | } 138 | }, 139 | "flake-utils_5": { 140 | "inputs": { 141 | "systems": "systems_5" 142 | }, 143 | "locked": { 144 | "lastModified": 1705309234, 145 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 146 | "owner": "numtide", 147 | "repo": "flake-utils", 148 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 149 | "type": "github" 150 | }, 151 | "original": { 152 | "owner": "numtide", 153 | "repo": "flake-utils", 154 | "type": "github" 155 | } 156 | }, 157 | "flake-utils_6": { 158 | "inputs": { 159 | "systems": "systems_6" 160 | }, 161 | "locked": { 162 | "lastModified": 1705309234, 163 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 164 | "owner": "numtide", 165 | "repo": "flake-utils", 166 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 167 | "type": "github" 168 | }, 169 | "original": { 170 | "owner": "numtide", 171 | "repo": "flake-utils", 172 | "type": "github" 173 | } 174 | }, 175 | "flake-utils_7": { 176 | "inputs": { 177 | "systems": "systems_7" 178 | }, 179 | "locked": { 180 | "lastModified": 1705309234, 181 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 182 | "owner": "numtide", 183 | "repo": "flake-utils", 184 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 185 | "type": "github" 186 | }, 187 | "original": { 188 | "owner": "numtide", 189 | "repo": "flake-utils", 190 | "type": "github" 191 | } 192 | }, 193 | "gitignore": { 194 | "inputs": { 195 | "nixpkgs": [ 196 | "iguana", 197 | "zls-0-13", 198 | "nixpkgs" 199 | ] 200 | }, 201 | "locked": { 202 | "lastModified": 1709087332, 203 | "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", 204 | "owner": "hercules-ci", 205 | "repo": "gitignore.nix", 206 | "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 207 | "type": "github" 208 | }, 209 | "original": { 210 | "owner": "hercules-ci", 211 | "repo": "gitignore.nix", 212 | "type": "github" 213 | } 214 | }, 215 | "gitignore_2": { 216 | "inputs": { 217 | "nixpkgs": [ 218 | "iguana", 219 | "zls-0-14", 220 | "nixpkgs" 221 | ] 222 | }, 223 | "locked": { 224 | "lastModified": 1709087332, 225 | "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", 226 | "owner": "hercules-ci", 227 | "repo": "gitignore.nix", 228 | "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 229 | "type": "github" 230 | }, 231 | "original": { 232 | "owner": "hercules-ci", 233 | "repo": "gitignore.nix", 234 | "type": "github" 235 | } 236 | }, 237 | "gitignore_3": { 238 | "inputs": { 239 | "nixpkgs": [ 240 | "iguana", 241 | "zls-master", 242 | "nixpkgs" 243 | ] 244 | }, 245 | "locked": { 246 | "lastModified": 1709087332, 247 | "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", 248 | "owner": "hercules-ci", 249 | "repo": "gitignore.nix", 250 | "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 251 | "type": "github" 252 | }, 253 | "original": { 254 | "owner": "hercules-ci", 255 | "repo": "gitignore.nix", 256 | "type": "github" 257 | } 258 | }, 259 | "iguana": { 260 | "inputs": { 261 | "flake-utils": "flake-utils_2", 262 | "nixpkgs": "nixpkgs", 263 | "zigPkgs": "zigPkgs", 264 | "zls-0-13": "zls-0-13", 265 | "zls-0-14": "zls-0-14", 266 | "zls-master": "zls-master" 267 | }, 268 | "locked": { 269 | "lastModified": 1744132258, 270 | "narHash": "sha256-H9QCGxgA0I7YZJ103GJh+bvrKMGhiHdOK9WNASN8kdk=", 271 | "owner": "mookums", 272 | "repo": "iguana", 273 | "rev": "4afa252ff735c25130a5cea1b03327e9c69e1323", 274 | "type": "github" 275 | }, 276 | "original": { 277 | "owner": "mookums", 278 | "repo": "iguana", 279 | "type": "github" 280 | } 281 | }, 282 | "langref": { 283 | "flake": false, 284 | "locked": { 285 | "narHash": "sha256-O6p2tiKD8ZMhSX+DeA/o5hhAvcPkU2J9lFys/r11peY=", 286 | "type": "file", 287 | "url": "https://raw.githubusercontent.com/ziglang/zig/0fb2015fd3422fc1df364995f9782dfe7255eccd/doc/langref.html.in" 288 | }, 289 | "original": { 290 | "type": "file", 291 | "url": "https://raw.githubusercontent.com/ziglang/zig/0fb2015fd3422fc1df364995f9782dfe7255eccd/doc/langref.html.in" 292 | } 293 | }, 294 | "nixpkgs": { 295 | "locked": { 296 | "lastModified": 0, 297 | "narHash": "sha256-Yxv6ix/U5WO3+MPldLYQn432b3g6TXOs/73KUa3aTFI=", 298 | "path": "/nix/store/3yh6za04jn874v5jz9283anvq4d3zizv-source", 299 | "type": "path" 300 | }, 301 | "original": { 302 | "id": "nixpkgs", 303 | "type": "indirect" 304 | } 305 | }, 306 | "nixpkgs_2": { 307 | "locked": { 308 | "lastModified": 1708161998, 309 | "narHash": "sha256-6KnemmUorCvlcAvGziFosAVkrlWZGIc6UNT9GUYr0jQ=", 310 | "owner": "NixOS", 311 | "repo": "nixpkgs", 312 | "rev": "84d981bae8b5e783b3b548de505b22880559515f", 313 | "type": "github" 314 | }, 315 | "original": { 316 | "owner": "NixOS", 317 | "ref": "nixos-23.11", 318 | "repo": "nixpkgs", 319 | "type": "github" 320 | } 321 | }, 322 | "nixpkgs_3": { 323 | "locked": { 324 | "lastModified": 1717696253, 325 | "narHash": "sha256-1+ua0ggXlYYPLTmMl3YeYYsBXDSCqT+Gw3u6l4gvMhA=", 326 | "owner": "NixOS", 327 | "repo": "nixpkgs", 328 | "rev": "9b5328b7f761a7bbdc0e332ac4cf076a3eedb89b", 329 | "type": "github" 330 | }, 331 | "original": { 332 | "owner": "NixOS", 333 | "ref": "nixos-24.05", 334 | "repo": "nixpkgs", 335 | "type": "github" 336 | } 337 | }, 338 | "nixpkgs_4": { 339 | "locked": { 340 | "lastModified": 1741196730, 341 | "narHash": "sha256-0Sj6ZKjCpQMfWnN0NURqRCQn2ob7YtXTAOTwCuz7fkA=", 342 | "owner": "NixOS", 343 | "repo": "nixpkgs", 344 | "rev": "48913d8f9127ea6530a2a2f1bd4daa1b8685d8a3", 345 | "type": "github" 346 | }, 347 | "original": { 348 | "owner": "NixOS", 349 | "ref": "nixos-24.11", 350 | "repo": "nixpkgs", 351 | "type": "github" 352 | } 353 | }, 354 | "nixpkgs_5": { 355 | "locked": { 356 | "lastModified": 1742937945, 357 | "narHash": "sha256-lWc+79eZRyvHp/SqMhHTMzZVhpxkRvthsP1Qx6UCq0E=", 358 | "owner": "NixOS", 359 | "repo": "nixpkgs", 360 | "rev": "d02d88f8de5b882ccdde0465d8fa2db3aa1169f7", 361 | "type": "github" 362 | }, 363 | "original": { 364 | "owner": "NixOS", 365 | "ref": "nixos-24.11", 366 | "repo": "nixpkgs", 367 | "type": "github" 368 | } 369 | }, 370 | "nixpkgs_6": { 371 | "locked": { 372 | "lastModified": 1744120788, 373 | "narHash": "sha256-a5NZpBF8kunuAABFDwAfradQrnrQdQuFawZ57+x5RDg=", 374 | "owner": "nixos", 375 | "repo": "nixpkgs", 376 | "rev": "a62d20dd366a941a588bfe3c814826cf631a0554", 377 | "type": "github" 378 | }, 379 | "original": { 380 | "owner": "nixos", 381 | "ref": "release-24.11", 382 | "repo": "nixpkgs", 383 | "type": "github" 384 | } 385 | }, 386 | "root": { 387 | "inputs": { 388 | "flake-utils": "flake-utils", 389 | "iguana": "iguana", 390 | "nixpkgs": "nixpkgs_6" 391 | } 392 | }, 393 | "systems": { 394 | "locked": { 395 | "lastModified": 1681028828, 396 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 397 | "owner": "nix-systems", 398 | "repo": "default", 399 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 400 | "type": "github" 401 | }, 402 | "original": { 403 | "owner": "nix-systems", 404 | "repo": "default", 405 | "type": "github" 406 | } 407 | }, 408 | "systems_2": { 409 | "locked": { 410 | "lastModified": 1681028828, 411 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 412 | "owner": "nix-systems", 413 | "repo": "default", 414 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 415 | "type": "github" 416 | }, 417 | "original": { 418 | "owner": "nix-systems", 419 | "repo": "default", 420 | "type": "github" 421 | } 422 | }, 423 | "systems_3": { 424 | "locked": { 425 | "lastModified": 1681028828, 426 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 427 | "owner": "nix-systems", 428 | "repo": "default", 429 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 430 | "type": "github" 431 | }, 432 | "original": { 433 | "owner": "nix-systems", 434 | "repo": "default", 435 | "type": "github" 436 | } 437 | }, 438 | "systems_4": { 439 | "locked": { 440 | "lastModified": 1681028828, 441 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 442 | "owner": "nix-systems", 443 | "repo": "default", 444 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 445 | "type": "github" 446 | }, 447 | "original": { 448 | "owner": "nix-systems", 449 | "repo": "default", 450 | "type": "github" 451 | } 452 | }, 453 | "systems_5": { 454 | "locked": { 455 | "lastModified": 1681028828, 456 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 457 | "owner": "nix-systems", 458 | "repo": "default", 459 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 460 | "type": "github" 461 | }, 462 | "original": { 463 | "owner": "nix-systems", 464 | "repo": "default", 465 | "type": "github" 466 | } 467 | }, 468 | "systems_6": { 469 | "locked": { 470 | "lastModified": 1681028828, 471 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 472 | "owner": "nix-systems", 473 | "repo": "default", 474 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 475 | "type": "github" 476 | }, 477 | "original": { 478 | "owner": "nix-systems", 479 | "repo": "default", 480 | "type": "github" 481 | } 482 | }, 483 | "systems_7": { 484 | "locked": { 485 | "lastModified": 1681028828, 486 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 487 | "owner": "nix-systems", 488 | "repo": "default", 489 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 490 | "type": "github" 491 | }, 492 | "original": { 493 | "owner": "nix-systems", 494 | "repo": "default", 495 | "type": "github" 496 | } 497 | }, 498 | "zig-overlay": { 499 | "inputs": { 500 | "flake-compat": "flake-compat_2", 501 | "flake-utils": "flake-utils_5", 502 | "nixpkgs": [ 503 | "iguana", 504 | "zls-0-13", 505 | "nixpkgs" 506 | ] 507 | }, 508 | "locked": { 509 | "lastModified": 1717848532, 510 | "narHash": "sha256-d+xIUvSTreHl8pAmU1fnmkfDTGQYCn2Rb/zOwByxS2M=", 511 | "owner": "mitchellh", 512 | "repo": "zig-overlay", 513 | "rev": "02fc5cc555fc14fda40c42d7c3250efa43812b43", 514 | "type": "github" 515 | }, 516 | "original": { 517 | "owner": "mitchellh", 518 | "repo": "zig-overlay", 519 | "type": "github" 520 | } 521 | }, 522 | "zig-overlay_2": { 523 | "inputs": { 524 | "flake-compat": "flake-compat_3", 525 | "flake-utils": "flake-utils_6", 526 | "nixpkgs": [ 527 | "iguana", 528 | "zls-0-14", 529 | "nixpkgs" 530 | ] 531 | }, 532 | "locked": { 533 | "lastModified": 1741263138, 534 | "narHash": "sha256-qlX8tgtZMTSOEeAM8AmC7K6mixgYOguhl/xLj5xQrXc=", 535 | "owner": "mitchellh", 536 | "repo": "zig-overlay", 537 | "rev": "627055069ee1409e8c9be7bcc533e8823fb87b18", 538 | "type": "github" 539 | }, 540 | "original": { 541 | "owner": "mitchellh", 542 | "repo": "zig-overlay", 543 | "type": "github" 544 | } 545 | }, 546 | "zig-overlay_3": { 547 | "inputs": { 548 | "flake-compat": "flake-compat_4", 549 | "flake-utils": "flake-utils_7", 550 | "nixpkgs": [ 551 | "iguana", 552 | "zls-master", 553 | "nixpkgs" 554 | ] 555 | }, 556 | "locked": { 557 | "lastModified": 1743250246, 558 | "narHash": "sha256-gVFyxsxfqnEXSldeeURim7RRZGwPX4f/egLcSC7CXec=", 559 | "owner": "mitchellh", 560 | "repo": "zig-overlay", 561 | "rev": "b0da956a6db25564d0ee461e669fb07a348d2528", 562 | "type": "github" 563 | }, 564 | "original": { 565 | "owner": "mitchellh", 566 | "repo": "zig-overlay", 567 | "type": "github" 568 | } 569 | }, 570 | "zigPkgs": { 571 | "inputs": { 572 | "flake-compat": "flake-compat", 573 | "flake-utils": "flake-utils_3", 574 | "nixpkgs": "nixpkgs_2" 575 | }, 576 | "locked": { 577 | "lastModified": 1743899701, 578 | "narHash": "sha256-qbaGVwPyUGmmRh+u1EY+bFKKiBC2CDfuE7E9uGj1Iuk=", 579 | "owner": "mitchellh", 580 | "repo": "zig-overlay", 581 | "rev": "51f156aa0220947c6712b5846bf440136fd551db", 582 | "type": "github" 583 | }, 584 | "original": { 585 | "owner": "mitchellh", 586 | "repo": "zig-overlay", 587 | "type": "github" 588 | } 589 | }, 590 | "zls-0-13": { 591 | "inputs": { 592 | "flake-utils": "flake-utils_4", 593 | "gitignore": "gitignore", 594 | "langref": "langref", 595 | "nixpkgs": "nixpkgs_3", 596 | "zig-overlay": "zig-overlay" 597 | }, 598 | "locked": { 599 | "lastModified": 1717891507, 600 | "narHash": "sha256-l/Zo1OwdB3js3wXOpgFLozKwq+bdsPySZtKbEbYBb7U=", 601 | "owner": "zigtools", 602 | "repo": "zls", 603 | "rev": "a26718049a8657d4da04c331aeced1697bc7652b", 604 | "type": "github" 605 | }, 606 | "original": { 607 | "owner": "zigtools", 608 | "ref": "0.13.0", 609 | "repo": "zls", 610 | "type": "github" 611 | } 612 | }, 613 | "zls-0-14": { 614 | "inputs": { 615 | "gitignore": "gitignore_2", 616 | "nixpkgs": "nixpkgs_4", 617 | "zig-overlay": "zig-overlay_2" 618 | }, 619 | "locked": { 620 | "lastModified": 1741303397, 621 | "narHash": "sha256-A5Mn+mfIefOsX+eNBRHrDVkqFDVrD3iXDNsUL4TPhKo=", 622 | "owner": "zigtools", 623 | "repo": "zls", 624 | "rev": "7485feeeda45d1ad09422ae83af73307ab9e6c9e", 625 | "type": "github" 626 | }, 627 | "original": { 628 | "owner": "zigtools", 629 | "ref": "0.14.0", 630 | "repo": "zls", 631 | "type": "github" 632 | } 633 | }, 634 | "zls-master": { 635 | "inputs": { 636 | "gitignore": "gitignore_3", 637 | "nixpkgs": "nixpkgs_5", 638 | "zig-overlay": "zig-overlay_3" 639 | }, 640 | "locked": { 641 | "lastModified": 1743714173, 642 | "narHash": "sha256-siRiMBlRfHySRhwzenUp93oPm1l0ZgJ9sJ8dd/kXITk=", 643 | "owner": "zigtools", 644 | "repo": "zls", 645 | "rev": "99eaaf61abf3dc35c6c6c49e3459b632eeda3dfe", 646 | "type": "github" 647 | }, 648 | "original": { 649 | "owner": "zigtools", 650 | "ref": "master", 651 | "repo": "zls", 652 | "type": "github" 653 | } 654 | } 655 | }, 656 | "root": "root", 657 | "version": 7 658 | } 659 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "a framework for writing performant and reliable networked services"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs/release-24.11"; 6 | iguana.url = "github:mookums/iguana"; 7 | flake-utils.url = "github:numtide/flake-utils"; 8 | }; 9 | 10 | outputs = 11 | { 12 | nixpkgs, 13 | iguana, 14 | flake-utils, 15 | ... 16 | }: 17 | flake-utils.lib.eachDefaultSystem ( 18 | system: 19 | let 20 | pkgs = import nixpkgs { inherit system; }; 21 | iguanaLib = iguana.lib.${system}; 22 | in 23 | { 24 | devShells.default = iguanaLib.mkShell { 25 | zigVersion = "0.14.0"; 26 | withZls = true; 27 | 28 | extraPackages = with pkgs; [ 29 | openssl 30 | wrk 31 | ]; 32 | }; 33 | } 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/core/any_case_string_map.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | 4 | const Pool = @import("tardy").Pool; 5 | 6 | const AnyCaseStringContext = struct { 7 | const Self = @This(); 8 | 9 | pub fn hash(_: Self, key: []const u8) u64 { 10 | var wyhash = std.hash.Wyhash.init(0); 11 | for (key) |b| wyhash.update(&.{std.ascii.toLower(b)}); 12 | return wyhash.final(); 13 | } 14 | 15 | pub fn eql(_: Self, key1: []const u8, key2: []const u8) bool { 16 | if (key1.len != key2.len) return false; 17 | for (key1, key2) |b1, b2| if (std.ascii.toLower(b1) != std.ascii.toLower(b2)) return false; 18 | return true; 19 | } 20 | }; 21 | 22 | pub const AnyCaseStringMap = std.HashMap([]const u8, []const u8, AnyCaseStringContext, 80); 23 | 24 | const testing = std.testing; 25 | 26 | test "AnyCaseStringMap: Add Stuff" { 27 | var map = AnyCaseStringMap.init(testing.allocator); 28 | defer map.deinit(); 29 | 30 | try map.put("Content-Length", "100"); 31 | try map.put("Host", "localhost:9999"); 32 | 33 | const content_length = map.get("Content-length"); 34 | try testing.expect(content_length != null); 35 | 36 | const host = map.get("host"); 37 | try testing.expect(host != null); 38 | } 39 | -------------------------------------------------------------------------------- /src/core/lib.zig: -------------------------------------------------------------------------------- 1 | pub const Pseudoslice = @import("pseudoslice.zig").Pseudoslice; 2 | 3 | pub fn Pair(comptime A: type, comptime B: type) type { 4 | return struct { A, B }; 5 | } 6 | -------------------------------------------------------------------------------- /src/core/pseudoslice.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | const log = std.log.scoped(.@"zzz/core/pseudoslice"); 4 | 5 | // The Pseudoslice will basically stitch together two different buffers, using 6 | // a third provided buffer as the output. 7 | pub const Pseudoslice = struct { 8 | first: []const u8, 9 | second: []const u8, 10 | shared: []u8, 11 | len: usize, 12 | 13 | pub fn init(first: []const u8, second: []const u8, shared: []u8) Pseudoslice { 14 | return Pseudoslice{ 15 | .first = first, 16 | .second = second, 17 | .shared = shared, 18 | .len = first.len + second.len, 19 | }; 20 | } 21 | 22 | /// Operates like a slice. That means it does not capture the end. 23 | /// Start is an inclusive bound and end is an exclusive bound. 24 | pub fn get(self: *const Pseudoslice, start: usize, end: usize) []const u8 { 25 | assert(end >= start); 26 | assert(self.shared.len >= end - start); 27 | const clamped_end = @min(end, self.len); 28 | 29 | if (start < self.first.len) { 30 | if (clamped_end <= self.first.len) { 31 | // within first slice 32 | return self.first[start..clamped_end]; 33 | } else { 34 | // across both slices 35 | const first_len = self.first.len - start; 36 | const second_len = clamped_end - self.first.len; 37 | const total_len = clamped_end - start; 38 | 39 | if (self.first.ptr == self.shared.ptr) { 40 | // just copy over the second. 41 | std.mem.copyForwards(u8, self.shared[self.first.len..], self.second[0..second_len]); 42 | return self.shared[start..clamped_end]; 43 | } else { 44 | // copy both over. 45 | std.mem.copyForwards(u8, self.shared[0..first_len], self.first[start..]); 46 | std.mem.copyForwards(u8, self.shared[first_len..], self.second[0..second_len]); 47 | return self.shared[0..total_len]; 48 | } 49 | } 50 | } else { 51 | // within second slice 52 | const second_start = start - self.first.len; 53 | const second_end = clamped_end - self.first.len; 54 | return self.second[second_start..second_end]; 55 | } 56 | } 57 | }; 58 | 59 | const testing = std.testing; 60 | 61 | test "Pseudoslice General" { 62 | var buffer = [_]u8{0} ** 1024; 63 | const value = "hello, my name is muki"; 64 | var pseudo = Pseudoslice.init(value[0..6], value[6..], buffer[0..]); 65 | 66 | for (0..pseudo.len) |i| { 67 | for (0..i) |j| { 68 | try testing.expectEqualStrings(value[j..i], pseudo.get(j, i)); 69 | } 70 | } 71 | } 72 | 73 | test "Pseudoslice Empty Second" { 74 | var buffer = [_]u8{0} ** 1024; 75 | const value = "hello, my name is muki"; 76 | var pseudo = Pseudoslice.init(value[0..], &.{}, buffer[0..]); 77 | 78 | for (0..pseudo.len) |i| { 79 | try testing.expectEqualStrings(value[0..i], pseudo.get(0, i)); 80 | } 81 | } 82 | 83 | test "Pseudoslice First and Shared Same" { 84 | const buffer = try testing.allocator.alloc(u8, 1024); 85 | defer testing.allocator.free(buffer); 86 | 87 | const value = "hello, my name is muki"; 88 | std.mem.copyForwards(u8, buffer, value[0..6]); 89 | 90 | var pseudo = Pseudoslice.init(buffer[0..6], value[6..], buffer); 91 | 92 | for (0..pseudo.len) |i| { 93 | for (0..i) |j| { 94 | try testing.expectEqualStrings(value[j..i], pseudo.get(j, i)); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/core/typed_storage.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const TypedStorage = struct { 4 | arena: std.heap.ArenaAllocator, 5 | storage: std.AutoHashMapUnmanaged(u64, *anyopaque), 6 | 7 | pub fn init(allocator: std.mem.Allocator) TypedStorage { 8 | return .{ 9 | .arena = std.heap.ArenaAllocator.init(allocator), 10 | .storage = std.AutoHashMapUnmanaged(u64, *anyopaque){}, 11 | }; 12 | } 13 | 14 | pub fn deinit(self: *TypedStorage) void { 15 | self.arena.deinit(); 16 | } 17 | 18 | /// Clears the Storage. 19 | pub fn clear(self: *TypedStorage) void { 20 | self.storage.clearAndFree(self.arena.allocator()); 21 | _ = self.arena.reset(.retain_capacity); 22 | } 23 | 24 | /// Inserts a value into the Storage. 25 | /// It uses the given type as the K. 26 | pub fn put(self: *TypedStorage, comptime T: type, value: T) !void { 27 | const allocator = self.arena.allocator(); 28 | const ptr = try allocator.create(T); 29 | ptr.* = value; 30 | const type_id = comptime std.hash.Wyhash.hash(0, @typeName(T)); 31 | try self.storage.put(allocator, type_id, @ptrCast(ptr)); 32 | } 33 | 34 | /// Extracts a value out of the Storage. 35 | /// It uses the given type as the K. 36 | pub fn get(self: *TypedStorage, comptime T: type) ?T { 37 | const type_id = comptime std.hash.Wyhash.hash(0, @typeName(T)); 38 | const ptr = self.storage.get(type_id) orelse return null; 39 | return @as(*T, @ptrCast(@alignCast(ptr))).*; 40 | } 41 | }; 42 | 43 | const testing = std.testing; 44 | 45 | test "TypedStorage: Basic" { 46 | var storage = TypedStorage.init(testing.allocator); 47 | defer storage.deinit(); 48 | 49 | // Test inserting and getting different types 50 | try storage.put(u32, 42); 51 | try storage.put([]const u8, "hello"); 52 | try storage.put(f32, 3.14); 53 | 54 | try testing.expectEqual(@as(u32, 42), storage.get(u32).?); 55 | try testing.expectEqualStrings("hello", storage.get([]const u8).?); 56 | try testing.expectEqual(@as(f32, 3.14), storage.get(f32).?); 57 | 58 | // Test overwriting a value 59 | try storage.put(u32, 100); 60 | try testing.expectEqual(@as(u32, 100), storage.get(u32).?); 61 | 62 | // Test getting non-existent type 63 | try testing.expectEqual(@as(?bool, null), storage.get(bool)); 64 | 65 | // Test clearing 66 | storage.clear(); 67 | try testing.expectEqual(@as(?u32, null), storage.get(u32)); 68 | try testing.expectEqual(@as(?[]const u8, null), storage.get([]const u8)); 69 | try testing.expectEqual(@as(?f32, null), storage.get(f32)); 70 | 71 | // Test inserting after clear 72 | try storage.put(u32, 200); 73 | try testing.expectEqual(@as(u32, 200), storage.get(u32).?); 74 | } 75 | -------------------------------------------------------------------------------- /src/core/wrapping.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | 4 | /// Special values for Wrapped types. 5 | const Wrapped = enum(usize) { null = 0, true = 1, false = 2, void = 3 }; 6 | 7 | /// Wraps the given value into a specified integer type. 8 | /// The value must fit within the size of the given I. 9 | pub fn wrap(comptime I: type, value: anytype) I { 10 | const T = @TypeOf(value); 11 | assert(@typeInfo(I) == .int); 12 | assert(@typeInfo(I).int.signedness == .unsigned); 13 | 14 | if (comptime @bitSizeOf(@TypeOf(value)) > @bitSizeOf(I)) { 15 | @compileError("type: " ++ @typeName(T) ++ " is larger than given integer (" ++ @typeName(I) ++ ")"); 16 | } 17 | 18 | return context: { 19 | switch (comptime @typeInfo(@TypeOf(value))) { 20 | .pointer => break :context @intFromPtr(value), 21 | .void => break :context @intFromEnum(Wrapped.void), 22 | .int => |info| { 23 | const uint = @Type(std.builtin.Type{ 24 | .int = .{ 25 | .signedness = .unsigned, 26 | .bits = info.bits, 27 | }, 28 | }); 29 | break :context @intCast(@as(uint, @bitCast(value))); 30 | }, 31 | .comptime_int => break :context @intCast(value), 32 | .float => |info| { 33 | const uint = @Type(std.builtin.Type{ 34 | .int = .{ 35 | .signedness = .unsigned, 36 | .bits = info.bits, 37 | }, 38 | }); 39 | break :context @intCast(@as(uint, @bitCast(value))); 40 | }, 41 | .comptime_float => break :context @intCast(@as(I, @bitCast(value))), 42 | .@"struct" => |info| { 43 | const uint = @Type(std.builtin.Type{ 44 | .int = .{ 45 | .signedness = .unsigned, 46 | .bits = @bitSizeOf(info.backing_integer.?), 47 | }, 48 | }); 49 | break :context @intCast(@as(uint, @bitCast(value))); 50 | }, 51 | .bool => break :context if (value) @intFromEnum(Wrapped.true) else @intFromEnum(Wrapped.false), 52 | .optional => break :context if (value) |v| wrap(I, v) else @intFromEnum(Wrapped.null), 53 | else => @compileError("wrapping unsupported type: " ++ @typeName(@TypeOf(value))), 54 | } 55 | }; 56 | } 57 | 58 | /// Unwraps a specified type from an underlying value. 59 | /// The value must be an unsigned integer type, typically a usize. 60 | pub fn unwrap(comptime T: type, value: anytype) T { 61 | const I = @TypeOf(value); 62 | assert(@typeInfo(I) == .int); 63 | assert(@typeInfo(I).int.signedness == .unsigned); 64 | if (comptime @bitSizeOf(@TypeOf(T)) > @bitSizeOf(I)) { 65 | @compileError("type: " ++ @typeName(T) ++ "is larger than given integer (" ++ @typeName(I) ++ ")"); 66 | } 67 | 68 | return context: { 69 | switch (comptime @typeInfo(T)) { 70 | .pointer => break :context @ptrFromInt(value), 71 | .void => break :context {}, 72 | .int => |info| { 73 | const uint = @Type(std.builtin.Type{ 74 | .int = .{ 75 | .signedness = .unsigned, 76 | .bits = info.bits, 77 | }, 78 | }); 79 | break :context @bitCast(@as(uint, @intCast(value))); 80 | }, 81 | .float => |info| { 82 | const uint = @Type(std.builtin.Type{ 83 | .int = .{ 84 | .signedness = .unsigned, 85 | .bits = info.bits, 86 | }, 87 | }); 88 | const float = @Type(std.builtin.Type{ 89 | .float = .{ 90 | .bits = info.bits, 91 | }, 92 | }); 93 | break :context @as(float, @bitCast(@as(uint, @intCast(value)))); 94 | }, 95 | .@"struct" => |info| { 96 | const uint = @Type(std.builtin.Type{ 97 | .int = .{ 98 | .signedness = .unsigned, 99 | .bits = @bitSizeOf(info.backing_integer.?), 100 | }, 101 | }); 102 | break :context @bitCast(@as(uint, @intCast(value))); 103 | }, 104 | .bool => { 105 | assert(value == @intFromEnum(Wrapped.true) or value == @intFromEnum(Wrapped.false)); 106 | break :context if (value == @intFromEnum(Wrapped.false)) false else true; 107 | }, 108 | .optional => |info| break :context if (value == @intFromEnum(Wrapped.null)) 109 | null 110 | else 111 | unwrap(info.child, value), 112 | else => unreachable, 113 | } 114 | }; 115 | } 116 | 117 | const testing = std.testing; 118 | 119 | test "wrap/unwrap - integers" { 120 | try testing.expectEqual(@as(usize, 42), wrap(usize, @as(u8, 42))); 121 | try testing.expectEqual(@as(usize, 42), wrap(usize, @as(u16, 42))); 122 | try testing.expectEqual(@as(usize, 42), wrap(usize, @as(u32, 42))); 123 | 124 | try testing.expectEqual(@as(usize, 42), wrap(usize, @as(i8, 42))); 125 | try testing.expectEqual(@as(usize, 42), wrap(usize, @as(i16, 42))); 126 | try testing.expectEqual(@as(usize, 42), wrap(usize, @as(i32, 42))); 127 | 128 | try testing.expectEqual(@as(u8, 42), unwrap(u8, @as(usize, 42))); 129 | try testing.expectEqual(@as(i16, 42), unwrap(i16, @as(usize, 42))); 130 | } 131 | 132 | test "wrap/unwrap - floats" { 133 | const pi_32: f32 = 3.14159; 134 | const pi_64: f64 = 3.14159; 135 | 136 | const wrapped_f32 = wrap(usize, pi_32); 137 | const wrapped_f64 = wrap(usize, pi_64); 138 | 139 | try testing.expectEqual(pi_32, unwrap(f32, wrapped_f32)); 140 | try testing.expectEqual(pi_64, unwrap(f64, wrapped_f64)); 141 | } 142 | 143 | test "wrap/unwrap - booleans" { 144 | try testing.expectEqual(@as(usize, @intFromEnum(Wrapped.true)), wrap(usize, true)); 145 | try testing.expectEqual(@as(usize, @intFromEnum(Wrapped.false)), wrap(usize, false)); 146 | 147 | try testing.expectEqual(true, unwrap(bool, @as(usize, @intFromEnum(Wrapped.true)))); 148 | try testing.expectEqual(false, unwrap(bool, @as(usize, @intFromEnum(Wrapped.false)))); 149 | } 150 | 151 | test "wrap/unwrap - optionals" { 152 | const optional_int: ?i32 = 42; 153 | const optional_none: ?i32 = null; 154 | 155 | try testing.expectEqual(@as(usize, 42), wrap(usize, optional_int)); 156 | try testing.expectEqual(@as(usize, 0), wrap(usize, optional_none)); 157 | 158 | try testing.expectEqual(@as(?i32, 42), unwrap(?i32, @as(usize, 42))); 159 | try testing.expectEqual(@as(?i32, null), unwrap(?i32, @as(usize, 0))); 160 | } 161 | 162 | test "wrap/unwrap - void" { 163 | try testing.expectEqual(@as(usize, @intFromEnum(Wrapped.void)), wrap(usize, {})); 164 | try testing.expectEqual({}, unwrap(void, @as(usize, @intFromEnum(Wrapped.void)))); 165 | } 166 | 167 | test "wrap/unwrap - pointers" { 168 | var value: i32 = 42; 169 | const ptr = &value; 170 | 171 | const wrapped = wrap(usize, ptr); 172 | const unwrapped = unwrap(*i32, wrapped); 173 | 174 | try testing.expectEqual(&value, unwrapped); 175 | try testing.expectEqual(@as(i32, 42), unwrapped.*); 176 | } 177 | -------------------------------------------------------------------------------- /src/http/context.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Request = @import("request.zig").Request; 3 | const Response = @import("response.zig").Response; 4 | const Runtime = @import("tardy").Runtime; 5 | 6 | const secsock = @import("secsock"); 7 | const SecureSocket = secsock.SecureSocket; 8 | 9 | const Capture = @import("router/routing_trie.zig").Capture; 10 | 11 | const TypedStorage = @import("../core/typed_storage.zig").TypedStorage; 12 | const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap; 13 | 14 | /// HTTP Context. Contains all of the various information 15 | /// that will persist throughout the lifetime of this Request/Response. 16 | pub const Context = struct { 17 | allocator: std.mem.Allocator, 18 | /// Not safe to access unless you are manually sending the headers 19 | /// and returning the .responded variant of Respond. 20 | header_buffer: *std.ArrayList(u8), 21 | runtime: *Runtime, 22 | /// The Request that triggered this handler. 23 | request: *const Request, 24 | response: *Response, 25 | /// Storage 26 | storage: *TypedStorage, 27 | /// Socket for this Connection. 28 | socket: SecureSocket, 29 | /// Slice of the URL Slug Captures 30 | captures: []const Capture, 31 | /// Map of the KV Query pairs in the URL 32 | queries: *const AnyCaseStringMap, 33 | }; 34 | -------------------------------------------------------------------------------- /src/http/cookie.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Date = @import("date.zig").Date; 3 | 4 | pub const Cookie = struct { 5 | name: []const u8, 6 | value: []const u8, 7 | path: ?[]const u8 = null, 8 | domain: ?[]const u8 = null, 9 | expires: ?Date = null, 10 | max_age: ?u32 = null, 11 | secure: bool = false, 12 | http_only: bool = false, 13 | same_site: ?SameSite = null, 14 | 15 | pub fn init(name: []const u8, value: []const u8) Cookie { 16 | return .{ 17 | .name = name, 18 | .value = value, 19 | }; 20 | } 21 | 22 | pub const SameSite = enum { 23 | strict, 24 | lax, 25 | none, 26 | 27 | pub fn to_string(self: SameSite) []const u8 { 28 | return switch (self) { 29 | .strict => "Strict", 30 | .lax => "Lax", 31 | .none => "None", 32 | }; 33 | } 34 | }; 35 | 36 | pub fn to_string_buf(self: Cookie, buf: []u8) ![]const u8 { 37 | var list = std.ArrayListUnmanaged(u8).initBuffer(buf); 38 | const writer = list.fixedWriter(); 39 | 40 | try writer.print("{s}={s}", .{ self.name, self.value }); 41 | if (self.domain) |domain| try writer.print("; Domain={s}", .{domain}); 42 | if (self.path) |path| try writer.print("; Path={s}", .{path}); 43 | if (self.expires) |exp| { 44 | try writer.writeAll("; Expires="); 45 | try exp.to_http_date().into_writer(writer); 46 | } 47 | if (self.max_age) |age| try writer.print("; Max-Age={d}", .{age}); 48 | if (self.same_site) |same_site| try writer.print( 49 | "; SameSite={s}", 50 | .{same_site.to_string()}, 51 | ); 52 | if (self.secure) try writer.writeAll("; Secure"); 53 | if (self.http_only) try writer.writeAll("; HttpOnly"); 54 | 55 | return list.items; 56 | } 57 | 58 | pub fn to_string_alloc(self: Cookie, allocator: std.mem.Allocator) ![]const u8 { 59 | var list = try std.ArrayListUnmanaged(u8).initCapacity(allocator, 128); 60 | errdefer list.deinit(allocator); 61 | const writer = list.writer(allocator); 62 | 63 | try writer.print("{s}={s}", .{ self.name, self.value }); 64 | if (self.domain) |domain| try writer.print("; Domain={s}", .{domain}); 65 | if (self.path) |path| try writer.print("; Path={s}", .{path}); 66 | if (self.expires) |exp| { 67 | try writer.writeAll("; Expires="); 68 | try exp.to_http_date().into_writer(writer); 69 | } 70 | if (self.max_age) |age| try writer.print("; Max-Age={d}", .{age}); 71 | if (self.same_site) |same_site| try writer.print( 72 | "; SameSite={s}", 73 | .{same_site.to_string()}, 74 | ); 75 | if (self.secure) try writer.writeAll("; Secure"); 76 | if (self.http_only) try writer.writeAll("; HttpOnly"); 77 | 78 | return list.toOwnedSlice(allocator); 79 | } 80 | }; 81 | 82 | pub const CookieMap = struct { 83 | allocator: std.mem.Allocator, 84 | map: std.StringHashMap([]const u8), 85 | 86 | pub fn init(allocator: std.mem.Allocator) CookieMap { 87 | return .{ 88 | .allocator = allocator, 89 | .map = std.StringHashMap([]const u8).init(allocator), 90 | }; 91 | } 92 | 93 | pub fn deinit(self: *CookieMap) void { 94 | var iter = self.map.iterator(); 95 | while (iter.next()) |entry| { 96 | self.allocator.free(entry.key_ptr.*); 97 | self.allocator.free(entry.value_ptr.*); 98 | } 99 | self.map.deinit(); 100 | } 101 | 102 | pub fn clear(self: *CookieMap) void { 103 | var iter = self.map.iterator(); 104 | while (iter.next()) |entry| { 105 | self.allocator.free(entry.key_ptr.*); 106 | self.allocator.free(entry.value_ptr.*); 107 | } 108 | self.map.clearRetainingCapacity(); 109 | } 110 | 111 | pub fn get(self: CookieMap, name: []const u8) ?[]const u8 { 112 | return self.map.get(name); 113 | } 114 | 115 | pub fn count(self: CookieMap) usize { 116 | return self.map.count(); 117 | } 118 | 119 | pub fn iterator(self: *const CookieMap) std.StringHashMap([]const u8).Iterator { 120 | return self.map.iterator(); 121 | } 122 | 123 | // For parsing request cookies (simple key=value pairs) 124 | pub fn parse_from_header(self: *CookieMap, cookie_header: []const u8) !void { 125 | self.clear(); 126 | 127 | var pairs = std.mem.splitSequence(u8, cookie_header, "; "); 128 | while (pairs.next()) |pair| { 129 | var kv = std.mem.splitScalar(u8, pair, '='); 130 | const key = kv.next() orelse continue; 131 | const value = kv.next() orelse continue; 132 | if (kv.next() != null) continue; 133 | 134 | const key_dup = try self.allocator.dupe(u8, key); 135 | errdefer self.allocator.free(key_dup); 136 | const value_dup = try self.allocator.dupe(u8, value); 137 | errdefer self.allocator.free(value_dup); 138 | 139 | if (try self.map.fetchPut(key_dup, value_dup)) |existing| { 140 | self.allocator.free(existing.key); 141 | self.allocator.free(existing.value); 142 | } 143 | } 144 | } 145 | }; 146 | 147 | const testing = std.testing; 148 | 149 | test "Cookie: Header Parsing" { 150 | var cookie_map = CookieMap.init(testing.allocator); 151 | defer cookie_map.deinit(); 152 | 153 | try cookie_map.parse_from_header("sessionId=abc123; java=slop"); 154 | try testing.expectEqualStrings("abc123", cookie_map.get("sessionId").?); 155 | try testing.expectEqualStrings("slop", cookie_map.get("java").?); 156 | } 157 | 158 | test "Cookie: Response Formatting" { 159 | const cookie = Cookie{ 160 | .name = "session", 161 | .value = "abc123", 162 | .path = "/", 163 | .domain = "example.com", 164 | .secure = true, 165 | .http_only = true, 166 | .same_site = .strict, 167 | .max_age = 3600, 168 | }; 169 | 170 | const formatted = try cookie.to_string_alloc(testing.allocator); 171 | defer testing.allocator.free(formatted); 172 | 173 | try testing.expectEqualStrings( 174 | "session=abc123; Domain=example.com; Path=/; Max-Age=3600; SameSite=Strict; Secure; HttpOnly", 175 | formatted, 176 | ); 177 | } 178 | -------------------------------------------------------------------------------- /src/http/date.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | 4 | const day_names: []const []const u8 = &.{ 5 | "Mon", 6 | "Tue", 7 | "Wed", 8 | "Thu", 9 | "Fri", 10 | "Sat", 11 | "Sun", 12 | }; 13 | 14 | const Month = struct { 15 | name: []const u8, 16 | days: u32, 17 | }; 18 | 19 | const months: []const Month = &.{ 20 | Month{ .name = "Jan", .days = 31 }, 21 | Month{ .name = "Feb", .days = 28 }, 22 | Month{ .name = "Mar", .days = 31 }, 23 | Month{ .name = "Apr", .days = 30 }, 24 | Month{ .name = "May", .days = 31 }, 25 | Month{ .name = "Jun", .days = 30 }, 26 | Month{ .name = "Jul", .days = 31 }, 27 | Month{ .name = "Aug", .days = 31 }, 28 | Month{ .name = "Sep", .days = 30 }, 29 | Month{ .name = "Oct", .days = 31 }, 30 | Month{ .name = "Nov", .days = 30 }, 31 | Month{ .name = "Dec", .days = 31 }, 32 | }; 33 | 34 | pub const Date = struct { 35 | const HTTPDate = struct { 36 | const format = std.fmt.comptimePrint( 37 | "{s}, {s} {s} {s} {s}:{s}:{s} GMT", 38 | .{ 39 | "{[day_name]s}", 40 | "{[day]d}", 41 | "{[month]s}", 42 | "{[year]d}", 43 | "{[hour]d:0>2}", 44 | "{[minute]d:0>2}", 45 | "{[second]d:0>2}", 46 | }, 47 | ); 48 | 49 | day_name: []const u8, 50 | day: u8, 51 | month: []const u8, 52 | year: u16, 53 | hour: u8, 54 | minute: u8, 55 | second: u8, 56 | 57 | pub fn into_buf(date: HTTPDate, buffer: []u8) ![]u8 { 58 | assert(buffer.len >= 29); 59 | return try std.fmt.bufPrint(buffer, format, date); 60 | } 61 | 62 | pub fn into_alloc(date: HTTPDate, allocator: std.mem.Allocator) ![]const u8 { 63 | return try std.fmt.allocPrint(allocator, format, date); 64 | } 65 | 66 | pub fn into_writer(date: HTTPDate, writer: anytype) !void { 67 | assert(std.meta.hasMethod(@TypeOf(writer), "print")); 68 | try writer.print(format, date); 69 | } 70 | }; 71 | 72 | ts: i64, 73 | 74 | pub fn init(ts: i64) Date { 75 | return Date{ .ts = ts }; 76 | } 77 | 78 | fn is_leap_year(year: i64) bool { 79 | return (@rem(year, 4) == 0 and @rem(year, 100) != 0) or (@rem(year, 400) == 0); 80 | } 81 | 82 | pub fn to_http_date(date: Date) HTTPDate { 83 | const secs = date.ts; 84 | const days = @divFloor(secs, 86400); 85 | const remsecs = @mod(secs, 86400); 86 | 87 | var year: i64 = 1970; 88 | var remaining_days = days; 89 | while (true) { 90 | const days_in_year: i64 = if (is_leap_year(year)) 366 else 365; 91 | if (remaining_days < days_in_year) break; 92 | remaining_days -= days_in_year; 93 | year += 1; 94 | } 95 | 96 | var month: usize = 0; 97 | for (months, 0..) |m, i| { 98 | const days_in_month = if (i == 1 and is_leap_year(year)) 29 else m.days; 99 | if (remaining_days < days_in_month) break; 100 | remaining_days -= days_in_month; 101 | month += 1; 102 | } 103 | 104 | const day = remaining_days + 1; 105 | const week_day = @mod((days + 3), 7); 106 | 107 | const hour: u8 = @intCast(@divFloor(remsecs, 3600)); 108 | const minute: u8 = @intCast(@mod(@divFloor(remsecs, 60), 60)); 109 | const second: u8 = @intCast(@mod(remsecs, 60)); 110 | 111 | return HTTPDate{ 112 | .day_name = day_names[@intCast(week_day)], 113 | .day = @intCast(day), 114 | .month = months[month].name, 115 | .year = @intCast(year), 116 | .hour = hour, 117 | .minute = minute, 118 | .second = second, 119 | }; 120 | } 121 | }; 122 | 123 | const testing = std.testing; 124 | 125 | test "Parse Basic Date (Buffer)" { 126 | const ts = 1727411110; 127 | var date: Date = Date.init(ts); 128 | var buffer = [_]u8{0} ** 29; 129 | const http_date = date.to_http_date(); 130 | try testing.expectEqualStrings("Fri, 27 Sep 2024 04:25:10 GMT", try http_date.into_buf(buffer[0..])); 131 | } 132 | 133 | test "Parse Basic Date (Alloc)" { 134 | const ts = 1727464105; 135 | var date: Date = Date.init(ts); 136 | const http_date = date.to_http_date(); 137 | const http_string = try http_date.into_alloc(testing.allocator); 138 | defer testing.allocator.free(http_string); 139 | try testing.expectEqualStrings("Fri, 27 Sep 2024 19:08:25 GMT", http_string); 140 | } 141 | 142 | test "Parse Basic Date (Writer)" { 143 | const ts = 672452112; 144 | var date: Date = Date.init(ts); 145 | const http_date = date.to_http_date(); 146 | var buffer = [_]u8{0} ** 29; 147 | var stream = std.io.fixedBufferStream(buffer[0..]); 148 | try http_date.into_writer(stream.writer()); 149 | const http_string = stream.getWritten(); 150 | try testing.expectEqualStrings("Wed, 24 Apr 1991 00:15:12 GMT", http_string); 151 | } 152 | -------------------------------------------------------------------------------- /src/http/encoding.zig: -------------------------------------------------------------------------------- 1 | pub const Encoding = enum { 2 | gzip, 3 | compress, 4 | deflate, 5 | br, 6 | zstd, 7 | }; 8 | -------------------------------------------------------------------------------- /src/http/form.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | 4 | const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap; 5 | const Context = @import("context.zig").Context; 6 | 7 | pub fn decode_alloc(allocator: std.mem.Allocator, input: []const u8) ![]const u8 { 8 | var list = try std.ArrayListUnmanaged(u8).initCapacity(allocator, input.len); 9 | defer list.deinit(allocator); 10 | 11 | var input_index: usize = 0; 12 | while (input_index < input.len) { 13 | defer input_index += 1; 14 | const byte = input[input_index]; 15 | switch (byte) { 16 | '%' => { 17 | if (input_index + 2 >= input.len) return error.InvalidEncoding; 18 | list.appendAssumeCapacity( 19 | try std.fmt.parseInt(u8, input[input_index + 1 .. input_index + 3], 16), 20 | ); 21 | input_index += 2; 22 | }, 23 | '+' => list.appendAssumeCapacity(' '), 24 | else => list.appendAssumeCapacity(byte), 25 | } 26 | } 27 | 28 | return list.toOwnedSlice(allocator); 29 | } 30 | 31 | fn parse_from(allocator: std.mem.Allocator, comptime T: type, comptime name: []const u8, value: []const u8) !T { 32 | return switch (@typeInfo(T)) { 33 | .int => |info| switch (info.signedness) { 34 | .unsigned => try std.fmt.parseUnsigned(T, value, 10), 35 | .signed => try std.fmt.parseInt(T, value, 10), 36 | }, 37 | .float => try std.fmt.parseFloat(T, value), 38 | .optional => |info| @as(T, try parse_from(allocator, info.child, name, value)), 39 | .@"enum" => std.meta.stringToEnum(T, value) orelse return error.InvalidEnumValue, 40 | .bool => std.mem.eql(u8, value, "true"), 41 | else => switch (T) { 42 | []const u8 => try allocator.dupe(u8, value), 43 | [:0]const u8 => try allocator.dupeZ(u8, value), 44 | else => std.debug.panic("Unsupported field type \"{s}\"", .{@typeName(T)}), 45 | }, 46 | }; 47 | } 48 | 49 | fn parse_struct(allocator: std.mem.Allocator, comptime T: type, map: *const AnyCaseStringMap) !T { 50 | var ret: T = undefined; 51 | assert(@typeInfo(T) == .@"struct"); 52 | const struct_info = @typeInfo(T).@"struct"; 53 | inline for (struct_info.fields) |field| { 54 | const entry = map.getEntry(field.name); 55 | 56 | if (entry) |e| { 57 | @field(ret, field.name) = try parse_from(allocator, field.type, field.name, e.value_ptr.*); 58 | } else if (field.defaultValue()) |default| { 59 | @field(ret, field.name) = default; 60 | } else if (@typeInfo(field.type) == .optional) { 61 | @field(ret, field.name) = null; 62 | } else return error.FieldEmpty; 63 | } 64 | 65 | return ret; 66 | } 67 | 68 | fn construct_map_from_body(allocator: std.mem.Allocator, m: *AnyCaseStringMap, body: []const u8) !void { 69 | var pairs = std.mem.splitScalar(u8, body, '&'); 70 | 71 | while (pairs.next()) |pair| { 72 | const field_idx = std.mem.indexOfScalar(u8, pair, '=') orelse return error.MissingSeperator; 73 | if (pair.len < field_idx + 2) return error.MissingValue; 74 | 75 | const key = pair[0..field_idx]; 76 | const value = pair[(field_idx + 1)..]; 77 | 78 | if (std.mem.indexOfScalar(u8, value, '=') != null) return error.MalformedPair; 79 | 80 | const decoded_key = try decode_alloc(allocator, key); 81 | errdefer allocator.free(decoded_key); 82 | 83 | const decoded_value = try decode_alloc(allocator, value); 84 | errdefer allocator.free(decoded_value); 85 | 86 | // Allow for duplicates (like with the URL params), 87 | // The last one just takes precedent. 88 | const entry = try m.getOrPut(decoded_key); 89 | if (entry.found_existing) { 90 | allocator.free(decoded_key); 91 | allocator.free(entry.value_ptr.*); 92 | } 93 | entry.value_ptr.* = decoded_value; 94 | } 95 | } 96 | 97 | /// Parses Form data from a request body in `x-www-form-urlencoded` format. 98 | pub fn Form(comptime T: type) type { 99 | return struct { 100 | pub fn parse(allocator: std.mem.Allocator, ctx: *const Context) !T { 101 | var m = AnyCaseStringMap.init(ctx.allocator); 102 | defer { 103 | var it = m.iterator(); 104 | while (it.next()) |entry| { 105 | allocator.free(entry.key_ptr.*); 106 | allocator.free(entry.value_ptr.*); 107 | } 108 | m.deinit(); 109 | } 110 | 111 | if (ctx.request.body) |body| 112 | try construct_map_from_body(allocator, &m, body) 113 | else 114 | return error.BodyEmpty; 115 | 116 | return parse_struct(allocator, T, &m); 117 | } 118 | }; 119 | } 120 | 121 | /// Parses Form data from request URL query parameters. 122 | pub fn Query(comptime T: type) type { 123 | return struct { 124 | pub fn parse(allocator: std.mem.Allocator, ctx: *const Context) !T { 125 | return parse_struct(allocator, T, ctx.queries); 126 | } 127 | }; 128 | } 129 | 130 | const testing = std.testing; 131 | 132 | test "FormData: Parsing from Body" { 133 | const UserRole = enum { admin, visitor }; 134 | const User = struct { id: u32, name: []const u8, age: u8, role: UserRole }; 135 | const body: []const u8 = "id=10&name=John&age=12&role=visitor"; 136 | 137 | var m = AnyCaseStringMap.init(testing.allocator); 138 | defer { 139 | var it = m.iterator(); 140 | while (it.next()) |entry| { 141 | testing.allocator.free(entry.key_ptr.*); 142 | testing.allocator.free(entry.value_ptr.*); 143 | } 144 | m.deinit(); 145 | } 146 | try construct_map_from_body(testing.allocator, &m, body); 147 | 148 | const parsed = try parse_struct(testing.allocator, User, &m); 149 | defer testing.allocator.free(parsed.name); 150 | 151 | try testing.expectEqual(10, parsed.id); 152 | try testing.expectEqualSlices(u8, "John", parsed.name); 153 | try testing.expectEqual(12, parsed.age); 154 | try testing.expectEqual(UserRole.visitor, parsed.role); 155 | } 156 | 157 | test "FormData: Parsing Missing Fields" { 158 | const User = struct { id: u32, name: []const u8, age: u8 }; 159 | const body: []const u8 = "id=10"; 160 | 161 | var m = AnyCaseStringMap.init(testing.allocator); 162 | defer { 163 | var it = m.iterator(); 164 | while (it.next()) |entry| { 165 | testing.allocator.free(entry.key_ptr.*); 166 | testing.allocator.free(entry.value_ptr.*); 167 | } 168 | m.deinit(); 169 | } 170 | 171 | try construct_map_from_body(testing.allocator, &m, body); 172 | 173 | const parsed = parse_struct(testing.allocator, User, &m); 174 | try testing.expectError(error.FieldEmpty, parsed); 175 | } 176 | 177 | test "FormData: Parsing Missing Value" { 178 | const body: []const u8 = "abc=abc&id="; 179 | 180 | var m = AnyCaseStringMap.init(testing.allocator); 181 | defer { 182 | var it = m.iterator(); 183 | while (it.next()) |entry| { 184 | testing.allocator.free(entry.key_ptr.*); 185 | testing.allocator.free(entry.value_ptr.*); 186 | } 187 | m.deinit(); 188 | } 189 | 190 | const result = construct_map_from_body(testing.allocator, &m, body); 191 | try testing.expectError(error.MissingValue, result); 192 | } 193 | -------------------------------------------------------------------------------- /src/http/lib.zig: -------------------------------------------------------------------------------- 1 | pub const Status = @import("status.zig").Status; 2 | pub const Method = @import("method.zig").Method; 3 | pub const Request = @import("request.zig").Request; 4 | pub const Response = @import("response.zig").Response; 5 | pub const Respond = @import("response.zig").Respond; 6 | pub const Mime = @import("mime.zig").Mime; 7 | pub const Encoding = @import("encoding.zig").Encoding; 8 | pub const Date = @import("date.zig").Date; 9 | pub const Cookie = @import("cookie.zig").Cookie; 10 | 11 | pub const Form = @import("form.zig").Form; 12 | pub const Query = @import("form.zig").Query; 13 | 14 | pub const Context = @import("context.zig").Context; 15 | 16 | pub const Router = @import("router.zig").Router; 17 | pub const Route = @import("router/route.zig").Route; 18 | pub const SSE = @import("sse.zig").SSE; 19 | 20 | pub const Layer = @import("router/middleware.zig").Layer; 21 | pub const Middleware = @import("router/middleware.zig").Middleware; 22 | pub const MiddlewareFn = @import("router/middleware.zig").MiddlewareFn; 23 | pub const Next = @import("router/middleware.zig").Next; 24 | pub const Middlewares = @import("middlewares/lib.zig"); 25 | 26 | pub const FsDir = @import("router/fs_dir.zig").FsDir; 27 | 28 | pub const Server = @import("server.zig").Server; 29 | 30 | pub const HTTPError = error{ 31 | TooManyHeaders, 32 | ContentTooLarge, 33 | MalformedRequest, 34 | InvalidMethod, 35 | URITooLong, 36 | HTTPVersionNotSupported, 37 | }; 38 | -------------------------------------------------------------------------------- /src/http/method.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const log = std.log.scoped(.@"zzz/http/method"); 3 | 4 | pub const Method = enum(u8) { 5 | GET = 0, 6 | HEAD = 1, 7 | POST = 2, 8 | PUT = 3, 9 | DELETE = 4, 10 | CONNECT = 5, 11 | OPTIONS = 6, 12 | TRACE = 7, 13 | PATCH = 8, 14 | 15 | fn encode(method: []const u8) u64 { 16 | var buffer = [1]u8{0} ** @sizeOf(u64); 17 | std.mem.copyForwards(u8, buffer[0..], method); 18 | 19 | return std.mem.readPackedIntNative(u64, buffer[0..], 0); 20 | } 21 | 22 | pub fn parse(method: []const u8) !Method { 23 | if (method.len > (comptime @sizeOf(u64)) or method.len == 0) { 24 | log.warn("unable to encode method: {s}", .{method}); 25 | return error.CannotEncode; 26 | } 27 | 28 | const encoded = encode(method); 29 | 30 | return switch (encoded) { 31 | encode("GET") => Method.GET, 32 | encode("HEAD") => Method.HEAD, 33 | encode("POST") => Method.POST, 34 | encode("PUT") => Method.PUT, 35 | encode("DELETE") => Method.DELETE, 36 | encode("CONNECT") => Method.CONNECT, 37 | encode("OPTIONS") => Method.OPTIONS, 38 | encode("TRACE") => Method.TRACE, 39 | encode("PATCH") => Method.PATCH, 40 | else => { 41 | log.warn("unable to match method: {s} | {d}", .{ method, encoded }); 42 | return error.CannotParse; 43 | }, 44 | }; 45 | } 46 | }; 47 | 48 | const testing = std.testing; 49 | 50 | test "Parsing Strings" { 51 | for (std.meta.tags(Method)) |method| { 52 | const method_string = @tagName(method); 53 | try testing.expectEqual(method, Method.parse(method_string)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/http/middlewares/compression.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const Respond = @import("../response.zig").Respond; 4 | const Middleware = @import("../router/middleware.zig").Middleware; 5 | const Next = @import("../router/middleware.zig").Next; 6 | const Layer = @import("../router/middleware.zig").Layer; 7 | const TypedMiddlewareFn = @import("../router/middleware.zig").TypedMiddlewareFn; 8 | 9 | const Kind = union(enum) { 10 | gzip: std.compress.gzip.Options, 11 | }; 12 | 13 | /// Compression Middleware. 14 | /// 15 | /// Provides a Compression Layer for all routes under this that 16 | /// will properly compress the body and add the proper `Content-Encoding` header. 17 | pub fn Compression(comptime compression: Kind) Layer { 18 | const func: TypedMiddlewareFn(void) = switch (compression) { 19 | .gzip => |inner| struct { 20 | fn gzip_mw(next: *Next, _: void) !Respond { 21 | const respond = try next.run(); 22 | const response = next.context.response; 23 | if (response.body) |body| if (respond == .standard) { 24 | var compressed = try std.ArrayListUnmanaged(u8).initCapacity(next.context.allocator, body.len); 25 | errdefer compressed.deinit(next.context.allocator); 26 | 27 | var body_stream = std.io.fixedBufferStream(body); 28 | try std.compress.gzip.compress( 29 | body_stream.reader(), 30 | compressed.writer(next.context.allocator), 31 | inner, 32 | ); 33 | 34 | try response.headers.put("Content-Encoding", "gzip"); 35 | response.body = try compressed.toOwnedSlice(next.context.allocator); 36 | return .standard; 37 | }; 38 | 39 | return respond; 40 | } 41 | }.gzip_mw, 42 | }; 43 | 44 | return Middleware.init({}, func).layer(); 45 | } 46 | -------------------------------------------------------------------------------- /src/http/middlewares/lib.zig: -------------------------------------------------------------------------------- 1 | pub const Compression = @import("compression.zig").Compression; 2 | 3 | pub const RateLimitConfig = @import("rate_limit.zig").RateLimitConfig; 4 | pub const RateLimiting = @import("rate_limit.zig").RateLimiting; 5 | -------------------------------------------------------------------------------- /src/http/middlewares/rate_limit.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const Mime = @import("../mime.zig").Mime; 4 | const Respond = @import("../response.zig").Respond; 5 | const Response = @import("../response.zig").Response; 6 | const Middleware = @import("../router/middleware.zig").Middleware; 7 | const Next = @import("../router/middleware.zig").Next; 8 | const Layer = @import("../router/middleware.zig").Layer; 9 | const TypedMiddlewareFn = @import("../router/middleware.zig").TypedMiddlewareFn; 10 | 11 | /// Rate Limiting Middleware. 12 | /// 13 | /// Provides a IP-matching Bucket-based Rate Limiter. 14 | pub fn RateLimiting(config: *RateLimitConfig) Layer { 15 | const func: TypedMiddlewareFn(*RateLimitConfig) = struct { 16 | fn rate_limit_mw(next: *Next, c: *RateLimitConfig) !Respond { 17 | const ip = get_ip(next.context.socket.inner.addr); 18 | const time = std.time.milliTimestamp(); 19 | 20 | c.mutex.lock(); 21 | const entry = try c.map.getOrPut(ip); 22 | 23 | if (entry.found_existing) { 24 | entry.value_ptr.replenish(time, c.tokens_per_sec, c.max_tokens); 25 | if (entry.value_ptr.take()) { 26 | c.mutex.unlock(); 27 | return try next.run(); 28 | } 29 | c.mutex.unlock(); 30 | 31 | return c.response_on_limited; 32 | } 33 | 34 | entry.value_ptr.* = .{ .tokens = c.max_tokens, .last_refill_ms = time }; 35 | c.mutex.unlock(); 36 | return try next.run(); 37 | } 38 | }.rate_limit_mw; 39 | 40 | return Middleware.init(config, func).layer(); 41 | } 42 | 43 | pub const RateLimitConfig = struct { 44 | map: std.AutoHashMap(u128, Bucket), 45 | tokens_per_sec: u16, 46 | max_tokens: u16, 47 | response_on_limited: Response.Fields, 48 | mutex: std.Thread.Mutex = .{}, 49 | 50 | pub fn init( 51 | allocator: std.mem.Allocator, 52 | tokens_per_sec: u16, 53 | max_tokens: u16, 54 | response_on_limited: ?Respond, 55 | ) RateLimitConfig { 56 | const map = std.AutoHashMap(u128, Bucket).init(allocator); 57 | const respond = response_on_limited orelse Response.Fields{ 58 | .status = .@"Too Many Requests", 59 | .mime = Mime.TEXT, 60 | .body = "", 61 | }; 62 | 63 | return .{ 64 | .map = map, 65 | .tokens_per_sec = tokens_per_sec, 66 | .max_tokens = max_tokens, 67 | .response_on_limited = respond, 68 | }; 69 | } 70 | 71 | pub fn deinit(self: *RateLimitConfig) void { 72 | self.map.deinit(); 73 | } 74 | }; 75 | 76 | const Bucket = struct { 77 | tokens: u16, 78 | last_refill_ms: i64, 79 | 80 | pub fn replenish(self: *Bucket, time_ms: i64, tokens_per_sec: u16, max_tokens: u16) void { 81 | const delta_ms = time_ms - self.last_refill_ms; 82 | const new_tokens: u16 = @intCast(@divFloor(delta_ms * tokens_per_sec, std.time.ms_per_s)); 83 | self.tokens = @min(max_tokens, self.tokens + new_tokens); 84 | self.last_refill_ms = time_ms; 85 | } 86 | 87 | pub fn take(self: *Bucket) bool { 88 | if (self.tokens > 0) { 89 | self.tokens -= 1; 90 | return true; 91 | } 92 | 93 | return false; 94 | } 95 | }; 96 | 97 | fn get_ip(addr: std.net.Address) u128 { 98 | return switch (addr.any.family) { 99 | std.posix.AF.INET => @intCast(addr.in.sa.addr), 100 | std.posix.AF.INET6 => std.mem.bytesAsValue(u128, &addr.in6.sa.addr[0]).*, 101 | else => @panic("Not an IP address."), 102 | }; 103 | } 104 | -------------------------------------------------------------------------------- /src/http/mime.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | 4 | const Pair = @import("../core/lib.zig").Pair; 5 | 6 | const MimeOption = union(enum) { 7 | single: []const u8, 8 | /// The first one should be the priority one. 9 | /// The rest should just be there for compatibility reasons. 10 | multiple: []const []const u8, 11 | }; 12 | 13 | fn generate_mime_helper(any: anytype) MimeOption { 14 | assert(@typeInfo(@TypeOf(any)) == .pointer); 15 | const ptr_info = @typeInfo(@TypeOf(any)).pointer; 16 | assert(ptr_info.is_const); 17 | 18 | switch (ptr_info.size) { 19 | else => unreachable, 20 | .one => { 21 | switch (@typeInfo(ptr_info.child)) { 22 | else => unreachable, 23 | .array => |arr_info| { 24 | assert(arr_info.child == u8); 25 | return MimeOption{ .single = any }; 26 | }, 27 | .@"struct" => |struct_info| { 28 | for (struct_info.fields) |field| { 29 | assert(@typeInfo(field.type) == .pointer); 30 | const p_info = @typeInfo(field.type).pointer; 31 | assert(@typeInfo(p_info.child) == .array); 32 | const a_info = @typeInfo(p_info.child).array; 33 | assert(a_info.child == u8); 34 | } 35 | 36 | return MimeOption{ .multiple = any }; 37 | }, 38 | } 39 | }, 40 | } 41 | } 42 | 43 | /// MIME Types. 44 | pub const Mime = struct { 45 | /// This is the actual MIME type. 46 | content_type: MimeOption, 47 | extension: MimeOption, 48 | description: []const u8, 49 | 50 | pub const AAC = generate("audio/acc", "acc", "AAC Audio"); 51 | pub const APNG = generate("image/apng", "apng", "Animated Portable Network Graphics (APNG) Image"); 52 | pub const AVIF = generate("image/avif", "avif", "AVIF Image"); 53 | pub const AVI = generate("video/x-msvideo", "avi", "AVI: Audio Video Interleave"); 54 | pub const AZW = generate("application/vnd.amazon.ebook", "azw", "AZW: Amazon Kindle eBook format"); 55 | pub const BIN = generate("application/octet-stream", "bin", "Any kind of binary data"); 56 | pub const BMP = generate("image/bmp", "bmp", "Windows OS/2 Bitmap Graphics"); 57 | pub const BZ = generate("application/x-bzip", "bz", "BZip archive"); 58 | pub const BZ2 = generate("application/x-bzip2", "bz2", "BZip2 archive"); 59 | pub const CDA = generate("application/x-cdf", "cda", "CD audio"); 60 | pub const CSS = generate("text/css", "css", "Cascading Style Sheets (CSS)"); 61 | pub const CSV = generate("text/csv", "csv", "Comma-separated values (CSV)"); 62 | pub const DOC = generate("application/msword", "doc", "Microsoft Word"); 63 | pub const DOCX = generate( 64 | "application/vnd.openxlformats-officedocument.wordprocessingml.document", 65 | "docx", 66 | "Microsoft Word (OpenXML)", 67 | ); 68 | pub const EPUB = generate("application/epub+zip", "epub", "Electronic Publication"); 69 | pub const GIF = generate("image/gif", "gif", "Graphics Interchange Format (GIF)"); 70 | pub const GZ = generate(&.{ "application/gzip", "application/x-gzip" }, "gz", "GZip Compressed Archive"); 71 | pub const HTML = generate("text/html", &.{ "html", "htm" }, "HyperText Markup Language (HTML)"); 72 | pub const ICO = generate(&.{ "image/x-icon", "image/vnd.microsoft.icon" }, "ico", "Icon Format"); 73 | pub const ICS = generate("text/calander", "ics", "iCalendar format"); 74 | pub const JAR = generate("application/java-archive", "jar", "Java Archive"); 75 | pub const JPEG = generate("image/jpeg", &.{ "jpeg", "jpg" }, "JPEG Image"); 76 | pub const JS = generate(&.{ "text/javascript", "application/javascript" }, "js", "JavaScript"); 77 | pub const JSON = generate("application/json", "json", "JSON Format"); 78 | pub const MP3 = generate("audio/mpeg", "mp3", "MP3 audio"); 79 | pub const MP4 = generate("video/mp4", "mp4", "MP4 Video"); 80 | pub const OGA = generate("audio/ogg", "ogg", "Ogg audio"); 81 | pub const OGV = generate("video/ogg", "ogv", "Ogg video"); 82 | pub const OGX = generate("application/ogg", "ogx", "Ogg multiplexed audo and video"); 83 | pub const OTF = generate("font/otf", "otf", "OpenType font"); 84 | pub const PDF = generate("application/pdf", "pdf", "Adobe Portable Document Format"); 85 | pub const PHP = generate("application/x-httpd-php", "php", "Hypertext Preprocessor (Personal Home Page)"); 86 | pub const PNG = generate("image/png", "png", "Portable Network Graphics"); 87 | pub const RAR = generate("application/vnd.rar", "rar", "RAR archive"); 88 | pub const RTF = generate("application/rtf", "rtf", "Rich Text Format (RTF)"); 89 | pub const SH = generate("application/x-sh", "sh", "Bourne shell script"); 90 | pub const SVG = generate("image/svg+xml", "svg", "Scalable Vector Graphics (SVG)"); 91 | pub const TAR = generate("application/x-tar", "tar", "Tape Archive (TAR)"); 92 | pub const TEXT = generate("text/plain", "txt", "Text (generally ASCII or ISO-8859-n)"); 93 | pub const TSV = generate("text/tab-seperated-values", "tsv", "Tab-seperated values (TSV)"); 94 | pub const TTF = generate("font/ttf", "ttf", "TrueType Font"); 95 | pub const WAV = generate("audio/wav", "wav", "Waveform Audio Format"); 96 | pub const WEBA = generate("audio/webm", "weba", "WEBM Audio"); 97 | pub const WEBM = generate("video/webm", "webm", "WEBM Video"); 98 | pub const WEBP = generate("image/webp", "webp", "WEBP Image"); 99 | pub const WOFF = generate("font/woff", "woff", "Web Open Font Format (WOFF)"); 100 | pub const WOFF2 = generate("font/woff2", "woff2", "Web Open Font Format (WOFF)"); 101 | pub const XML = generate("application/xml", "xml", "XML"); 102 | pub const ZIP = generate("application/zip", "zip", "ZIP Archive"); 103 | pub const @"7Z" = generate("application/x-7z-compressed", "7z", "7-zip archive"); 104 | 105 | pub fn generate( 106 | comptime content_type: anytype, 107 | comptime extension: anytype, 108 | description: []const u8, 109 | ) Mime { 110 | return Mime{ 111 | .content_type = generate_mime_helper(content_type), 112 | .extension = generate_mime_helper(extension), 113 | .description = description, 114 | }; 115 | } 116 | 117 | pub fn from_extension(extension: []const u8) Mime { 118 | assert(extension.len > 0); 119 | return mime_extension_map.get(extension) orelse Mime.BIN; 120 | } 121 | 122 | pub fn from_content_type(content_type: []const u8) Mime { 123 | assert(content_type.len > 0); 124 | return mime_content_map.get(content_type) orelse Mime.BIN; 125 | } 126 | }; 127 | 128 | const all_mime_types = blk: { 129 | const decls = @typeInfo(Mime).@"struct".decls; 130 | var mimes: [decls.len]Mime = undefined; 131 | var index: usize = 0; 132 | for (decls) |decl| { 133 | if (@TypeOf(@field(Mime, decl.name)) == Mime) { 134 | mimes[index] = @field(Mime, decl.name); 135 | index += 1; 136 | } 137 | } 138 | 139 | var return_mimes: [index]Mime = undefined; 140 | for (0..index) |i| { 141 | return_mimes[i] = mimes[i]; 142 | } 143 | 144 | break :blk return_mimes; 145 | }; 146 | 147 | const mime_extension_map = blk: { 148 | const num_pairs = num: { 149 | var count: usize = 0; 150 | for (all_mime_types) |mime| { 151 | var value: usize = 0; 152 | value += switch (mime.extension) { 153 | .single => 1, 154 | .multiple => |items| items.len, 155 | }; 156 | count += value; 157 | } 158 | 159 | break :num count; 160 | }; 161 | 162 | var pairs: [num_pairs]Pair([]const u8, Mime) = undefined; 163 | 164 | var index: usize = 0; 165 | for (all_mime_types[0..]) |mime| { 166 | switch (mime.extension) { 167 | .single => |inner| { 168 | defer index += 1; 169 | pairs[index] = .{ inner, mime }; 170 | }, 171 | .multiple => |extensions| { 172 | for (extensions) |ext| { 173 | defer index += 1; 174 | pairs[index] = .{ ext, mime }; 175 | } 176 | }, 177 | } 178 | } 179 | 180 | break :blk std.StaticStringMap(Mime).initComptime(pairs); 181 | }; 182 | 183 | const mime_content_map = blk: { 184 | const num_pairs = num: { 185 | var count: usize = 0; 186 | for (all_mime_types) |mime| { 187 | var value: usize = 0; 188 | value += switch (mime.content_type) { 189 | .single => 1, 190 | .multiple => |items| items.len, 191 | }; 192 | count += value; 193 | } 194 | 195 | break :num count; 196 | }; 197 | 198 | var pairs: [num_pairs]Pair([]const u8, Mime) = undefined; 199 | 200 | var index: usize = 0; 201 | for (all_mime_types[0..]) |mime| { 202 | switch (mime.content_type) { 203 | .single => |inner| { 204 | defer index += 1; 205 | pairs[index] = .{ inner, mime }; 206 | }, 207 | .multiple => |content_types| { 208 | for (content_types) |ext| { 209 | defer index += 1; 210 | pairs[index] = .{ ext, mime }; 211 | } 212 | }, 213 | } 214 | } 215 | 216 | break :blk std.StaticStringMap(Mime).initComptime(pairs); 217 | }; 218 | 219 | const testing = std.testing; 220 | 221 | test "MIME from extensions" { 222 | for (all_mime_types) |mime| { 223 | switch (mime.extension) { 224 | .single => |inner| { 225 | try testing.expectEqualStrings( 226 | mime.description, 227 | Mime.from_extension(inner).description, 228 | ); 229 | }, 230 | .multiple => |extensions| { 231 | for (extensions) |ext| { 232 | try testing.expectEqualStrings( 233 | mime.description, 234 | Mime.from_extension(ext).description, 235 | ); 236 | } 237 | }, 238 | } 239 | } 240 | } 241 | 242 | test "MIME from unknown extension" { 243 | const extension = ".whatami"; 244 | const mime = Mime.from_extension(extension); 245 | try testing.expectEqual(Mime.BIN, mime); 246 | } 247 | 248 | test "MIME from content types" { 249 | for (all_mime_types) |mime| { 250 | switch (mime.content_type) { 251 | .single => |inner| { 252 | try testing.expectEqualStrings( 253 | mime.description, 254 | Mime.from_content_type(inner).description, 255 | ); 256 | }, 257 | .multiple => |content_types| { 258 | for (content_types) |ext| { 259 | try testing.expectEqualStrings( 260 | mime.description, 261 | Mime.from_content_type(ext).description, 262 | ); 263 | } 264 | }, 265 | } 266 | } 267 | } 268 | 269 | test "MIME from unknown content type" { 270 | const content_type = "application/whatami"; 271 | const mime = Mime.from_content_type(content_type); 272 | try testing.expectEqual(Mime.BIN, mime); 273 | } 274 | -------------------------------------------------------------------------------- /src/http/request.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const log = std.log.scoped(.@"zzz/http/request"); 3 | const assert = std.debug.assert; 4 | 5 | const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap; 6 | const CookieMap = @import("cookie.zig").CookieMap; 7 | const HTTPError = @import("lib.zig").HTTPError; 8 | const Method = @import("lib.zig").Method; 9 | 10 | pub const Request = struct { 11 | allocator: std.mem.Allocator, 12 | method: ?Method = null, 13 | uri: ?[]const u8 = null, 14 | version: ?std.http.Version = .@"HTTP/1.1", 15 | headers: AnyCaseStringMap, 16 | cookies: CookieMap, 17 | body: ?[]const u8 = null, 18 | 19 | /// This is for constructing a Request. 20 | pub fn init(allocator: std.mem.Allocator) Request { 21 | const headers = AnyCaseStringMap.init(allocator); 22 | const cookies = CookieMap.init(allocator); 23 | 24 | return Request{ 25 | .allocator = allocator, 26 | .headers = headers, 27 | .cookies = cookies, 28 | }; 29 | } 30 | 31 | pub fn deinit(self: *Request) void { 32 | self.cookies.deinit(); 33 | self.headers.deinit(); 34 | } 35 | 36 | pub fn clear(self: *Request) void { 37 | self.method = null; 38 | self.uri = null; 39 | self.body = null; 40 | self.cookies.clear(); 41 | self.headers.clearRetainingCapacity(); 42 | } 43 | 44 | const RequestParseOptions = struct { 45 | request_bytes_max: u32, 46 | request_uri_bytes_max: u32, 47 | }; 48 | 49 | pub fn parse_headers(self: *Request, bytes: []const u8, options: RequestParseOptions) !void { 50 | self.clear(); 51 | var total_size: u32 = 0; 52 | var lines = std.mem.tokenizeAny(u8, bytes, "\r\n"); 53 | 54 | if (lines.peek() == null) { 55 | return HTTPError.MalformedRequest; 56 | } 57 | 58 | var parsing_first_line = true; 59 | while (lines.next()) |line| { 60 | total_size += @intCast(line.len); 61 | 62 | if (total_size > options.request_bytes_max) { 63 | return HTTPError.ContentTooLarge; 64 | } 65 | 66 | if (parsing_first_line) { 67 | var chunks = std.mem.tokenizeScalar(u8, line, ' '); 68 | 69 | const method_string = chunks.next() orelse return HTTPError.MalformedRequest; 70 | const method = Method.parse(method_string) catch { 71 | log.warn("invalid method: {s}", .{method_string}); 72 | return HTTPError.InvalidMethod; 73 | }; 74 | 75 | const uri_string = chunks.next() orelse return HTTPError.MalformedRequest; 76 | if (uri_string.len >= options.request_uri_bytes_max) return HTTPError.URITooLong; 77 | if (uri_string[0] != '/') return HTTPError.MalformedRequest; 78 | 79 | const version_string = chunks.next() orelse return HTTPError.MalformedRequest; 80 | if (!std.mem.eql(u8, version_string, "HTTP/1.1")) return HTTPError.HTTPVersionNotSupported; 81 | self.set(.{ .method = method, .uri = uri_string }); 82 | 83 | // There shouldn't be anything else. 84 | if (chunks.next() != null) return HTTPError.MalformedRequest; 85 | parsing_first_line = false; 86 | } else { 87 | var header_iter = std.mem.tokenizeScalar(u8, line, ':'); 88 | const key = header_iter.next() orelse return HTTPError.MalformedRequest; 89 | const value = std.mem.trimLeft(u8, header_iter.rest(), &.{' '}); 90 | if (value.len == 0) return HTTPError.MalformedRequest; 91 | try self.headers.put(key, value); 92 | } 93 | } 94 | 95 | if (self.headers.get("Cookie")) |cookies| try self.cookies.parse_from_header(cookies); 96 | } 97 | 98 | pub const RequestSetOptions = struct { 99 | method: ?Method = null, 100 | uri: ?[]const u8 = null, 101 | body: ?[]const u8 = null, 102 | }; 103 | 104 | pub fn set(self: *Request, options: RequestSetOptions) void { 105 | if (options.method) |method| { 106 | self.method = method; 107 | } 108 | 109 | if (options.uri) |uri| { 110 | self.uri = uri; 111 | } 112 | 113 | if (options.body) |body| { 114 | self.body = body; 115 | } 116 | } 117 | 118 | /// Should this specific Request expect to capture a body. 119 | pub fn expect_body(self: Request) bool { 120 | return switch (self.method orelse return false) { 121 | .POST, .PUT, .PATCH => true, 122 | .GET, .HEAD, .DELETE, .CONNECT, .OPTIONS, .TRACE => false, 123 | }; 124 | } 125 | }; 126 | 127 | const testing = std.testing; 128 | 129 | test "Parse Request" { 130 | const request_text = 131 | \\GET / HTTP/1.1 132 | \\Host: localhost:9862 133 | \\Connection: keep-alive 134 | \\Accept: text/html 135 | ; 136 | 137 | var request = Request.init(testing.allocator); 138 | defer request.deinit(); 139 | 140 | try request.parse_headers(request_text[0..], .{ 141 | .request_bytes_max = 1024, 142 | .request_uri_bytes_max = 256, 143 | }); 144 | 145 | try testing.expectEqual(.GET, request.method); 146 | try testing.expectEqualStrings("/", request.uri.?); 147 | try testing.expectEqual(.@"HTTP/1.1", request.version); 148 | 149 | try testing.expectEqualStrings("localhost:9862", request.headers.get("Host").?); 150 | try testing.expectEqualStrings("keep-alive", request.headers.get("Connection").?); 151 | try testing.expectEqualStrings("text/html", request.headers.get("Accept").?); 152 | } 153 | 154 | test "Expect ContentTooLong Error" { 155 | const request_text_format = 156 | \\GET {s} HTTP/1.1 157 | \\Host: localhost:9862 158 | \\Connection: keep-alive 159 | \\Accept: text/html 160 | ; 161 | 162 | const request_text = std.fmt.comptimePrint(request_text_format, .{[_]u8{'a'} ** 4096}); 163 | var request = Request.init(testing.allocator); 164 | defer request.deinit(); 165 | 166 | const err = request.parse_headers(request_text[0..], .{ 167 | .request_bytes_max = 128, 168 | .request_uri_bytes_max = 64, 169 | }); 170 | try testing.expectError(HTTPError.ContentTooLarge, err); 171 | } 172 | 173 | test "Expect URITooLong Error" { 174 | const request_text_format = 175 | \\GET {s} HTTP/1.1 176 | \\Host: localhost:9862 177 | \\Connection: keep-alive 178 | \\Accept: text/html 179 | ; 180 | 181 | const request_text = std.fmt.comptimePrint(request_text_format, .{[_]u8{'a'} ** 4096}); 182 | var request = Request.init(testing.allocator); 183 | defer request.deinit(); 184 | 185 | const err = request.parse_headers(request_text[0..], .{ 186 | .request_bytes_max = 1024 * 1024, 187 | .request_uri_bytes_max = 2048, 188 | }); 189 | try testing.expectError(HTTPError.URITooLong, err); 190 | } 191 | 192 | test "Expect Malformed when URI missing /" { 193 | const request_text_format = 194 | \\GET {s} HTTP/1.1 195 | \\Host: localhost:9862 196 | \\Connection: keep-alive 197 | \\Accept: text/html 198 | ; 199 | 200 | const request_text = std.fmt.comptimePrint(request_text_format, .{[_]u8{'a'} ** 256}); 201 | var request = Request.init(testing.allocator); 202 | defer request.deinit(); 203 | 204 | const err = request.parse_headers(request_text[0..], .{ 205 | .request_bytes_max = 1024, 206 | .request_uri_bytes_max = 512, 207 | }); 208 | try testing.expectError(HTTPError.MalformedRequest, err); 209 | } 210 | 211 | test "Expect Incorrect HTTP Version" { 212 | const request_text = 213 | \\GET / HTTP/1.4 214 | \\Host: localhost:9862 215 | \\Connection: keep-alive 216 | \\Accept: text/html 217 | ; 218 | 219 | var request = Request.init(testing.allocator); 220 | defer request.deinit(); 221 | 222 | const err = request.parse_headers(request_text[0..], .{ 223 | .request_bytes_max = 1024, 224 | .request_uri_bytes_max = 512, 225 | }); 226 | try testing.expectError(HTTPError.HTTPVersionNotSupported, err); 227 | } 228 | 229 | test "Malformed AnyCaseStringMap" { 230 | const request_text = 231 | \\GET / HTTP/1.1 232 | \\Host: localhost:9862 233 | \\Connection: 234 | \\Accept: text/html 235 | ; 236 | 237 | var request = Request.init(testing.allocator); 238 | defer request.deinit(); 239 | 240 | const err = request.parse_headers(request_text[0..], .{ 241 | .request_bytes_max = 1024, 242 | .request_uri_bytes_max = 512, 243 | }); 244 | try testing.expectError(HTTPError.MalformedRequest, err); 245 | } 246 | -------------------------------------------------------------------------------- /src/http/response.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | 4 | const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap; 5 | const Status = @import("lib.zig").Status; 6 | const Mime = @import("lib.zig").Mime; 7 | const Date = @import("lib.zig").Date; 8 | 9 | const Stream = @import("tardy").Stream; 10 | 11 | pub const Respond = enum { 12 | // When we are returning a real HTTP request, we use this. 13 | standard, 14 | // If we responded and we want to give control back to the HTTP engine. 15 | responded, 16 | // If we want the connection to close. 17 | close, 18 | }; 19 | 20 | pub const Response = struct { 21 | status: ?Status = null, 22 | mime: ?Mime = null, 23 | body: ?[]const u8 = null, 24 | headers: AnyCaseStringMap, 25 | 26 | pub const Fields = struct { 27 | status: Status, 28 | mime: Mime, 29 | body: []const u8 = "", 30 | headers: []const [2][]const u8 = &.{}, 31 | }; 32 | 33 | pub fn init(allocator: std.mem.Allocator) Response { 34 | const headers = AnyCaseStringMap.init(allocator); 35 | return Response{ .headers = headers }; 36 | } 37 | 38 | pub fn deinit(self: *Response) void { 39 | self.headers.deinit(); 40 | } 41 | 42 | pub fn apply(self: *Response, into: Fields) !Respond { 43 | self.status = into.status; 44 | self.mime = into.mime; 45 | self.body = into.body; 46 | for (into.headers) |pair| try self.headers.put(pair[0], pair[1]); 47 | return .standard; 48 | } 49 | 50 | pub fn clear(self: *Response) void { 51 | self.status = null; 52 | self.mime = null; 53 | self.body = null; 54 | self.headers.clearRetainingCapacity(); 55 | } 56 | 57 | pub fn headers_into_writer(self: *Response, writer: anytype, content_length: ?usize) !void { 58 | // Status Line 59 | const status = self.status.?; 60 | try writer.print("HTTP/1.1 {d} {s}\r\n", .{ @intFromEnum(status), @tagName(status) }); 61 | 62 | // Headers 63 | try writer.writeAll("Server: zzz\r\nConnection: keep-alive\r\n"); 64 | var iter = self.headers.iterator(); 65 | while (iter.next()) |entry| try writer.print( 66 | "{s}: {s}\r\n", 67 | .{ entry.key_ptr.*, entry.value_ptr.* }, 68 | ); 69 | 70 | // Content-Type 71 | const mime = self.mime.?; 72 | const content_type = switch (mime.content_type) { 73 | .single => |inner| inner, 74 | .multiple => |content_types| content_types[0], 75 | }; 76 | try writer.print("Content-Type: {s}\r\n", .{content_type}); 77 | 78 | // Content-Length 79 | if (content_length) |length| try writer.print("Content-Length: {d}\r\n", .{length}); 80 | 81 | try writer.writeAll("\r\n"); 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /src/http/router.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const log = std.log.scoped(.@"zzz/http/router"); 3 | const assert = std.debug.assert; 4 | 5 | const Layer = @import("router/middleware.zig").Layer; 6 | const Route = @import("router/route.zig").Route; 7 | const TypedHandlerFn = @import("router/route.zig").TypedHandlerFn; 8 | 9 | const Bundle = @import("router/routing_trie.zig").Bundle; 10 | 11 | const Capture = @import("router/routing_trie.zig").Capture; 12 | const Request = @import("request.zig").Request; 13 | const Response = @import("response.zig").Response; 14 | const Respond = @import("response.zig").Respond; 15 | const Mime = @import("mime.zig").Mime; 16 | const Context = @import("context.zig").Context; 17 | 18 | const RoutingTrie = @import("router/routing_trie.zig").RoutingTrie; 19 | const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap; 20 | 21 | /// Default not found handler: send a plain text response. 22 | pub const default_not_found_handler = struct { 23 | fn not_found_handler(ctx: *const Context, _: void) !Respond { 24 | const response = ctx.response; 25 | response.status = .@"Not Found"; 26 | response.mime = Mime.TEXT; 27 | response.body = "404 | Not Found"; 28 | 29 | return .standard; 30 | } 31 | }.not_found_handler; 32 | 33 | /// Initialize a router with the given routes. 34 | pub const Router = struct { 35 | /// Router configuration structure. 36 | pub const Configuration = struct { 37 | not_found: TypedHandlerFn(void) = default_not_found_handler, 38 | }; 39 | 40 | routes: RoutingTrie, 41 | configuration: Configuration, 42 | 43 | pub fn init( 44 | allocator: std.mem.Allocator, 45 | layers: []const Layer, 46 | configuration: Configuration, 47 | ) !Router { 48 | const self = Router{ 49 | .routes = try RoutingTrie.init(allocator, layers), 50 | .configuration = configuration, 51 | }; 52 | 53 | return self; 54 | } 55 | 56 | pub fn deinit(self: *Router, allocator: std.mem.Allocator) void { 57 | self.routes.deinit(allocator); 58 | } 59 | 60 | pub fn get_bundle_from_host( 61 | self: *const Router, 62 | allocator: std.mem.Allocator, 63 | path: []const u8, 64 | captures: []Capture, 65 | queries: *AnyCaseStringMap, 66 | ) !Bundle { 67 | queries.clearRetainingCapacity(); 68 | 69 | return try self.routes.get_bundle(allocator, path, captures, queries) orelse Bundle{ 70 | .route = Route.init("").all({}, self.configuration.not_found), 71 | .captures = captures[0..], 72 | .queries = queries, 73 | .duped = &.{}, 74 | }; 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/http/router/fs_dir.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const log = std.log.scoped(.@"zzz/http/router"); 3 | const assert = std.debug.assert; 4 | 5 | const Route = @import("route.zig").Route; 6 | const Layer = @import("middleware.zig").Layer; 7 | const Request = @import("../request.zig").Request; 8 | const Respond = @import("../response.zig").Respond; 9 | const Mime = @import("../mime.zig").Mime; 10 | const Context = @import("../context.zig").Context; 11 | 12 | const Runtime = @import("tardy").Runtime; 13 | const ZeroCopy = @import("tardy").ZeroCopy; 14 | const Dir = @import("tardy").Dir; 15 | const Stat = @import("tardy").Stat; 16 | 17 | const Stream = @import("tardy").Stream; 18 | 19 | pub const FsDir = struct { 20 | fn fs_dir_handler(ctx: *const Context, dir: Dir) !Respond { 21 | if (ctx.captures.len == 0) return ctx.response.apply(.{ 22 | .status = .@"Not Found", 23 | .mime = Mime.HTML, 24 | }); 25 | 26 | const response = ctx.response; 27 | 28 | // Resolving the requested file. 29 | const search_path = ctx.captures[0].remaining; 30 | const file_path_z = try ctx.allocator.dupeZ(u8, search_path); 31 | 32 | // TODO: check that the path is valid. 33 | 34 | const extension_start = std.mem.lastIndexOfScalar(u8, search_path, '.'); 35 | const mime: Mime = blk: { 36 | if (extension_start) |start| { 37 | if (search_path.len - start == 0) break :blk Mime.BIN; 38 | break :blk Mime.from_extension(search_path[start + 1 ..]); 39 | } else { 40 | break :blk Mime.BIN; 41 | } 42 | }; 43 | 44 | const file = dir.open_file(ctx.runtime, file_path_z, .{ .mode = .read }) catch |e| switch (e) { 45 | error.NotFound => { 46 | return ctx.response.apply(.{ 47 | .status = .@"Not Found", 48 | .mime = Mime.HTML, 49 | }); 50 | }, 51 | else => return e, 52 | }; 53 | const stat = try file.stat(ctx.runtime); 54 | 55 | var hash = std.hash.Wyhash.init(0); 56 | hash.update(std.mem.asBytes(&stat.size)); 57 | if (stat.modified) |modified| { 58 | hash.update(std.mem.asBytes(&modified.seconds)); 59 | hash.update(std.mem.asBytes(&modified.nanos)); 60 | } 61 | const etag_hash = hash.final(); 62 | 63 | const calc_etag = try std.fmt.allocPrint(ctx.allocator, "\"{d}\"", .{etag_hash}); 64 | try response.headers.put("ETag", calc_etag); 65 | 66 | // If we have an ETag on the request... 67 | if (ctx.request.headers.get("If-None-Match")) |etag| { 68 | if (std.mem.eql(u8, etag, calc_etag)) { 69 | // If the ETag matches. 70 | return ctx.response.apply(.{ 71 | .status = .@"Not Modified", 72 | .mime = Mime.HTML, 73 | }); 74 | } 75 | } 76 | 77 | // apply the fields. 78 | response.status = .OK; 79 | response.mime = mime; 80 | 81 | try response.headers_into_writer(ctx.header_buffer.writer(), stat.size); 82 | const headers = ctx.header_buffer.items; 83 | const length = try ctx.socket.send_all(ctx.runtime, headers); 84 | if (headers.len != length) return error.SendingHeadersFailed; 85 | 86 | var buffer = ctx.header_buffer.allocatedSlice(); 87 | while (true) { 88 | const read_count = file.read(ctx.runtime, buffer, null) catch |e| switch (e) { 89 | error.EndOfFile => break, 90 | else => return e, 91 | }; 92 | 93 | _ = ctx.socket.send(ctx.runtime, buffer[0..read_count]) catch |e| switch (e) { 94 | error.Closed => break, 95 | else => return e, 96 | }; 97 | } 98 | 99 | return .responded; 100 | } 101 | 102 | /// Serve a Filesystem Directory as a Layer. 103 | pub fn serve(comptime url_path: []const u8, dir: Dir) Layer { 104 | const url_with_match_all = comptime std.fmt.comptimePrint( 105 | "{s}/%r", 106 | .{std.mem.trimRight(u8, url_path, "/")}, 107 | ); 108 | 109 | return Route.init(url_with_match_all).get(dir, fs_dir_handler).layer(); 110 | } 111 | }; 112 | -------------------------------------------------------------------------------- /src/http/router/middleware.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const log = std.log.scoped(.@"zzz/router/middleware"); 3 | const assert = std.debug.assert; 4 | 5 | const Runtime = @import("tardy").Runtime; 6 | 7 | const wrap = @import("../../core/wrapping.zig").wrap; 8 | const Pseudoslice = @import("../../core/pseudoslice.zig").Pseudoslice; 9 | const Server = @import("../server.zig").Server; 10 | 11 | const Mime = @import("../mime.zig").Mime; 12 | const Route = @import("route.zig").Route; 13 | const HandlerWithData = @import("route.zig").HandlerWithData; 14 | const Context = @import("../context.zig").Context; 15 | const Respond = @import("../response.zig").Respond; 16 | 17 | pub const Layer = union(enum) { 18 | /// Route 19 | route: Route, 20 | /// Middleware 21 | middleware: MiddlewareWithData, 22 | }; 23 | 24 | pub const Next = struct { 25 | context: *const Context, 26 | middlewares: []const MiddlewareWithData, 27 | handler: HandlerWithData, 28 | 29 | pub fn run(self: *Next) !Respond { 30 | if (self.middlewares.len > 0) { 31 | const middleware = self.middlewares[0]; 32 | self.middlewares = self.middlewares[1..]; 33 | return try middleware.func(self, middleware.data); 34 | } else return try self.handler.handler(self.context, self.handler.data); 35 | } 36 | }; 37 | 38 | pub const MiddlewareFn = *const fn (*Next, usize) anyerror!Respond; 39 | pub fn TypedMiddlewareFn(comptime T: type) type { 40 | return *const fn (*Next, T) anyerror!Respond; 41 | } 42 | 43 | pub const MiddlewareWithData = struct { 44 | func: MiddlewareFn, 45 | data: usize, 46 | }; 47 | 48 | pub const Middleware = struct { 49 | inner: MiddlewareWithData, 50 | 51 | pub fn init(data: anytype, func: TypedMiddlewareFn(@TypeOf(data))) Middleware { 52 | return .{ 53 | .inner = .{ 54 | .func = @ptrCast(func), 55 | .data = wrap(usize, data), 56 | }, 57 | }; 58 | } 59 | 60 | pub fn layer(self: Middleware) Layer { 61 | return .{ .middleware = self.inner }; 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /src/http/router/route.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const log = std.log.scoped(.@"zzz/http/route"); 4 | const assert = std.debug.assert; 5 | 6 | const wrap = @import("../../core/wrapping.zig").wrap; 7 | 8 | const Method = @import("../method.zig").Method; 9 | const Request = @import("../request.zig").Request; 10 | const Response = @import("../response.zig").Response; 11 | const Respond = @import("../response.zig").Respond; 12 | const Mime = @import("../mime.zig").Mime; 13 | const Encoding = @import("../encoding.zig").Encoding; 14 | 15 | const FsDir = @import("fs_dir.zig").FsDir; 16 | const Context = @import("../context.zig").Context; 17 | const Layer = @import("middleware.zig").Layer; 18 | 19 | const MiddlewareWithData = @import("middleware.zig").MiddlewareWithData; 20 | 21 | pub const HandlerFn = *const fn (*const Context, usize) anyerror!Respond; 22 | pub fn TypedHandlerFn(comptime T: type) type { 23 | return *const fn (*const Context, T) anyerror!Respond; 24 | } 25 | 26 | pub const HandlerWithData = struct { 27 | handler: HandlerFn, 28 | middlewares: []const MiddlewareWithData, 29 | data: usize, 30 | }; 31 | 32 | /// Structure of a server route definition. 33 | pub const Route = struct { 34 | /// Defined route path. 35 | path: []const u8, 36 | 37 | /// Route Handlers. 38 | handlers: [9]?HandlerWithData = .{null} ** 9, 39 | 40 | /// Initialize a route for the given path. 41 | pub fn init(path: []const u8) Route { 42 | return Route{ .path = path }; 43 | } 44 | 45 | /// Returns a comma delinated list of allowed Methods for this route. This 46 | /// is meant to be used as the value for the 'Allow' header in the Response. 47 | pub fn get_allowed(self: Route, allocator: std.mem.Allocator) ![]const u8 { 48 | // This gets allocated within the context of the connection's arena. 49 | const allowed_size = comptime blk: { 50 | var size = 0; 51 | for (std.meta.tags(Method)) |method| { 52 | size += @tagName(method).len + 1; 53 | } 54 | break :blk size; 55 | }; 56 | 57 | const buffer = try allocator.alloc(u8, allowed_size); 58 | 59 | var current: []u8 = ""; 60 | inline for (std.meta.tags(Method)) |method| { 61 | if (self.handlers[@intFromEnum(method)] != null) { 62 | current = std.fmt.bufPrint( 63 | buffer, 64 | "{s},{s}", 65 | .{ @tagName(method), current }, 66 | ) catch unreachable; 67 | } 68 | } 69 | 70 | if (current.len == 0) { 71 | return current; 72 | } else { 73 | return current[0 .. current.len - 1]; 74 | } 75 | } 76 | 77 | /// Get a defined request handler for the provided method. 78 | /// Return NULL if no handler is defined for this method. 79 | pub fn get_handler(self: Route, method: Method) ?HandlerWithData { 80 | return self.handlers[@intFromEnum(method)]; 81 | } 82 | 83 | pub fn layer(self: Route) Layer { 84 | return .{ .route = self }; 85 | } 86 | 87 | /// Set a handler function for the provided method. 88 | inline fn inner_route( 89 | comptime method: Method, 90 | self: Route, 91 | data: anytype, 92 | handler_fn: TypedHandlerFn(@TypeOf(data)), 93 | ) Route { 94 | const wrapped = wrap(usize, data); 95 | var new_handlers = self.handlers; 96 | new_handlers[comptime @intFromEnum(method)] = .{ 97 | .handler = @ptrCast(handler_fn), 98 | .middlewares = &.{}, 99 | .data = wrapped, 100 | }; 101 | 102 | return Route{ .path = self.path, .handlers = new_handlers }; 103 | } 104 | 105 | /// Set a handler function for all methods. 106 | pub fn all(self: Route, data: anytype, handler_fn: TypedHandlerFn(@TypeOf(data))) Route { 107 | const wrapped = wrap(usize, data); 108 | var new_handlers = self.handlers; 109 | 110 | for (&new_handlers) |*new_handler| { 111 | new_handler.* = .{ 112 | .handler = @ptrCast(handler_fn), 113 | .middlewares = &.{}, 114 | .data = wrapped, 115 | }; 116 | } 117 | 118 | return Route{ 119 | .path = self.path, 120 | .handlers = new_handlers, 121 | }; 122 | } 123 | 124 | pub fn get(self: Route, data: anytype, handler_fn: TypedHandlerFn(@TypeOf(data))) Route { 125 | return inner_route(.GET, self, data, handler_fn); 126 | } 127 | 128 | pub fn head(self: Route, data: anytype, handler_fn: TypedHandlerFn(@TypeOf(data))) Route { 129 | return inner_route(.HEAD, self, data, handler_fn); 130 | } 131 | 132 | pub fn post(self: Route, data: anytype, handler_fn: TypedHandlerFn(@TypeOf(data))) Route { 133 | return inner_route(.POST, self, data, handler_fn); 134 | } 135 | 136 | pub fn put(self: Route, data: anytype, handler_fn: TypedHandlerFn(@TypeOf(data))) Route { 137 | return inner_route(.PUT, self, data, handler_fn); 138 | } 139 | 140 | pub fn delete(self: Route, data: anytype, handler_fn: TypedHandlerFn(@TypeOf(data))) Route { 141 | return inner_route(.DELETE, self, data, handler_fn); 142 | } 143 | 144 | pub fn connect(self: Route, data: anytype, handler_fn: TypedHandlerFn(@TypeOf(data))) Route { 145 | return inner_route(.CONNECT, self, data, handler_fn); 146 | } 147 | 148 | pub fn options(self: Route, data: anytype, handler_fn: TypedHandlerFn(@TypeOf(data))) Route { 149 | return inner_route(.OPTIONS, self, data, handler_fn); 150 | } 151 | 152 | pub fn trace(self: Route, data: anytype, handler_fn: TypedHandlerFn(@TypeOf(data))) Route { 153 | return inner_route(.TRACE, self, data, handler_fn); 154 | } 155 | 156 | pub fn patch(self: Route, data: anytype, handler_fn: TypedHandlerFn(@TypeOf(data))) Route { 157 | return inner_route(.PATCH, self, data, handler_fn); 158 | } 159 | 160 | const ServeEmbeddedOptions = struct { 161 | /// If you are serving a compressed file, please 162 | /// set the correct encoding type. 163 | encoding: ?Encoding = null, 164 | mime: ?Mime = null, 165 | }; 166 | 167 | /// Define a GET handler to serve an embedded file. 168 | pub fn embed_file( 169 | self: *const Route, 170 | comptime opts: ServeEmbeddedOptions, 171 | comptime bytes: []const u8, 172 | ) Route { 173 | return self.get({}, struct { 174 | fn handler_fn(ctx: *const Context, _: void) !Respond { 175 | const response = ctx.response; 176 | 177 | const cache_control: []const u8 = if (comptime builtin.mode == .Debug) 178 | "no-cache" 179 | else 180 | comptime std.fmt.comptimePrint( 181 | "max-age={d}", 182 | .{std.time.s_per_day * 30}, 183 | ); 184 | 185 | try response.headers.put("Cache-Control", cache_control); 186 | 187 | // If our static item is greater than 1KB, 188 | // it might be more beneficial to using caching. 189 | if (comptime bytes.len > 1024) { 190 | @setEvalBranchQuota(1_000_000); 191 | const etag = comptime std.fmt.comptimePrint( 192 | "\"{d}\"", 193 | .{std.hash.Wyhash.hash(0, bytes)}, 194 | ); 195 | try response.headers.put("ETag", etag[0..]); 196 | 197 | if (ctx.request.headers.get("If-None-Match")) |match| { 198 | if (std.mem.eql(u8, etag, match)) { 199 | return response.apply(.{ 200 | .status = .@"Not Modified", 201 | .mime = Mime.HTML, 202 | }); 203 | } 204 | } 205 | } 206 | 207 | if (opts.encoding) |encoding| try response.headers.put("Content-Encoding", @tagName(encoding)); 208 | return response.apply(.{ 209 | .status = .OK, 210 | .mime = opts.mime orelse Mime.BIN, 211 | .body = bytes, 212 | }); 213 | } 214 | }.handler_fn); 215 | } 216 | }; 217 | -------------------------------------------------------------------------------- /src/http/router/routing_trie.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | const log = std.log.scoped(.@"zzz/http/routing_trie"); 4 | 5 | const Layer = @import("middleware.zig").Layer; 6 | const Route = @import("route.zig").Route; 7 | 8 | const Respond = @import("../response.zig").Respond; 9 | const Context = @import("../lib.zig").Context; 10 | 11 | const HandlerWithData = @import("route.zig").HandlerWithData; 12 | const MiddlewareWithData = @import("middleware.zig").MiddlewareWithData; 13 | const AnyCaseStringMap = @import("../../core/any_case_string_map.zig").AnyCaseStringMap; 14 | 15 | const decode_alloc = @import("../form.zig").decode_alloc; 16 | 17 | fn TokenHashMap(comptime V: type) type { 18 | return std.HashMap(Token, V, struct { 19 | pub fn hash(self: @This(), input: Token) u64 { 20 | _ = self; 21 | 22 | const bytes = blk: { 23 | switch (input) { 24 | .fragment => |inner| break :blk inner, 25 | .match => |inner| break :blk @tagName(inner), 26 | } 27 | }; 28 | 29 | return std.hash.Wyhash.hash(0, bytes); 30 | } 31 | 32 | pub fn eql(self: @This(), first: Token, second: Token) bool { 33 | _ = self; 34 | 35 | const result = blk: { 36 | switch (first) { 37 | .fragment => |f_inner| { 38 | switch (second) { 39 | .fragment => |s_inner| break :blk std.mem.eql(u8, f_inner, s_inner), 40 | else => break :blk false, 41 | } 42 | }, 43 | .match => |f_inner| { 44 | switch (second) { 45 | .match => |s_inner| break :blk f_inner == s_inner, 46 | else => break :blk false, 47 | } 48 | }, 49 | } 50 | }; 51 | 52 | return result; 53 | } 54 | }, 80); 55 | } 56 | 57 | const TokenEnum = enum(u8) { 58 | fragment = 0, 59 | match = 1, 60 | }; 61 | 62 | pub const TokenMatch = enum { 63 | unsigned, 64 | signed, 65 | float, 66 | string, 67 | remaining, 68 | 69 | pub fn as_type(match: TokenMatch) type { 70 | switch (match) { 71 | .unsigned => return u64, 72 | .signed => return i64, 73 | .float => return f64, 74 | .string => return []const u8, 75 | .remaining => return []const u8, 76 | } 77 | } 78 | }; 79 | 80 | pub const Token = union(TokenEnum) { 81 | fragment: []const u8, 82 | match: TokenMatch, 83 | 84 | pub fn parse_chunk(chunk: []const u8) Token { 85 | if (std.mem.startsWith(u8, chunk, "%")) { 86 | // Needs to be only % and an identifier. 87 | assert(chunk.len == 2); 88 | 89 | switch (chunk[1]) { 90 | 'i', 'd' => return .{ .match = .signed }, 91 | 'u' => return .{ .match = .unsigned }, 92 | 'f' => return .{ .match = .float }, 93 | 's' => return .{ .match = .string }, 94 | 'r' => return .{ .match = .remaining }, 95 | else => @panic("Unsupported Match!"), 96 | } 97 | } else { 98 | return .{ .fragment = chunk }; 99 | } 100 | } 101 | }; 102 | 103 | pub const Query = struct { 104 | key: []const u8, 105 | value: []const u8, 106 | }; 107 | 108 | pub const Capture = union(TokenMatch) { 109 | unsigned: TokenMatch.unsigned.as_type(), 110 | signed: TokenMatch.signed.as_type(), 111 | float: TokenMatch.float.as_type(), 112 | string: TokenMatch.string.as_type(), 113 | remaining: TokenMatch.remaining.as_type(), 114 | }; 115 | 116 | /// Structure of a matched route. 117 | pub const Bundle = struct { 118 | route: Route, 119 | captures: []Capture, 120 | queries: *AnyCaseStringMap, 121 | duped: []const []const u8, 122 | }; 123 | 124 | // This RoutingTrie is deleteless. It only can create new routes or update existing ones. 125 | pub const RoutingTrie = struct { 126 | const Self = @This(); 127 | 128 | /// Structure of a node of the trie. 129 | pub const Node = struct { 130 | token: Token, 131 | route: ?Route = null, 132 | children: TokenHashMap(Node), 133 | 134 | /// Initialize a new empty node. 135 | pub fn init(allocator: std.mem.Allocator, token: Token, route: ?Route) Node { 136 | return .{ 137 | .token = token, 138 | .route = route, 139 | .children = TokenHashMap(Node).init(allocator), 140 | }; 141 | } 142 | 143 | pub fn deinit(self: *Node) void { 144 | var iter = self.children.valueIterator(); 145 | 146 | while (iter.next()) |node| { 147 | node.deinit(); 148 | } 149 | 150 | self.children.deinit(); 151 | } 152 | }; 153 | 154 | root: Node, 155 | middlewares: std.ArrayListUnmanaged(MiddlewareWithData), 156 | 157 | /// Initialize the routing tree with the given routes. 158 | pub fn init(allocator: std.mem.Allocator, layers: []const Layer) !Self { 159 | var self: Self = .{ 160 | .root = Node.init(allocator, .{ .fragment = "" }, null), 161 | .middlewares = try std.ArrayListUnmanaged(MiddlewareWithData).initCapacity(allocator, 0), 162 | }; 163 | 164 | for (layers) |layer| { 165 | switch (layer) { 166 | .route => |route| { 167 | var current = &self.root; 168 | var iter = std.mem.tokenizeScalar(u8, route.path, '/'); 169 | 170 | while (iter.next()) |chunk| { 171 | const token: Token = Token.parse_chunk(chunk); 172 | if (current.children.getPtr(token)) |child| { 173 | current = child; 174 | } else { 175 | try current.children.put(token, Node.init(allocator, token, null)); 176 | current = current.children.getPtr(token).?; 177 | } 178 | } 179 | 180 | const r: *Route = if (current.route) |*inner| inner else blk: { 181 | current.route = route; 182 | break :blk ¤t.route.?; 183 | }; 184 | 185 | for (route.handlers, 0..) |handler, i| if (handler) |h| { 186 | r.handlers[i] = HandlerWithData{ 187 | .handler = h.handler, 188 | .middlewares = self.middlewares.items, 189 | .data = h.data, 190 | }; 191 | }; 192 | }, 193 | .middleware => |mw| try self.middlewares.append(allocator, mw), 194 | } 195 | } 196 | 197 | return self; 198 | } 199 | 200 | pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { 201 | self.root.deinit(); 202 | self.middlewares.deinit(allocator); 203 | } 204 | 205 | pub fn get_bundle( 206 | self: Self, 207 | allocator: std.mem.Allocator, 208 | path: []const u8, 209 | captures: []Capture, 210 | queries: *AnyCaseStringMap, 211 | ) !?Bundle { 212 | var capture_idx: usize = 0; 213 | const query_pos = std.mem.indexOfScalar(u8, path, '?'); 214 | var iter = std.mem.tokenizeScalar(u8, path[0..(query_pos orelse path.len)], '/'); 215 | 216 | var current = self.root; 217 | 218 | slash_loop: while (iter.next()) |chunk| { 219 | var child_iter = current.children.iterator(); 220 | child_loop: while (child_iter.next()) |entry| { 221 | const token = entry.key_ptr.*; 222 | const child = entry.value_ptr.*; 223 | 224 | switch (token) { 225 | .fragment => |inner| if (std.mem.eql(u8, inner, chunk)) { 226 | current = child; 227 | continue :slash_loop; 228 | }, 229 | .match => |kind| { 230 | switch (kind) { 231 | .signed => if (std.fmt.parseInt(i64, chunk, 10)) |value| { 232 | captures[capture_idx] = Capture{ .signed = value }; 233 | } else |_| continue :child_loop, 234 | .unsigned => if (std.fmt.parseInt(u64, chunk, 10)) |value| { 235 | captures[capture_idx] = Capture{ .unsigned = value }; 236 | } else |_| continue :child_loop, 237 | // Float types MUST have a '.' to differentiate them. 238 | .float => if (std.mem.indexOfScalar(u8, chunk, '.')) |_| { 239 | if (std.fmt.parseFloat(f64, chunk)) |value| { 240 | captures[capture_idx] = Capture{ .float = value }; 241 | } else |_| continue :child_loop; 242 | } else continue :child_loop, 243 | .string => captures[capture_idx] = Capture{ .string = chunk }, 244 | .remaining => { 245 | const rest = iter.buffer[(iter.index - chunk.len)..]; 246 | captures[capture_idx] = Capture{ .remaining = rest }; 247 | 248 | current = child; 249 | capture_idx += 1; 250 | 251 | break :slash_loop; 252 | }, 253 | } 254 | 255 | current = child; 256 | capture_idx += 1; 257 | if (capture_idx > captures.len) return error.TooManyCaptures; 258 | continue :slash_loop; 259 | }, 260 | } 261 | } 262 | 263 | // If we failed to match, this is an invalid route. 264 | return null; 265 | } 266 | 267 | var duped = try std.ArrayListUnmanaged([]const u8).initCapacity(allocator, 0); 268 | defer duped.deinit(allocator); 269 | errdefer for (duped.items) |d| allocator.free(d); 270 | 271 | if (query_pos) |pos| { 272 | if (path.len > pos + 1) { 273 | var query_iter = std.mem.tokenizeScalar(u8, path[pos + 1 ..], '&'); 274 | 275 | while (query_iter.next()) |chunk| { 276 | const field_idx = std.mem.indexOfScalar(u8, chunk, '=') orelse return error.MissingValue; 277 | if (chunk.len < field_idx + 2) return error.MissingValue; 278 | 279 | const key = chunk[0..field_idx]; 280 | const value = chunk[(field_idx + 1)..]; 281 | 282 | if (std.mem.indexOfScalar(u8, value, '=') != null) return error.MalformedPair; 283 | 284 | const decoded_key = try decode_alloc(allocator, key); 285 | try duped.append(allocator, decoded_key); 286 | 287 | const decoded_value = try decode_alloc(allocator, value); 288 | try duped.append(allocator, decoded_value); 289 | 290 | // Later values will clobber earlier ones. 291 | try queries.put(decoded_key, decoded_value); 292 | } 293 | } 294 | } 295 | 296 | return Bundle{ 297 | .route = current.route orelse return null, 298 | .captures = captures[0..capture_idx], 299 | .queries = queries, 300 | .duped = try duped.toOwnedSlice(allocator), 301 | }; 302 | } 303 | }; 304 | 305 | const testing = std.testing; 306 | 307 | test "Chunk Parsing (Fragment)" { 308 | const chunk = "thisIsAFragment"; 309 | const token: Token = Token.parse_chunk(chunk); 310 | 311 | switch (token) { 312 | .fragment => |inner| try testing.expectEqualStrings(chunk, inner), 313 | .match => return error.IncorrectTokenParsing, 314 | } 315 | } 316 | 317 | test "Chunk Parsing (Match)" { 318 | const chunks: [5][]const u8 = .{ 319 | "%i", 320 | "%d", 321 | "%u", 322 | "%f", 323 | "%s", 324 | }; 325 | 326 | const matches = [_]TokenMatch{ 327 | TokenMatch.signed, 328 | TokenMatch.signed, 329 | TokenMatch.unsigned, 330 | TokenMatch.float, 331 | TokenMatch.string, 332 | }; 333 | 334 | for (chunks, matches) |chunk, match| { 335 | const token: Token = Token.parse_chunk(chunk); 336 | 337 | switch (token) { 338 | .fragment => return error.IncorrectTokenParsing, 339 | .match => |inner| try testing.expectEqual(match, inner), 340 | } 341 | } 342 | } 343 | 344 | test "Path Parsing (Mixed)" { 345 | const path = "/item/%i/description"; 346 | 347 | const parsed: [3]Token = .{ 348 | .{ .fragment = "item" }, 349 | .{ .match = .signed }, 350 | .{ .fragment = "description" }, 351 | }; 352 | 353 | var iter = std.mem.tokenizeScalar(u8, path, '/'); 354 | 355 | for (parsed) |expected| { 356 | const token = Token.parse_chunk(iter.next().?); 357 | switch (token) { 358 | .fragment => |inner| try testing.expectEqualStrings(expected.fragment, inner), 359 | .match => |inner| try testing.expectEqual(expected.match, inner), 360 | } 361 | } 362 | } 363 | 364 | test "Constructing Routing from Path" { 365 | var s = try RoutingTrie.init(testing.allocator, &.{ 366 | Route.init("/item").layer(), 367 | Route.init("/item/%i/description").layer(), 368 | Route.init("/item/%i/hello").layer(), 369 | Route.init("/item/%f/price_float").layer(), 370 | Route.init("/item/name/%s").layer(), 371 | Route.init("/item/list").layer(), 372 | }); 373 | defer s.deinit(testing.allocator); 374 | 375 | try testing.expectEqual(1, s.root.children.count()); 376 | } 377 | 378 | test "Routing with Paths" { 379 | var s = try RoutingTrie.init(testing.allocator, &.{ 380 | Route.init("/item").layer(), 381 | Route.init("/item/%i/description").layer(), 382 | Route.init("/item/%i/hello").layer(), 383 | Route.init("/item/%f/price_float").layer(), 384 | Route.init("/item/name/%s").layer(), 385 | Route.init("/item/list").layer(), 386 | }); 387 | defer s.deinit(testing.allocator); 388 | 389 | var q = AnyCaseStringMap.init(testing.allocator); 390 | defer q.deinit(); 391 | 392 | var captures: [8]Capture = [_]Capture{undefined} ** 8; 393 | 394 | try testing.expectEqual(null, try s.get_bundle(testing.allocator, "/item/name", captures[0..], &q)); 395 | 396 | { 397 | const captured = (try s.get_bundle(testing.allocator, "/item/name/HELLO", captures[0..], &q)).?; 398 | 399 | try testing.expectEqual(Route.init("/item/name/%s"), captured.route); 400 | try testing.expectEqualStrings("HELLO", captured.captures[0].string); 401 | } 402 | 403 | { 404 | const captured = (try s.get_bundle(testing.allocator, "/item/2112.22121/price_float", captures[0..], &q)).?; 405 | 406 | try testing.expectEqual(Route.init("/item/%f/price_float"), captured.route); 407 | try testing.expectEqual(2112.22121, captured.captures[0].float); 408 | } 409 | } 410 | 411 | test "Routing with Remaining" { 412 | var s = try RoutingTrie.init(testing.allocator, &.{ 413 | Route.init("/item").layer(), 414 | Route.init("/item/%f/price_float").layer(), 415 | Route.init("/item/name/%r").layer(), 416 | Route.init("/item/%i/price/%f").layer(), 417 | }); 418 | defer s.deinit(testing.allocator); 419 | 420 | var q = AnyCaseStringMap.init(testing.allocator); 421 | defer q.deinit(); 422 | 423 | var captures: [8]Capture = [_]Capture{undefined} ** 8; 424 | 425 | try testing.expectEqual(null, try s.get_bundle(testing.allocator, "/item/name", captures[0..], &q)); 426 | 427 | { 428 | const captured = (try s.get_bundle(testing.allocator, "/item/name/HELLO", captures[0..], &q)).?; 429 | try testing.expectEqual(Route.init("/item/name/%r"), captured.route); 430 | try testing.expectEqualStrings("HELLO", captured.captures[0].remaining); 431 | } 432 | { 433 | const captured = (try s.get_bundle( 434 | testing.allocator, 435 | "/item/name/THIS/IS/A/FILE/SYSTEM/PATH.html", 436 | captures[0..], 437 | &q, 438 | )).?; 439 | try testing.expectEqual(Route.init("/item/name/%r"), captured.route); 440 | try testing.expectEqualStrings("THIS/IS/A/FILE/SYSTEM/PATH.html", captured.captures[0].remaining); 441 | } 442 | 443 | { 444 | const captured = (try s.get_bundle( 445 | testing.allocator, 446 | "/item/2112.22121/price_float", 447 | captures[0..], 448 | &q, 449 | )).?; 450 | try testing.expectEqual(Route.init("/item/%f/price_float"), captured.route); 451 | try testing.expectEqual(2112.22121, captured.captures[0].float); 452 | } 453 | 454 | { 455 | const captured = (try s.get_bundle( 456 | testing.allocator, 457 | "/item/100/price/283.21", 458 | captures[0..], 459 | &q, 460 | )).?; 461 | try testing.expectEqual(Route.init("/item/%i/price/%f"), captured.route); 462 | try testing.expectEqual(100, captured.captures[0].signed); 463 | try testing.expectEqual(283.21, captured.captures[1].float); 464 | } 465 | } 466 | 467 | test "Routing with Queries" { 468 | var s = try RoutingTrie.init(testing.allocator, &.{ 469 | Route.init("/item").layer(), 470 | Route.init("/item/%f/price_float").layer(), 471 | Route.init("/item/name/%r").layer(), 472 | Route.init("/item/%i/price/%f").layer(), 473 | }); 474 | defer s.deinit(testing.allocator); 475 | 476 | var q = AnyCaseStringMap.init(testing.allocator); 477 | defer q.deinit(); 478 | 479 | var captures: [8]Capture = [_]Capture{undefined} ** 8; 480 | 481 | try testing.expectEqual(null, try s.get_bundle( 482 | testing.allocator, 483 | "/item/name", 484 | captures[0..], 485 | &q, 486 | )); 487 | 488 | { 489 | q.clearRetainingCapacity(); 490 | const captured = (try s.get_bundle( 491 | testing.allocator, 492 | "/item/name/HELLO?name=muki&food=waffle", 493 | captures[0..], 494 | &q, 495 | )).?; 496 | defer testing.allocator.free(captured.duped); 497 | defer for (captured.duped) |dupe| testing.allocator.free(dupe); 498 | try testing.expectEqual(Route.init("/item/name/%r"), captured.route); 499 | try testing.expectEqualStrings("HELLO", captured.captures[0].remaining); 500 | try testing.expectEqual(2, q.count()); 501 | try testing.expectEqualStrings("muki", q.get("name").?); 502 | try testing.expectEqualStrings("waffle", q.get("food").?); 503 | } 504 | 505 | { 506 | q.clearRetainingCapacity(); 507 | // Purposefully bad format with no keys or values. 508 | const captured = (try s.get_bundle( 509 | testing.allocator, 510 | "/item/2112.22121/price_float?", 511 | captures[0..], 512 | &q, 513 | )).?; 514 | defer testing.allocator.free(captured.duped); 515 | defer for (captured.duped) |dupe| testing.allocator.free(dupe); 516 | try testing.expectEqual(Route.init("/item/%f/price_float"), captured.route); 517 | try testing.expectEqual(2112.22121, captured.captures[0].float); 518 | try testing.expectEqual(0, q.count()); 519 | } 520 | 521 | { 522 | q.clearRetainingCapacity(); 523 | // Purposefully bad format with incomplete key/value pair. 524 | const captured = s.get_bundle(testing.allocator, "/item/100/price/283.21?help", captures[0..], &q); 525 | try testing.expectError(error.MissingValue, captured); 526 | } 527 | 528 | { 529 | q.clearRetainingCapacity(); 530 | // Purposefully bad format with incomplete key/value pair. 531 | const captured = s.get_bundle(testing.allocator, "/item/100/price/283.21?help=", captures[0..], &q); 532 | try testing.expectError(error.MissingValue, captured); 533 | } 534 | 535 | { 536 | q.clearRetainingCapacity(); 537 | // Purposefully bad format with invalid charactes. 538 | const captured = s.get_bundle( 539 | testing.allocator, 540 | "/item/999/price/100.221?page_count=pages=2020&abc=200", 541 | captures[0..], 542 | &q, 543 | ); 544 | try testing.expectError(error.MalformedPair, captured); 545 | } 546 | } 547 | -------------------------------------------------------------------------------- /src/http/server.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const tag = builtin.os.tag; 4 | const assert = std.debug.assert; 5 | const log = std.log.scoped(.@"zzz/http/server"); 6 | 7 | const TypedStorage = @import("../core/typed_storage.zig").TypedStorage; 8 | const Pseudoslice = @import("../core/pseudoslice.zig").Pseudoslice; 9 | const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap; 10 | 11 | const Context = @import("context.zig").Context; 12 | const Request = @import("request.zig").Request; 13 | const Response = @import("response.zig").Response; 14 | const Respond = @import("response.zig").Respond; 15 | const Capture = @import("router/routing_trie.zig").Capture; 16 | const SSE = @import("sse.zig").SSE; 17 | 18 | const Mime = @import("mime.zig").Mime; 19 | const Router = @import("router.zig").Router; 20 | const Route = @import("router/route.zig").Route; 21 | const Layer = @import("router/middleware.zig").Layer; 22 | const Middleware = @import("router/middleware.zig").Middleware; 23 | const HTTPError = @import("lib.zig").HTTPError; 24 | 25 | const HandlerWithData = @import("router/route.zig").HandlerWithData; 26 | 27 | const Next = @import("router/middleware.zig").Next; 28 | 29 | pub const Runtime = @import("tardy").Runtime; 30 | pub const Task = @import("tardy").Task; 31 | const TardyCreator = @import("tardy").Tardy; 32 | 33 | const Cross = @import("tardy").Cross; 34 | const Pool = @import("tardy").Pool; 35 | const PoolKind = @import("tardy").PoolKind; 36 | const Socket = @import("tardy").Socket; 37 | const ZeroCopy = @import("tardy").ZeroCopy; 38 | 39 | const AcceptResult = @import("tardy").AcceptResult; 40 | const RecvResult = @import("tardy").RecvResult; 41 | const SendResult = @import("tardy").SendResult; 42 | 43 | const secsock = @import("secsock"); 44 | const SecureSocket = secsock.SecureSocket; 45 | 46 | pub const TLSFileOptions = union(enum) { 47 | buffer: []const u8, 48 | file: struct { 49 | path: []const u8, 50 | size_buffer_max: u32 = 1024 * 1024, 51 | }, 52 | }; 53 | 54 | /// These are various general configuration 55 | /// options that are important for the actual framework. 56 | /// 57 | /// This includes various different options and limits 58 | /// for interacting with the underlying network. 59 | pub const ServerConfig = struct { 60 | /// Stack Size 61 | /// 62 | /// If you have a large number of middlewares or 63 | /// create a LOT of stack memory, you may want to increase this. 64 | /// 65 | /// P.S: A lot of functions in the standard library do end up allocating 66 | /// a lot on the stack (such as std.log). 67 | /// 68 | /// Default: 1MB 69 | stack_size: usize = 1024 * 1024, 70 | /// Number of Maximum Concurrent Connections. 71 | /// 72 | /// This is applied PER runtime. 73 | /// zzz will drop/close any connections greater 74 | /// than this. 75 | /// 76 | /// You can set this to `null` to have no maximum. 77 | /// 78 | /// Default: `null` 79 | connection_count_max: ?u32 = null, 80 | /// Number of times a Request-Response can happen with keep-alive. 81 | /// 82 | /// Setting this to `null` will set no limit. 83 | /// 84 | /// Default: `null` 85 | keepalive_count_max: ?u16 = null, 86 | /// Amount of allocated memory retained 87 | /// after an arena is cleared. 88 | /// 89 | /// A higher value will increase memory usage but 90 | /// should make allocators faster. 91 | /// 92 | /// A lower value will reduce memory usage but 93 | /// will make allocators slower. 94 | /// 95 | /// Default: 1KB 96 | connection_arena_bytes_retain: u32 = 1024, 97 | /// Amount of space on the `recv_buffer` retained 98 | /// after every send. 99 | /// 100 | /// Default: 1KB 101 | list_recv_bytes_retain: u32 = 1024, 102 | /// Maximum size (in bytes) of the Recv buffer. 103 | /// This is mainly a concern when you are reading in 104 | /// large requests before responding. 105 | /// 106 | /// Default: 2MB 107 | list_recv_bytes_max: u32 = 1024 * 1024 * 2, 108 | /// Size of the buffer (in bytes) used for 109 | /// interacting with the socket. 110 | /// 111 | /// Default: 1 KB 112 | socket_buffer_bytes: u32 = 1024, 113 | /// Maximum number of Captures in a Route 114 | /// 115 | /// Default: 8 116 | capture_count_max: u16 = 8, 117 | /// Maximum size (in bytes) of the Request. 118 | /// 119 | /// Default: 2MB 120 | request_bytes_max: u32 = 1024 * 1024 * 2, 121 | /// Maximum size (in bytes) of the Request URI. 122 | /// 123 | /// Default: 2KB 124 | request_uri_bytes_max: u32 = 1024 * 2, 125 | }; 126 | 127 | pub const Provision = struct { 128 | initalized: bool = false, 129 | recv_slice: []u8, 130 | zc_recv_buffer: ZeroCopy(u8), 131 | header_buffer: std.ArrayList(u8), 132 | arena: std.heap.ArenaAllocator, 133 | storage: TypedStorage, 134 | captures: []Capture, 135 | queries: AnyCaseStringMap, 136 | request: Request, 137 | response: Response, 138 | }; 139 | 140 | pub const Server = struct { 141 | const Self = @This(); 142 | config: ServerConfig, 143 | 144 | pub fn init(config: ServerConfig) Self { 145 | return Self{ .config = config }; 146 | } 147 | 148 | pub fn deinit(self: *const Self) void { 149 | if (self.tls_ctx) |tls| { 150 | tls.deinit(); 151 | } 152 | } 153 | 154 | const RequestBodyState = struct { 155 | content_length: usize, 156 | current_length: usize, 157 | }; 158 | 159 | const RequestState = union(enum) { 160 | header, 161 | body: RequestBodyState, 162 | }; 163 | 164 | const State = union(enum) { 165 | request: RequestState, 166 | handler, 167 | respond, 168 | }; 169 | 170 | fn prepare_new_request(state: ?*State, provision: *Provision, config: ServerConfig) !void { 171 | assert(provision.initalized); 172 | provision.request.clear(); 173 | provision.response.clear(); 174 | provision.storage.clear(); 175 | provision.zc_recv_buffer.clear_retaining_capacity(); 176 | provision.header_buffer.clearRetainingCapacity(); 177 | _ = provision.arena.reset(.{ .retain_with_limit = config.connection_arena_bytes_retain }); 178 | provision.recv_slice = try provision.zc_recv_buffer.get_write_area(config.socket_buffer_bytes); 179 | 180 | if (state) |s| s.* = .{ .request = .header }; 181 | } 182 | 183 | pub fn main_frame( 184 | rt: *Runtime, 185 | config: ServerConfig, 186 | router: *const Router, 187 | server_socket: SecureSocket, 188 | provisions: *Pool(Provision), 189 | connection_count: *usize, 190 | accept_queued: *bool, 191 | ) !void { 192 | accept_queued.* = false; 193 | const secure = server_socket.accept(rt) catch |e| { 194 | if (!accept_queued.*) { 195 | try rt.spawn( 196 | .{ rt, config, router, server_socket, provisions, connection_count, accept_queued }, 197 | main_frame, 198 | config.stack_size, 199 | ); 200 | accept_queued.* = true; 201 | } 202 | return e; 203 | }; 204 | defer secure.socket.close_blocking(); 205 | defer secure.deinit(); 206 | 207 | connection_count.* += 1; 208 | defer connection_count.* -= 1; 209 | 210 | if (secure.socket.addr.any.family != std.posix.AF.UNIX) { 211 | try Cross.socket.disable_nagle(secure.socket.handle); 212 | } 213 | 214 | if (config.connection_count_max) |max| if (connection_count.* > max) { 215 | log.debug("over connection max, closing", .{}); 216 | return; 217 | }; 218 | 219 | log.debug("queuing up a new accept request", .{}); 220 | try rt.spawn( 221 | .{ rt, config, router, server_socket, provisions, connection_count, accept_queued }, 222 | main_frame, 223 | config.stack_size, 224 | ); 225 | accept_queued.* = true; 226 | 227 | const index = try provisions.borrow(); 228 | defer provisions.release(index); 229 | const provision = provisions.get_ptr(index); 230 | 231 | // if we are growing, we can handle a newly allocated provision here. 232 | // otherwise, it should be initalized. 233 | if (!provision.initalized) { 234 | log.debug("initalizing new provision", .{}); 235 | provision.zc_recv_buffer = ZeroCopy(u8).init(rt.allocator, config.socket_buffer_bytes) catch { 236 | @panic("attempting to allocate more memory than available. (ZeroCopyBuffer)"); 237 | }; 238 | provision.header_buffer = std.ArrayList(u8).init(rt.allocator); 239 | provision.arena = std.heap.ArenaAllocator.init(rt.allocator); 240 | provision.captures = rt.allocator.alloc(Capture, config.capture_count_max) catch { 241 | @panic("attempting to allocate more memory than available. (Captures)"); 242 | }; 243 | provision.queries = AnyCaseStringMap.init(rt.allocator); 244 | provision.storage = TypedStorage.init(rt.allocator); 245 | provision.request = Request.init(rt.allocator); 246 | provision.response = Response.init(rt.allocator); 247 | provision.initalized = true; 248 | } 249 | defer prepare_new_request(null, provision, config) catch unreachable; 250 | 251 | var state: State = .{ .request = .header }; 252 | const buffer = try provision.zc_recv_buffer.get_write_area(config.socket_buffer_bytes); 253 | _ = buffer; 254 | provision.recv_slice = try provision.zc_recv_buffer.get_write_area(config.socket_buffer_bytes); 255 | 256 | var keepalive_count: u16 = 0; 257 | 258 | http_loop: while (true) switch (state) { 259 | .request => |*kind| switch (kind.*) { 260 | .header => { 261 | const recv_count = secure.recv(rt, provision.recv_slice) catch |e| switch (e) { 262 | error.Closed => break, 263 | else => { 264 | log.debug("recv failed on socket | {}", .{e}); 265 | break; 266 | }, 267 | }; 268 | 269 | provision.zc_recv_buffer.mark_written(recv_count); 270 | provision.recv_slice = try provision.zc_recv_buffer.get_write_area(config.socket_buffer_bytes); 271 | if (provision.zc_recv_buffer.len > config.request_bytes_max) break; 272 | const search_area_start = (provision.zc_recv_buffer.len - recv_count) -| 4; 273 | 274 | if (std.mem.indexOf( 275 | u8, 276 | // Minimize the search area. 277 | provision.zc_recv_buffer.subslice(.{ .start = search_area_start }), 278 | "\r\n\r\n", 279 | )) |header_end| { 280 | const real_header_end = header_end + 4; 281 | try provision.request.parse_headers( 282 | // Add 4 to account for the actual header end sequence. 283 | provision.zc_recv_buffer.subslice(.{ .end = real_header_end }), 284 | .{ 285 | .request_bytes_max = config.request_bytes_max, 286 | .request_uri_bytes_max = config.request_uri_bytes_max, 287 | }, 288 | ); 289 | 290 | log.info("rt{d} - \"{s} {s}\" {s} ({})", .{ 291 | rt.id, 292 | @tagName(provision.request.method.?), 293 | provision.request.uri.?, 294 | provision.request.headers.get("User-Agent") orelse "N/A", 295 | secure.socket.addr, 296 | }); 297 | 298 | const content_length_str = provision.request.headers.get("Content-Length") orelse "0"; 299 | const content_length = try std.fmt.parseUnsigned(usize, content_length_str, 10); 300 | log.debug("content length={d}", .{content_length}); 301 | 302 | if (provision.request.expect_body() and content_length != 0) { 303 | state = .{ 304 | .request = .{ 305 | .body = .{ 306 | .current_length = provision.zc_recv_buffer.len - real_header_end, 307 | .content_length = content_length, 308 | }, 309 | }, 310 | }; 311 | } else state = .handler; 312 | } 313 | }, 314 | .body => |*info| { 315 | if (info.current_length == info.content_length) { 316 | provision.request.body = provision.zc_recv_buffer.subslice( 317 | .{ .start = provision.zc_recv_buffer.len - info.content_length }, 318 | ); 319 | state = .handler; 320 | continue; 321 | } 322 | 323 | const recv_count = secure.recv(rt, provision.recv_slice) catch |e| switch (e) { 324 | error.Closed => break, 325 | else => { 326 | log.debug("recv failed on socket | {}", .{e}); 327 | break; 328 | }, 329 | }; 330 | 331 | provision.zc_recv_buffer.mark_written(recv_count); 332 | provision.recv_slice = try provision.zc_recv_buffer.get_write_area(config.socket_buffer_bytes); 333 | if (provision.zc_recv_buffer.len > config.request_bytes_max) break; 334 | 335 | info.current_length += recv_count; 336 | assert(info.current_length <= info.content_length); 337 | }, 338 | }, 339 | .handler => { 340 | const found = try router.get_bundle_from_host( 341 | rt.allocator, 342 | provision.request.uri.?, 343 | provision.captures, 344 | &provision.queries, 345 | ); 346 | defer rt.allocator.free(found.duped); 347 | defer for (found.duped) |dupe| rt.allocator.free(dupe); 348 | 349 | const h_with_data: HandlerWithData = found.route.get_handler( 350 | provision.request.method.?, 351 | ) orelse { 352 | provision.response.headers.clearRetainingCapacity(); 353 | provision.response.status = .@"Method Not Allowed"; 354 | provision.response.mime = Mime.TEXT; 355 | provision.response.body = ""; 356 | 357 | state = .respond; 358 | continue; 359 | }; 360 | 361 | const context: Context = .{ 362 | .runtime = rt, 363 | .allocator = provision.arena.allocator(), 364 | .header_buffer = &provision.header_buffer, 365 | .request = &provision.request, 366 | .response = &provision.response, 367 | .storage = &provision.storage, 368 | .socket = secure, 369 | .captures = found.captures, 370 | .queries = found.queries, 371 | }; 372 | 373 | var next: Next = .{ 374 | .context = &context, 375 | .middlewares = h_with_data.middlewares, 376 | .handler = h_with_data, 377 | }; 378 | 379 | const next_respond: Respond = next.run() catch |e| blk: { 380 | log.warn("rt{d} - \"{s} {s}\" {} ({})", .{ 381 | rt.id, 382 | @tagName(provision.request.method.?), 383 | provision.request.uri.?, 384 | e, 385 | secure.socket.addr, 386 | }); 387 | 388 | // If in Debug Mode, we will return the error name. In other modes, 389 | // we won't to avoid leaking implemenation details. 390 | const body = if (comptime builtin.mode == .Debug) @errorName(e) else ""; 391 | 392 | break :blk try provision.response.apply(.{ 393 | .status = .@"Internal Server Error", 394 | .mime = Mime.TEXT, 395 | .body = body, 396 | }); 397 | }; 398 | 399 | switch (next_respond) { 400 | .standard => { 401 | // applies the respond onto the response 402 | //try provision.response.apply(respond); 403 | state = .respond; 404 | }, 405 | .responded => { 406 | const connection = provision.request.headers.get("Connection") orelse "keep-alive"; 407 | if (std.mem.eql(u8, connection, "close")) break :http_loop; 408 | if (config.keepalive_count_max) |max| { 409 | if (keepalive_count > max) { 410 | log.debug("closing connection, exceeded keepalive max", .{}); 411 | break :http_loop; 412 | } 413 | 414 | keepalive_count += 1; 415 | } 416 | 417 | try prepare_new_request(&state, provision, config); 418 | }, 419 | .close => break :http_loop, 420 | } 421 | }, 422 | .respond => { 423 | const body = provision.response.body orelse ""; 424 | const content_length = body.len; 425 | 426 | try provision.response.headers_into_writer(provision.header_buffer.writer(), content_length); 427 | const headers = provision.header_buffer.items; 428 | 429 | var sent: usize = 0; 430 | const pseudo = Pseudoslice.init(headers, body, provision.recv_slice); 431 | 432 | while (sent < pseudo.len) { 433 | const send_slice = pseudo.get(sent, sent + provision.recv_slice.len); 434 | 435 | const sent_length = secure.send_all(rt, send_slice) catch |e| { 436 | log.debug("send failed on socket | {}", .{e}); 437 | break; 438 | }; 439 | if (sent_length != send_slice.len) break :http_loop; 440 | sent += sent_length; 441 | } 442 | 443 | const connection = provision.request.headers.get("Connection") orelse "keep-alive"; 444 | if (std.mem.eql(u8, connection, "close")) break; 445 | if (config.keepalive_count_max) |max| { 446 | if (keepalive_count > max) { 447 | log.debug("closing connection, exceeded keepalive max", .{}); 448 | break; 449 | } 450 | 451 | keepalive_count += 1; 452 | } 453 | 454 | try prepare_new_request(&state, provision, config); 455 | }, 456 | }; 457 | 458 | log.info("connection ({}) closed", .{secure.socket.addr}); 459 | 460 | if (!accept_queued.*) { 461 | try rt.spawn( 462 | .{ rt, config, router, server_socket, provisions, connection_count, accept_queued }, 463 | main_frame, 464 | config.stack_size, 465 | ); 466 | accept_queued.* = true; 467 | } 468 | } 469 | 470 | const SocketKind = union(enum) { 471 | normal: Socket, 472 | secure: SecureSocket, 473 | }; 474 | 475 | /// Serve an HTTP server. 476 | pub fn serve(self: *Self, rt: *Runtime, router: *const Router, sock: SocketKind) !void { 477 | log.info("security mode: {s}", .{@tagName(sock)}); 478 | 479 | const secure: SecureSocket = switch (sock) { 480 | .normal => |s| SecureSocket.unsecured(s), 481 | .secure => |sec| sec, 482 | }; 483 | 484 | const count = self.config.connection_count_max orelse 1024; 485 | const pooling: PoolKind = if (self.config.connection_count_max == null) .grow else .static; 486 | 487 | const provision_pool = try rt.allocator.create(Pool(Provision)); 488 | provision_pool.* = try Pool(Provision).init(rt.allocator, count, pooling); 489 | errdefer rt.allocator.destroy(provision_pool); 490 | 491 | const connection_count = try rt.allocator.create(usize); 492 | errdefer rt.allocator.destroy(connection_count); 493 | connection_count.* = 0; 494 | 495 | const accept_queued = try rt.allocator.create(bool); 496 | errdefer rt.allocator.destroy(accept_queued); 497 | accept_queued.* = true; 498 | 499 | // initialize first batch of provisions :) 500 | for (provision_pool.items) |*provision| { 501 | provision.initalized = true; 502 | provision.zc_recv_buffer = ZeroCopy(u8).init( 503 | rt.allocator, 504 | self.config.socket_buffer_bytes, 505 | ) catch { 506 | @panic("attempting to allocate more memory than available. (ZeroCopy)"); 507 | }; 508 | provision.header_buffer = std.ArrayList(u8).init(rt.allocator); 509 | provision.arena = std.heap.ArenaAllocator.init(rt.allocator); 510 | provision.captures = rt.allocator.alloc(Capture, self.config.capture_count_max) catch { 511 | @panic("attempting to allocate more memory than available. (Captures)"); 512 | }; 513 | provision.queries = AnyCaseStringMap.init(rt.allocator); 514 | provision.storage = TypedStorage.init(rt.allocator); 515 | provision.request = Request.init(rt.allocator); 516 | provision.response = Response.init(rt.allocator); 517 | } 518 | 519 | try rt.spawn( 520 | .{ 521 | rt, 522 | self.config, 523 | router, 524 | secure, 525 | provision_pool, 526 | connection_count, 527 | accept_queued, 528 | }, 529 | main_frame, 530 | self.config.stack_size, 531 | ); 532 | } 533 | }; 534 | -------------------------------------------------------------------------------- /src/http/sse.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const Pseudoslice = @import("../core/pseudoslice.zig").Pseudoslice; 4 | 5 | const Provision = @import("server.zig").Provision; 6 | const Context = @import("context.zig").Context; 7 | const Mime = @import("mime.zig").Mime; 8 | 9 | const Runtime = @import("tardy").Runtime; 10 | 11 | const secsock = @import("secsock"); 12 | const SecureSocket = secsock.SecureSocket; 13 | 14 | const SSEMessage = struct { 15 | id: ?[]const u8 = null, 16 | event: ?[]const u8 = null, 17 | data: ?[]const u8 = null, 18 | retry: ?u64 = null, 19 | }; 20 | 21 | pub const SSE = struct { 22 | socket: SecureSocket, 23 | allocator: std.mem.Allocator, 24 | list: std.ArrayListUnmanaged(u8), 25 | runtime: *Runtime, 26 | 27 | pub fn init(ctx: *const Context) !SSE { 28 | const response = ctx.response; 29 | response.status = .OK; 30 | response.mime = Mime{ 31 | .content_type = .{ .single = "text/event-stream" }, 32 | .extension = .{ .single = "" }, 33 | .description = "SSE", 34 | }; 35 | 36 | var list = try std.ArrayListUnmanaged(u8).initCapacity(ctx.allocator, 0); 37 | errdefer list.deinit(ctx.allocator); 38 | 39 | try ctx.response.headers_into_writer(ctx.header_buffer.writer(), null); 40 | const headers = ctx.header_buffer.items; 41 | 42 | const sent = try ctx.socket.send_all(ctx.runtime, headers); 43 | if (sent != headers.len) return error.Closed; 44 | 45 | return .{ 46 | .socket = ctx.socket, 47 | .allocator = ctx.allocator, 48 | .list = list, 49 | .runtime = ctx.runtime, 50 | }; 51 | } 52 | 53 | pub fn send(self: *SSE, message: SSEMessage) !void { 54 | // just reuse the list 55 | defer self.list.clearRetainingCapacity(); 56 | const writer = self.list.writer(self.allocator); 57 | 58 | if (message.id) |id| try writer.print("id: {s}\n", .{id}); 59 | if (message.event) |event| try writer.print("event: {s}\n", .{event}); 60 | if (message.data) |data| { 61 | var iter = std.mem.splitScalar(u8, data, '\n'); 62 | while (iter.next()) |line| try writer.print("data: {s}\n", .{line}); 63 | } 64 | if (message.retry) |retry| try writer.print("retry: {d}\n", .{retry}); 65 | try writer.writeByte('\n'); 66 | 67 | const sent = try self.socket.send_all(self.runtime, self.list.items); 68 | if (sent != self.list.items.len) return error.Closed; 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /src/http/status.zig: -------------------------------------------------------------------------------- 1 | // These purposefully do not fit the general snake_case enum style. 2 | // This is so that we can just use @tagName for the Status. 3 | pub const Status = enum(u16) { 4 | /// 100 Continue 5 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/100 6 | Continue = 100, 7 | /// 101 Switching Protocols 8 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/101 9 | @"Switching Protocols" = 101, 10 | /// 102 Processing 11 | /// This is deprecrated and should generally not be used. 12 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/102 13 | Processing = 102, 14 | /// 103 Early Hints 15 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/103 16 | @"Early Hints" = 103, 17 | /// 200 OK 18 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/200 19 | OK = 200, 20 | /// 201 Created 21 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/201 22 | Created = 201, 23 | /// 202 Accepted 24 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/202 25 | Accepted = 202, 26 | /// 203 Non-Authoritative Information 27 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/203 28 | @"Non-Authoritative Informaton" = 203, 29 | /// 204 No Content 30 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204 31 | @"No Content" = 204, 32 | /// 205 Reset Content 33 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/205 34 | @"Reset Content" = 205, 35 | /// 206 Partial Content 36 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/206 37 | @"Partial Content" = 206, 38 | /// 207 Multi-Status 39 | /// Used exclusively with WebDAV 40 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/207 41 | @"Multi-Status" = 207, 42 | /// 208 Already Reported 43 | /// Used exclusively with WebDAV 44 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/208 45 | @"Already Reported" = 208, 46 | /// 226 IM Used 47 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/226 48 | @"IM Used" = 226, 49 | /// 300 Multiple Choices 50 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/300 51 | @"Multiple Choices" = 300, 52 | /// 301 Moved Permanently 53 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/301 54 | @"Moved Permanently" = 301, 55 | /// 302 Found 56 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302 57 | Found = 302, 58 | /// 303 See Other 59 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/303 60 | @"See Other" = 303, 61 | /// 304 Not Modified 62 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304 63 | @"Not Modified" = 304, 64 | /// 307 Temporary Redirect 65 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307 66 | @"Temporary Redirect" = 307, 67 | /// 308 Permanent Redirect 68 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/308 69 | @"Permanent Redirect" = 308, 70 | /// 400 Bad Request 71 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400 72 | @"Bad Request" = 400, 73 | /// 401 Unauthorized 74 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401 75 | Unauthorized = 401, 76 | /// 402 Payment Required 77 | /// Nonstandard 78 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/402 79 | @"Payment Required" = 402, 80 | /// 403 Forbidden 81 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403 82 | Forbidden = 403, 83 | /// 404 Not Found 84 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404 85 | @"Not Found" = 404, 86 | /// 405 Method Not Allowed 87 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405 88 | @"Method Not Allowed" = 405, 89 | /// Not Acceptable 90 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/406 91 | @"Not Acceptable" = 406, 92 | /// 407 Proxy Authentication Required 93 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/407 94 | @"Proxy Authentication Required" = 407, 95 | /// 408 Request Timeout 96 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408 97 | @"Request Timeout" = 408, 98 | /// 409 Conflict 99 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409 100 | Conflict = 409, 101 | /// 410 Gone 102 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/410 103 | Gone = 410, 104 | /// 411 Length Required 105 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/411 106 | @"Length Required" = 411, 107 | /// 412 Precondition Failed 108 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/412 109 | @"Precondition Failed" = 412, 110 | /// 413 Content Too Large 111 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413 112 | @"Content Too Large" = 413, 113 | /// 414 URI Too Long 114 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/414 115 | @"URI Too Long" = 414, 116 | /// 415 Unsupported Media Type 117 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/415 118 | @"Unsupported Media Type" = 415, 119 | /// 416 Range Not Satisfiable 120 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/416 121 | @"Range Not Satisfiable" = 416, 122 | /// 417 Expectation Failed 123 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/417 124 | @"Expectation Failed" = 417, 125 | /// 418 I'm a Teapot 126 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/418 127 | @"I'm a Teapot" = 418, 128 | /// 421 Misdirected Request 129 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/421 130 | @"Misdirected Request" = 421, 131 | /// 422 Unprocessable Content 132 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422 133 | @"Unprocessable Content" = 422, 134 | /// 423 Locked 135 | /// Used exclusively with WebDAV 136 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/423 137 | Locked = 423, 138 | /// 424 Failed Dependency 139 | /// Used (almost) exclusively with WebDAV 140 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/424 141 | @"Failed Dependency" = 424, 142 | /// 425 Too Early 143 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/425 144 | @"Too Early" = 425, 145 | /// 426 Upgrade Required 146 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/426 147 | @"Upgrade Required" = 426, 148 | /// 428 Precondition Required 149 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/428 150 | @"Precondition Required" = 428, 151 | /// 429 Too Many Requests 152 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429 153 | @"Too Many Requests" = 429, 154 | /// 431 Request Header Fields Too Large 155 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/431 156 | @"Request Header Fields Too Large" = 431, 157 | /// 451 Unavailable for Legal Reasons 158 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/451 159 | @"Unavailable for Legal Reasons" = 451, 160 | /// 500 Internal Server Error 161 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500 162 | @"Internal Server Error" = 500, 163 | /// 501 Not Implemented 164 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/501 165 | @"Not Implemented" = 501, 166 | /// 502 Bad Gateway 167 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502 168 | @"Bad Gateway" = 502, 169 | /// 503 Service Unavailable 170 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503 171 | @"Service Unavailable" = 503, 172 | /// 504 Gateway Timeout 173 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504 174 | @"Gateway Timeout" = 504, 175 | /// 505 HTTP Version Not Supported 176 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/505 177 | @"HTTP Version Not Supported" = 505, 178 | /// 506 Variant Also Negotiates 179 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/506 180 | @"Variant Also Negotiates" = 506, 181 | /// 507 Insufficient Storage 182 | /// Used exclusively with WebDAV 183 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/507 184 | @"Insufficient Storage" = 507, 185 | /// 508 Loop Detected 186 | /// Used exclusively with WebDAV 187 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/508 188 | @"Loop Detected" = 508, 189 | /// 510 Not Extended 190 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/510 191 | @"Not Extended" = 510, 192 | /// 511 Network Authentication Required 193 | /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/511 194 | @"Network Authentication Required" = 511, 195 | /// Interally used, will cause the thread that accepts it 196 | /// to gracefully shutdown. 197 | Kill = 999, 198 | }; 199 | -------------------------------------------------------------------------------- /src/lib.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | /// Internally exposed Tardy. 4 | pub const tardy = @import("tardy"); 5 | 6 | /// Internally exposed secsock. 7 | pub const secsock = @import("secsock"); 8 | 9 | /// HyperText Transfer Protocol. 10 | /// Supports: HTTP/1.1 11 | pub const HTTP = @import("http/lib.zig"); 12 | -------------------------------------------------------------------------------- /src/tests.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const testing = std.testing; 3 | 4 | test "zzz unit tests" { 5 | // Core 6 | testing.refAllDecls(@import("./core/any_case_string_map.zig")); 7 | testing.refAllDecls(@import("./core/pseudoslice.zig")); 8 | testing.refAllDecls(@import("./core/typed_storage.zig")); 9 | 10 | // HTTP 11 | testing.refAllDecls(@import("./http/context.zig")); 12 | testing.refAllDecls(@import("./http/date.zig")); 13 | testing.refAllDecls(@import("./http/method.zig")); 14 | testing.refAllDecls(@import("./http/mime.zig")); 15 | testing.refAllDecls(@import("./http/request.zig")); 16 | testing.refAllDecls(@import("./http/response.zig")); 17 | testing.refAllDecls(@import("./http/server.zig")); 18 | testing.refAllDecls(@import("./http/sse.zig")); 19 | testing.refAllDecls(@import("./http/status.zig")); 20 | testing.refAllDecls(@import("./http/form.zig")); 21 | 22 | // Router 23 | testing.refAllDecls(@import("./http/router.zig")); 24 | testing.refAllDecls(@import("./http/router/route.zig")); 25 | testing.refAllDecls(@import("./http/router/routing_trie.zig")); 26 | } 27 | --------------------------------------------------------------------------------