1const std = @import("std");
2const builtin = @import("builtin");
3const net = @import("net");
4const url = @import("url");
5const nio = @import("nio");
6const extras = @import("extras");
7const nfs = @import("nfs");
8
9pub const Method = enum {
10 GET,
11 HEAD,
12 POST,
13 PUT,
14 DELETE,
15 CONNECT,
16 OPTIONS,
17 TRACE,
18 PATCH,
19 QUERY,
20};
21
22pub const Status = enum(u10) {
23 invalid = 0,
24
25 @"continue" = 100, // RFC7231, Section 6.2.1
26 switching_protocols = 101, // RFC7231, Section 6.2.2
27 processing = 102, // RFC2518
28 early_hints = 103, // RFC8297
29
30 ok = 200, // RFC7231, Section 6.3.1
31 created = 201, // RFC7231, Section 6.3.2
32 accepted = 202, // RFC7231, Section 6.3.3
33 non_authoritative_info = 203, // RFC7231, Section 6.3.4
34 no_content = 204, // RFC7231, Section 6.3.5
35 reset_content = 205, // RFC7231, Section 6.3.6
36 partial_content = 206, // RFC7233, Section 4.1
37 multi_status = 207, // RFC4918
38 already_reported = 208, // RFC5842
39 im_used = 226, // RFC3229
40
41 multiple_choice = 300, // RFC7231, Section 6.4.1
42 moved_permanently = 301, // RFC7231, Section 6.4.2
43 found = 302, // RFC7231, Section 6.4.3
44 see_other = 303, // RFC7231, Section 6.4.4
45 not_modified = 304, // RFC7232, Section 4.1
46 use_proxy = 305, // RFC7231, Section 6.4.5
47 temporary_redirect = 307, // RFC7231, Section 6.4.7
48 permanent_redirect = 308, // RFC7538
49
50 bad_request = 400, // RFC7231, Section 6.5.1
51 unauthorized = 401, // RFC7235, Section 3.1
52 payment_required = 402, // RFC7231, Section 6.5.2
53 forbidden = 403, // RFC7231, Section 6.5.3
54 not_found = 404, // RFC7231, Section 6.5.4
55 method_not_allowed = 405, // RFC7231, Section 6.5.5
56 not_acceptable = 406, // RFC7231, Section 6.5.6
57 proxy_auth_required = 407, // RFC7235, Section 3.2
58 request_timeout = 408, // RFC7231, Section 6.5.7
59 conflict = 409, // RFC7231, Section 6.5.8
60 gone = 410, // RFC7231, Section 6.5.9
61 length_required = 411, // RFC7231, Section 6.5.10
62 precondition_failed = 412, // RFC7232, Section 4.2][RFC8144, Section 3.2
63 payload_too_large = 413, // RFC7231, Section 6.5.11
64 uri_too_long = 414, // RFC7231, Section 6.5.12
65 unsupported_media_type = 415, // RFC7231, Section 6.5.13][RFC7694, Section 3
66 range_not_satisfiable = 416, // RFC7233, Section 4.4
67 expectation_failed = 417, // RFC7231, Section 6.5.14
68 teapot = 418, // RFC 7168, 2.3.3
69 misdirected_request = 421, // RFC7540, Section 9.1.2
70 unprocessable_entity = 422, // RFC4918
71 locked = 423, // RFC4918
72 failed_dependency = 424, // RFC4918
73 too_early = 425, // RFC8470
74 upgrade_required = 426, // RFC7231, Section 6.5.15
75 precondition_required = 428, // RFC6585
76 too_many_requests = 429, // RFC6585
77 request_header_fields_too_large = 431, // RFC6585
78 unavailable_for_legal_reasons = 451, // RFC7725
79
80 internal_server_error = 500, // RFC7231, Section 6.6.1
81 not_implemented = 501, // RFC7231, Section 6.6.2
82 bad_gateway = 502, // RFC7231, Section 6.6.3
83 service_unavailable = 503, // RFC7231, Section 6.6.4
84 gateway_timeout = 504, // RFC7231, Section 6.6.5
85 http_version_not_supported = 505, // RFC7231, Section 6.6.6
86 variant_also_negotiates = 506, // RFC2295
87 insufficient_storage = 507, // RFC4918
88 loop_detected = 508, // RFC5842
89 not_extended = 510, // RFC2774
90 network_authentication_required = 511, // RFC6585
91
92 pub fn phrase(self: Status) []const u8 {
93 return switch (self) {
94 .invalid => unreachable,
95
96 // 1xx statuses
97 .@"continue" => "Continue",
98 .switching_protocols => "Switching Protocols",
99 .processing => "Processing",
100 .early_hints => "Early Hints",
101
102 // 2xx statuses
103 .ok => "OK",
104 .created => "Created",
105 .accepted => "Accepted",
106 .non_authoritative_info => "Non-Authoritative Information",
107 .no_content => "No Content",
108 .reset_content => "Reset Content",
109 .partial_content => "Partial Content",
110 .multi_status => "Multi-Status",
111 .already_reported => "Already Reported",
112 .im_used => "IM Used",
113
114 // 3xx statuses
115 .multiple_choice => "Multiple Choice",
116 .moved_permanently => "Moved Permanently",
117 .found => "Found",
118 .see_other => "See Other",
119 .not_modified => "Not Modified",
120 .use_proxy => "Use Proxy",
121 .temporary_redirect => "Temporary Redirect",
122 .permanent_redirect => "Permanent Redirect",
123
124 // 4xx statuses
125 .bad_request => "Bad Request",
126 .unauthorized => "Unauthorized",
127 .payment_required => "Payment Required",
128 .forbidden => "Forbidden",
129 .not_found => "Not Found",
130 .method_not_allowed => "Method Not Allowed",
131 .not_acceptable => "Not Acceptable",
132 .proxy_auth_required => "Proxy Authentication Required",
133 .request_timeout => "Request Timeout",
134 .conflict => "Conflict",
135 .gone => "Gone",
136 .length_required => "Length Required",
137 .precondition_failed => "Precondition Failed",
138 .payload_too_large => "Payload Too Large",
139 .uri_too_long => "URI Too Long",
140 .unsupported_media_type => "Unsupported Media Type",
141 .range_not_satisfiable => "Range Not Satisfiable",
142 .expectation_failed => "Expectation Failed",
143 .teapot => "I'm a teapot",
144 .misdirected_request => "Misdirected Request",
145 .unprocessable_entity => "Unprocessable Entity",
146 .locked => "Locked",
147 .failed_dependency => "Failed Dependency",
148 .too_early => "Too Early",
149 .upgrade_required => "Upgrade Required",
150 .precondition_required => "Precondition Required",
151 .too_many_requests => "Too Many Requests",
152 .request_header_fields_too_large => "Request Header Fields Too Large",
153 .unavailable_for_legal_reasons => "Unavailable For Legal Reasons",
154
155 // 5xx statuses
156 .internal_server_error => "Internal Server Error",
157 .not_implemented => "Not Implemented",
158 .bad_gateway => "Bad Gateway",
159 .service_unavailable => "Service Unavailable",
160 .gateway_timeout => "Gateway Timeout",
161 .http_version_not_supported => "HTTP Version Not Supported",
162 .variant_also_negotiates => "Variant Also Negotiates",
163 .insufficient_storage => "Insufficient Storage",
164 .loop_detected => "Loop Detected",
165 .not_extended => "Not Extended",
166 .network_authentication_required => "Network Authentication Required",
167 };
168 }
169
170 pub fn digits(self: Status) [3]u8 {
171 var result: [3]u8 = undefined;
172 result[0] = @intCast((@intFromEnum(self) / 100) + '0');
173 result[1] = @intCast((@intFromEnum(self) / 10 % 10) + '0');
174 result[2] = @intCast((@intFromEnum(self) % 10) + '0');
175 return result;
176 }
177};
178
179pub fn open(allocator: std.mem.Allocator, method: Method, input: []const u8) !ClientRequest {
180 const u = try url.URL.parse(allocator, input, null);
181 defer allocator.free(u.href);
182
183 const addr: net.Address = try .fromUrl(&u, allocator);
184
185 const conn = try addr.tcpConnect();
186 errdefer conn.close();
187
188 var bufw: nio.BufferedWriter(4096, net.Stream) = .init(conn);
189
190 try bufw.writeAll(@tagName(method));
191 try bufw.writeAll(" ");
192 try bufw.writeAll(u.pathname);
193 try bufw.writeAll(" ");
194 try bufw.writeAll("HTTP/1.1");
195 try bufw.writeAll("\r\n");
196
197 try bufw.writeAll("Host: ");
198 try bufw.writeAll(u.hostname);
199 try bufw.writeAll("\r\n");
200
201 try bufw.writeAll("Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n");
202
203 try bufw.writeAll("Connection: close\r\n");
204
205 return .{
206 .allocator = allocator,
207 .stream = conn,
208 .writer = bufw,
209 .reader = .init(conn),
210 .status = .invalid,
211 .headers = .init(allocator),
212 };
213}
214
215pub const ClientRequest = struct {
216 allocator: std.mem.Allocator,
217 stream: net.Stream,
218 writer: nio.BufferedWriter(4096, net.Stream),
219 reader: nio.BufferedReader(4096, net.Stream),
220 status: Status,
221 headers: HeadersMap,
222
223 pub fn close(req: *ClientRequest) void {
224 req.stream.close();
225 req.headers.deinit();
226 }
227
228 pub fn writeHeader(req: *ClientRequest, name: []const u8, value: []const u8) !void {
229 try req.writer.writeAll(name);
230 try req.writer.writeAll(": ");
231 try req.writer.writeAll(value);
232 try req.writer.writeAll("\r\n");
233 }
234
235 pub fn writeUA(req: *ClientRequest) !void {
236 return req.writeHeader(
237 "User-Agent",
238 (if (builtin.is_test) "WIP " else "") ++ "https://github.com/nektro/zig-net-http",
239 );
240 }
241
242 pub fn send(req: *ClientRequest) !void {
243 try req.writer.writeAll("\r\n");
244 try req.writer.flush();
245
246 // HTTP/1.1 200 OK
247 if (!std.mem.eql(u8, &try req.readArray(9), "HTTP/1.1 ")) return error.Bad;
248 const status_int = extras.parseDigits(u16, &try req.readArray(3), 10) catch return error.Bad;
249 const status = std.enums.fromInt(Status, status_int) orelse return error.Bad;
250 if (!std.mem.eql(u8, &try req.readArray(1), " ")) return error.Bad;
251 var phrase_buf: [64]u8 = undefined;
252 _ = try req.readUntilDelimitersBuf(&phrase_buf, "\r\n");
253 req.status = status;
254
255 var headers_list = req.headers.data.list.toManaged(req.allocator);
256 defer req.headers.data.list = headers_list.moveToUnmanaged();
257 while (true) {
258 const header_line = try req.readUntilDelimitersArrayList(&headers_list, "\r\n", 1024);
259 if (header_line.len == 0) break;
260 const colon_pos = std.mem.indexOfScalar(u8, header_line, ':') orelse return error.Bad;
261 const name = header_line[0..colon_pos];
262 if (!extras.matchesAll(u8, name, std.ascii.isAscii)) return error.Bad;
263 for (name) |*c| c.* = std.ascii.toLower(c.*);
264 if (header_line.len == colon_pos or header_line[colon_pos + 1] != ' ') return error.Bad;
265 const value = header_line[colon_pos + 2 ..];
266 try req.headers.data.lengths.appendSlice(req.allocator, &.{ name.len, 2, value.len, 2 });
267 }
268 }
269
270 const R = nio.Readable(@This(), ._var);
271 pub const readAll = R.readAll;
272 pub const readAtLeast = R.readAtLeast;
273 pub const readNoEof = R.readNoEof;
274 pub const readAllAlloc = R.readAllAlloc;
275 pub const readArray = R.readArray;
276 pub const readByte = R.readByte;
277 pub const readUntilDelimiterArrayList = R.readUntilDelimiterArrayList;
278 pub const readUntilDelimiterAlloc = R.readUntilDelimiterAlloc;
279 pub const readUntilDelimiterOrEofAlloc = R.readUntilDelimiterOrEofAlloc;
280 pub const readUntilDelimitersBuf = R.readUntilDelimitersBuf;
281 pub const readUntilDelimitersArrayList = R.readUntilDelimitersArrayList;
282 pub const readAlloc = R.readAlloc;
283 pub const readInt = R.readInt;
284 pub const readUntilDelimitersAlloc = R.readUntilDelimitersAlloc;
285
286 pub const ReadError = net.Stream.ReadError;
287 pub fn read(req: *ClientRequest, buffer: []u8) ReadError!usize {
288 return req.reader.read(buffer);
289 }
290 pub fn anyReadable(self: *ClientRequest) nio.AnyReadable {
291 const S = struct {
292 fn read(s: *allowzero anyopaque, buffer: []u8) anyerror!usize {
293 const req: *ClientRequest = @ptrCast(@alignCast(s));
294 return req.read(buffer);
295 }
296 };
297 return .{
298 .vtable = &.{ .read = S.read },
299 .state = @ptrCast(self),
300 };
301 }
302};
303
304pub const HeadersMap = struct {
305 data: extras.ManyArrayList(u8),
306
307 pub fn init(allocator: std.mem.Allocator) HeadersMap {
308 return .{
309 .data = .init(allocator),
310 };
311 }
312
313 pub fn deinit(map: *HeadersMap) void {
314 map.data.deinit();
315 }
316
317 fn findIndex(map: *const HeadersMap, n: []const u8) ?usize {
318 for (n) |c| switch (c) {
319 'a'...'z', '-' => {},
320 '0'...'9' => {},
321 else => unreachable,
322 };
323 for (0..map.count()) |i| {
324 if (std.mem.eql(u8, map.name(i), n)) {
325 return i;
326 }
327 }
328 return null;
329 }
330
331 pub fn append(map: *HeadersMap, n: []const u8, v: []const u8) !void {
332 if (!std.mem.eql(u8, n, "set-cookie")) if (map.findIndex(n)) |i| {
333 try map.data.appendSlice(i * 4 + 2, ", ");
334 try map.data.appendSlice(i * 4 + 2, v);
335 return;
336 };
337 try map.data.appendSlice(try map.data.add(), n);
338 try map.data.appendSlice(try map.data.add(), ": ");
339 try map.data.appendSlice(try map.data.add(), v);
340 try map.data.appendSlice(try map.data.add(), "\r\n");
341 }
342
343 pub fn set(map: *HeadersMap, n: []const u8, v: []const u8) !void {
344 if (map.findIndex(n)) |i| {
345 try map.data.set(i * 4 + 2, v);
346 return;
347 }
348 try map.data.appendSlice(try map.data.add(), n);
349 try map.data.appendSlice(try map.data.add(), ": ");
350 try map.data.appendSlice(try map.data.add(), v);
351 try map.data.appendSlice(try map.data.add(), "\r\n");
352 }
353
354 pub fn remove(map: *HeadersMap, n: []const u8) void {
355 const i = map.findIndex(n) orelse return;
356 map.data.remove(i * 4);
357 map.data.remove(i * 4);
358 map.data.remove(i * 4);
359 map.data.remove(i * 4);
360 }
361
362 pub fn count(map: *const HeadersMap) usize {
363 return map.data.lengths.items.len / 4;
364 }
365
366 pub fn name(map: *const HeadersMap, idx: usize) []const u8 {
367 return map.data.items(idx * 4 + 0);
368 }
369
370 pub fn value(map: *const HeadersMap, idx: usize) []const u8 {
371 return map.data.items(idx * 4 + 2);
372 }
373
374 pub fn find(map: *const HeadersMap, needle: []const u8) ?[]const u8 {
375 for (0..map.count()) |i| {
376 if (std.mem.eql(u8, map.name(i), needle)) {
377 return map.value(i);
378 }
379 }
380 return null;
381 }
382};
383
384pub const Server = struct {
385 conn: net.Server.Connection,
386 reader: nio.BufferedReader(4096, net.Stream),
387 writer: nio.BufferedWriter(4096, net.Stream),
388 state: enum {
389 ready,
390 receiving_head,
391 received_head,
392 },
393
394 pub fn init(conn: net.Server.Connection) Server {
395 return .{
396 .conn = conn,
397 .reader = .init(conn.stream),
398 .writer = .init(conn.stream),
399 .state = .ready,
400 };
401 }
402
403 pub fn receiveHead(server: *Server, allocator: std.mem.Allocator) !ServerRequest {
404 std.debug.assert(server.state == .ready);
405 server.state = .receiving_head;
406 var scratch_buffer: [8192]u8 = undefined;
407
408 const method_s = try server.reader.readUntilDelimitersBuf(&scratch_buffer, " ");
409 const method = std.meta.stringToEnum(Method, method_s) orelse return error.InvalidRequest;
410
411 const target_s = try server.reader.readUntilDelimitersBuf(&scratch_buffer, " ");
412 const target_url_root: url.URL = .{
413 .href = "file:///",
414 .protocol = "file:",
415 .username = "",
416 .password = "",
417 .hostname = "",
418 .hostname_kind = .unset,
419 .port = "",
420 .host = "",
421 .pathname = "/",
422 .search = "",
423 .hash = "",
424 .has_opaque_path = false,
425 };
426 const target_url = try url.URL.parseBasic(allocator, target_s, &target_url_root, null);
427 errdefer allocator.free(target_url.href);
428
429 const version_s = try server.reader.readUntilDelimitersBuf(&scratch_buffer, "\r\n");
430 if (version_s.len != 8) return error.InvalidRequest;
431 if (std.mem.bytesToValue(u64, version_s[0..8]) != comptime std.mem.bytesToValue(u64, "HTTP/1.1")) return error.InvalidRequest;
432
433 var headers: HeadersMap = .init(allocator);
434 errdefer headers.deinit();
435 try headers.data.list.ensureUnusedCapacity(allocator, 512);
436 try headers.data.lengths.ensureUnusedCapacity(allocator, 40);
437
438 while (true) {
439 const line = try server.reader.readUntilDelimitersBuf(&scratch_buffer, "\r\n");
440 if (line.len == 0) break;
441 if (headers.count() == 128) return error.InvalidRequest;
442 const name_end = std.mem.indexOfScalar(u8, line, ':') orelse return error.InvalidRequest;
443 const name = line[0..name_end];
444 for (name) |*c| {
445 switch (c.*) {
446 'A'...'Z' => {},
447 'a'...'z' => {},
448 '0'...'9' => {},
449 '-' => {},
450 else => return error.InvalidRequest,
451 }
452 switch (c.*) {
453 'A'...'Z' => c.* = c.* - 'A' + 'a',
454 else => {},
455 }
456 }
457 const value = std.mem.trim(u8, line[name_end + 1 ..], " ");
458 try headers.append(name, value);
459 }
460 server.state = .received_head;
461
462 return .{
463 .server = server,
464 .method = method,
465 .target = target_url,
466 .headers = headers,
467 };
468 }
469};
470
471pub const ServerRequest = struct {
472 server: *Server,
473 method: Method,
474 target: url.URL,
475 headers: HeadersMap,
476
477 pub fn deinit(req: *ServerRequest, allocator: std.mem.Allocator) void {
478 allocator.free(req.target.href);
479 req.headers.deinit();
480 }
481
482 pub fn readAllAlloc(req: *ServerRequest, allocator: std.mem.Allocator, max_size: usize) ![]u8 {
483 if (req.headers.find("content-length")) |s| {
484 const content_length = try extras.parseDigits(u64, s, 10);
485 if (content_length > max_size) return error.StreamTooLong;
486 var list: std.ArrayListUnmanaged(u8) = .empty;
487 try list.ensureUnusedCapacity(allocator, content_length);
488 var total: usize = 0;
489 while (total < content_length) {
490 const len = try req.server.reader.read(list.items.ptr[total..list.capacity]);
491 if (len == 0) break;
492 total += len;
493 list.items.len += len;
494 }
495 return list.toOwnedSlice(allocator);
496 }
497 if (req.headers.find("transfer-encoding")) |s| {
498 if (std.mem.eql(u8, s, "chunked")) {
499 return error.TEChunked;
500 }
501 return error.TE;
502 }
503 return "";
504 }
505
506 pub fn pipeTo(req: *ServerRequest, writable: anytype, max_size: ?usize) !void {
507 if (req.headers.find("content-length")) |s| {
508 const content_length = try extras.parseDigits(u64, s, 10);
509 if (max_size) |max| if (content_length > max) return error.StreamTooLong;
510 var total: usize = 0;
511 var scratch_buffer: [4096]u8 = undefined;
512 while (total < content_length) {
513 const len = try req.server.reader.read(&scratch_buffer);
514 if (len == 0) break;
515 total += len;
516 try writable.writeAll(scratch_buffer[0..len]);
517 }
518 return;
519 }
520 if (req.headers.find("transfer-encoding")) |s| {
521 if (std.mem.eql(u8, s, "chunked")) {
522 return error.TEChunked;
523 }
524 return error.TE;
525 }
526 }
527
528 pub fn respondFull(req: *ServerRequest, status: Status, headers: *HeadersMap, body: []const u8) !void {
529 try req.server.writer.writevAll(&.{ "HTTP/1.1", " ", &status.digits(), " ", status.phrase(), "\r\n" });
530
531 try headers.set("connection", "close");
532 headers.remove("content-length");
533 try req.server.writer.writeAll(headers.data.list.items);
534 try req.server.writer.writeAll("content-length: ");
535 try req.server.writer.writeIntPretty(body.len, 10, .lower);
536 try req.server.writer.writeAll("\r\n");
537
538 try req.server.writer.writeAll("\r\n");
539 try req.server.writer.writeAll(body);
540 try req.server.writer.flush();
541 }
542
543 pub fn respondStreaming(req: *ServerRequest, status: Status, headers: *HeadersMap, body_length: ?u64) !void {
544 try req.server.writer.writevAll(&.{ "HTTP/1.1", " ", &status.digits(), " ", status.phrase(), "\r\n" });
545
546 headers.remove("connection");
547 headers.remove("content-length");
548 headers.remove("transfer-encoding");
549 try req.server.writer.writeAll(headers.data.list.items);
550 try req.server.writer.writeAll("connection: close\r\n");
551 if (body_length) |len| {
552 try req.server.writer.writeAll("content-length: ");
553 try req.server.writer.writeIntPretty(len, 10, .lower);
554 try req.server.writer.writeAll("\r\n");
555 } else {
556 try req.server.writer.writeAll("transfer-encoding: chunked\r\n");
557 }
558
559 try req.server.writer.writeAll("\r\n");
560 try req.server.writer.flush();
561 }
562
563 pub fn sendfile(req: *ServerRequest, file: nfs.File, offset: net.off_t, count: ?usize) !void {
564 return req.server.conn.stream.sendfile(file, offset, count);
565 }
566};