├── .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 | 
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 | 
45 |
46 | [Raw Data](./docs/benchmark/request_ccx63_24.csv)
47 |
48 | 
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 | \\
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 | Start SSE Connection
11 |
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 |
--------------------------------------------------------------------------------