Start work on new http pkg
This commit is contained in:
parent
0f7754802d
commit
687fd6ef9d
4 changed files with 788 additions and 132 deletions
20
src/http/lib.zig
Normal file
20
src/http/lib.zig
Normal file
|
@ -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;
|
||||||
|
}
|
376
src/http/response_stream.zig
Normal file
376
src/http/response_stream.zig
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
};
|
|
@ -3,10 +3,11 @@ const root = @import("root");
|
||||||
|
|
||||||
const ciutf8 = @import("./util.zig").ciutf8;
|
const ciutf8 = @import("./util.zig").ciutf8;
|
||||||
const Reader = std.net.Stream.Reader;
|
const Reader = std.net.Stream.Reader;
|
||||||
const Writer = std.net.Stream.Writer;
|
//const Writer = std.net.Stream.Writer;
|
||||||
|
|
||||||
const Status = std.http.Status;
|
const Status = std.http.Status;
|
||||||
const Method = std.http.Method;
|
const Method = std.http.Method;
|
||||||
|
const Connection = std.net.StreamServer.Connection;
|
||||||
pub const Handler = fn (*Context) anyerror!Response;
|
pub const Handler = fn (*Context) anyerror!Response;
|
||||||
|
|
||||||
const HeaderMap = std.HashMap([]const u8, []const u8, struct {
|
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);
|
return ciutf8.hash(str);
|
||||||
}
|
}
|
||||||
}, std.hash_map.default_max_load_percentage);
|
}, 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", .{});
|
std.log.info("400 Bad Request", .{});
|
||||||
try writer.writeAll("HTTP/1.1 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", .{});
|
std.log.info("501", .{});
|
||||||
try writer.writeAll("HTTP/1.1 501 Not Implemented");
|
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", .{});
|
std.log.info("500", .{});
|
||||||
try writer.writeAll("HTTP/1.1 500 Internal Server Error");
|
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;
|
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();
|
defer conn.stream.close();
|
||||||
const reader = conn.stream.reader();
|
std.log.debug("New connection conn={}", .{conn_id});
|
||||||
const writer = conn.stream.writer();
|
|
||||||
|
|
||||||
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 {
|
fn handleRequest(reader: Reader, writer: std.net.Stream.Writer, handler: Handler) anyerror!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 {
|
|
||||||
const method = try parseHttpMethod(reader);
|
const method = try parseHttpMethod(reader);
|
||||||
|
std.log.debug("Request recieved", .{});
|
||||||
|
|
||||||
var header_buf: [1 << 16]u8 = undefined;
|
var header_buf: [1 << 16]u8 = undefined;
|
||||||
var fba = std.heap.FixedBufferAllocator.init(&header_buf);
|
var fba = std.heap.FixedBufferAllocator.init(&header_buf);
|
||||||
|
@ -186,80 +186,393 @@ fn handleHttpRequest(reader: Reader, writer: Writer, handler: Handler) anyerror!
|
||||||
|
|
||||||
_ = ctx;
|
_ = 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;
|
_ = handler;
|
||||||
_ = writer;
|
_ = writer;
|
||||||
_ = path;
|
_ = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const Context = struct {
|
pub const Context = struct {};
|
||||||
request: Request2,
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Response = struct {
|
const chunk_size = 16 * 1024;
|
||||||
|
pub fn openResponse(
|
||||||
|
alloc: std.mem.Allocator,
|
||||||
|
writer: anytype,
|
||||||
|
headers: *const HeaderMap,
|
||||||
status: Status,
|
status: Status,
|
||||||
headers: HeaderMap,
|
) !ResponseStream(@TypeOf(writer)) {
|
||||||
body: ?[]const u8 = null,
|
const buf = try alloc.alloc(u8, chunk_size);
|
||||||
};
|
errdefer alloc.free(buf);
|
||||||
|
|
||||||
pub const Request2 = struct {
|
try writeStatusLine(writer, status);
|
||||||
method: Method,
|
try writeHeaders(writer, headers);
|
||||||
path: []const u8,
|
|
||||||
headers: HeaderMap,
|
|
||||||
body: ?[]const u8 = null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ResponseOld = struct {
|
return ResponseStream(@TypeOf(writer)){
|
||||||
headers: HeaderMap,
|
.allocator = alloc,
|
||||||
writer: Writer,
|
.base_writer = writer,
|
||||||
|
.headers = headers,
|
||||||
|
.buffer = buf,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
fn writeHeaders(self: *Response) !void {
|
fn writeStatusLine(writer: anytype, status: Status) !void {
|
||||||
var iter = self.headers.iterator();
|
const status_text = status.phrase() orelse "";
|
||||||
var it = iter.next();
|
try writer.print("HTTP/1.1 {} {s}\r\n", .{ @enumToInt(status), status_text });
|
||||||
while (it != null) : (it = iter.next()) {
|
}
|
||||||
try self.writer.print("{s}: {s}\r\n", .{ it.?.key_ptr.*, it.?.value_ptr.* });
|
|
||||||
|
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 {
|
fn writeChunk(writer: anytype, contents: []const u8) @TypeOf(writer).Error!void {
|
||||||
return switch (status) {
|
try writer.print("{x}\r\n", .{contents.len});
|
||||||
200 => "OK",
|
try writer.writeAll(contents);
|
||||||
204 => "No Content",
|
try writer.writeAll("\r\n");
|
||||||
404 => "Not Found",
|
}
|
||||||
else => "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn openInternal(self: *Response, status: u16) !void {
|
fn writeLastChunk(writer: anytype) writer.Error!void {
|
||||||
try self.writer.print("HTTP/1.1 {} {s}\r\n", .{ status, statusText(status) });
|
try writer.writeAll("0\r\n");
|
||||||
try self.writeHeaders();
|
}
|
||||||
try self.writer.writeAll("Connection: close\r\n"); // TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn open(self: *Response, status: u16) !Writer {
|
fn ResponseStream(comptime BaseWriter: type) type {
|
||||||
try self.openInternal(status);
|
return struct {
|
||||||
try self.writer.writeAll("\r\n");
|
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 {
|
fn writeToBuffer(self: *Self, bytes: []const u8) void {
|
||||||
try self.openInternal(status);
|
std.mem.copy(u8, self.buffer[self.buffer_pos..], bytes);
|
||||||
if (body.len != 0) {
|
self.buffer_pos += bytes.len;
|
||||||
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 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");
|
fn write(self: *Self, bytes: []const u8) Error!usize {
|
||||||
if (body.len != 0) {
|
if (!self.chunked and bytes.len > self.buffer.len - self.buffer_pos) {
|
||||||
try self.writer.writeAll(body);
|
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 {
|
const test_buffer_size = chunk_size * 4;
|
||||||
try self.openInternal(status);
|
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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -8,65 +8,13 @@ pub const routing = @import("./routing.zig");
|
||||||
pub const Uuid = util.Uuid;
|
pub const Uuid = util.Uuid;
|
||||||
pub const ciutf8 = util.ciutf8;
|
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){
|
try stream.writer().writeAll("Index page");
|
||||||
//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("\"}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
pub fn main() anyerror!void {
|
||||||
var srv = std.net.StreamServer.init(.{ .reuse_address = true });
|
var srv = std.net.StreamServer.init(.{ .reuse_address = true });
|
||||||
defer srv.deinit();
|
defer srv.deinit();
|
||||||
|
@ -80,10 +28,9 @@ pub fn main() anyerror!void {
|
||||||
const conn = try srv.accept();
|
const conn = try srv.accept();
|
||||||
|
|
||||||
// todo: keep track of connections
|
// todo: keep track of connections
|
||||||
_ = async http.handleConnection(conn, struct {
|
_ = async http.handleConnection(std.heap.page_allocator, conn, struct {
|
||||||
fn func(ctx: *http.Context) anyerror!http.Response {
|
fn func(ctx: *const http.Context) !void {
|
||||||
return try router(ctx.request.method, ctx.request.path, ctx);
|
return testHandler(ctx);
|
||||||
//_ = ctx;
|
|
||||||
}
|
}
|
||||||
}.func);
|
}.func);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue