From 687fd6ef9d940a9c9655715f864f08ed93a4fcda Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Wed, 8 Jun 2022 23:37:09 -0700 Subject: [PATCH] Start work on new http pkg --- src/http/lib.zig | 20 ++ src/http/response_stream.zig | 376 ++++++++++++++++++++++++++++ src/main/http.zig | 457 +++++++++++++++++++++++++++++------ src/main/main.zig | 67 +---- 4 files changed, 788 insertions(+), 132 deletions(-) create mode 100644 src/http/lib.zig create mode 100644 src/http/response_stream.zig diff --git a/src/http/lib.zig b/src/http/lib.zig new file mode 100644 index 0000000..eeebf35 --- /dev/null +++ b/src/http/lib.zig @@ -0,0 +1,20 @@ +const std = @import("std"); +const ciutf8 = @import("util").ciutf8; + +pub const Status = std.http.Status; +pub const Method = std.http.Method; +pub const ResponseStream = @import("./response_stream.zig").ResponseStream; + +pub const Headers = std.HashMap([]const u8, []const u8, struct { + pub fn eql(_: @This(), a: []const u8, b: []const u8) bool { + return ciutf8.eql(a, b); + } + + pub fn hash(_: @This(), str: []const u8) u64 { + return ciutf8.hash(str); + } +}, std.hash_map.default_max_load_percentage); + +test { + _ = ResponseStream; +} diff --git a/src/http/response_stream.zig b/src/http/response_stream.zig new file mode 100644 index 0000000..e5fde22 --- /dev/null +++ b/src/http/response_stream.zig @@ -0,0 +1,376 @@ +const std = @import("std"); +const http = @import("./lib.zig"); + +const Status = http.Status; +const Headers = http.Headers; + +const chunk_size = 16 * 1024; +pub fn openResponse( + alloc: std.mem.Allocator, + writer: anytype, + headers: *const Headers, + status: Status, +) !ResponseStream(@TypeOf(writer)) { + const buf = try alloc.alloc(u8, chunk_size); + errdefer alloc.free(buf); + + try writeStatusLine(writer, status); + try writeHeaders(writer, headers); + + return ResponseStream(@TypeOf(writer)){ + .allocator = alloc, + .base_writer = writer, + .headers = headers, + .buffer = buf, + }; +} + +fn writeStatusLine(writer: anytype, status: Status) !void { + const status_text = status.phrase() orelse ""; + try writer.print("HTTP/1.1 {} {s}\r\n", .{ @enumToInt(status), status_text }); +} + +fn writeHeaders(writer: anytype, headers: *const Headers) !void { + var iter = headers.iterator(); + while (iter.next()) |header| { + for (header.value_ptr.*) |ch| { + if (ch == '\r' or ch == '\n') @panic("newlines not yet supported in headers"); + } + + try writer.print("{s}: {s}\r\n", .{ header.key_ptr.*, header.value_ptr.* }); + } +} + +fn writeChunk(writer: anytype, contents: []const u8) @TypeOf(writer).Error!void { + try writer.print("{x}\r\n", .{contents.len}); + try writer.writeAll(contents); + try writer.writeAll("\r\n"); +} + +fn writeLastChunk(writer: anytype) writer.Error!void { + try writer.writeAll("0\r\n"); +} + +pub fn ResponseStream(comptime BaseWriter: type) type { + return struct { + const Self = @This(); + const Error = BaseWriter.Error; + const Writer = std.io.Writer(*Self, Error, write); + + allocator: std.mem.Allocator, + base_writer: BaseWriter, + headers: *const Headers, + buffer: []u8, + buffer_pos: usize = 0, + chunked: bool = false, + + fn writeToBuffer(self: *Self, bytes: []const u8) void { + std.mem.copy(u8, self.buffer[self.buffer_pos..], bytes); + self.buffer_pos += bytes.len; + } + + fn startChunking(self: *Self) Error!void { + try self.base_writer.writeAll("Transfer-Encoding: chunked\r\n\r\n"); + self.chunked = true; + } + + fn flushChunk(self: *Self) Error!void { + try writeChunk(self.base_writer, self.buffer[0..self.buffer_pos]); + self.buffer_pos = 0; + } + + fn writeToChunks(self: *Self, bytes: []const u8) Error!void { + var cursor: usize = 0; + while (true) { + const remaining_in_chunk = self.buffer.len - self.buffer_pos; + const remaining_to_write = bytes.len - cursor; + if (remaining_to_write <= remaining_in_chunk) { + self.writeToBuffer(bytes[cursor..]); + return; + } + + std.debug.print("{}\n", .{cursor}); + self.writeToBuffer(bytes[cursor .. cursor + remaining_in_chunk]); + cursor += remaining_in_chunk; + try self.flushChunk(); + } + } + + fn write(self: *Self, bytes: []const u8) Error!usize { + if (!self.chunked and bytes.len > self.buffer.len - self.buffer_pos) { + try self.startChunking(); + } + + if (self.chunked) { + try self.writeToChunks(bytes); + } else { + self.writeToBuffer(bytes); + } + + return bytes.len; + } + + fn flushBodyUnchunked(self: *Self) Error!void { + if (self.buffer_pos != 0) { + try self.base_writer.print("Content-Length: {}\r\n", .{self.buffer_pos}); + } + + try self.base_writer.writeAll("\r\n"); + + if (self.buffer_pos != 0) { + try self.base_writer.writeAll(self.buffer[0..self.buffer_pos]); + } + self.buffer_pos = 0; + } + + pub fn writer(self: *Self) Writer { + return Writer{ .context = self }; + } + + pub fn finish(self: *Self) Error!void { + if (!self.chunked) { + try self.flushBodyUnchunked(); + } else { + if (self.buffer_pos != 0) { + try self.flushChunk(); + } + try self.base_writer.writeAll("0\r\n"); + } + } + + pub fn close(self: *const Self) void { + self.allocator.free(self.buffer); + } + }; +} + +test { + _ = _tests; +} +const _tests = struct { + fn toCrlf(comptime str: []const u8) []const u8 { + comptime { + var buf: [str.len * 2]u8 = undefined; + @setEvalBranchQuota(@as(u32, str.len * 2)); + + var len: usize = 0; + for (str) |ch| { + if (ch == '\n') { + buf[len] = '\r'; + len += 1; + } + + buf[len] = ch; + len += 1; + } + + return buf[0..len]; + } + } + + const test_buffer_size = chunk_size * 4; + test "ResponseStream no headers empty body" { + var buffer: [test_buffer_size]u8 = undefined; + var test_stream = std.io.fixedBufferStream(&buffer); + var headers = Headers.init(std.testing.allocator); + defer headers.deinit(); + + { + var stream = try openResponse( + std.testing.allocator, + test_stream.writer(), + &headers, + .ok, + ); + defer stream.close(); + + try stream.finish(); + } + + try std.testing.expectEqualStrings( + toCrlf( + \\HTTP/1.1 200 OK + \\ + \\ + ), + buffer[0..(try test_stream.getPos())], + ); + } + + test "ResponseStream empty body" { + var buffer: [test_buffer_size]u8 = undefined; + var test_stream = std.io.fixedBufferStream(&buffer); + var headers = Headers.init(std.testing.allocator); + defer headers.deinit(); + + try headers.put("Content-Type", "text/plain"); + + { + var stream = try openResponse( + std.testing.allocator, + test_stream.writer(), + &headers, + .ok, + ); + defer stream.close(); + + try stream.finish(); + } + + try std.testing.expectEqualStrings( + toCrlf( + \\HTTP/1.1 200 OK + \\Content-Type: text/plain + \\ + \\ + ), + buffer[0..(try test_stream.getPos())], + ); + } + + test "ResponseStream not 200 OK" { + var buffer: [test_buffer_size]u8 = undefined; + var test_stream = std.io.fixedBufferStream(&buffer); + var headers = Headers.init(std.testing.allocator); + defer headers.deinit(); + + try headers.put("Content-Type", "text/plain"); + + { + var stream = try openResponse( + std.testing.allocator, + test_stream.writer(), + &headers, + .not_found, + ); + defer stream.close(); + + try stream.finish(); + } + + try std.testing.expectEqualStrings( + toCrlf( + \\HTTP/1.1 404 Not Found + \\Content-Type: text/plain + \\ + \\ + ), + buffer[0..(try test_stream.getPos())], + ); + } + test "ResponseStream small body" { + var buffer: [test_buffer_size]u8 = undefined; + var test_stream = std.io.fixedBufferStream(&buffer); + var headers = Headers.init(std.testing.allocator); + defer headers.deinit(); + + try headers.put("Content-Type", "text/plain"); + + { + var stream = try openResponse( + std.testing.allocator, + test_stream.writer(), + &headers, + .ok, + ); + defer stream.close(); + + try stream.writer().writeAll("Index Page"); + + try stream.finish(); + } + + try std.testing.expectEqualStrings( + toCrlf( + \\HTTP/1.1 200 OK + \\Content-Type: text/plain + \\Content-Length: 10 + \\ + \\Index Page + ), + buffer[0..(try test_stream.getPos())], + ); + } + + test "ResponseStream large body" { + var buffer: [test_buffer_size]u8 = undefined; + var test_stream = std.io.fixedBufferStream(&buffer); + var headers = Headers.init(std.testing.allocator); + defer headers.deinit(); + + try headers.put("Content-Type", "text/plain"); + + { + var stream = try openResponse( + std.testing.allocator, + test_stream.writer(), + &headers, + .ok, + ); + defer stream.close(); + + try stream.writer().writeAll("quuz" ** 6000); + + try stream.finish(); + } + + // zig fmt: off + try std.testing.expectEqualStrings( + toCrlf( + \\HTTP/1.1 200 OK + \\Content-Type: text/plain + \\Transfer-Encoding: chunked + \\ + \\ + ++ "4000\n" + ++ "quuz" ** 4096 ++ "\n" + ++ "1dc0\n" + ++ "quuz" ** (1904) ++ "\n" + ++ "0\n" + ), + buffer[0..(try test_stream.getPos())], + ); + // zig fmt: on + } + + test "ResponseStream large body ending on chunk boundary" { + var buffer: [test_buffer_size]u8 = undefined; + var test_stream = std.io.fixedBufferStream(&buffer); + var headers = Headers.init(std.testing.allocator); + defer headers.deinit(); + + try headers.put("Content-Type", "text/plain"); + + { + var stream = try openResponse( + std.testing.allocator, + test_stream.writer(), + &headers, + .ok, + ); + defer stream.close(); + + try stream.writer().writeAll("quuz" ** (chunk_size / 2)); + + try stream.finish(); + } + + // zig fmt: off + try std.testing.expectEqualStrings( + toCrlf( + \\HTTP/1.1 200 OK + \\Content-Type: text/plain + \\Transfer-Encoding: chunked + \\ + \\ + ++ "4000\n" + ++ "quuz" ** 4096 ++ "\n" + ++ "4000\n" + ++ "quuz" ** 4096 ++ "\n" + ++ "0\n" + ), + buffer[0..(try test_stream.getPos())], + ); + // zig fmt: on + } +}; diff --git a/src/main/http.zig b/src/main/http.zig index 9afe67c..a2729bf 100644 --- a/src/main/http.zig +++ b/src/main/http.zig @@ -3,10 +3,11 @@ const root = @import("root"); const ciutf8 = @import("./util.zig").ciutf8; const Reader = std.net.Stream.Reader; -const Writer = std.net.Stream.Writer; +//const Writer = std.net.Stream.Writer; const Status = std.http.Status; const Method = std.http.Method; +const Connection = std.net.StreamServer.Connection; pub const Handler = fn (*Context) anyerror!Response; const HeaderMap = std.HashMap([]const u8, []const u8, struct { @@ -18,18 +19,19 @@ const HeaderMap = std.HashMap([]const u8, []const u8, struct { return ciutf8.hash(str); } }, std.hash_map.default_max_load_percentage); +pub const Headers = HeaderMap; -fn handleBadRequest(writer: Writer) !void { +fn handleBadRequest(writer: std.net.Stream.Writer) !void { std.log.info("400 Bad Request", .{}); try writer.writeAll("HTTP/1.1 400 Bad Request"); } -fn handleNotImplemented(writer: Writer) !void { +fn handleNotImplemented(writer: std.net.Stream.Writer) !void { std.log.info("501", .{}); try writer.writeAll("HTTP/1.1 501 Not Implemented"); } -fn handleInternalError(writer: Writer) !void { +fn handleInternalError(writer: std.net.Stream.Writer) !void { std.log.info("500", .{}); try writer.writeAll("HTTP/1.1 500 Internal Server Error"); } @@ -117,27 +119,25 @@ fn parseHeaders(allocator: std.mem.Allocator, reader: Reader) !HeaderMap { return map; } -pub fn handleConnection(conn: std.net.StreamServer.Connection, handler: Handler) void { +const ConnectionId = u64; +var next_connection_id = std.atomic.Atomic(ConnectionId).init(1); +pub fn handleConnection( + base_alloc: std.mem.Allocator, + conn: std.net.StreamServer.Connection, + handler: Handler, +) void { + const conn_id = next_connection_id.fetchAdd(1, .SeqCst); defer conn.stream.close(); - const reader = conn.stream.reader(); - const writer = conn.stream.writer(); + std.log.debug("New connection conn={}", .{conn_id}); - handleRequest(reader, writer, handler) catch |err| std.log.err("unhandled error processing connection: {}", .{err}); + _ = base_alloc; + handleRequest(conn.stream.reader(), conn.stream.writer(), handler) catch |err| std.log.err("conn={}; Unhandled error processing connection {}. Closing", .{ conn_id, err }); + std.log.debug("Terminating connection conn={}", .{conn_id}); } -fn handleRequest(reader: Reader, writer: Writer, handler: Handler) !void { - handleHttpRequest(reader, writer, handler) catch |err| switch (err) { - error.BadRequest, error.UnknownProtocol => try handleBadRequest(writer), - error.MethodNotImplemented, error.HttpVersionNotSupported => try handleNotImplemented(writer), - else => { - std.log.err("unknown error handling request: {}", .{err}); - try handleInternalError(writer); - }, - }; -} - -fn handleHttpRequest(reader: Reader, writer: Writer, handler: Handler) anyerror!void { +fn handleRequest(reader: Reader, writer: std.net.Stream.Writer, handler: Handler) anyerror!void { const method = try parseHttpMethod(reader); + std.log.debug("Request recieved", .{}); var header_buf: [1 << 16]u8 = undefined; var fba = std.heap.FixedBufferAllocator.init(&header_buf); @@ -186,80 +186,393 @@ fn handleHttpRequest(reader: Reader, writer: Writer, handler: Handler) anyerror! _ = ctx; - //try handler(&ctx); + const response = try handler(&ctx); + const status_text = response.status.phrase() orelse ""; + try writer.print("HTTP/1.1 {} {s}\r\n", .{ @enumToInt(response.status), status_text }); + + var iter = response.headers.iterator(); + while (iter.next()) |it| { + try writer.print("{s}: {s}\r\n", .{ it.key_ptr.*, it.value_ptr.* }); + } else try writer.writeAll("\r\n"); + + if (response.body) |body| { + try writer.writeAll(body); + } _ = handler; _ = writer; _ = path; } -pub const Context = struct { - request: Request2, - allocator: std.mem.Allocator, -}; +pub const Context = struct {}; -pub const Response = struct { +const chunk_size = 16 * 1024; +pub fn openResponse( + alloc: std.mem.Allocator, + writer: anytype, + headers: *const HeaderMap, status: Status, - headers: HeaderMap, - body: ?[]const u8 = null, -}; +) !ResponseStream(@TypeOf(writer)) { + const buf = try alloc.alloc(u8, chunk_size); + errdefer alloc.free(buf); -pub const Request2 = struct { - method: Method, - path: []const u8, - headers: HeaderMap, - body: ?[]const u8 = null, -}; + try writeStatusLine(writer, status); + try writeHeaders(writer, headers); -const ResponseOld = struct { - headers: HeaderMap, - writer: Writer, + return ResponseStream(@TypeOf(writer)){ + .allocator = alloc, + .base_writer = writer, + .headers = headers, + .buffer = buf, + }; +} - fn writeHeaders(self: *Response) !void { - var iter = self.headers.iterator(); - var it = iter.next(); - while (it != null) : (it = iter.next()) { - try self.writer.print("{s}: {s}\r\n", .{ it.?.key_ptr.*, it.?.value_ptr.* }); +fn writeStatusLine(writer: anytype, status: Status) !void { + const status_text = status.phrase() orelse ""; + try writer.print("HTTP/1.1 {} {s}\r\n", .{ @enumToInt(status), status_text }); +} + +fn writeHeaders(writer: anytype, headers: *const HeaderMap) !void { + var iter = headers.iterator(); + while (iter.next()) |header| { + for (header.value_ptr.*) |ch| { + if (ch == '\r' or ch == '\n') @panic("newlines not yet supported in headers"); } + + try writer.print("{s}: {s}\r\n", .{ header.key_ptr.*, header.value_ptr.* }); } +} - fn statusText(status: u16) []const u8 { - return switch (status) { - 200 => "OK", - 204 => "No Content", - 404 => "Not Found", - else => "", - }; - } +fn writeChunk(writer: anytype, contents: []const u8) @TypeOf(writer).Error!void { + try writer.print("{x}\r\n", .{contents.len}); + try writer.writeAll(contents); + try writer.writeAll("\r\n"); +} - fn openInternal(self: *Response, status: u16) !void { - try self.writer.print("HTTP/1.1 {} {s}\r\n", .{ status, statusText(status) }); - try self.writeHeaders(); - try self.writer.writeAll("Connection: close\r\n"); // TODO - } +fn writeLastChunk(writer: anytype) writer.Error!void { + try writer.writeAll("0\r\n"); +} - pub fn open(self: *Response, status: u16) !Writer { - try self.openInternal(status); - try self.writer.writeAll("\r\n"); +fn ResponseStream(comptime BaseWriter: type) type { + return struct { + const Self = @This(); + const Error = BaseWriter.Error; + const Writer = std.io.Writer(*Self, Error, write); - return self.writer; - } + allocator: std.mem.Allocator, + base_writer: BaseWriter, + headers: *const HeaderMap, + buffer: []u8, + buffer_pos: usize = 0, + chunked: bool = false, - pub fn write(self: *Response, status: u16, body: []const u8) !void { - try self.openInternal(status); - if (body.len != 0) { - try self.writer.print("Content-Length: {}\r\n", .{body.len}); - if (self.headers.get("Content-Type") == null) { - try self.writer.writeAll("Content-Type: application/octet-stream\r\n"); + fn writeToBuffer(self: *Self, bytes: []const u8) void { + std.mem.copy(u8, self.buffer[self.buffer_pos..], bytes); + self.buffer_pos += bytes.len; + } + + fn startChunking(self: *Self) Error!void { + try self.base_writer.writeAll("Transfer-Encoding: chunked\r\n\r\n"); + self.chunked = true; + } + + fn flushChunk(self: *Self) Error!void { + try writeChunk(self.base_writer, self.buffer[0..self.buffer_pos]); + self.buffer_pos = 0; + } + + fn writeToChunks(self: *Self, bytes: []const u8) Error!void { + var cursor: usize = 0; + while (true) { + const remaining_in_chunk = self.buffer.len - self.buffer_pos; + const remaining_to_write = bytes.len - cursor; + if (remaining_to_write <= remaining_in_chunk) { + self.writeToBuffer(bytes[cursor..]); + return; + } + + std.debug.print("{}\n", .{cursor}); + self.writeToBuffer(bytes[cursor .. cursor + remaining_in_chunk]); + cursor += remaining_in_chunk; + try self.flushChunk(); } } - try self.writer.writeAll("\r\n"); - if (body.len != 0) { - try self.writer.writeAll(body); + fn write(self: *Self, bytes: []const u8) Error!usize { + if (!self.chunked and bytes.len > self.buffer.len - self.buffer_pos) { + try self.startChunking(); + } + + if (self.chunked) { + try self.writeToChunks(bytes); + } else { + self.writeToBuffer(bytes); + } + + return bytes.len; + } + + fn flushBodyUnchunked(self: *Self) Error!void { + if (self.buffer_pos != 0) { + try self.base_writer.print("Content-Length: {}\r\n", .{self.buffer_pos}); + } + + try self.base_writer.writeAll("\r\n"); + + if (self.buffer_pos != 0) { + try self.base_writer.writeAll(self.buffer[0..self.buffer_pos]); + } + self.buffer_pos = 0; + } + + pub fn writer(self: *Self) Writer { + return Writer{ .context = self }; + } + + pub fn finish(self: *Self) Error!void { + if (!self.chunked) { + try self.flushBodyUnchunked(); + } else { + if (self.buffer_pos != 0) { + try self.flushChunk(); + } + try self.base_writer.writeAll("0\r\n"); + } + } + + pub fn close(self: *const Self) void { + self.allocator.free(self.buffer); + } + }; +} + +test { + _ = _tests; +} +const _tests = struct { + fn toCrlf(comptime str: []const u8) []const u8 { + comptime { + var buf: [str.len * 2]u8 = undefined; + @setEvalBranchQuota(@as(u32, str.len * 2)); + + var len: usize = 0; + for (str) |ch| { + if (ch == '\n') { + buf[len] = '\r'; + len += 1; + } + + buf[len] = ch; + len += 1; + } + + return buf[0..len]; } } - pub fn statusOnly(self: *Response, status: u16) !void { - try self.openInternal(status); + const test_buffer_size = chunk_size * 4; + test "ResponseStream no headers empty body" { + var buffer: [test_buffer_size]u8 = undefined; + var test_stream = std.io.fixedBufferStream(&buffer); + var headers = HeaderMap.init(std.testing.allocator); + defer headers.deinit(); + + { + var stream = try openResponse( + std.testing.allocator, + test_stream.writer(), + &headers, + .ok, + ); + defer stream.close(); + + try stream.finish(); + } + + try std.testing.expectEqualStrings( + toCrlf( + \\HTTP/1.1 200 OK + \\ + \\ + ), + buffer[0..(try test_stream.getPos())], + ); + } + + test "ResponseStream empty body" { + var buffer: [test_buffer_size]u8 = undefined; + var test_stream = std.io.fixedBufferStream(&buffer); + var headers = HeaderMap.init(std.testing.allocator); + defer headers.deinit(); + + try headers.put("Content-Type", "text/plain"); + + { + var stream = try openResponse( + std.testing.allocator, + test_stream.writer(), + &headers, + .ok, + ); + defer stream.close(); + + try stream.finish(); + } + + try std.testing.expectEqualStrings( + toCrlf( + \\HTTP/1.1 200 OK + \\Content-Type: text/plain + \\ + \\ + ), + buffer[0..(try test_stream.getPos())], + ); + } + + test "ResponseStream not 200 OK" { + var buffer: [test_buffer_size]u8 = undefined; + var test_stream = std.io.fixedBufferStream(&buffer); + var headers = HeaderMap.init(std.testing.allocator); + defer headers.deinit(); + + try headers.put("Content-Type", "text/plain"); + + { + var stream = try openResponse( + std.testing.allocator, + test_stream.writer(), + &headers, + .not_found, + ); + defer stream.close(); + + try stream.finish(); + } + + try std.testing.expectEqualStrings( + toCrlf( + \\HTTP/1.1 404 Not Found + \\Content-Type: text/plain + \\ + \\ + ), + buffer[0..(try test_stream.getPos())], + ); + } + test "ResponseStream small body" { + var buffer: [test_buffer_size]u8 = undefined; + var test_stream = std.io.fixedBufferStream(&buffer); + var headers = HeaderMap.init(std.testing.allocator); + defer headers.deinit(); + + try headers.put("Content-Type", "text/plain"); + + { + var stream = try openResponse( + std.testing.allocator, + test_stream.writer(), + &headers, + .ok, + ); + defer stream.close(); + + try stream.writer().writeAll("Index Page"); + + try stream.finish(); + } + + try std.testing.expectEqualStrings( + toCrlf( + \\HTTP/1.1 200 OK + \\Content-Type: text/plain + \\Content-Length: 10 + \\ + \\Index Page + ), + buffer[0..(try test_stream.getPos())], + ); + } + + test "ResponseStream large body" { + var buffer: [test_buffer_size]u8 = undefined; + var test_stream = std.io.fixedBufferStream(&buffer); + var headers = HeaderMap.init(std.testing.allocator); + defer headers.deinit(); + + try headers.put("Content-Type", "text/plain"); + + { + var stream = try openResponse( + std.testing.allocator, + test_stream.writer(), + &headers, + .ok, + ); + defer stream.close(); + + try stream.writer().writeAll("quuz" ** 6000); + + try stream.finish(); + } + + // zig fmt: off + try std.testing.expectEqualStrings( + toCrlf( + \\HTTP/1.1 200 OK + \\Content-Type: text/plain + \\Transfer-Encoding: chunked + \\ + \\ + ++ "4000\n" + ++ "quuz" ** 4096 ++ "\n" + ++ "1dc0\n" + ++ "quuz" ** (1904) ++ "\n" + ++ "0\n" + ), + buffer[0..(try test_stream.getPos())], + ); + // zig fmt: on + } + + test "ResponseStream large body ending on chunk boundary" { + var buffer: [test_buffer_size]u8 = undefined; + var test_stream = std.io.fixedBufferStream(&buffer); + var headers = HeaderMap.init(std.testing.allocator); + defer headers.deinit(); + + try headers.put("Content-Type", "text/plain"); + + { + var stream = try openResponse( + std.testing.allocator, + test_stream.writer(), + &headers, + .ok, + ); + defer stream.close(); + + try stream.writer().writeAll("quuz" ** (chunk_size / 2)); + + try stream.finish(); + } + + // zig fmt: off + try std.testing.expectEqualStrings( + toCrlf( + \\HTTP/1.1 200 OK + \\Content-Type: text/plain + \\Transfer-Encoding: chunked + \\ + \\ + ++ "4000\n" + ++ "quuz" ** 4096 ++ "\n" + ++ "4000\n" + ++ "quuz" ** 4096 ++ "\n" + ++ "0\n" + ), + buffer[0..(try test_stream.getPos())], + ); + // zig fmt: on } }; + diff --git a/src/main/main.zig b/src/main/main.zig index 4e99bf3..80f355b 100644 --- a/src/main/main.zig +++ b/src/main/main.zig @@ -8,65 +8,13 @@ pub const routing = @import("./routing.zig"); pub const Uuid = util.Uuid; pub const ciutf8 = util.ciutf8; -//pub const io_mode = .evented; +fn testHandler(ctx: *const http.Context) !void { + const stream = try ctx.response.open(.ok); + defer stream.close(); -pub const router = routing.makeRouter(*http.Context, &[_]routing.RouteFn(*http.Context){ - //routing.makeRoute(.GET, "/", staticString("Index Page")), - //routing.makeRoute(.GET, "/abc", staticString("abc")), - //routing.makeRoute(.GET, "/user/:id", getUser), - //routing.makeRoute(.POST, "/note/", postNote), -}); - -const this_scheme = "http"; -const this_host = "localhost:8080"; - -fn postNote(ctx: *http.Context, _: struct {}) anyerror!void { - const id = try db.createNote(.{ - .author = Uuid.randV4(util.getRandom()), - .content = "test post", - }); - - var writer = try ctx.response.open(200); - try writer.writeAll("{\"id\":\""); - try writer.print("{}", .{id}); - try writer.writeAll("\"}"); + try stream.writer().writeAll("Index page"); } -fn getUser(ctx: *http.Context, args: struct { id: []const u8 }) anyerror!void { - const host = ctx.request.headers.get("host") orelse { - return; - }; - - const uuid = Uuid.parse(args.id) catch { - try ctx.response.statusOnly(400); - return; - }; - - const actor = try db.getActorById(args.id); - - if (actor == null or !std.mem.eql(u8, actor.?.host, host)) { - try ctx.response.statusOnly(404); - return; - } - - try ctx.response.headers.put("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""); - - var writer = try ctx.response.open(200); - try writer.writeAll("{\"type\":\"Person\","); - try writer.print("\"id\":\"{s}://{s}/user/{}\",", .{ this_scheme, this_host, uuid }); - try writer.print("\"preferredUsername\":\"{s}\"", .{actor.?.handle}); - try writer.writeAll("}"); -} - -const DummyArgs = struct {}; -fn staticString(comptime str: []const u8) fn (*http.Context, DummyArgs) anyerror!void { - return (struct { - fn func(ctx: *http.Context, _: DummyArgs) anyerror!void { - try ctx.response.headers.put("Content-Type", "text/plain"); - try ctx.response.write(200, str); - } - }).func; -} pub fn main() anyerror!void { var srv = std.net.StreamServer.init(.{ .reuse_address = true }); defer srv.deinit(); @@ -80,10 +28,9 @@ pub fn main() anyerror!void { const conn = try srv.accept(); // todo: keep track of connections - _ = async http.handleConnection(conn, struct { - fn func(ctx: *http.Context) anyerror!http.Response { - return try router(ctx.request.method, ctx.request.path, ctx); - //_ = ctx; + _ = async http.handleConnection(std.heap.page_allocator, conn, struct { + fn func(ctx: *const http.Context) !void { + return testHandler(ctx); } }.func); }