forked from
karlseguin.tngl.sh/http.zig
An HTTP/1.1 server for zig
1const std = @import("std");
2
3const os = std.os;
4const http = @import("httpz.zig");
5const buffer = @import("buffer.zig");
6const metrics = @import("metrics.zig");
7
8const Self = @This();
9
10const posix = @import("posix.zig");
11const Url = @import("url.zig").Url;
12const Params = @import("params.zig").Params;
13const HTTPConn = @import("worker.zig").HTTPConn;
14const Config = @import("config.zig").Config.Request;
15const StringKeyValue = @import("key_value.zig").StringKeyValue;
16const MultiFormKeyValue = @import("key_value.zig").MultiFormKeyValue;
17
18const Address = std.Io.net.IpAddress;
19const Allocator = std.mem.Allocator;
20const ArenaAllocator = std.heap.ArenaAllocator;
21
22pub const Request = struct {
23 // The URL of the request
24 url: Url,
25
26 // httpz's wrapper around a stream, the brave can access the underlying .stream
27 conn: *HTTPConn,
28
29 // the address of the client
30 address: Address,
31
32 // Path params (extracted from the URL based on the route).
33 // Using req.param(NAME) is preferred.
34 params: *Params,
35
36 // The headers of the request. Using req.header(NAME) is preferred.
37 headers: *StringKeyValue,
38
39 // The request method.
40 method: http.Method,
41
42 // The method as a string, only set when method == .OTHER, else emtpy
43 method_string: []const u8,
44
45 // The request protocol.
46 protocol: http.Protocol,
47
48 // The body of the request, if any.
49 body_buffer: ?buffer.Buffer = null,
50 body_len: usize = 0,
51
52 // The number of unread bytes from the body. This can only happen when
53 // lazy_read_size is configured and the request is larger that this value.
54 // There can still be _part_ of the body in body_buffer.
55 unread_body: usize,
56
57 // cannot use an optional on qs, because it's pre-allocated so always exists
58 qs_read: bool = false,
59
60 // The query string lookup.
61 qs: *StringKeyValue,
62
63 // cannot use an optional on fd, because it's pre-allocated so always exists
64 fd_read: bool = false,
65
66 // The formData lookup.
67 fd: *StringKeyValue,
68
69 // The multiFormData lookup.
70 mfd: *MultiFormKeyValue,
71
72 // Spare space we still have in our static buffer after parsing the request
73 // We can use this, if needed, for example to unescape querystring parameters
74 spare: []u8,
75
76 // An arena that will be reset at the end of each request. Can be used
77 // internally by this framework. The application is also free to make use of
78 // this arena. This is the same arena as response.arena.
79 arena: Allocator,
80
81 route_data: ?*const anyopaque,
82
83 // Arbitrary place for middlewares (or really anyone), to store data.
84 // Middleware can store data here while executing, and then provide a function
85 // to retrieved the [typed] data to the action.
86 middlewares: *std.StringHashMap(*anyopaque),
87
88 pub const State = Self.State;
89
90 pub fn init(arena: Allocator, conn: *HTTPConn) Request {
91 const state = &conn.req_state;
92 return .{
93 .conn = conn,
94 .arena = arena,
95 .qs = &state.qs,
96 .fd = &state.fd,
97 .mfd = &state.mfd,
98 .method = state.method.?,
99 .unread_body = state.unread_body,
100 .method_string = state.method_string orelse "",
101 .protocol = state.protocol.?,
102 .url = Url.parse(state.url.?),
103 .address = conn.address,
104 .route_data = null,
105 .params = &state.params,
106 .headers = &state.headers,
107 .body_buffer = state.body,
108 .body_len = state.body_len,
109 .spare = state.buf[state.pos..],
110 .middlewares = &state.middlewares,
111 };
112 }
113
114 pub fn canKeepAlive(self: *const Request) bool {
115 return switch (self.protocol) {
116 http.Protocol.HTTP11 => {
117 if (self.headers.get("connection")) |conn| {
118 return !std.mem.eql(u8, conn, "close");
119 }
120 return true;
121 },
122 http.Protocol.HTTP10 => return false, // TODO: support this in the cases where it can be
123 };
124 }
125
126 pub fn body(self: *const Request) ?[]const u8 {
127 const buf = self.body_buffer orelse return null;
128 return buf.data[0..self.body_len];
129 }
130
131 /// `name` should be full lowercase
132 pub fn header(self: *const Request, name: []const u8) ?[]const u8 {
133 return self.headers.get(name);
134 }
135
136 pub fn param(self: *const Request, name: []const u8) ?[]const u8 {
137 return self.params.get(name);
138 }
139
140 pub fn query(self: *Request) !*StringKeyValue {
141 if (self.qs_read) {
142 return self.qs;
143 }
144 return self.parseQuery();
145 }
146
147 pub fn cookies(self: *const Request) Cookie {
148 return .{
149 .header = self.header("cookie") orelse "",
150 };
151 }
152
153 pub fn json(self: *Request, comptime T: type) !?T {
154 const b = self.body() orelse return null;
155 return try std.json.parseFromSliceLeaky(T, self.arena, b, .{});
156 }
157
158 pub fn jsonValue(self: *Request) !?std.json.Value {
159 const b = self.body() orelse return null;
160 return try std.json.parseFromSliceLeaky(std.json.Value, self.arena, b, .{});
161 }
162
163 pub fn jsonObject(self: *Request) !?std.json.ObjectMap {
164 const value = try self.jsonValue() orelse return null;
165 switch (value) {
166 .object => |o| return o,
167 else => return null,
168 }
169 }
170
171 pub fn formData(self: *Request) !*StringKeyValue {
172 if (self.fd_read) {
173 return self.fd;
174 }
175 return self.parseFormData();
176 }
177
178 pub fn multiFormData(self: *Request) !*MultiFormKeyValue {
179 if (self.fd_read) {
180 return self.mfd;
181 }
182 return self.parseMultiFormData();
183 }
184
185 pub fn reader(self: *Request, timeout_ms: usize) !Reader {
186 var buf: []const u8 = &.{};
187 if (self.body_buffer) |bb| {
188 if (bb.type == .static) {
189 buf = bb.data;
190 } else {
191 buf = bb.data[0..self.body_len];
192 }
193 }
194
195 const conn = self.conn;
196 if (self.unread_body > 0) {
197 try conn.blockingMode();
198 const timeval = std.mem.toBytes(posix.timeval{
199 .sec = @intCast(@divTrunc(timeout_ms, 1000)),
200 .usec = @intCast(@mod(timeout_ms, 1000) * 1000),
201 });
202 try posix.setsockopt(conn.stream.socket.handle, posix.SOL.SOCKET, posix.SO.RCVTIMEO, &timeval);
203 }
204
205 return .{
206 .buffer = buf,
207 .socket = conn.stream.socket.handle,
208 .unread_body = &self.unread_body,
209 .interface = .{
210 .end = 0,
211 .seek = 0,
212 .buffer = &.{},
213 .vtable = &.{
214 .stream = Reader.stream,
215 },
216 },
217 };
218 }
219
220 // OK, this is a bit complicated.
221 // We might need to allocate memory to parse the querystring. Specifically, if
222 // there's a url-escaped component (a key or value), we need memory to store
223 // the un-escaped version. Ideally, we'd like to use our static buffer for this
224 // but, we might not have enough space.
225 fn parseQuery(self: *Request) !*StringKeyValue {
226 const raw = self.url.query;
227 if (raw.len == 0) {
228 self.qs_read = true;
229 return self.qs;
230 }
231
232 var qs = self.qs;
233 var buf = self.spare;
234 const allocator = self.arena;
235
236 var it = std.mem.splitScalar(u8, raw, '&');
237 while (it.next()) |pair| {
238 if (std.mem.indexOfScalarPos(u8, pair, 0, '=')) |sep| {
239 const key_res = try Url.unescape(allocator, buf, pair[0..sep]);
240 if (key_res.buffered) {
241 buf = buf[key_res.value.len..];
242 }
243
244 const value_res = try Url.unescape(allocator, buf, pair[sep + 1 ..]);
245 if (value_res.buffered) {
246 buf = buf[value_res.value.len..];
247 }
248
249 qs.add(key_res.value, value_res.value);
250 } else {
251 const key_res = try Url.unescape(allocator, buf, pair);
252 if (key_res.buffered) {
253 buf = buf[key_res.value.len..];
254 }
255 qs.add(key_res.value, "");
256 }
257 }
258
259 self.spare = buf;
260 self.qs_read = true;
261 return self.qs;
262 }
263
264 fn parseFormData(self: *Request) !*StringKeyValue {
265 const b = self.body() orelse "";
266 if (b.len == 0) {
267 self.fd_read = true;
268 return self.fd;
269 }
270
271 var fd = self.fd;
272 var buf = self.spare;
273 const allocator = self.arena;
274
275 var it = std.mem.splitScalar(u8, b, '&');
276 while (it.next()) |pair| {
277 if (std.mem.indexOfScalarPos(u8, pair, 0, '=')) |sep| {
278 const key_res = try Url.unescape(allocator, buf, pair[0..sep]);
279 if (key_res.buffered) {
280 buf = buf[key_res.value.len..];
281 }
282
283 const value_res = try Url.unescape(allocator, buf, pair[sep + 1 ..]);
284 if (value_res.buffered) {
285 buf = buf[value_res.value.len..];
286 }
287 fd.add(key_res.value, value_res.value);
288 } else {
289 const key_res = try Url.unescape(allocator, buf, pair);
290 if (key_res.buffered) {
291 buf = buf[key_res.value.len..];
292 }
293 fd.add(key_res.value, "");
294 }
295 }
296
297 self.spare = buf;
298 self.fd_read = true;
299 return self.fd;
300 }
301
302 fn parseMultiFormData(self: *Request) !*MultiFormKeyValue {
303 const body_ = self.body() orelse "";
304 if (body_.len == 0) {
305 self.fd_read = true;
306 return self.mfd;
307 }
308
309 const content_type = blk: {
310 if (self.header("content-type")) |content_type| {
311 if (std.ascii.startsWithIgnoreCase(content_type, "multipart/form-data")) {
312 break :blk content_type;
313 }
314 }
315 return error.NotMultipartForm;
316 };
317
318 // Max boundary length is 70. Plus the two leading dashes (--)
319 var boundary_buf: [72]u8 = undefined;
320 const boundary = blk: {
321 const directive = content_type["multipart/form-data".len..];
322 for (directive, 0..) |b, i| loop: {
323 if (b != ' ' and b != ';') {
324 if (std.ascii.startsWithIgnoreCase(directive[i..], "boundary=")) {
325 const raw_boundary = directive["boundary=".len + i ..];
326 if (raw_boundary.len > 0 and raw_boundary.len <= 70) {
327 boundary_buf[0] = '-';
328 boundary_buf[1] = '-';
329 if (raw_boundary[0] == '"') {
330 if (raw_boundary.len > 2 and raw_boundary[raw_boundary.len - 1] == '"') {
331 // it's really -2, since we need to strip out the two quotes
332 // but buf is already at + 2, so they cancel out.
333 const end = raw_boundary.len;
334 @memcpy(boundary_buf[2..end], raw_boundary[1 .. raw_boundary.len - 1]);
335 break :blk boundary_buf[0..end];
336 }
337 } else {
338 const end = 2 + raw_boundary.len;
339 @memcpy(boundary_buf[2..end], raw_boundary);
340 break :blk boundary_buf[0..end];
341 }
342 }
343 }
344 // not valid, break out of the loop so we can return
345 // an error.InvalidMultiPartFormDataHeader
346 break :loop;
347 }
348 }
349 return error.InvalidMultiPartFormDataHeader;
350 };
351
352 var mfd = self.mfd;
353 var entry_it = std.mem.splitSequence(u8, body_, boundary);
354
355 {
356 // We expect the body to begin with a boundary
357 const first = entry_it.next() orelse {
358 self.fd_read = true;
359 return self.mfd;
360 };
361 if (first.len != 0) {
362 return error.InvalidMultiPartEncoding;
363 }
364 }
365
366 while (entry_it.next()) |entry| {
367 // body ends with -- after a final boundary
368 if (entry.len == 4 and entry[0] == '-' and entry[1] == '-' and entry[2] == '\r' and entry[3] == '\n') {
369 break;
370 }
371
372 if (entry.len < 2 or entry[0] != '\r' or entry[1] != '\n') return error.InvalidMultiPartEncoding;
373
374 // [2..] to skip our boundary's trailing line terminator
375 const field = try parseMultiPartEntry(entry[2..]);
376 mfd.add(field.name, field.value);
377 }
378
379 self.fd_read = true;
380 return self.mfd;
381 }
382
383 const MultiPartField = struct {
384 name: []const u8,
385 value: MultiFormKeyValue.Value,
386 };
387
388 // I'm sorry
389 fn parseMultiPartEntry(entry: []const u8) !MultiPartField {
390 var pos: usize = 0;
391 var attributes: ?ContentDispositionAttributes = null;
392
393 while (true) {
394 const end_line_pos = std.mem.indexOfScalarPos(u8, entry, pos, '\n') orelse return error.InvalidMultiPartEncoding;
395 const line = entry[pos..end_line_pos];
396
397 pos = end_line_pos + 1;
398 if (line.len == 0 or line[line.len - 1] != '\r') return error.InvalidMultiPartEncoding;
399
400 if (line.len == 1) {
401 break;
402 }
403
404 // we need to look for the name
405 if (std.ascii.startsWithIgnoreCase(line, "content-disposition:") == false) {
406 continue;
407 }
408
409 const value = trimLeadingSpace(line["content-disposition:".len..]);
410 if (std.ascii.startsWithIgnoreCase(value, "form-data;") == false) {
411 return error.InvalidMultiPartEncoding;
412 }
413
414 // constCast is safe here because we know this ultilately comes from one of our buffers
415 const value_start = "form-data;".len;
416 const value_end = value.len - 1; // remove the trailing \r
417 attributes = try getContentDispotionAttributes(@constCast(trimLeadingSpace(value[value_start..value_end])));
418 }
419
420 const value = entry[pos..];
421 if (value.len < 2 or value[value.len - 2] != '\r' or value[value.len - 1] != '\n') {
422 return error.InvalidMultiPartEncoding;
423 }
424
425 const attr = attributes orelse return error.InvalidMultiPartEncoding;
426
427 return .{
428 .name = attr.name,
429 .value = .{
430 .value = value[0 .. value.len - 2],
431 .filename = attr.filename,
432 },
433 };
434 }
435
436 const ContentDispositionAttributes = struct {
437 name: []const u8,
438 filename: ?[]const u8 = null,
439 };
440
441 // I'm sorry
442 fn getContentDispotionAttributes(fields: []u8) !ContentDispositionAttributes {
443 var pos: usize = 0;
444
445 var name: ?[]const u8 = null;
446 var filename: ?[]const u8 = null;
447
448 while (pos < fields.len) {
449 {
450 const b = fields[pos];
451 if (b == ';' or b == ' ' or b == '\t') {
452 pos += 1;
453 continue;
454 }
455 }
456
457 const sep = std.mem.indexOfScalarPos(u8, fields, pos, '=') orelse return error.InvalidMultiPartEncoding;
458 const field_name = fields[pos..sep];
459
460 // skip the equal
461 const value_start = sep + 1;
462 if (value_start == fields.len) {
463 return error.InvalidMultiPartEncoding;
464 }
465
466 var value: []const u8 = undefined;
467 if (fields[value_start] != '"') {
468 const value_end = std.mem.indexOfScalarPos(u8, fields, pos, ';') orelse fields.len;
469 pos = value_end;
470 value = fields[value_start..value_end];
471 } else blk: {
472 // skip the double quote
473 pos = value_start + 1;
474 var write_pos = pos;
475 while (pos < fields.len) {
476 switch (fields[pos]) {
477 '\\' => {
478 if (pos == fields.len) {
479 return error.InvalidMultiPartEncoding;
480 }
481 // supposedly MSIE doesn't always escape \, so if the \ isn't escape
482 // one of the special characters, it must be a single \. This is what Go does.
483 switch (fields[pos + 1]) {
484 // from Go's mime parser func isTSpecial(r rune) bool
485 '(', ')', '<', '>', '@', ',', ';', ':', '"', '/', '[', ']', '?', '=' => |n| {
486 fields[write_pos] = n;
487 pos += 1;
488 },
489 else => fields[write_pos] = '\\',
490 }
491 },
492 '"' => {
493 pos += 1;
494 value = fields[value_start + 1 .. write_pos];
495 break :blk;
496 },
497 else => |b| fields[write_pos] = b,
498 }
499 pos += 1;
500 write_pos += 1;
501 }
502 return error.InvalidMultiPartEncoding;
503 }
504
505 if (std.mem.eql(u8, field_name, "name")) {
506 name = value;
507 } else if (std.mem.eql(u8, field_name, "filename")) {
508 filename = value;
509 }
510 }
511
512 return .{
513 .name = name orelse return error.InvalidMultiPartEncoding,
514 .filename = filename,
515 };
516 }
517
518 pub const Reader = struct {
519 buffer: []const u8,
520 unread_body: *usize,
521 socket: posix.socket_t,
522 interface: std.Io.Reader,
523
524 pub fn stream(io_r: *std.Io.Reader, w: *std.Io.Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize {
525 const self: *Reader = @alignCast(@fieldParentPtr("interface", io_r));
526 const buf = limit.slice(try w.writableSliceGreedy(1));
527 const n = self.read(buf) catch return error.ReadFailed;
528 w.advance(n);
529 return n;
530 }
531
532 pub fn read(self: *Reader, into: []u8) !usize {
533 const b = self.buffer;
534 if (b.len != 0) {
535 const l = @min(b.len, into.len);
536 @memcpy(into[0..l], b[0..l]);
537 self.buffer = b[l..];
538 return l;
539 }
540
541 const unread = self.unread_body.*;
542 if (unread == 0) {
543 return 0;
544 }
545
546 const buf = if (into.len > unread) into[0..unread] else into;
547 const n = try posix.read(self.socket, buf);
548 self.unread_body.* = unread - n;
549 return n;
550 }
551 };
552
553 pub const Cookie = struct {
554 header: []const u8,
555
556 pub fn get(self: Cookie, name: []const u8) ?[]const u8 {
557 var it = std.mem.splitScalar(u8, self.header, ';');
558 while (it.next()) |kv| {
559 const trimmed = std.mem.trimStart(u8, kv, " ");
560 if (name.len >= trimmed.len) {
561 // need at least an '=' beyond the name
562 continue;
563 }
564
565 if (std.mem.startsWith(u8, trimmed, name) == false) {
566 continue;
567 }
568 if (trimmed[name.len] != '=') {
569 continue;
570 }
571 return trimmed[name.len + 1 ..];
572 }
573 return null;
574 }
575 };
576};
577
578// All the upfront memory allocation that we can do. Each worker keeps a pool
579// of these to re-use.
580pub const State = struct {
581 // Header must fit in here. Extra space can be used to fit the body or decode
582 // URL parameters.
583 buf: []u8,
584
585 // position in buf that we've parsed up to
586 pos: usize,
587
588 // length of buffer for which we have valid data
589 len: usize,
590
591 // Lazy-loaded in request.query();
592 qs: StringKeyValue,
593
594 // Lazy-loaded in request.formData();
595 fd: StringKeyValue,
596
597 // Lazy-loaded in request.multiFormData();
598 mfd: MultiFormKeyValue,
599
600 // Populated after we've parsed the request, once we're matching the request
601 // to a route.
602 params: Params,
603
604 // constant config
605 max_body_size: usize,
606
607 // constant config
608 lazy_read_size: ?usize,
609
610 // For reading the body, we might need more than `buf`.
611 buffer_pool: *buffer.Pool,
612
613 // URL, if we've parsed it
614 url: ?[]u8,
615
616 // Method, if we've parsed it
617 method: ?http.Method,
618
619 // Only set when method == .OTHER
620 method_string: ?[]const u8,
621
622 // Protocol, if we've parsed it
623 protocol: ?http.Protocol,
624
625 // The headers, might be partially parsed. From the outside, there's no way
626 // to know if this is fully parsed or not. There doesn't have to be. This
627 // is because once we finish parsing the headers, if there's no body, we'll
628 // signal the worker that we have a complete request and it can proceed to
629 // handle it. Thus, body == null or body_len == 0 doesn't mean anything.
630 headers: StringKeyValue,
631
632 // Our body. This be a slice pointing to` buf`, or be from the buffer_pool or
633 // be dynamically allocated.
634 body: ?buffer.Buffer,
635
636 // position in body.data that we have valid data for
637 body_pos: usize,
638
639 // the full length of the body, we might not have that much data yet, but we
640 // know what it is from the content-length header
641 body_len: usize,
642
643 // Happens when lazy_read_size is enabled and we get a large body.
644 // It'll be up to the app to read it!
645 unread_body: usize,
646
647 // Set when the request is using Transfer-Encoding: chunked. While decoding,
648 // body_pos tracks how many decoded bytes we've written into self.body.data,
649 // and body_len is set to body_pos once the terminating 0-sized chunk is seen.
650 chunked: ?Chunked,
651
652 middlewares: std.StringHashMap(*anyopaque),
653
654 const asUint = @import("url.zig").asUint;
655
656 const Chunked = struct {
657 state: Chunked.State,
658
659 // bytes left in the current chunk's data section
660 chunk_remaining: usize,
661
662 // current capacity of self.body.?.data
663 body_capacity: usize,
664
665 // Dedicated raw-input buffer. We can't recycle self.buf here because
666 // URL and header-value slices point into it; mutating it would
667 // invalidate them.
668 raw: []u8,
669 raw_pos: usize,
670 raw_len: usize,
671
672 const State = enum {
673 size_line,
674 data,
675 trailing_crlf,
676 trailers,
677 done,
678 };
679 };
680
681 pub fn init(arena: Allocator, buffer_pool: *buffer.Pool, config: *const Config) !Request.State {
682 return .{
683 .pos = 0,
684 .len = 0,
685 .url = null,
686 .body = null,
687 .body_pos = 0,
688 .body_len = 0,
689 .method = null,
690 .method_string = "",
691 .protocol = null,
692 .unread_body = 0,
693 .chunked = null,
694 .buffer_pool = buffer_pool,
695 .lazy_read_size = config.lazy_read_size,
696 .max_body_size = config.max_body_size orelse 1_048_576,
697 .middlewares = std.StringHashMap(*anyopaque).init(arena),
698 .qs = try StringKeyValue.init(arena, config.max_query_count orelse 32),
699 .fd = try StringKeyValue.init(arena, config.max_form_count orelse 0),
700 .mfd = try MultiFormKeyValue.init(arena, config.max_multiform_count orelse 0),
701 .buf = try arena.alloc(u8, config.buffer_size orelse 4_096),
702 .headers = try StringKeyValue.init(arena, config.max_header_count orelse 32),
703 .params = try Params.init(arena, config.max_param_count orelse 10),
704 };
705 }
706
707 pub fn deinit(self: *State) void {
708 if (self.body) |buf| {
709 self.buffer_pool.release(buf);
710 self.body = null;
711 }
712 }
713
714 pub fn reset(self: *State) void {
715 // not our job to clear the arena!
716 self.pos = 0;
717 self.len = 0;
718 self.url = null;
719 self.method = null;
720 self.unread_body = 0;
721 self.chunked = null;
722 self.method_string = null;
723 self.protocol = null;
724
725 self.body_pos = 0;
726 self.body_len = 0;
727 if (self.body) |buf| {
728 self.buffer_pool.release(buf);
729 self.body = null;
730 }
731
732 self.qs.reset();
733 self.fd.reset();
734 self.mfd.reset();
735 self.params.reset();
736 self.headers.reset();
737 self.middlewares.clearRetainingCapacity();
738 }
739
740 // returns true if the header has been fully parsed
741 pub fn parse(self: *State, conn: *HTTPConn, source: anytype) !bool {
742 if (self.body != null) {
743 if (self.chunked != null) {
744 return self.readChunked(conn, source);
745 }
746 // if we have a body, then we've read the header. We want to read into
747 // self.body, not self.buf.
748 return self.readBody(source);
749 }
750
751 var len = self.len;
752 const buf = self.buf;
753 const n = try zig016HackRead(source, buf[len..]);
754 if (n == 0) {
755 return false;
756 }
757 len = len + n;
758 self.len = len;
759
760 // When https://github.com/ziglang/zig/issues/8220 lands, we can probably
761 // DRY this up.
762 if (self.method == null) {
763 if (try self.parseMethod(buf[0..len]) == false) {
764 return false;
765 }
766 if (try self.parseUrl(buf[self.pos..len]) == false) {
767 return false;
768 }
769 if (try self.parseProtocol(buf[self.pos..len]) == false) {
770 return false;
771 }
772 if (try self.parseHeaders(conn, buf[self.pos..len]) == true) {
773 return true;
774 }
775 } else if (self.url == null) {
776 if (try self.parseUrl(buf[self.pos..len]) == false) {
777 return false;
778 }
779 if (try self.parseProtocol(buf[self.pos..len]) == false) {
780 return false;
781 }
782 if (try self.parseHeaders(conn, buf[self.pos..len]) == true) {
783 return true;
784 }
785 } else if (self.protocol == null) {
786 if (try self.parseProtocol(buf[self.pos..len]) == false) {
787 return false;
788 }
789 if (try self.parseHeaders(conn, buf[self.pos..len]) == true) {
790 return true;
791 }
792 } else {
793 if (try self.parseHeaders(conn, buf[self.pos..len]) == true) {
794 return true;
795 }
796 }
797
798 if (self.body == null and len == buf.len) {
799 metrics.headerTooBig();
800 return error.HeaderTooBig;
801 }
802
803 return false;
804 }
805
806 fn parseMethod(self: *State, buf: []u8) !bool {
807 const buf_len = buf.len;
808
809 // Shortest method is only 3 characters (+1 trailing space), so
810 // this seems like it should be: if (buf_len < 4)
811 // But the longest method, OPTIONS, is 7 characters (+1 trailing space).
812 // Now even if we have a short method, like "GET ", we'll eventually expect
813 // a URL + protocol. The shorter valid line is: e.g. GET / HTTP/1.1
814 // If buf_len < 8, we _might_ have a method, but we still need more data
815 // and might as well break early.
816 // If buf_len > = 8, then we can safely parse any (valid) method without
817 // having to do any other bound-checking.
818 if (buf_len < 8) return false;
819
820 // this approach to matching method name comes from zhp
821 switch (@as(u32, @bitCast(buf[0..4].*))) {
822 asUint("GET ") => {
823 self.pos = 4;
824 self.method = .GET;
825 },
826 asUint("PUT ") => {
827 self.pos = 4;
828 self.method = .PUT;
829 },
830 asUint("POST") => {
831 if (buf[4] != ' ') return error.UnknownMethod;
832 self.pos = 5;
833 self.method = .POST;
834 },
835 asUint("HEAD") => {
836 if (buf[4] != ' ') return error.UnknownMethod;
837 self.pos = 5;
838 self.method = .HEAD;
839 },
840 asUint("PATC") => {
841 if (buf[4] != 'H' or buf[5] != ' ') return error.UnknownMethod;
842 self.pos = 6;
843 self.method = .PATCH;
844 },
845 asUint("DELE") => {
846 if (@as(u32, @bitCast(buf[3..7].*)) != asUint("ETE ")) return error.UnknownMethod;
847 self.pos = 7;
848 self.method = .DELETE;
849 },
850 asUint("OPTI") => {
851 if (@as(u32, @bitCast(buf[4..8].*)) != asUint("ONS ")) return error.UnknownMethod;
852 self.pos = 8;
853 self.method = .OPTIONS;
854 },
855 asUint("CONN") => {
856 if (@as(u32, @bitCast(buf[4..8].*)) != asUint("ECT ")) return error.UnknownMethod;
857 self.pos = 8;
858 self.method = .CONNECT;
859 },
860 else => {
861 const space = std.mem.indexOfScalarPos(u8, buf, 0, ' ') orelse return error.UnknownMethod;
862 if (space == 0) {
863 return error.UnknownMethod;
864 }
865
866 const candidate = buf[0..space];
867 for (candidate) |c| {
868 if (c < 'A' or c > 'Z') {
869 return error.UnknownMethod;
870 }
871 }
872
873 // + 1 to skip the space
874 self.pos = space + 1;
875 self.method = .OTHER;
876 self.method_string = candidate;
877 },
878 }
879 return true;
880 }
881
882 fn parseUrl(self: *State, buf: []u8) !bool {
883 const buf_len = buf.len;
884 if (buf_len == 0) return false;
885
886 var len: usize = 0;
887 switch (buf[0]) {
888 '/' => {
889 const end_index = std.mem.indexOfScalarPos(u8, buf[1..buf_len], 0, ' ') orelse return false;
890 // +1 since we skipped the leading / in our indexOfScalar and +1 to consume the space
891 len = end_index + 2;
892 const url = buf[0 .. end_index + 1];
893 if (!Url.isValid(url)) return error.InvalidRequestTarget;
894 self.url = url;
895 },
896 '*' => {
897 if (buf_len == 1) return false;
898 // Read never returns 0, so if we're here, buf.len >= 1
899 if (buf[1] != ' ') return error.InvalidRequestTarget;
900 len = 2;
901 self.url = buf[0..1];
902 },
903 // TODO: Support absolute-form target (e.g. http://....)
904 else => return error.InvalidRequestTarget,
905 }
906
907 self.pos += len;
908 return true;
909 }
910
911 fn parseProtocol(self: *State, buf: []u8) !bool {
912 if (buf.len < 10) return false;
913
914 if (@as(u32, @bitCast(buf[0..4].*)) != asUint("HTTP")) {
915 return error.UnknownProtocol;
916 }
917
918 self.protocol = switch (@as(u32, @bitCast(buf[4..8].*))) {
919 asUint("/1.1") => http.Protocol.HTTP11,
920 asUint("/1.0") => http.Protocol.HTTP10,
921 else => return error.UnsupportedProtocol,
922 };
923
924 if (buf[8] != '\r' or buf[9] != '\n') {
925 return error.UnknownProtocol;
926 }
927
928 self.pos += 10;
929 return true;
930 }
931
932 fn parseHeaders(self: *State, conn: *HTTPConn, full: []u8) !bool {
933 var buf = full;
934 var headers = &self.headers;
935 const req_arena = conn.req_arena.allocator();
936
937 line: while (buf.len > 0) {
938 for (buf, 0..) |bn, i| {
939 switch (bn) {
940 'a'...'z', '0'...'9', '-', '_' => {},
941 'A'...'Z' => buf[i] = bn + 32,
942 ':' => {
943 const value_start = i + 1; // skip the colon
944 var value, const skip_len = trimLeadingSpaceCount(buf[value_start..]);
945 for (value, 0..) |bv, j| {
946 if (allowedHeaderValueByte[bv] == true) {
947 continue;
948 }
949
950 // To keep ALLOWED_HEADER_VALUE small, we said \r
951 // was illegal. I mean, it _is_ illegal in a header value
952 // but it isn't part of the header value, it's (probably) the end of line
953 if (bv != '\r') {
954 return error.InvalidHeaderLine;
955 }
956
957 const next = j + 1;
958 if (next == value.len) {
959 // we don't have any more data, we can't tell
960 return false;
961 }
962
963 if (value[next] != '\n') {
964 // we have a \r followed by something that isn't
965 // a \n. Can't be valid
966 return error.InvalidHeaderLine;
967 }
968
969 // If we're here, it means our value had valid characters
970 // up until the point of a newline (\r\n), which means
971 // we have a valid value (and name)
972 value = value[0..j];
973 break;
974 } else {
975 // for loop reached the end without finding a \r
976 // we need more data
977 return false;
978 }
979
980 // +2 to skip the \r\n. Use the untrimmed length here so
981 // the advance still covers any trailing whitespace.
982 const next_line = value_start + skip_len + value.len + 2;
983
984 // RFC 7230 §3.2.4: OWS surrounding the field value is
985 // not part of the value. Leading was handled by
986 // trimLeadingSpaceCount; trim trailing here.
987 while (value.len > 0) {
988 const last = value[value.len - 1];
989 if (last != ' ' and last != '\t') break;
990 value = value[0 .. value.len - 1];
991 }
992
993 const name = buf[0..i];
994 headers.add(name, value);
995
996 self.pos += next_line;
997 buf = buf[next_line..];
998 continue :line;
999 },
1000 '\r' => {
1001 if (i != 0) {
1002 // We're still parsing the header name, so a
1003 // \r should either be at the very start (to indicate the end of our headers)
1004 // or not be there at all
1005 return error.InvalidHeaderLine;
1006 }
1007
1008 if (buf.len == 1) {
1009 // we don't have any more data, we need more data
1010 return false;
1011 }
1012
1013 if (buf[1] == '\n') {
1014 // we have \r\n at the start of a line, we're done
1015 self.pos += 2;
1016 return try self.prepareForBody(conn, req_arena);
1017 }
1018 // we have a \r followed by something that isn't a \n, can't be right
1019 return error.InvalidHeaderLine;
1020 },
1021 else => return error.InvalidHeaderLine,
1022 }
1023 } else {
1024 // didn't find a colon or blank line, we need more data
1025 return false;
1026 }
1027 }
1028 return false;
1029 }
1030
1031 // rejects duplicate Content-Length and duplicate Host
1032 // returns the Content-Length
1033 fn checkHeaderValidity(self: *State) !?[]const u8 {
1034 var content_length: ?[]const u8 = null;
1035 var host_seen: bool = false;
1036 var it = self.headers.iterator();
1037 while (it.next()) |kv| {
1038 if (std.mem.eql(u8, kv.key, "content-length")) {
1039 if (content_length != null) return error.InvalidContentLength;
1040 content_length = kv.value;
1041 } else if (std.mem.eql(u8, kv.key, "host")) {
1042 if (host_seen) return error.InvalidHost;
1043 host_seen = true;
1044 }
1045 }
1046 return content_length;
1047 }
1048
1049 // we've finished reading the header
1050 fn prepareForBody(self: *State, conn: *HTTPConn, req_arena: Allocator) !bool {
1051 const content_length_str = try self.checkHeaderValidity();
1052
1053 if (self.headers.get("transfer-encoding")) |te| {
1054 if (!std.ascii.eqlIgnoreCase(te, "chunked")) {
1055 return error.InvalidTransferEncoding;
1056 }
1057 // RFC 7230 §3.3.3: a sender MUST NOT send Content-Length together with
1058 // Transfer-Encoding. Reject to avoid request smuggling ambiguity.
1059 if (content_length_str != null) {
1060 return error.InvalidTransferEncoding;
1061 }
1062 if (self.headers.get("expect")) |expect| {
1063 if (std.ascii.eqlIgnoreCase(expect, "100-continue")) {
1064 try conn.writeAll("HTTP/1.1 100 Continue\r\n\r\n");
1065 }
1066 }
1067 return self.prepareForChunkedBody(req_arena);
1068 }
1069
1070 const str = content_length_str orelse return true;
1071 const cl = atoi(str) orelse return error.InvalidContentLength;
1072
1073 if (self.headers.get("expect")) |expect| {
1074 if (std.ascii.eqlIgnoreCase(expect, "100-continue")) {
1075 // TODO: Maybe support an application-defined handler for this
1076 try conn.writeAll("HTTP/1.1 100 Continue\r\n\r\n");
1077 }
1078 }
1079
1080 self.body_len = cl;
1081 if (cl == 0) return true;
1082
1083 if (self.lazy_read_size == null and cl > self.max_body_size) {
1084 return error.BodyTooBig;
1085 }
1086
1087 const pos = self.pos;
1088 const len = self.len;
1089 const buf = self.buf;
1090
1091 // how much (if any) of the body we've already read
1092 const read = len - pos;
1093
1094 if (read > cl) {
1095 return error.InvalidContentLength;
1096 }
1097
1098 // how much of the body are we missing
1099 const missing = cl - read;
1100
1101 if (self.lazy_read_size) |lazy_read| {
1102 if (cl >= lazy_read) {
1103 self.pos = len;
1104 self.unread_body = missing;
1105 self.body = .{ .type = .static, .data = buf[pos..len] };
1106 return true;
1107 }
1108 }
1109
1110 if (missing == 0) {
1111 // we've read the entire body into buf, point to that.
1112 self.pos = len;
1113 self.body = .{ .type = .static, .data = buf[pos..len] };
1114 return true;
1115 }
1116
1117 // how much spare space we have in our static buffer
1118 const spare = buf.len - len;
1119 if (missing < spare) {
1120 // we don't have the [full] body, but we have enough space in our static
1121 // buffer for it
1122 self.body = .{ .type = .static, .data = buf[pos .. pos + cl] };
1123
1124 // While we don't have this yet, we know that this will be the final
1125 // position of valid data within self.buf. We need this so that
1126 // we create create our `spare` slice, we can slice starting from
1127 // self.pos (everything before that is the full raw request)
1128 self.pos = len + missing;
1129 } else {
1130 // We don't have the [full] body, and our static buffer is too small
1131 const body_buf = try self.buffer_pool.arenaAlloc(req_arena, cl);
1132 @memcpy(body_buf.data[0..read], buf[pos .. pos + read]);
1133 self.body = body_buf;
1134 }
1135 self.body_pos = read;
1136 return false;
1137 }
1138
1139 fn readBody(self: *State, source: anytype) !bool {
1140 const buf = self.body.?.data;
1141 self.body_pos += try zig016HackRead(source, buf[self.body_pos..]);
1142 return (self.body_pos == self.body_len);
1143 }
1144
1145 fn prepareForChunkedBody(self: *State, req_arena: Allocator) !bool {
1146 // Initial decoded-body buffer. Try the pool first; if empty, arena-alloc
1147 // a buffer of the same size so the consumer always sees a contiguous []u8
1148 // regardless of which path we took.
1149 const initial: buffer.Buffer = self.buffer_pool.tryAlloc() orelse blk: {
1150 const data = try req_arena.alloc(u8, self.buffer_pool.buffer_size);
1151 break :blk .{ .type = .arena, .data = data };
1152 };
1153 self.body = initial;
1154 self.body_pos = 0;
1155 self.body_len = 0;
1156
1157 // Allocate a dedicated raw-input buffer; copy whatever already sits
1158 // past the headers in self.buf. We never write back into self.buf
1159 // because URL/header-value slices reference it.
1160 const raw = try req_arena.alloc(u8, self.buf.len);
1161 const carry = self.len - self.pos;
1162 @memcpy(raw[0..carry], self.buf[self.pos..self.len]);
1163
1164 self.chunked = .{
1165 .state = .size_line,
1166 .chunk_remaining = 0,
1167 .body_capacity = initial.data.len,
1168 .raw = raw,
1169 .raw_pos = 0,
1170 .raw_len = carry,
1171 };
1172 // Drain anything that arrived in the same read as the headers.
1173 if (carry > 0) {
1174 return try self.processChunked(req_arena);
1175 }
1176 return false;
1177 }
1178
1179 fn readChunked(self: *State, conn: *HTTPConn, source: anytype) !bool {
1180 const req_arena = conn.req_arena.allocator();
1181 var ck = &self.chunked.?;
1182
1183 // Compact the raw buffer to make room for the upcoming read.
1184 if (ck.raw_pos > 0) {
1185 if (ck.raw_pos == ck.raw_len) {
1186 ck.raw_pos = 0;
1187 ck.raw_len = 0;
1188 } else {
1189 const remaining = ck.raw_len - ck.raw_pos;
1190 std.mem.copyForwards(u8, ck.raw[0..remaining], ck.raw[ck.raw_pos..ck.raw_len]);
1191 ck.raw_pos = 0;
1192 ck.raw_len = remaining;
1193 }
1194 }
1195
1196 if (ck.raw_len == ck.raw.len) {
1197 // Couldn't progress and buffer is full — a chunk size line or
1198 // trailer line longer than ck.raw. Treat as malformed.
1199 return error.InvalidChunkSize;
1200 }
1201
1202 // Match readBody's shape: one read per parse() call. The worker's
1203 // event loop re-invokes us when more data arrives, so we must not
1204 // try to read past what's currently available.
1205 const n = try zig016HackRead(source, ck.raw[ck.raw_len..]);
1206 if (n == 0) {
1207 return false;
1208 }
1209 ck.raw_len += n;
1210 return try self.processChunked(req_arena);
1211 }
1212
1213 // Decodes as much of the raw buffer as possible into self.body. Returns
1214 // true once the terminating 0-sized chunk and trailers have been
1215 // consumed. Returns false to indicate "need more raw input".
1216 fn processChunked(self: *State, req_arena: Allocator) !bool {
1217 var ck = &self.chunked.?;
1218 while (true) {
1219 switch (ck.state) {
1220 .size_line => {
1221 const slice = ck.raw[ck.raw_pos..ck.raw_len];
1222 const lf = std.mem.indexOfScalar(u8, slice, '\n') orelse {
1223 // No \r\n yet. The size line is small in practice; if we
1224 // already have more bytes than any reasonable size line,
1225 // it's malformed.
1226 if (slice.len > MAX_CHUNK_SIZE_LINE) {
1227 return error.InvalidChunkSize;
1228 }
1229 return false;
1230 };
1231
1232 if (lf == 0 or slice[lf - 1] != '\r') {
1233 return error.InvalidChunkLine;
1234 }
1235
1236 // size = hex digits up to ';' (extension) or '\r'
1237 var end_hex: usize = 0;
1238 while (end_hex < lf and slice[end_hex] != ';' and slice[end_hex] != '\r') : (end_hex += 1) {}
1239 if (end_hex == 0) {
1240 return error.InvalidChunkSize;
1241 }
1242
1243 const size = parseHex(slice[0..end_hex]) orelse return error.InvalidChunkSize;
1244 ck.raw_pos += lf + 1;
1245 if (size == 0) {
1246 ck.state = .trailers;
1247 } else {
1248 ck.chunk_remaining = size;
1249 ck.state = .data;
1250 }
1251 },
1252 .data => {
1253 const avail = ck.raw_len - ck.raw_pos;
1254 if (avail == 0) {
1255 return false;
1256 }
1257 const take = @min(avail, ck.chunk_remaining);
1258 const new_body_pos = self.body_pos + take;
1259
1260 if (new_body_pos > self.max_body_size) {
1261 metrics.bodyTooBig();
1262 return error.BodyTooBig;
1263 }
1264
1265 if (new_body_pos > ck.body_capacity) {
1266 try self.growBody(req_arena, new_body_pos);
1267 }
1268
1269 @memcpy(self.body.?.data[self.body_pos..new_body_pos], ck.raw[ck.raw_pos .. ck.raw_pos + take]);
1270 ck.raw_pos += take;
1271 self.body_pos = new_body_pos;
1272 ck.chunk_remaining -= take;
1273 if (ck.chunk_remaining == 0) {
1274 ck.state = .trailing_crlf;
1275 }
1276 },
1277 .trailing_crlf => {
1278 if (ck.raw_len - ck.raw_pos < 2) {
1279 return false;
1280 }
1281 if (ck.raw[ck.raw_pos] != '\r' or ck.raw[ck.raw_pos + 1] != '\n') {
1282 return error.InvalidChunkLine;
1283 }
1284 ck.raw_pos += 2;
1285 ck.state = .size_line;
1286 },
1287 .trailers => {
1288 const slice = ck.raw[ck.raw_pos..ck.raw_len];
1289 const lf = std.mem.indexOfScalar(u8, slice, '\n') orelse return false;
1290 if (lf == 0 or slice[lf - 1] != '\r') {
1291 return error.InvalidChunkLine;
1292 }
1293
1294 ck.raw_pos += lf + 1;
1295 if (lf == 1) {
1296 // empty line — end of trailers, body fully decoded
1297 self.body_len = self.body_pos;
1298 ck.state = .done;
1299 return true;
1300 }
1301 // non-empty trailer line; discard and keep looking for the
1302 // empty terminator
1303 },
1304 .done => return true,
1305 }
1306 }
1307 }
1308
1309 fn growBody(self: *State, req_arena: Allocator, needed: usize) !void {
1310 var ck = &self.chunked.?;
1311 const cur = ck.body_capacity;
1312 var new_cap = cur * 2;
1313
1314 if (new_cap < needed) {
1315 new_cap = needed;
1316 }
1317
1318 if (new_cap > self.max_body_size) {
1319 new_cap = self.max_body_size;
1320 }
1321
1322 if (new_cap < needed) {
1323 metrics.bodyTooBig();
1324 return error.BodyTooBig;
1325 }
1326
1327 const new_data = try req_arena.alloc(u8, new_cap);
1328 @memcpy(new_data[0..self.body_pos], self.body.?.data[0..self.body_pos]);
1329 // releases pool buffers; no-op for arena buffers
1330 self.buffer_pool.release(self.body.?);
1331 self.body = .{ .type = .arena, .data = new_data };
1332 ck.body_capacity = new_cap;
1333 }
1334};
1335
1336// Hex digits, optional ';' + extension, then '\r\n'. Generous cap; real-world
1337// values are always tiny.
1338const MAX_CHUNK_SIZE_LINE: usize = 64;
1339
1340fn parseHex(str: []const u8) ?usize {
1341 if (str.len == 0) {
1342 return null;
1343 }
1344
1345 var n: usize = 0;
1346 for (str) |b| {
1347 const d: usize = switch (b) {
1348 '0'...'9' => @intCast(b - '0'),
1349 'a'...'f' => @intCast(b - 'a' + 10),
1350 'A'...'F' => @intCast(b - 'A' + 10),
1351 else => return null,
1352 };
1353 n = std.math.shlExact(usize, n, 4) catch return null;
1354 n = std.math.add(usize, n, d) catch return null;
1355 }
1356 return n;
1357}
1358
1359// Zig 0.16's Io.net.Stream doesn't expose WouldBlock. It just panics. I don't
1360// understand why it's like that. But we're in a transition, and I just want to
1361// make this work. So, in "real" code, `source` will be a socket_t. In tests,
1362// `source` will be an Io.Reader.
1363// In theory, I woulc wrap the `socket_t` in a `Io.Reader` that behaves like I
1364// want it to, but this is _a lot_ easier, especially since all of this will
1365// be re-worked when networking is fully working in Zig.
1366fn zig016HackRead(source: anytype, buf: []u8) !usize {
1367 if (@TypeOf(source) == posix.socket_t) {
1368 return posix.read(source, buf);
1369 }
1370 // source is a reader
1371 var vecs: [1][]u8 = .{buf};
1372 return source.readVec(&vecs);
1373}
1374
1375const allowedHeaderValueByte = blk: {
1376 var v = [_]bool{false} ** 256;
1377 for ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_ :;.,/\"'?!(){}[]@<>=-+*#$&`|~^%\t\\") |b| {
1378 v[b] = true;
1379 }
1380 break :blk v;
1381};
1382
1383inline fn trimLeadingSpaceCount(in: []const u8) struct { []const u8, usize } {
1384 if (in.len > 1 and in[0] == ' ') {
1385 // very common case
1386 const n = in[1];
1387 if (n != ' ' and n != '\t') {
1388 return .{ in[1..], 1 };
1389 }
1390 }
1391
1392 for (in, 0..) |b, i| {
1393 if (b != ' ' and b != '\t') return .{ in[i..], i };
1394 }
1395 return .{ "", in.len };
1396}
1397
1398inline fn trimLeadingSpace(in: []const u8) []const u8 {
1399 const out, _ = trimLeadingSpaceCount(in);
1400 return out;
1401}
1402
1403fn atoi(str: []const u8) ?usize {
1404 if (str.len == 0) {
1405 return null;
1406 }
1407
1408 var n: usize = 0;
1409 for (str) |b| {
1410 if (b < '0' or b > '9') {
1411 return null;
1412 }
1413 n = std.math.mul(usize, n, 10) catch return null;
1414 n = std.math.add(usize, n, @intCast(b - '0')) catch return null;
1415 }
1416 return n;
1417}
1418
1419const t = @import("t.zig");
1420test "atoi" {
1421 var buf: [5]u8 = undefined;
1422 for (0..99999) |i| {
1423 var writer = std.Io.Writer.fixed(&buf);
1424 try writer.printInt(i, 10, .lower, .{});
1425 try t.expectEqual(i, atoi(buf[0..writer.end]).?);
1426 }
1427
1428 try t.expectEqual(null, atoi(""));
1429 try t.expectEqual(null, atoi("392a"));
1430 try t.expectEqual(null, atoi("b392"));
1431 try t.expectEqual(null, atoi("3c92"));
1432}
1433
1434test "allowedHeaderValueByte" {
1435 var all = std.mem.zeroes([255]bool);
1436 for ('a'..('z' + 1)) |b| all[b] = true;
1437 for ('A'..('Z' + 1)) |b| all[b] = true;
1438 for ('0'..('9' + 1)) |b| all[b] = true;
1439 for ([_]u8{ '_', ' ', ',', ':', ';', '.', ',', '\\', '/', '"', '\'', '?', '!', '(', ')', '{', '}', '[', ']', '@', '<', '>', '=', '-', '+', '*', '#', '$', '&', '`', '|', '~', '^', '%', '\t' }) |b| {
1440 all[b] = true;
1441 }
1442 for (128..255) |b| all[b] = false;
1443
1444 for (all, 0..) |allowed, b| {
1445 try t.expectEqual(allowed, allowedHeaderValueByte[@intCast(b)]);
1446 }
1447}
1448
1449test "request: header too big" {
1450 try expectParseError(error.HeaderTooBig, "GET / HTTP/1.1\r\n\r\n", .{ .buffer_size = 17 });
1451 try expectParseError(error.HeaderTooBig, "GET / HTTP/1.1\r\nH: v\r\n\r\n", .{ .buffer_size = 23 });
1452}
1453
1454test "request: parse method" {
1455 defer t.reset();
1456 {
1457 try expectParseError(error.UnknownMethod, " PUT / HTTP/1.1", .{});
1458 try expectParseError(error.UnknownMethod, "GET/HTTP/1.1", .{});
1459 try expectParseError(error.UnknownMethod, "PUT/HTTP/1.1", .{});
1460
1461 try expectParseError(error.UnknownMethod, "get / HTTP/1.1", .{}); // lowecase
1462 try expectParseError(error.UnknownMethod, "Other / HTTP/1.1", .{}); // lowercase
1463 }
1464
1465 {
1466 const r = try testParse("GET / HTTP/1.1\r\n\r\n", .{});
1467 try t.expectEqual(http.Method.GET, r.method);
1468 try t.expectString("", r.method_string);
1469 }
1470
1471 {
1472 const r = try testParse("PUT / HTTP/1.1\r\n\r\n", .{});
1473 try t.expectEqual(http.Method.PUT, r.method);
1474 try t.expectString("", r.method_string);
1475 }
1476
1477 {
1478 const r = try testParse("POST / HTTP/1.1\r\n\r\n", .{});
1479 try t.expectEqual(http.Method.POST, r.method);
1480 try t.expectString("", r.method_string);
1481 }
1482
1483 {
1484 const r = try testParse("HEAD / HTTP/1.1\r\n\r\n", .{});
1485 try t.expectEqual(http.Method.HEAD, r.method);
1486 try t.expectString("", r.method_string);
1487 }
1488
1489 {
1490 const r = try testParse("PATCH / HTTP/1.1\r\n\r\n", .{});
1491 try t.expectEqual(http.Method.PATCH, r.method);
1492 try t.expectString("", r.method_string);
1493 }
1494
1495 {
1496 const r = try testParse("DELETE / HTTP/1.1\r\n\r\n", .{});
1497 try t.expectEqual(http.Method.DELETE, r.method);
1498 try t.expectString("", r.method_string);
1499 }
1500
1501 {
1502 const r = try testParse("OPTIONS / HTTP/1.1\r\n\r\n", .{});
1503 try t.expectEqual(http.Method.OPTIONS, r.method);
1504 try t.expectString("", r.method_string);
1505 }
1506
1507 {
1508 const r = try testParse("TEA / HTTP/1.1\r\n\r\n", .{});
1509 try t.expectEqual(http.Method.OTHER, r.method);
1510 try t.expectString("TEA", r.method_string);
1511 }
1512}
1513
1514test "request: parse request target" {
1515 defer t.reset();
1516 {
1517 try expectParseError(error.InvalidRequestTarget, "GET NOPE", .{});
1518 try expectParseError(error.InvalidRequestTarget, "GET nope ", .{});
1519 try expectParseError(error.InvalidRequestTarget, "GET http://www.pondzpondz.com/test ", .{}); // this should be valid
1520 try expectParseError(error.InvalidRequestTarget, "PUT hello ", .{});
1521 try expectParseError(error.InvalidRequestTarget, "POST /hello ", .{});
1522 try expectParseError(error.InvalidRequestTarget, "POST *hello ", .{});
1523 }
1524
1525 {
1526 const r = try testParse("PUT / HTTP/1.1\r\n\r\n", .{});
1527 try t.expectString("/", r.url.raw);
1528 }
1529
1530 {
1531 const r = try testParse("PUT /api/v2 HTTP/1.1\r\n\r\n", .{});
1532 try t.expectString("/api/v2", r.url.raw);
1533 }
1534
1535 {
1536 const r = try testParse("DELETE /API/v2?hack=true&over=9000%20!! HTTP/1.1\r\n\r\n", .{});
1537 try t.expectString("/API/v2?hack=true&over=9000%20!!", r.url.raw);
1538 }
1539
1540 {
1541 const r = try testParse("PUT * HTTP/1.1\r\n\r\n", .{});
1542 try t.expectString("*", r.url.raw);
1543 }
1544}
1545
1546test "request: parse protocol" {
1547 defer t.reset();
1548 {
1549 try expectParseError(error.UnknownProtocol, "GET / http/1.1\r\n", .{});
1550 try expectParseError(error.UnsupportedProtocol, "GET / HTTP/2.0\r\n", .{});
1551 }
1552
1553 {
1554 const r = try testParse("PUT / HTTP/1.0\r\n\r\n", .{});
1555 try t.expectEqual(http.Protocol.HTTP10, r.protocol);
1556 }
1557
1558 {
1559 const r = try testParse("PUT / HTTP/1.1\r\n\r\n", .{});
1560 try t.expectEqual(http.Protocol.HTTP11, r.protocol);
1561 }
1562}
1563
1564test "request: parse headers" {
1565 defer t.reset();
1566 {
1567 try expectParseError(error.InvalidHeaderLine, "GET / HTTP/1.1\r\nHost\r\n", .{});
1568 }
1569
1570 {
1571 const r = try testParse("PUT / HTTP/1.0\r\n\r\n", .{});
1572 try t.expectEqual(0, r.headers.len);
1573 }
1574
1575 {
1576 var r = try testParse("PUT / HTTP/1.0\r\nHost: pondzpondz.com\r\n\r\n", .{});
1577
1578 try t.expectEqual(1, r.headers.len);
1579 try t.expectString("pondzpondz.com", r.headers.get("host").?);
1580 }
1581
1582 {
1583 var r = try testParse("PUT / HTTP/1.0\r\nHost: pondzpondz.com\r\nMisc: Some-Value\r\nAuthorization:none\r\n\r\n", .{});
1584 try t.expectEqual(3, r.headers.len);
1585 try t.expectString("pondzpondz.com", r.header("host").?);
1586 try t.expectString("Some-Value", r.header("misc").?);
1587 try t.expectString("none", r.header("authorization").?);
1588 }
1589
1590 {
1591 // RFC 7230 §3.2.4 — leading and trailing OWS (space, tab) is not
1592 // part of the field value.
1593 var r = try testParse("PUT / HTTP/1.0\r\nA: trailing-space \r\nB:\tboth\t \r\nC:no-trim\r\nD: \r\n\r\n", .{});
1594 try t.expectString("trailing-space", r.header("a").?);
1595 try t.expectString("both", r.header("b").?);
1596 try t.expectString("no-trim", r.header("c").?);
1597 try t.expectString("", r.header("d").?);
1598 }
1599}
1600
1601test "request: canKeepAlive" {
1602 defer t.reset();
1603 {
1604 // implicitly keepalive for 1.1
1605 var r = try testParse("GET / HTTP/1.1\r\n\r\n", .{});
1606 try t.expectEqual(true, r.canKeepAlive());
1607 }
1608
1609 {
1610 // explicitly keepalive for 1.1
1611 var r = try testParse("GET / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n", .{});
1612 try t.expectEqual(true, r.canKeepAlive());
1613 }
1614
1615 {
1616 // explicitly not keepalive for 1.1
1617 var r = try testParse("GET / HTTP/1.1\r\nConnection: close\r\n\r\n", .{});
1618 try t.expectEqual(false, r.canKeepAlive());
1619 }
1620}
1621
1622test "request: query" {
1623 defer t.reset();
1624 {
1625 // none
1626 var r = try testParse("PUT / HTTP/1.1\r\n\r\n", .{});
1627 try t.expectEqual(0, (try r.query()).len);
1628 }
1629
1630 {
1631 // none with path
1632 var r = try testParse("PUT /why/would/this/matter HTTP/1.1\r\n\r\n", .{});
1633 try t.expectEqual(0, (try r.query()).len);
1634 }
1635
1636 {
1637 // value-less
1638 var r = try testParse("PUT /?a HTTP/1.1\r\n\r\n", .{});
1639 const query = try r.query();
1640 try t.expectEqual(1, query.len);
1641 try t.expectString("", query.get("a").?);
1642 try t.expectEqual(null, query.get("b"));
1643 }
1644
1645 {
1646 // single
1647 var r = try testParse("PUT /?a=1 HTTP/1.1\r\n\r\n", .{});
1648 const query = try r.query();
1649 try t.expectEqual(1, query.len);
1650 try t.expectString("1", query.get("a").?);
1651 try t.expectEqual(null, query.get("b"));
1652 }
1653
1654 {
1655 // multiple
1656 var r = try testParse("PUT /path?Teg=Tea&it%20%20IS=over%209000%24&ha%09ck HTTP/1.1\r\n\r\n", .{});
1657 const query = try r.query();
1658 try t.expectEqual(3, query.len);
1659 try t.expectString("Tea", query.get("Teg").?);
1660 try t.expectString("over 9000$", query.get("it IS").?);
1661 try t.expectString("", query.get("ha\tck").?);
1662 }
1663}
1664
1665test "request: body content-length" {
1666 defer t.reset();
1667 {
1668 // too big
1669 try expectParseError(error.BodyTooBig, "POST / HTTP/1.0\r\nContent-Length: 10\r\n\r\nOver 9000!", .{ .max_body_size = 9 });
1670 }
1671
1672 {
1673 // no body
1674 var r = try testParse("PUT / HTTP/1.0\r\nHost: pondzpondz.com\r\nContent-Length: 0\r\n\r\n", .{ .max_body_size = 10 });
1675 try t.expectEqual(null, r.body());
1676 try t.expectEqual(null, r.body());
1677 }
1678
1679 {
1680 // fits into static buffer
1681 var r = try testParse("POST / HTTP/1.0\r\nContent-Length: 10\r\n\r\nOver 9000!", .{});
1682 try t.expectString("Over 9000!", r.body().?);
1683 try t.expectString("Over 9000!", r.body().?);
1684 }
1685
1686 {
1687 // Requires dynamic buffer
1688 var r = try testParse("POST / HTTP/1.0\r\nContent-Length: 11\r\n\r\nOver 9001!!", .{ .buffer_size = 40 });
1689 try t.expectString("Over 9001!!", r.body().?);
1690 try t.expectString("Over 9001!!", r.body().?);
1691 }
1692}
1693
1694// the query and body both (can) occupy space in our static buffer
1695test "request: query & body" {
1696 defer t.reset();
1697
1698 // query then body
1699 var r = try testParse("POST /?search=keemun%20tea HTTP/1.0\r\nContent-Length: 10\r\n\r\nOver 9000!", .{});
1700 try t.expectString("keemun tea", (try r.query()).get("search").?);
1701 try t.expectString("Over 9000!", r.body().?);
1702
1703 // results should be cached internally, but let's double check
1704 try t.expectString("keemun tea", (try r.query()).get("search").?);
1705}
1706
1707test "request: invalid content-length" {
1708 defer t.reset();
1709 try expectParseError(error.InvalidContentLength, "GET / HTTP/1.0\r\nContent-Length: 1\r\n\r\nabc", .{});
1710 try expectParseError(error.InvalidContentLength, "POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 0\r\nContent-Length: 1\r\n\r\n", .{});
1711 try expectParseError(error.InvalidContentLength, "POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 0\r\nContent-Length: 0\r\n\r\n", .{});
1712}
1713
1714test "request: invalid host" {
1715 defer t.reset();
1716 try expectParseError(error.InvalidHost, "GET / HTTP/1.1\r\nHost: a\r\nHost: b\r\n\r\n", .{});
1717 try expectParseError(error.InvalidHost, "GET / HTTP/1.1\r\nHost: a\r\nHost: a\r\n\r\n", .{});
1718}
1719
1720test "request: body chunked" {
1721 defer t.reset();
1722 {
1723 // simple two-chunk body that fits in initial pooled buffer
1724 var r = try testParse("POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHello\r\n6\r\n World\r\n0\r\n\r\n", .{});
1725 try t.expectString("Hello World", r.body().?);
1726 try t.expectString("Hello World", r.body().?);
1727 }
1728
1729 {
1730 // empty body via single 0-sized chunk
1731 var r = try testParse("POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n", .{});
1732 try t.expectString("", r.body().?);
1733 }
1734
1735 {
1736 // chunk size with extension is accepted (extension ignored)
1737 var r = try testParse("POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n5;name=value\r\nHello\r\n0\r\n\r\n", .{});
1738 try t.expectString("Hello", r.body().?);
1739 }
1740
1741 {
1742 // hex size > 9 (uppercase and lowercase)
1743 var r = try testParse("POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\nA\r\n0123456789\r\nb\r\nabcdefghijk\r\n0\r\n\r\n", .{});
1744 try t.expectString("0123456789abcdefghijk", r.body().?);
1745 }
1746
1747 {
1748 // trailers are accepted and discarded
1749 var r = try testParse("POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHello\r\n0\r\nX-Trailer: yes\r\nX-Other: 1\r\n\r\n", .{});
1750 try t.expectString("Hello", r.body().?);
1751 }
1752
1753 {
1754 // case-insensitive transfer-encoding value, surrounding whitespace OK
1755 var r = try testParse("POST / HTTP/1.1\r\nTransfer-Encoding: ChUnKeD \r\n\r\n3\r\nfoo\r\n0\r\n\r\n", .{});
1756 try t.expectString("foo", r.body().?);
1757 }
1758}
1759
1760test "request: chunked overflows initial pool buffer" {
1761 defer t.reset();
1762 // pool buffer_size is 256 in test setup; send a chunked body that's larger
1763 // so the grow path runs.
1764 const big_chunk_size = 1000;
1765 const aa = t.arena.allocator();
1766 const buf = aa.alloc(u8, big_chunk_size) catch unreachable;
1767 for (buf, 0..) |*b, i| b.* = @intCast(('a' + (i % 26)));
1768
1769 const req = std.fmt.allocPrint(aa, "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n{x}\r\n{s}\r\n0\r\n\r\n", .{ big_chunk_size, buf }) catch unreachable;
1770 var r = try testParse(req, .{});
1771 try t.expectString(buf, r.body().?);
1772}
1773
1774test "request: chunked too big" {
1775 defer t.reset();
1776 // 11-byte body, max_body_size = 10
1777 try expectParseError(
1778 error.BodyTooBig,
1779 "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHello\r\n6\r\n World\r\n0\r\n\r\n",
1780 .{ .max_body_size = 10 },
1781 );
1782}
1783
1784test "request: invalid transfer-encoding" {
1785 defer t.reset();
1786 // Non-chunked TE
1787 try expectParseError(error.InvalidTransferEncoding, "POST / HTTP/1.1\r\nTransfer-Encoding: gzip\r\n\r\n", .{});
1788 // Combined with content-length is rejected
1789 try expectParseError(error.InvalidTransferEncoding, "POST / HTTP/1.1\r\nContent-Length: 5\r\nTransfer-Encoding: chunked\r\n\r\n", .{});
1790 // Multi-coding with chunked is rejected
1791 try expectParseError(error.InvalidTransferEncoding, "POST / HTTP/1.1\r\nTransfer-Encoding: gzip, chunked\r\n\r\n", .{});
1792}
1793
1794test "request: invalid chunked framing" {
1795 defer t.reset();
1796 // bad hex in chunk size
1797 try expectParseError(error.InvalidChunkSize, "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\nZZ\r\nHello\r\n0\r\n\r\n", .{});
1798 // empty hex
1799 try expectParseError(error.InvalidChunkSize, "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n;ext\r\n0\r\n\r\n", .{});
1800 // missing trailing \r\n after chunk data
1801 try expectParseError(error.InvalidChunkLine, "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHelloXX0\r\n\r\n", .{});
1802}
1803
1804test "request: chunked across fragmented reads" {
1805 defer t.reset();
1806 const aa = t.arena.allocator();
1807
1808 // Build a chunked body where the body itself exceeds the initial pool
1809 // buffer (256 bytes), forcing a grow, and feed it through the FakeReader
1810 // which fragments reads at random byte boundaries.
1811 const big = 600;
1812 var body = aa.alloc(u8, big) catch unreachable;
1813 for (body, 0..) |*b, i| b.* = @intCast('A' + (i % 26));
1814
1815 const req = std.fmt.allocPrint(
1816 aa,
1817 // two chunks split, plus a trailer line
1818 "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n{x}\r\n{s}\r\n{x}\r\n{s}\r\n0\r\nX-Note: ok\r\n\r\n",
1819 .{ 200, body[0..200], big - 200, body[200..] },
1820 ) catch unreachable;
1821
1822 var ctx = t.Context.init(.{ .request = .{ .buffer_size = 128 } });
1823 ctx.fake = true;
1824 defer ctx.deinit();
1825
1826 ctx.write(req);
1827 var fake = ctx.fakeReader();
1828 const fr = &fake.interface;
1829 while (true) {
1830 const done = try ctx.conn.req_state.parse(ctx.conn, fr);
1831 if (done) break;
1832 }
1833 var request = Request.init(ctx.conn.req_arena.allocator(), ctx.conn);
1834 try t.expectString(body, request.body().?);
1835}
1836
1837test "parseHex" {
1838 try t.expectEqual(@as(?usize, 0), parseHex("0"));
1839 try t.expectEqual(@as(?usize, 10), parseHex("a"));
1840 try t.expectEqual(@as(?usize, 10), parseHex("A"));
1841 try t.expectEqual(@as(?usize, 255), parseHex("ff"));
1842 try t.expectEqual(@as(?usize, 255), parseHex("Ff"));
1843 try t.expectEqual(@as(?usize, 0x1A2b), parseHex("1A2b"));
1844 try t.expectEqual(@as(?usize, null), parseHex(""));
1845 try t.expectEqual(@as(?usize, null), parseHex("g"));
1846 try t.expectEqual(@as(?usize, null), parseHex("12g"));
1847}
1848
1849test "body: json" {
1850 defer t.reset();
1851 const Tea = struct {
1852 type: []const u8,
1853 };
1854
1855 {
1856 // too big
1857 try expectParseError(error.BodyTooBig, "POST / HTTP/1.0\r\nContent-Length: 17\r\n\r\n{\"type\":\"keemun\"}", .{ .max_body_size = 16 });
1858 }
1859
1860 {
1861 // no body
1862 var r = try testParse("PUT / HTTP/1.0\r\nHost: pondzpondz.com\r\nContent-Length: 0\r\n\r\n", .{ .max_body_size = 10 });
1863 try t.expectEqual(null, try r.json(Tea));
1864 try t.expectEqual(null, try r.json(Tea));
1865 }
1866
1867 {
1868 // parses json
1869 var r = try testParse("POST / HTTP/1.0\r\nContent-Length: 17\r\n\r\n{\"type\":\"keemun\"}", .{});
1870 try t.expectString("keemun", (try r.json(Tea)).?.type);
1871 try t.expectString("keemun", (try r.json(Tea)).?.type);
1872 }
1873}
1874
1875test "body: jsonValue" {
1876 defer t.reset();
1877 {
1878 // too big
1879 try expectParseError(error.BodyTooBig, "POST / HTTP/1.0\r\nContent-Length: 17\r\n\r\n{\"type\":\"keemun\"}", .{ .max_body_size = 16 });
1880 }
1881
1882 {
1883 // no body
1884 var r = try testParse("PUT / HTTP/1.0\r\nHost: pondzpondz.com\r\nContent-Length: 0\r\n\r\n", .{ .max_body_size = 10 });
1885 try t.expectEqual(null, try r.jsonValue());
1886 try t.expectEqual(null, try r.jsonValue());
1887 }
1888
1889 {
1890 // parses json
1891 var r = try testParse("POST / HTTP/1.0\r\nContent-Length: 17\r\n\r\n{\"type\":\"keemun\"}", .{});
1892 try t.expectString("keemun", (try r.jsonValue()).?.object.get("type").?.string);
1893 try t.expectString("keemun", (try r.jsonValue()).?.object.get("type").?.string);
1894 }
1895}
1896
1897test "body: jsonObject" {
1898 defer t.reset();
1899 {
1900 // too big
1901 try expectParseError(error.BodyTooBig, "POST / HTTP/1.0\r\nContent-Length: 17\r\n\r\n{\"type\":\"keemun\"}", .{ .max_body_size = 16 });
1902 }
1903
1904 {
1905 // no body
1906 var r = try testParse("PUT / HTTP/1.0\r\nHost: pondzpondz.com\r\nContent-Length: 0\r\n\r\n", .{ .max_body_size = 10 });
1907 try t.expectEqual(null, try r.jsonObject());
1908 try t.expectEqual(null, try r.jsonObject());
1909 }
1910
1911 {
1912 // not an object
1913 var r = try testParse("POST / HTTP/1.0\r\nContent-Length: 7\r\n\r\n\"hello\"", .{});
1914 try t.expectEqual(null, try r.jsonObject());
1915 try t.expectEqual(null, try r.jsonObject());
1916 }
1917
1918 {
1919 // parses json
1920 var r = try testParse("POST / HTTP/1.0\r\nContent-Length: 17\r\n\r\n{\"type\":\"keemun\"}", .{});
1921 try t.expectString("keemun", (try r.jsonObject()).?.get("type").?.string);
1922 try t.expectString("keemun", (try r.jsonObject()).?.get("type").?.string);
1923 }
1924}
1925
1926test "body: formData" {
1927 defer t.reset();
1928 {
1929 // too big
1930 try expectParseError(error.BodyTooBig, "POST / HTTP/1.0\r\nContent-Length: 22\r\n\r\nname=test", .{ .max_body_size = 21 });
1931 }
1932
1933 {
1934 // no body
1935 var r = try testParse("POST / HTTP/1.0\r\n\r\nContent-Length: 0\r\n\r\n", .{ .max_body_size = 10 });
1936 const formData = try r.formData();
1937 try t.expectEqual(null, formData.get("name"));
1938 try t.expectEqual(null, formData.get("name"));
1939 }
1940
1941 {
1942 // parses formData
1943 var r = try testParse("POST / HTTP/1.0\r\nContent-Length: 9\r\n\r\nname=test", .{ .max_form_count = 2 });
1944 const formData = try r.formData();
1945 try t.expectString("test", formData.get("name").?);
1946 try t.expectString("test", formData.get("name").?);
1947 }
1948
1949 {
1950 // multiple inputs
1951 var r = try testParse("POST / HTTP/1.0\r\nContent-Length: 25\r\n\r\nname=test1&password=test2", .{ .max_form_count = 2 });
1952
1953 const formData = try r.formData();
1954 try t.expectString("test1", formData.get("name").?);
1955 try t.expectString("test1", formData.get("name").?);
1956
1957 try t.expectString("test2", formData.get("password").?);
1958 try t.expectString("test2", formData.get("password").?);
1959 }
1960
1961 {
1962 // test decoding
1963 var r = try testParse("POST / HTTP/1.0\r\nContent-Length: 44\r\n\r\ntest=%21%40%23%24%25%5E%26*%29%28-%3D%2B%7C+", .{ .max_form_count = 2 });
1964
1965 const formData = try r.formData();
1966 try t.expectString("!@#$%^&*)(-=+| ", formData.get("test").?);
1967 try t.expectString("!@#$%^&*)(-=+| ", formData.get("test").?);
1968 }
1969}
1970
1971test "body: multiFormData valid" {
1972 defer t.reset();
1973
1974 {
1975 // no body
1976 var r = try testParse(buildRequest(&.{ "POST / HTTP/1.0", "Content-Type: multipart/form-data; boundary=BX1" }, &.{}), .{ .max_multiform_count = 5 });
1977 const formData = try r.multiFormData();
1978 try t.expectEqual(0, formData.len);
1979 try t.expectString("multipart/form-data; boundary=BX1", r.header("content-type").?);
1980 }
1981
1982 {
1983 // parses single field
1984 var r = try testParse(buildRequest(&.{ "POST / HTTP/1.0", "Content-Type: multipart/form-data; boundary=-90x" }, &.{ "---90x\r\n", "Content-Disposition: form-data; name=\"description\"\r\n\r\n", "the-desc\r\n", "---90x--\r\n" }), .{ .max_multiform_count = 5 });
1985
1986 const formData = try r.multiFormData();
1987 try t.expectString("the-desc", formData.get("description").?.value);
1988 try t.expectString("the-desc", formData.get("description").?.value);
1989 }
1990
1991 {
1992 // parses single field with filename
1993 var r = try testParse(buildRequest(&.{ "POST / HTTP/1.0", "Content-Type: multipart/form-data; boundary=-90x" }, &.{ "---90x\r\n", "Content-Disposition: form-data; filename=\"file1.zig\"; name=file\r\n\r\n", "some binary data\r\n", "---90x--\r\n" }), .{ .max_multiform_count = 5 });
1994
1995 const formData = try r.multiFormData();
1996 const field = formData.get("file").?;
1997 try t.expectString("some binary data", field.value);
1998 try t.expectString("file1.zig", field.filename.?);
1999 }
2000
2001 {
2002 // quoted boundary
2003 var r = try testParse(buildRequest(&.{ "POST / HTTP/1.0", "Content-Type: multipart/form-data; boundary=\"-90x\"" }, &.{ "---90x\r\n", "Content-Disposition: form-data; name=\"description\"\r\n\r\n", "the-desc\r\n", "---90x--\r\n" }), .{ .max_multiform_count = 5 });
2004
2005 const formData = try r.multiFormData();
2006 const field = formData.get("description").?;
2007 try t.expectEqual(null, field.filename);
2008 try t.expectString("the-desc", field.value);
2009 }
2010
2011 {
2012 // multiple fields
2013 var r = try testParse(buildRequest(&.{ "GET /something HTTP/1.1", "Content-Type: multipart/form-data; boundary=----99900AB" }, &.{ "------99900AB\r\n", "content-type: text/plain; charset=utf-8\r\n", "content-disposition: form-data; name=\"fie\\\" \\?l\\d\"\r\n\r\n", "Value - 1\r\n", "------99900AB\r\n", "Content-Disposition: form-data; filename=another.zip; name=field2\r\n\r\n", "Value - 2\r\n", "------99900AB--\r\n" }), .{ .max_multiform_count = 5 });
2014
2015 const formData = try r.multiFormData();
2016 try t.expectEqual(2, formData.len);
2017
2018 const field1 = formData.get("fie\" ?l\\d").?;
2019 try t.expectEqual(null, field1.filename);
2020 try t.expectString("Value - 1", field1.value);
2021
2022 const field2 = formData.get("field2").?;
2023 try t.expectString("Value - 2", field2.value);
2024 try t.expectString("another.zip", field2.filename.?);
2025 }
2026
2027 {
2028 // enforce limit
2029 var r = try testParse(buildRequest(&.{ "GET /something HTTP/1.1", "Content-Type: multipart/form-data; boundary=----99900AB" }, &.{ "------99900AB\r\n", "Content-Type: text/plain; charset=utf-8\r\n", "Content-Disposition: form-data; name=\"fie\\\" \\?l\\d\"\r\n\r\n", "Value - 1\r\n", "------99900AB\r\n", "Content-Disposition: form-data; filename=another; name=field2\r\n\r\n", "Value - 2\r\n", "------99900AB--\r\n" }), .{ .max_multiform_count = 1 });
2030
2031 defer t.reset();
2032 const formData = try r.multiFormData();
2033 try t.expectEqual(1, formData.len);
2034 try t.expectString("Value - 1", formData.get("fie\" ?l\\d").?.value);
2035 }
2036}
2037
2038test "body: multiFormData invalid" {
2039 defer t.reset();
2040 {
2041 // large boundary
2042 var r = try testParse(buildRequest(&.{ "POST / HTTP/1.0", "Content-Type: multipart/form-data; boundary=12345678901234567890123456789012345678901234567890123456789012345678901" }, &.{"garbage"}), .{});
2043 try t.expectError(error.InvalidMultiPartFormDataHeader, r.multiFormData());
2044 }
2045
2046 {
2047 // no closing quote
2048 var r = try testParse(buildRequest(&.{ "POST / HTTP/1.0", "Content-Type: multipart/form-data; boundary=\"123" }, &.{"garbage"}), .{});
2049 try t.expectError(error.InvalidMultiPartFormDataHeader, r.multiFormData());
2050 }
2051
2052 {
2053 // no content-dispotion field header
2054 var r = try testParse(buildRequest(&.{ "POST / HTTP/1.0", "Content-Type: multipart/form-data; boundary=-90x" }, &.{ "---90x\r\n", "the-desc\r\n", "---90x--\r\n" }), .{ .max_multiform_count = 5 });
2055 try t.expectError(error.InvalidMultiPartEncoding, r.multiFormData());
2056 }
2057
2058 {
2059 // no content dispotion naem
2060 var r = try testParse(buildRequest(&.{ "POST / HTTP/1.0", "Content-Type: multipart/form-data; boundary=-90x" }, &.{ "---90x\r\n", "Content-Disposition: form-data; x=a", "the-desc\r\n", "---90x--\r\n" }), .{ .max_multiform_count = 5 });
2061 try t.expectError(error.InvalidMultiPartEncoding, r.multiFormData());
2062 }
2063
2064 {
2065 // missing name end quote
2066 var r = try testParse(buildRequest(&.{ "POST / HTTP/1.0", "Content-Type: multipart/form-data; boundary=-90x" }, &.{ "---90x\r\n", "Content-Disposition: form-data; name=\"hello\r\n\r\n", "the-desc\r\n", "---90x--\r\n" }), .{ .max_multiform_count = 5 });
2067 try t.expectError(error.InvalidMultiPartEncoding, r.multiFormData());
2068 }
2069
2070 {
2071 // missing missing newline
2072 var r = try testParse(buildRequest(&.{ "POST / HTTP/1.0", "Content-Type: multipart/form-data; boundary=-90x" }, &.{ "---90x\r\n", "Content-Disposition: form-data; name=hello\r\n", "the-desc\r\n", "---90x--\r\n" }), .{ .max_multiform_count = 5 });
2073 try t.expectError(error.InvalidMultiPartEncoding, r.multiFormData());
2074 }
2075
2076 {
2077 // missing missing newline x2
2078 var r = try testParse(buildRequest(&.{ "POST / HTTP/1.0", "Content-Type: multipart/form-data; boundary=-90x" }, &.{ "---90x\r\n", "Content-Disposition: form-data; name=hello", "the-desc\r\n", "---90x--\r\n" }), .{ .max_multiform_count = 5 });
2079 try t.expectError(error.InvalidMultiPartEncoding, r.multiFormData());
2080 }
2081}
2082
2083test "request: fuzz" {
2084 // We have a bunch of data to allocate for testing, like header names and
2085 // values. Easier to use this arena and reset it after each test run.
2086 const aa = t.arena.allocator();
2087 defer t.reset();
2088
2089 var r = t.getRandom();
2090 const random = r.random();
2091 for (0..1000) |_| {
2092 // important to test with different buffer sizes, since there's a lot of
2093 // special handling for different cases (e.g the buffer is full and has
2094 // some of the body in it, so we need to copy that to a dynamically allocated
2095 // buffer)
2096 const buffer_size = random.uintAtMost(u16, 1024) + 1024;
2097
2098 var ctx = t.Context.init(.{
2099 .request = .{ .buffer_size = buffer_size },
2100 });
2101
2102 // enable fake mode, we don't go through a real socket, instead we go
2103 // through a fake one, that can simulate having data spread across multiple
2104 // calls to read()
2105 ctx.fake = true;
2106 defer ctx.deinit();
2107
2108 // how many requests should we make on this 1 individual socket (simulating
2109 // keepalive AND the request pool)
2110 const number_of_requests = random.uintAtMost(u8, 10) + 1;
2111
2112 for (0..number_of_requests) |_| {
2113 defer ctx.conn.requestDone(4096, true) catch unreachable;
2114 const method = randomMethod(random);
2115 const url = t.randomString(random, aa, 20);
2116
2117 ctx.write(method);
2118 ctx.write(" /");
2119 ctx.write(url);
2120
2121 const number_of_qs = random.uintAtMost(u8, 4);
2122 if (number_of_qs != 0) {
2123 ctx.write("?");
2124 }
2125
2126 var query = std.StringHashMap([]const u8).init(aa);
2127 for (0..number_of_qs) |_| {
2128 const key = t.randomString(random, aa, 20);
2129 const value = t.randomString(random, aa, 20);
2130 if (!query.contains(key)) {
2131 // TODO: figure out how we want to handle duplicate query values
2132 // (the spec doesn't specify what to do)
2133 query.put(key, value) catch unreachable;
2134 ctx.write(key);
2135 ctx.write("=");
2136 ctx.write(value);
2137 ctx.write("&");
2138 }
2139 }
2140
2141 ctx.write(" HTTP/1.1\r\n");
2142
2143 var headers = std.StringHashMap([]const u8).init(aa);
2144 for (0..random.uintAtMost(u8, 4)) |_| {
2145 const name = t.randomString(random, aa, 20);
2146 const value = t.randomString(random, aa, 20);
2147 if (!headers.contains(name)) {
2148 // TODO: figure out how we want to handle duplicate query values
2149 // Note, the spec says we should merge these!
2150 headers.put(name, value) catch unreachable;
2151 ctx.write(name);
2152 ctx.write(": ");
2153 ctx.write(value);
2154 ctx.write("\r\n");
2155 }
2156 }
2157
2158 var body: ?[]u8 = null;
2159 if (random.uintAtMost(u8, 4) == 0) {
2160 ctx.write("\r\n"); // no body
2161 } else {
2162 body = t.randomString(random, aa, 8000);
2163 const cl = std.fmt.allocPrint(aa, "{d}", .{body.?.len}) catch unreachable;
2164 headers.put("content-length", cl) catch unreachable;
2165 ctx.write("content-length: ");
2166 ctx.write(cl);
2167 ctx.write("\r\n\r\n");
2168 ctx.write(body.?);
2169 }
2170
2171 var conn = ctx.conn;
2172 var fake_reader = ctx.fakeReader();
2173 const fr = &fake_reader.interface;
2174 while (true) {
2175 const done = try conn.req_state.parse(conn, fr);
2176 if (done) break;
2177 }
2178
2179 var request = Request.init(conn.req_arena.allocator(), conn);
2180
2181 // assert the headers
2182 var it = headers.iterator();
2183 while (it.next()) |entry| {
2184 try t.expectString(entry.value_ptr.*, request.header(entry.key_ptr.*).?);
2185 }
2186
2187 // assert the querystring
2188 var actualQuery = request.query() catch unreachable;
2189 it = query.iterator();
2190 while (it.next()) |entry| {
2191 try t.expectString(entry.value_ptr.*, actualQuery.get(entry.key_ptr.*).?);
2192 }
2193
2194 const actual_body = request.body();
2195 if (body) |b| {
2196 try t.expectString(b, actual_body.?);
2197 } else {
2198 try t.expectEqual(null, actual_body);
2199 }
2200 }
2201 }
2202}
2203
2204test "request: cookie" {
2205 defer t.reset();
2206 {
2207 const r = try testParse("PUT / HTTP/1.0\r\n\r\n", .{});
2208 const cookies = r.cookies();
2209 try t.expectEqual(null, cookies.get(""));
2210 try t.expectEqual(null, cookies.get("auth"));
2211 }
2212
2213 {
2214 const r = try testParse("PUT / HTTP/1.0\r\nCookie: \r\n\r\n", .{});
2215 const cookies = r.cookies();
2216 try t.expectEqual(null, cookies.get(""));
2217 try t.expectEqual(null, cookies.get("auth"));
2218 }
2219
2220 {
2221 const r = try testParse("PUT / HTTP/1.0\r\nCookie: auth=hello\r\n\r\n", .{});
2222 const cookies = r.cookies();
2223 try t.expectEqual(null, cookies.get(""));
2224 try t.expectString("hello", cookies.get("auth").?);
2225 try t.expectEqual(null, cookies.get("world"));
2226 }
2227
2228 {
2229 const r = try testParse("PUT / HTTP/1.0\r\nCookie: Name=leto; power=9000\r\n\r\n", .{});
2230 const cookies = r.cookies();
2231 try t.expectEqual(null, cookies.get(""));
2232 try t.expectEqual(null, cookies.get("name"));
2233 try t.expectString("leto", cookies.get("Name").?);
2234 try t.expectString("9000", cookies.get("power").?);
2235 }
2236
2237 {
2238 const r = try testParse("PUT / HTTP/1.0\r\nCookie: Name=Ghanima;id=Name\r\n\r\n", .{});
2239 const cookies = r.cookies();
2240 try t.expectEqual(null, cookies.get(""));
2241 try t.expectEqual(null, cookies.get("name"));
2242 try t.expectString("Ghanima", cookies.get("Name").?);
2243 try t.expectString("Name", cookies.get("id").?);
2244 }
2245}
2246
2247fn testParse(input: []const u8, config: Config) !Request {
2248 var ctx = t.Context.allocInit(t.arena.allocator(), .{ .request = config });
2249 ctx.write(input);
2250 var reader = ctx.stream.reader(t.io, &.{});
2251 const r = &reader.interface;
2252 while (true) {
2253 const done = try ctx.conn.req_state.parse(ctx.conn, r);
2254 if (done) break;
2255 }
2256 return Request.init(ctx.conn.req_arena.allocator(), ctx.conn);
2257}
2258
2259fn expectParseError(expected: anyerror, input: []const u8, config: Config) !void {
2260 var ctx = t.Context.init(.{ .request = config });
2261 defer ctx.deinit();
2262
2263 var reader = ctx.stream.reader(t.io, &.{});
2264 const r = &reader.interface;
2265 ctx.write(input);
2266 try t.expectError(expected, ctx.conn.req_state.parse(ctx.conn, r));
2267}
2268
2269fn randomMethod(random: std.Random) []const u8 {
2270 return switch (random.uintAtMost(usize, 6)) {
2271 0 => "GET",
2272 1 => "PUT",
2273 2 => "POST",
2274 3 => "PATCH",
2275 4 => "DELETE",
2276 5 => "OPTIONS",
2277 6 => "HEAD",
2278 else => unreachable,
2279 };
2280}
2281
2282fn buildRequest(header: []const []const u8, body: []const []const u8) []const u8 {
2283 var header_len: usize = 0;
2284 for (header) |h| {
2285 header_len += h.len;
2286 }
2287
2288 var body_len: usize = 0;
2289 for (body) |b| {
2290 body_len += b.len;
2291 }
2292
2293 var aw: std.Io.Writer.Allocating = .init(t.arena.allocator());
2294 // 100 for the Content-Length that we'll add and all the \r\n
2295 aw.ensureTotalCapacity(header_len + body_len + 100) catch unreachable;
2296
2297 for (header) |h| {
2298 aw.writer.writeAll(h) catch unreachable;
2299 aw.writer.writeAll("\r\n") catch unreachable;
2300 }
2301 aw.writer.writeAll("Content-Length: ") catch unreachable;
2302 aw.writer.print("{d}", .{body_len}) catch unreachable;
2303 aw.writer.writeAll("\r\n\r\n") catch unreachable;
2304
2305 for (body) |b| {
2306 aw.writer.writeAll(b) catch unreachable;
2307 }
2308
2309 return aw.written();
2310}