2022-06-09 06:37:09 +00:00
|
|
|
const std = @import("std");
|
2022-12-02 03:45:09 +00:00
|
|
|
const util = @import("util");
|
2022-07-09 22:43:35 +00:00
|
|
|
const http = @import("../lib.zig");
|
2022-06-09 06:37:09 +00:00
|
|
|
|
|
|
|
const Status = http.Status;
|
2022-11-05 07:26:53 +00:00
|
|
|
const Fields = http.Fields;
|
2022-06-09 06:37:09 +00:00
|
|
|
|
|
|
|
const chunk_size = 16 * 1024;
|
2022-07-09 19:39:36 +00:00
|
|
|
pub fn open(
|
2022-06-09 06:37:09 +00:00
|
|
|
alloc: std.mem.Allocator,
|
|
|
|
writer: anytype,
|
2022-11-05 07:26:53 +00:00
|
|
|
headers: *const Fields,
|
2022-06-09 06:37:09 +00:00
|
|
|
status: Status,
|
|
|
|
) !ResponseStream(@TypeOf(writer)) {
|
|
|
|
const buf = try alloc.alloc(u8, chunk_size);
|
|
|
|
errdefer alloc.free(buf);
|
|
|
|
|
|
|
|
try writeStatusLine(writer, status);
|
2022-11-05 07:26:53 +00:00
|
|
|
try writeFields(writer, headers);
|
2022-06-09 06:37:09 +00:00
|
|
|
|
|
|
|
return ResponseStream(@TypeOf(writer)){
|
|
|
|
.allocator = alloc,
|
|
|
|
.base_writer = writer,
|
|
|
|
.headers = headers,
|
|
|
|
.buffer = buf,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-11-05 07:26:53 +00:00
|
|
|
pub fn writeRequestHeader(writer: anytype, headers: *const Fields, status: Status) !void {
|
2022-10-16 12:48:12 +00:00
|
|
|
try writeStatusLine(writer, status);
|
2022-11-05 07:26:53 +00:00
|
|
|
try writeFields(writer, headers);
|
2022-10-16 12:48:12 +00:00
|
|
|
try writer.writeAll("\r\n");
|
|
|
|
}
|
|
|
|
|
2022-06-09 06:37:09 +00:00
|
|
|
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 });
|
|
|
|
}
|
|
|
|
|
2022-11-05 07:26:53 +00:00
|
|
|
fn writeFields(writer: anytype, headers: *const Fields) !void {
|
2022-06-09 06:37:09 +00:00
|
|
|
var iter = headers.iterator();
|
|
|
|
while (iter.next()) |header| {
|
2022-11-18 03:39:24 +00:00
|
|
|
if (std.ascii.eqlIgnoreCase("Set-Cookie", header.key_ptr.*)) continue;
|
|
|
|
|
2022-11-21 08:54:03 +00:00
|
|
|
try writer.print("{s}: {s}\r\n", .{ header.key_ptr.*, percentEncode(header.value_ptr.*) });
|
2022-06-09 06:37:09 +00:00
|
|
|
}
|
2022-11-18 03:39:24 +00:00
|
|
|
|
|
|
|
var cookie_iter = headers.getList("Set-Cookie");
|
|
|
|
while (cookie_iter.next()) |cookie| {
|
2022-11-21 08:54:03 +00:00
|
|
|
try writer.print("Set-Cookie: {s}\r\n", .{percentEncode(cookie)});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const PercentEncode = struct {
|
|
|
|
str: []const u8,
|
|
|
|
|
|
|
|
pub fn format(v: PercentEncode, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
|
|
|
|
for (v.str) |ch| switch (ch) {
|
|
|
|
' ', '\t', 0x21...0x7e, 0x80...0xff => try writer.writeByte(ch),
|
|
|
|
else => try std.fmt.format(writer, "%{x:0>2}", .{ch}),
|
|
|
|
};
|
2022-11-18 03:39:24 +00:00
|
|
|
}
|
2022-11-21 08:54:03 +00:00
|
|
|
};
|
|
|
|
fn percentEncode(str: []const u8) PercentEncode {
|
|
|
|
return PercentEncode{ .str = str };
|
2022-06-09 06:37:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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");
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
2022-11-05 07:26:53 +00:00
|
|
|
headers: *const Fields,
|
2022-06-09 06:37:09 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2022-10-13 09:23:57 +00:00
|
|
|
try self.base_writer.print("Content-Length: {}\r\n", .{self.buffer_pos});
|
2022-06-09 06:37:09 +00:00
|
|
|
|
|
|
|
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();
|
|
|
|
}
|
2022-12-09 11:37:46 +00:00
|
|
|
try self.base_writer.writeAll("0\r\n\r\n");
|
2022-06-09 06:37:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-09 22:12:17 +00:00
|
|
|
pub fn close(self: *Self) void {
|
2022-06-09 06:37:09 +00:00
|
|
|
self.allocator.free(self.buffer);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
test {
|
|
|
|
_ = _tests;
|
|
|
|
}
|
|
|
|
const _tests = struct {
|
2022-12-02 03:45:09 +00:00
|
|
|
const toCrlf = util.comptimeToCrlf;
|
2022-06-09 06:37:09 +00:00
|
|
|
|
|
|
|
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);
|
2022-11-05 07:26:53 +00:00
|
|
|
var headers = Fields.init(std.testing.allocator);
|
2022-06-09 06:37:09 +00:00
|
|
|
defer headers.deinit();
|
|
|
|
|
|
|
|
{
|
2022-07-09 19:39:36 +00:00
|
|
|
var stream = try open(
|
2022-06-09 06:37:09 +00:00
|
|
|
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);
|
2022-11-05 07:26:53 +00:00
|
|
|
var headers = Fields.init(std.testing.allocator);
|
2022-06-09 06:37:09 +00:00
|
|
|
defer headers.deinit();
|
|
|
|
|
|
|
|
try headers.put("Content-Type", "text/plain");
|
|
|
|
|
|
|
|
{
|
2022-07-09 19:39:36 +00:00
|
|
|
var stream = try open(
|
2022-06-09 06:37:09 +00:00
|
|
|
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);
|
2022-11-05 07:26:53 +00:00
|
|
|
var headers = Fields.init(std.testing.allocator);
|
2022-06-09 06:37:09 +00:00
|
|
|
defer headers.deinit();
|
|
|
|
|
|
|
|
try headers.put("Content-Type", "text/plain");
|
|
|
|
|
|
|
|
{
|
2022-07-09 19:39:36 +00:00
|
|
|
var stream = try open(
|
2022-06-09 06:37:09 +00:00
|
|
|
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);
|
2022-11-05 07:26:53 +00:00
|
|
|
var headers = Fields.init(std.testing.allocator);
|
2022-06-09 06:37:09 +00:00
|
|
|
defer headers.deinit();
|
|
|
|
|
|
|
|
try headers.put("Content-Type", "text/plain");
|
|
|
|
|
|
|
|
{
|
2022-07-09 19:39:36 +00:00
|
|
|
var stream = try open(
|
2022-06-09 06:37:09 +00:00
|
|
|
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);
|
2022-11-05 07:26:53 +00:00
|
|
|
var headers = Fields.init(std.testing.allocator);
|
2022-06-09 06:37:09 +00:00
|
|
|
defer headers.deinit();
|
|
|
|
|
|
|
|
try headers.put("Content-Type", "text/plain");
|
|
|
|
|
|
|
|
{
|
2022-07-09 19:39:36 +00:00
|
|
|
var stream = try open(
|
2022-06-09 06:37:09 +00:00
|
|
|
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);
|
2022-11-05 07:26:53 +00:00
|
|
|
var headers = Fields.init(std.testing.allocator);
|
2022-06-09 06:37:09 +00:00
|
|
|
defer headers.deinit();
|
|
|
|
|
|
|
|
try headers.put("Content-Type", "text/plain");
|
|
|
|
|
|
|
|
{
|
2022-07-09 19:39:36 +00:00
|
|
|
var stream = try open(
|
2022-06-09 06:37:09 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
};
|