std.http: add Headers
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
pub const Client = @import("http/Client.zig");
|
||||
pub const Server = @import("http/Server.zig");
|
||||
pub const protocol = @import("http/protocol.zig");
|
||||
const headers = @import("http/Headers.zig");
|
||||
|
||||
pub const Headers = headers.Headers;
|
||||
pub const Header = headers.HeaderEntry;
|
||||
|
||||
pub const Version = enum {
|
||||
@"HTTP/1.0",
|
||||
@@ -265,11 +269,6 @@ pub const Connection = enum {
|
||||
close,
|
||||
};
|
||||
|
||||
pub const Header = struct {
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
};
|
||||
|
||||
const std = @import("std.zig");
|
||||
|
||||
test {
|
||||
|
||||
@@ -348,140 +348,125 @@ pub const Compression = union(enum) {
|
||||
|
||||
/// A HTTP response originating from a server.
|
||||
pub const Response = struct {
|
||||
pub const Headers = struct {
|
||||
status: http.Status,
|
||||
version: http.Version,
|
||||
location: ?[]const u8 = null,
|
||||
content_length: ?u64 = null,
|
||||
transfer_encoding: ?http.TransferEncoding = null,
|
||||
transfer_compression: ?http.ContentEncoding = null,
|
||||
connection: http.Connection = .close,
|
||||
upgrade: ?[]const u8 = null,
|
||||
pub const ParseError = Allocator.Error || error{
|
||||
ShortHttpStatusLine,
|
||||
BadHttpVersion,
|
||||
HttpHeadersInvalid,
|
||||
HttpHeaderContinuationsUnsupported,
|
||||
HttpTransferEncodingUnsupported,
|
||||
HttpConnectionHeaderUnsupported,
|
||||
InvalidContentLength,
|
||||
CompressionNotSupported,
|
||||
};
|
||||
|
||||
pub const ParseError = error{
|
||||
ShortHttpStatusLine,
|
||||
BadHttpVersion,
|
||||
HttpHeadersInvalid,
|
||||
HttpHeaderContinuationsUnsupported,
|
||||
HttpTransferEncodingUnsupported,
|
||||
HttpConnectionHeaderUnsupported,
|
||||
InvalidContentLength,
|
||||
CompressionNotSupported,
|
||||
pub fn parse(res: *Response, bytes: []const u8) ParseError!void {
|
||||
var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n");
|
||||
|
||||
const first_line = it.next() orelse return error.HttpHeadersInvalid;
|
||||
if (first_line.len < 12)
|
||||
return error.ShortHttpStatusLine;
|
||||
|
||||
const version: http.Version = switch (int64(first_line[0..8])) {
|
||||
int64("HTTP/1.0") => .@"HTTP/1.0",
|
||||
int64("HTTP/1.1") => .@"HTTP/1.1",
|
||||
else => return error.BadHttpVersion,
|
||||
};
|
||||
if (first_line[8] != ' ') return error.HttpHeadersInvalid;
|
||||
const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*));
|
||||
const reason = mem.trimLeft(u8, first_line[12..], " ");
|
||||
|
||||
pub fn parse(bytes: []const u8) ParseError!Headers {
|
||||
var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n");
|
||||
res.version = version;
|
||||
res.status = status;
|
||||
res.reason = reason;
|
||||
|
||||
const first_line = it.next() orelse return error.HttpHeadersInvalid;
|
||||
if (first_line.len < 12)
|
||||
return error.ShortHttpStatusLine;
|
||||
while (it.next()) |line| {
|
||||
if (line.len == 0) return error.HttpHeadersInvalid;
|
||||
switch (line[0]) {
|
||||
' ', '\t' => return error.HttpHeaderContinuationsUnsupported,
|
||||
else => {},
|
||||
}
|
||||
|
||||
const version: http.Version = switch (int64(first_line[0..8])) {
|
||||
int64("HTTP/1.0") => .@"HTTP/1.0",
|
||||
int64("HTTP/1.1") => .@"HTTP/1.1",
|
||||
else => return error.BadHttpVersion,
|
||||
};
|
||||
if (first_line[8] != ' ') return error.HttpHeadersInvalid;
|
||||
const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*));
|
||||
var line_it = mem.tokenize(u8, line, ": ");
|
||||
const header_name = line_it.next() orelse return error.HttpHeadersInvalid;
|
||||
const header_value = line_it.rest();
|
||||
|
||||
var headers: Headers = .{
|
||||
.version = version,
|
||||
.status = status,
|
||||
};
|
||||
try res.headers.append(header_name, header_value);
|
||||
|
||||
while (it.next()) |line| {
|
||||
if (line.len == 0) return error.HttpHeadersInvalid;
|
||||
switch (line[0]) {
|
||||
' ', '\t' => return error.HttpHeaderContinuationsUnsupported,
|
||||
else => {},
|
||||
}
|
||||
if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {
|
||||
if (res.content_length != null) return error.HttpHeadersInvalid;
|
||||
res.content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength;
|
||||
} else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
|
||||
// Transfer-Encoding: second, first
|
||||
// Transfer-Encoding: deflate, chunked
|
||||
var iter = mem.splitBackwards(u8, header_value, ",");
|
||||
|
||||
var line_it = mem.tokenize(u8, line, ": ");
|
||||
const header_name = line_it.next() orelse return error.HttpHeadersInvalid;
|
||||
const header_value = line_it.rest();
|
||||
if (std.ascii.eqlIgnoreCase(header_name, "location")) {
|
||||
if (headers.location != null) return error.HttpHeadersInvalid;
|
||||
headers.location = header_value;
|
||||
} else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {
|
||||
if (headers.content_length != null) return error.HttpHeadersInvalid;
|
||||
headers.content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength;
|
||||
} else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
|
||||
// Transfer-Encoding: second, first
|
||||
// Transfer-Encoding: deflate, chunked
|
||||
var iter = mem.splitBackwards(u8, header_value, ",");
|
||||
if (iter.next()) |first| {
|
||||
const trimmed = mem.trim(u8, first, " ");
|
||||
|
||||
if (iter.next()) |first| {
|
||||
const trimmed = mem.trim(u8, first, " ");
|
||||
|
||||
if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| {
|
||||
if (headers.transfer_encoding != null) return error.HttpHeadersInvalid;
|
||||
headers.transfer_encoding = te;
|
||||
} else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
|
||||
if (headers.transfer_compression != null) return error.HttpHeadersInvalid;
|
||||
headers.transfer_compression = ce;
|
||||
} else {
|
||||
return error.HttpTransferEncodingUnsupported;
|
||||
}
|
||||
}
|
||||
|
||||
if (iter.next()) |second| {
|
||||
if (headers.transfer_compression != null) return error.HttpTransferEncodingUnsupported;
|
||||
|
||||
const trimmed = mem.trim(u8, second, " ");
|
||||
|
||||
if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
|
||||
headers.transfer_compression = ce;
|
||||
} else {
|
||||
return error.HttpTransferEncodingUnsupported;
|
||||
}
|
||||
}
|
||||
|
||||
if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;
|
||||
} else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {
|
||||
if (headers.transfer_compression != null) return error.HttpHeadersInvalid;
|
||||
|
||||
const trimmed = mem.trim(u8, header_value, " ");
|
||||
|
||||
if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
|
||||
headers.transfer_compression = ce;
|
||||
if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| {
|
||||
if (res.transfer_encoding != null) return error.HttpHeadersInvalid;
|
||||
res.transfer_encoding = te;
|
||||
} else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
|
||||
if (res.transfer_compression != null) return error.HttpHeadersInvalid;
|
||||
res.transfer_compression = ce;
|
||||
} else {
|
||||
return error.HttpTransferEncodingUnsupported;
|
||||
}
|
||||
} else if (std.ascii.eqlIgnoreCase(header_name, "connection")) {
|
||||
if (std.ascii.eqlIgnoreCase(header_value, "keep-alive")) {
|
||||
headers.connection = .keep_alive;
|
||||
} else if (std.ascii.eqlIgnoreCase(header_value, "close")) {
|
||||
headers.connection = .close;
|
||||
}
|
||||
|
||||
if (iter.next()) |second| {
|
||||
if (res.transfer_compression != null) return error.HttpTransferEncodingUnsupported;
|
||||
|
||||
const trimmed = mem.trim(u8, second, " ");
|
||||
|
||||
if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
|
||||
res.transfer_compression = ce;
|
||||
} else {
|
||||
return error.HttpConnectionHeaderUnsupported;
|
||||
return error.HttpTransferEncodingUnsupported;
|
||||
}
|
||||
} else if (std.ascii.eqlIgnoreCase(header_name, "upgrade")) {
|
||||
headers.upgrade = header_value;
|
||||
}
|
||||
|
||||
if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;
|
||||
} else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {
|
||||
if (res.transfer_compression != null) return error.HttpHeadersInvalid;
|
||||
|
||||
const trimmed = mem.trim(u8, header_value, " ");
|
||||
|
||||
if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
|
||||
res.transfer_compression = ce;
|
||||
} else {
|
||||
return error.HttpTransferEncodingUnsupported;
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
|
||||
inline fn int64(array: *const [8]u8) u64 {
|
||||
return @bitCast(u64, array.*);
|
||||
}
|
||||
inline fn int64(array: *const [8]u8) u64 {
|
||||
return @bitCast(u64, array.*);
|
||||
}
|
||||
|
||||
fn parseInt3(nnn: @Vector(3, u8)) u10 {
|
||||
const zero: @Vector(3, u8) = .{ '0', '0', '0' };
|
||||
const mmm: @Vector(3, u10) = .{ 100, 10, 1 };
|
||||
return @reduce(.Add, @as(@Vector(3, u10), nnn -% zero) *% mmm);
|
||||
}
|
||||
fn parseInt3(nnn: @Vector(3, u8)) u10 {
|
||||
const zero: @Vector(3, u8) = .{ '0', '0', '0' };
|
||||
const mmm: @Vector(3, u10) = .{ 100, 10, 1 };
|
||||
return @reduce(.Add, @as(@Vector(3, u10), nnn -% zero) *% mmm);
|
||||
}
|
||||
|
||||
test parseInt3 {
|
||||
const expectEqual = testing.expectEqual;
|
||||
try expectEqual(@as(u10, 0), parseInt3("000".*));
|
||||
try expectEqual(@as(u10, 418), parseInt3("418".*));
|
||||
try expectEqual(@as(u10, 999), parseInt3("999".*));
|
||||
}
|
||||
};
|
||||
test parseInt3 {
|
||||
const expectEqual = testing.expectEqual;
|
||||
try expectEqual(@as(u10, 0), parseInt3("000".*));
|
||||
try expectEqual(@as(u10, 418), parseInt3("418".*));
|
||||
try expectEqual(@as(u10, 999), parseInt3("999".*));
|
||||
}
|
||||
|
||||
headers: Headers = undefined,
|
||||
version: http.Version,
|
||||
status: http.Status,
|
||||
reason: []const u8,
|
||||
|
||||
content_length: ?u64 = null,
|
||||
transfer_encoding: ?http.TransferEncoding = null,
|
||||
transfer_compression: ?http.ContentEncoding = null,
|
||||
|
||||
headers: http.Headers,
|
||||
parser: proto.HeadersParser,
|
||||
compression: Compression = .none,
|
||||
skip: bool = false,
|
||||
@@ -491,22 +476,14 @@ pub const Response = struct {
|
||||
///
|
||||
/// Order of operations: request[ -> write -> finish] -> do -> read
|
||||
pub const Request = struct {
|
||||
pub const Headers = struct {
|
||||
version: http.Version = .@"HTTP/1.1",
|
||||
method: http.Method = .GET,
|
||||
user_agent: []const u8 = "zig (std.http)",
|
||||
connection: http.Connection = .keep_alive,
|
||||
transfer_encoding: RequestTransfer = .none,
|
||||
|
||||
custom: []const http.CustomHeader = &[_]http.CustomHeader{},
|
||||
};
|
||||
|
||||
uri: Uri,
|
||||
client: *Client,
|
||||
connection: *ConnectionPool.Node,
|
||||
/// These are stored in Request so that they are available when following
|
||||
/// redirects.
|
||||
headers: Headers,
|
||||
|
||||
method: http.Method,
|
||||
version: http.Version = .@"HTTP/1.1",
|
||||
headers: http.Headers,
|
||||
transfer_encoding: RequestTransfer = .none,
|
||||
|
||||
redirects_left: u32,
|
||||
handle_redirects: bool,
|
||||
@@ -526,6 +503,7 @@ pub const Request = struct {
|
||||
}
|
||||
|
||||
if (req.response.parser.header_bytes_owned) {
|
||||
req.response.headers.deinit();
|
||||
req.response.parser.header_bytes.deinit(req.client.allocator);
|
||||
}
|
||||
|
||||
@@ -540,14 +518,14 @@ pub const Request = struct {
|
||||
req.* = undefined;
|
||||
}
|
||||
|
||||
pub fn start(req: *Request, uri: Uri, headers: Headers) !void {
|
||||
pub fn start(req: *Request, uri: Uri) !void {
|
||||
var buffered = std.io.bufferedWriter(req.connection.data.buffered.writer());
|
||||
const w = buffered.writer();
|
||||
|
||||
try w.writeAll(@tagName(headers.method));
|
||||
try w.writeAll(@tagName(req.method));
|
||||
try w.writeByte(' ');
|
||||
|
||||
if (req.headers.method == .CONNECT) {
|
||||
if (req.method == .CONNECT) {
|
||||
try w.writeAll(uri.host.?);
|
||||
try w.writeByte(':');
|
||||
try w.print("{}", .{uri.port.?});
|
||||
@@ -559,33 +537,62 @@ pub const Request = struct {
|
||||
}
|
||||
|
||||
try w.writeByte(' ');
|
||||
try w.writeAll(@tagName(headers.version));
|
||||
try w.writeAll("\r\nHost: ");
|
||||
try w.writeAll(uri.host.?);
|
||||
try w.writeAll("\r\nUser-Agent: ");
|
||||
try w.writeAll(headers.user_agent);
|
||||
if (headers.connection == .close) {
|
||||
try w.writeAll("\r\nConnection: close");
|
||||
} else {
|
||||
try w.writeAll("\r\nConnection: keep-alive");
|
||||
}
|
||||
try w.writeAll("\r\nAccept-Encoding: gzip, deflate, zstd");
|
||||
try w.writeAll("\r\nTE: gzip, deflate"); // TODO: add trailers when someone finds a nice way to integrate them without completely invalidating all pointers to headers.
|
||||
try w.writeAll(@tagName(req.version));
|
||||
try w.writeAll("\r\n");
|
||||
|
||||
switch (headers.transfer_encoding) {
|
||||
.chunked => try w.writeAll("\r\nTransfer-Encoding: chunked"),
|
||||
.content_length => |content_length| try w.print("\r\nContent-Length: {d}", .{content_length}),
|
||||
.none => {},
|
||||
}
|
||||
|
||||
for (headers.custom) |header| {
|
||||
if (!req.headers.contains("host")) {
|
||||
try w.writeAll("Host: ");
|
||||
try w.writeAll(uri.host.?);
|
||||
try w.writeAll("\r\n");
|
||||
try w.writeAll(header.name);
|
||||
try w.writeAll(": ");
|
||||
try w.writeAll(header.value);
|
||||
}
|
||||
|
||||
try w.writeAll("\r\n\r\n");
|
||||
if (!req.headers.contains("user-agent")) {
|
||||
try w.writeAll("User-Agent: zig/");
|
||||
try w.writeAll(@import("builtin").zig_version_string);
|
||||
try w.writeAll(" (std.http)\r\n");
|
||||
}
|
||||
|
||||
if (!req.headers.contains("connection")) {
|
||||
try w.writeAll("Connection: keep-alive\r\n");
|
||||
}
|
||||
|
||||
if (!req.headers.contains("accept-encoding")) {
|
||||
try w.writeAll("Accept-Encoding: gzip, deflate, zstd\r\n");
|
||||
}
|
||||
|
||||
if (!req.headers.contains("te")) {
|
||||
try w.writeAll("TE: gzip, deflate, trailers\r\n");
|
||||
}
|
||||
|
||||
const has_transfer_encoding = req.headers.contains("transfer-encoding");
|
||||
const has_content_length = req.headers.contains("content-length");
|
||||
|
||||
if (!has_transfer_encoding and !has_content_length) {
|
||||
switch (req.transfer_encoding) {
|
||||
.chunked => try w.writeAll("Transfer-Encoding: chunked\r\n"),
|
||||
.content_length => |content_length| try w.print("Content-Length: {d}\r\n", .{content_length}),
|
||||
.none => {},
|
||||
}
|
||||
} else {
|
||||
if (has_content_length) {
|
||||
const content_length = try std.fmt.parseInt(u64, req.headers.getFirstValue("content-length").?, 10);
|
||||
|
||||
req.transfer_encoding = .{ .content_length = content_length };
|
||||
} else if (has_transfer_encoding) {
|
||||
const transfer_encoding = req.headers.getFirstValue("content-length").?;
|
||||
if (std.mem.eql(u8, transfer_encoding, "chunked")) {
|
||||
req.transfer_encoding = .chunked;
|
||||
} else {
|
||||
return error.UnsupportedTransferEncoding;
|
||||
}
|
||||
} else {
|
||||
req.transfer_encoding = .none;
|
||||
}
|
||||
}
|
||||
|
||||
try w.print("{}", .{req.headers});
|
||||
|
||||
try w.writeAll("\r\n");
|
||||
|
||||
try buffered.flush();
|
||||
}
|
||||
@@ -611,7 +618,7 @@ pub const Request = struct {
|
||||
return index;
|
||||
}
|
||||
|
||||
pub const DoError = RequestError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.Headers.ParseError || Uri.ParseError || error{ TooManyHttpRedirects, HttpRedirectMissingLocation, CompressionInitializationFailed };
|
||||
pub const DoError = RequestError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.ParseError || Uri.ParseError || error{ TooManyHttpRedirects, HttpRedirectMissingLocation, CompressionInitializationFailed };
|
||||
|
||||
/// Waits for a response from the server and parses any headers that are sent.
|
||||
/// This function will block until the final response is received.
|
||||
@@ -629,33 +636,39 @@ pub const Request = struct {
|
||||
if (req.response.parser.state.isContent()) break;
|
||||
}
|
||||
|
||||
req.response.headers = try Response.Headers.parse(req.response.parser.header_bytes.items);
|
||||
req.response.headers = http.Headers{ .allocator = req.client.allocator, .owned = false };
|
||||
try req.response.parse(req.response.parser.header_bytes.items);
|
||||
|
||||
if (req.response.headers.status == .switching_protocols) {
|
||||
if (req.response.status == .switching_protocols) {
|
||||
req.connection.data.closing = false;
|
||||
req.response.parser.done = true;
|
||||
}
|
||||
|
||||
if (req.headers.method == .CONNECT and req.response.headers.status == .ok) {
|
||||
if (req.method == .CONNECT and req.response.status == .ok) {
|
||||
req.connection.data.closing = false;
|
||||
req.connection.data.proxied = true;
|
||||
req.response.parser.done = true;
|
||||
}
|
||||
|
||||
if (req.headers.connection == .keep_alive and req.response.headers.connection == .keep_alive) {
|
||||
const req_connection = req.headers.getFirstValue("connection");
|
||||
const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);
|
||||
|
||||
const res_connection = req.response.headers.getFirstValue("connection");
|
||||
const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?);
|
||||
if (req_keepalive and res_keepalive) {
|
||||
req.connection.data.closing = false;
|
||||
} else {
|
||||
req.connection.data.closing = true;
|
||||
}
|
||||
|
||||
if (req.response.headers.transfer_encoding) |te| {
|
||||
if (req.response.transfer_encoding) |te| {
|
||||
switch (te) {
|
||||
.chunked => {
|
||||
req.response.parser.next_chunk_length = 0;
|
||||
req.response.parser.state = .chunk_head_size;
|
||||
},
|
||||
}
|
||||
} else if (req.response.headers.content_length) |cl| {
|
||||
} else if (req.response.content_length) |cl| {
|
||||
req.response.parser.next_chunk_length = cl;
|
||||
|
||||
if (cl == 0) req.response.parser.done = true;
|
||||
@@ -663,7 +676,7 @@ pub const Request = struct {
|
||||
req.response.parser.done = true;
|
||||
}
|
||||
|
||||
if (req.response.headers.status.class() == .redirect and req.handle_redirects) {
|
||||
if (req.response.status.class() == .redirect and req.handle_redirects) {
|
||||
req.response.skip = true;
|
||||
|
||||
const empty = @as([*]u8, undefined)[0..0];
|
||||
@@ -671,7 +684,7 @@ pub const Request = struct {
|
||||
|
||||
if (req.redirects_left == 0) return error.TooManyHttpRedirects;
|
||||
|
||||
const location = req.response.headers.location orelse
|
||||
const location = req.response.headers.getFirstValue("location") orelse
|
||||
return error.HttpRedirectMissingLocation;
|
||||
const new_url = Uri.parse(location) catch try Uri.parseWithoutScheme(location);
|
||||
|
||||
@@ -683,6 +696,8 @@ pub const Request = struct {
|
||||
req.arena = new_arena;
|
||||
|
||||
const new_req = try req.client.request(resolved_url, req.headers, .{
|
||||
.method = req.method,
|
||||
.version = req.version,
|
||||
.max_redirects = req.redirects_left - 1,
|
||||
.header_strategy = if (req.response.parser.header_bytes_owned) .{
|
||||
.dynamic = req.response.parser.max_header_bytes,
|
||||
@@ -695,7 +710,7 @@ pub const Request = struct {
|
||||
} else {
|
||||
req.response.skip = false;
|
||||
if (!req.response.parser.done) {
|
||||
if (req.response.headers.transfer_compression) |tc| switch (tc) {
|
||||
if (req.response.transfer_compression) |tc| switch (tc) {
|
||||
.compress => return error.CompressionNotSupported,
|
||||
.deflate => req.response.compression = .{
|
||||
.deflate = std.compress.zlib.zlibStream(req.client.allocator, req.transferReader()) catch return error.CompressionInitializationFailed,
|
||||
@@ -789,7 +804,7 @@ pub const Request = struct {
|
||||
|
||||
/// Finish the body of a request. This notifies the server that you have no more data to send.
|
||||
pub fn finish(req: *Request) FinishError!void {
|
||||
switch (req.headers.transfer_encoding) {
|
||||
switch (req.transfer_encoding) {
|
||||
.chunked => req.connection.data.conn.writeAll("0\r\n\r\n") catch |err| {
|
||||
req.client.last_error = .{ .write = err };
|
||||
return error.WriteFailed;
|
||||
@@ -908,14 +923,18 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio
|
||||
}
|
||||
}
|
||||
|
||||
pub const RequestError = ConnectUnproxiedError || ConnectErrorPartial || BufferedConnection.WriteError || error{
|
||||
pub const RequestError = ConnectUnproxiedError || ConnectErrorPartial || std.fmt.ParseIntError || BufferedConnection.WriteError || error{
|
||||
UnsupportedUrlScheme,
|
||||
UriMissingHost,
|
||||
|
||||
CertificateBundleLoadFailure,
|
||||
UnsupportedTransferEncoding,
|
||||
};
|
||||
|
||||
pub const Options = struct {
|
||||
method: http.Method = .GET,
|
||||
version: http.Version = .@"HTTP/1.1",
|
||||
|
||||
handle_redirects: bool = true,
|
||||
max_redirects: u32 = 3,
|
||||
header_strategy: HeaderStrategy = .{ .dynamic = 16 * 1024 },
|
||||
@@ -946,7 +965,7 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{
|
||||
|
||||
/// Form and send a http request to a server.
|
||||
/// This function is threadsafe.
|
||||
pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Options) RequestError!Request {
|
||||
pub fn request(client: *Client, uri: Uri, headers: http.Headers, options: Options) RequestError!Request {
|
||||
const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;
|
||||
|
||||
const port: u16 = uri.port orelse switch (protocol) {
|
||||
@@ -973,9 +992,14 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt
|
||||
.client = client,
|
||||
.connection = conn,
|
||||
.headers = headers,
|
||||
.method = options.method,
|
||||
.version = options.version,
|
||||
.redirects_left = options.max_redirects,
|
||||
.handle_redirects = options.handle_redirects,
|
||||
.response = .{
|
||||
.status = undefined,
|
||||
.version = undefined,
|
||||
.headers = undefined,
|
||||
.parser = switch (options.header_strategy) {
|
||||
.dynamic => |max| proto.HeadersParser.initDynamic(max),
|
||||
.static => |buf| proto.HeadersParser.initStatic(buf),
|
||||
@@ -987,7 +1011,7 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt
|
||||
|
||||
req.arena = std.heap.ArenaAllocator.init(client.allocator);
|
||||
|
||||
try req.start(uri, headers);
|
||||
try req.start(uri);
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
386
lib/std/http/Headers.zig
Normal file
386
lib/std/http/Headers.zig
Normal file
@@ -0,0 +1,386 @@
|
||||
const std = @import("../std.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const testing = std.testing;
|
||||
const ascii = std.ascii;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
pub const HeaderList = std.ArrayListUnmanaged(HeaderEntry);
|
||||
pub const HeaderIndexList = std.ArrayListUnmanaged(usize);
|
||||
pub const HeaderIndex = std.HashMapUnmanaged([]const u8, HeaderIndexList, CaseInsensitiveStringContext, std.hash_map.default_max_load_percentage);
|
||||
|
||||
pub const CaseInsensitiveStringContext = struct {
|
||||
pub fn hash(self: @This(), s: []const u8) u64 {
|
||||
_ = self;
|
||||
var buf: [64]u8 = undefined;
|
||||
var i: u8 = 0;
|
||||
|
||||
var h = std.hash.Wyhash.init(0);
|
||||
while (i < s.len) : (i += 64) {
|
||||
const left = @min(64, s.len - i);
|
||||
const ret = ascii.lowerString(buf[0..], s[i..][0..left]);
|
||||
h.update(ret);
|
||||
}
|
||||
|
||||
return h.final();
|
||||
}
|
||||
|
||||
pub fn eql(self: @This(), a: []const u8, b: []const u8) bool {
|
||||
_ = self;
|
||||
return ascii.eqlIgnoreCase(a, b);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HeaderEntry = struct {
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
|
||||
pub fn modify(entry: *HeaderEntry, allocator: Allocator, new_value: []const u8) !void {
|
||||
if (entry.value.len <= new_value.len) {
|
||||
std.mem.copy(u8, @constCast(entry.value), new_value);
|
||||
} else {
|
||||
allocator.free(entry.value);
|
||||
|
||||
entry.value = try allocator.dupe(u8, new_value);
|
||||
}
|
||||
}
|
||||
|
||||
fn lessThan(ctx: void, a: HeaderEntry, b: HeaderEntry) bool {
|
||||
_ = ctx;
|
||||
if (a.name.ptr == b.name.ptr) return false;
|
||||
|
||||
return ascii.lessThanIgnoreCase(a.name, b.name);
|
||||
}
|
||||
};
|
||||
|
||||
pub const Headers = struct {
|
||||
allocator: Allocator,
|
||||
list: HeaderList = .{},
|
||||
index: HeaderIndex = .{},
|
||||
|
||||
/// When this is false, names and values will not be duplicated.
|
||||
/// Use with caution.
|
||||
owned: bool = true,
|
||||
|
||||
pub fn init(allocator: Allocator) Headers {
|
||||
return .{ .allocator = allocator };
|
||||
}
|
||||
|
||||
pub fn deinit(headers: *Headers) void {
|
||||
var it = headers.index.iterator();
|
||||
while (it.next()) |entry| {
|
||||
entry.value_ptr.deinit(headers.allocator);
|
||||
|
||||
if (headers.owned) headers.allocator.free(entry.key_ptr.*);
|
||||
}
|
||||
|
||||
for (headers.list.items) |entry| {
|
||||
if (headers.owned) headers.allocator.free(entry.value);
|
||||
}
|
||||
|
||||
headers.index.deinit(headers.allocator);
|
||||
headers.list.deinit(headers.allocator);
|
||||
|
||||
headers.* = undefined;
|
||||
}
|
||||
|
||||
/// Appends a header to the list. Both name and value are copied.
|
||||
pub fn append(headers: *Headers, name: []const u8, value: []const u8) !void {
|
||||
const n = headers.list.items.len;
|
||||
|
||||
const value_duped = if (headers.owned) try headers.allocator.dupe(u8, value) else value;
|
||||
errdefer if (headers.owned) headers.allocator.free(value_duped);
|
||||
|
||||
var entry = HeaderEntry{ .name = undefined, .value = value_duped };
|
||||
|
||||
if (headers.index.getEntry(name)) |kv| {
|
||||
entry.name = kv.key_ptr.*;
|
||||
try kv.value_ptr.append(headers.allocator, n);
|
||||
} else {
|
||||
const name_duped = if (headers.owned) try headers.allocator.dupe(u8, name) else name;
|
||||
errdefer if (headers.owned) headers.allocator.free(name_duped);
|
||||
|
||||
entry.name = name_duped;
|
||||
|
||||
var new_index = try HeaderIndexList.initCapacity(headers.allocator, 1);
|
||||
errdefer new_index.deinit(headers.allocator);
|
||||
|
||||
new_index.appendAssumeCapacity(n);
|
||||
try headers.index.put(headers.allocator, name_duped, new_index);
|
||||
}
|
||||
|
||||
try headers.list.append(headers.allocator, entry);
|
||||
}
|
||||
|
||||
pub fn contains(headers: Headers, name: []const u8) bool {
|
||||
return headers.index.contains(name);
|
||||
}
|
||||
|
||||
pub fn delete(headers: *Headers, name: []const u8) bool {
|
||||
if (headers.index.fetchRemove(name)) |kv| {
|
||||
var index = kv.value;
|
||||
|
||||
// iterate backwards
|
||||
var i = index.items.len;
|
||||
while (i > 0) {
|
||||
i -= 1;
|
||||
const data_index = index.items[i];
|
||||
const removed = headers.list.orderedRemove(data_index);
|
||||
|
||||
assert(ascii.eqlIgnoreCase(removed.name, name)); // ensure the index hasn't been corrupted
|
||||
if (headers.owned) headers.allocator.free(removed.value);
|
||||
}
|
||||
|
||||
if (headers.owned) headers.allocator.free(kv.key);
|
||||
index.deinit(headers.allocator);
|
||||
headers.rebuildIndex();
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the index of the first occurrence of a header with the given name.
|
||||
pub fn firstIndexOf(headers: Headers, name: []const u8) ?usize {
|
||||
const index = headers.index.get(name) orelse return null;
|
||||
|
||||
return index.items[0];
|
||||
}
|
||||
|
||||
/// Returns a list of indices containing headers with the given name.
|
||||
pub fn getIndices(headers: Headers, name: []const u8) ?[]const usize {
|
||||
const index = headers.index.get(name) orelse return null;
|
||||
|
||||
return index.items;
|
||||
}
|
||||
|
||||
/// Returns the entry of the first occurrence of a header with the given name.
|
||||
pub fn getFirstEntry(headers: Headers, name: []const u8) ?HeaderEntry {
|
||||
const first_index = headers.firstIndexOf(name) orelse return null;
|
||||
|
||||
return headers.list.items[first_index];
|
||||
}
|
||||
|
||||
/// Returns a slice containing each header with the given name.
|
||||
/// The caller owns the returned slice, but NOT the values in the slice.
|
||||
pub fn getEntries(headers: Headers, allocator: Allocator, name: []const u8) !?[]const HeaderEntry {
|
||||
const indices = headers.getIndices(name) orelse return null;
|
||||
|
||||
const buf = try allocator.alloc(HeaderEntry, indices.len);
|
||||
for (indices, 0..) |idx, n| {
|
||||
buf[n] = headers.list.items[idx];
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
/// Returns the value in the entry of the first occurrence of a header with the given name.
|
||||
pub fn getFirstValue(headers: Headers, name: []const u8) ?[]const u8 {
|
||||
const first_index = headers.firstIndexOf(name) orelse return null;
|
||||
|
||||
return headers.list.items[first_index].value;
|
||||
}
|
||||
|
||||
/// Returns a slice containing the value of each header with the given name.
|
||||
/// The caller owns the returned slice, but NOT the values in the slice.
|
||||
pub fn getValues(headers: Headers, allocator: Allocator, name: []const u8) !?[]const []const u8 {
|
||||
const indices = headers.getIndices(name) orelse return null;
|
||||
|
||||
const buf = try allocator.alloc([]const u8, indices.len);
|
||||
for (indices, 0..) |idx, n| {
|
||||
buf[n] = headers.list.items[idx].value;
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
fn rebuildIndex(headers: *Headers) void {
|
||||
// clear out the indexes
|
||||
var it = headers.index.iterator();
|
||||
while (it.next()) |entry| {
|
||||
entry.value_ptr.shrinkRetainingCapacity(0);
|
||||
}
|
||||
|
||||
// fill up indexes again; we know capacity is fine from before
|
||||
for (headers.list.items, 0..) |entry, i| {
|
||||
headers.index.getEntry(entry.name).?.value_ptr.appendAssumeCapacity(i);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sorts the headers in lexicographical order.
|
||||
pub fn sort(headers: *Headers) void {
|
||||
std.sort.sort(HeaderEntry, headers.list.items, {}, HeaderEntry.lessThan);
|
||||
headers.rebuildIndex();
|
||||
}
|
||||
|
||||
/// Writes the headers to the given stream.
|
||||
pub fn format(
|
||||
headers: Headers,
|
||||
comptime fmt: []const u8,
|
||||
options: std.fmt.FormatOptions,
|
||||
out_stream: anytype,
|
||||
) !void {
|
||||
_ = fmt;
|
||||
_ = options;
|
||||
|
||||
for (headers.list.items) |entry| {
|
||||
if (entry.value.len == 0) continue;
|
||||
|
||||
try out_stream.writeAll(entry.name);
|
||||
try out_stream.writeAll(": ");
|
||||
try out_stream.writeAll(entry.value);
|
||||
try out_stream.writeAll("\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes all of the headers with the given name to the given stream, separated by commas.
|
||||
///
|
||||
/// This is useful for headers like `Set-Cookie` which can have multiple values. RFC 9110, Section 5.2
|
||||
pub fn formatCommaSeparated(
|
||||
headers: Headers,
|
||||
name: []const u8,
|
||||
out_stream: anytype,
|
||||
) !void {
|
||||
const indices = headers.getIndices(name) orelse return;
|
||||
|
||||
try out_stream.writeAll(name);
|
||||
try out_stream.writeAll(": ");
|
||||
|
||||
for (indices, 0..) |idx, n| {
|
||||
if (n != 0) try out_stream.writeAll(", ");
|
||||
try out_stream.writeAll(headers.list.items[idx].value);
|
||||
}
|
||||
|
||||
try out_stream.writeAll("\r\n");
|
||||
}
|
||||
};
|
||||
|
||||
test "Headers.append" {
|
||||
var h = Headers{ .allocator = std.testing.allocator };
|
||||
defer h.deinit();
|
||||
|
||||
try h.append("foo", "bar");
|
||||
try h.append("hello", "world");
|
||||
|
||||
try testing.expect(h.contains("Foo"));
|
||||
try testing.expect(!h.contains("Bar"));
|
||||
}
|
||||
|
||||
test "Headers.delete" {
|
||||
var h = Headers{ .allocator = std.testing.allocator };
|
||||
defer h.deinit();
|
||||
|
||||
try h.append("foo", "bar");
|
||||
try h.append("hello", "world");
|
||||
|
||||
try testing.expect(h.contains("Foo"));
|
||||
|
||||
_ = h.delete("Foo");
|
||||
|
||||
try testing.expect(!h.contains("foo"));
|
||||
}
|
||||
|
||||
test "Headers consistency" {
|
||||
var h = Headers{ .allocator = std.testing.allocator };
|
||||
defer h.deinit();
|
||||
|
||||
try h.append("foo", "bar");
|
||||
try h.append("hello", "world");
|
||||
_ = h.delete("Foo");
|
||||
|
||||
try h.append("foo", "bar");
|
||||
try h.append("bar", "world");
|
||||
try h.append("foo", "baz");
|
||||
try h.append("baz", "hello");
|
||||
|
||||
try testing.expectEqual(@as(?usize, 0), h.firstIndexOf("hello"));
|
||||
try testing.expectEqual(@as(?usize, 1), h.firstIndexOf("foo"));
|
||||
try testing.expectEqual(@as(?usize, 2), h.firstIndexOf("bar"));
|
||||
try testing.expectEqual(@as(?usize, 4), h.firstIndexOf("baz"));
|
||||
try testing.expectEqual(@as(?usize, null), h.firstIndexOf("pog"));
|
||||
|
||||
try testing.expectEqualSlices(usize, &[_]usize{0}, h.getIndices("hello").?);
|
||||
try testing.expectEqualSlices(usize, &[_]usize{ 1, 3 }, h.getIndices("foo").?);
|
||||
try testing.expectEqualSlices(usize, &[_]usize{2}, h.getIndices("bar").?);
|
||||
try testing.expectEqualSlices(usize, &[_]usize{4}, h.getIndices("baz").?);
|
||||
try testing.expectEqual(@as(?[]const usize, null), h.getIndices("pog"));
|
||||
|
||||
try testing.expectEqualStrings("world", h.getFirstEntry("hello").?.value);
|
||||
try testing.expectEqualStrings("bar", h.getFirstEntry("foo").?.value);
|
||||
try testing.expectEqualStrings("world", h.getFirstEntry("bar").?.value);
|
||||
try testing.expectEqualStrings("hello", h.getFirstEntry("baz").?.value);
|
||||
|
||||
const hello_entries = (try h.getEntries(testing.allocator, "hello")).?;
|
||||
defer testing.allocator.free(hello_entries);
|
||||
try testing.expectEqualDeep(@as([]const HeaderEntry, &[_]HeaderEntry{
|
||||
.{ .name = "hello", .value = "world" },
|
||||
}), hello_entries);
|
||||
|
||||
const foo_entries = (try h.getEntries(testing.allocator, "foo")).?;
|
||||
defer testing.allocator.free(foo_entries);
|
||||
try testing.expectEqualDeep(@as([]const HeaderEntry, &[_]HeaderEntry{
|
||||
.{ .name = "foo", .value = "bar" },
|
||||
.{ .name = "foo", .value = "baz" },
|
||||
}), foo_entries);
|
||||
|
||||
const bar_entries = (try h.getEntries(testing.allocator, "bar")).?;
|
||||
defer testing.allocator.free(bar_entries);
|
||||
try testing.expectEqualDeep(@as([]const HeaderEntry, &[_]HeaderEntry{
|
||||
.{ .name = "bar", .value = "world" },
|
||||
}), bar_entries);
|
||||
|
||||
const baz_entries = (try h.getEntries(testing.allocator, "baz")).?;
|
||||
defer testing.allocator.free(baz_entries);
|
||||
try testing.expectEqualDeep(@as([]const HeaderEntry, &[_]HeaderEntry{
|
||||
.{ .name = "baz", .value = "hello" },
|
||||
}), baz_entries);
|
||||
|
||||
const pog_entries = (try h.getEntries(testing.allocator, "pog"));
|
||||
try testing.expectEqual(@as(?[]const HeaderEntry, null), pog_entries);
|
||||
|
||||
try testing.expectEqualStrings("world", h.getFirstValue("hello").?);
|
||||
try testing.expectEqualStrings("bar", h.getFirstValue("foo").?);
|
||||
try testing.expectEqualStrings("world", h.getFirstValue("bar").?);
|
||||
try testing.expectEqualStrings("hello", h.getFirstValue("baz").?);
|
||||
try testing.expectEqual(@as(?[]const u8, null), h.getFirstValue("pog"));
|
||||
|
||||
const hello_values = (try h.getValues(testing.allocator, "hello")).?;
|
||||
defer testing.allocator.free(hello_values);
|
||||
try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"world"}), hello_values);
|
||||
|
||||
const foo_values = (try h.getValues(testing.allocator, "foo")).?;
|
||||
defer testing.allocator.free(foo_values);
|
||||
try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{ "bar", "baz" }), foo_values);
|
||||
|
||||
const bar_values = (try h.getValues(testing.allocator, "bar")).?;
|
||||
defer testing.allocator.free(bar_values);
|
||||
try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"world"}), bar_values);
|
||||
|
||||
const baz_values = (try h.getValues(testing.allocator, "baz")).?;
|
||||
defer testing.allocator.free(baz_values);
|
||||
try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"hello"}), baz_values);
|
||||
|
||||
const pog_values = (try h.getValues(testing.allocator, "pog"));
|
||||
try testing.expectEqual(@as(?[]const []const u8, null), pog_values);
|
||||
|
||||
h.sort();
|
||||
|
||||
try testing.expectEqualSlices(usize, &[_]usize{0}, h.getIndices("bar").?);
|
||||
try testing.expectEqualSlices(usize, &[_]usize{1}, h.getIndices("baz").?);
|
||||
try testing.expectEqualSlices(usize, &[_]usize{ 2, 3 }, h.getIndices("foo").?);
|
||||
try testing.expectEqualSlices(usize, &[_]usize{4}, h.getIndices("hello").?);
|
||||
|
||||
const formatted_values = try std.fmt.allocPrint(testing.allocator, "{}", .{h});
|
||||
defer testing.allocator.free(formatted_values);
|
||||
|
||||
try testing.expectEqualStrings("bar: world\r\nbaz: hello\r\nfoo: bar\r\nfoo: baz\r\nhello: world\r\n", formatted_values);
|
||||
|
||||
var buf: [128]u8 = undefined;
|
||||
var fbs = std.io.fixedBufferStream(&buf);
|
||||
const writer = fbs.writer();
|
||||
|
||||
try h.formatCommaSeparated("foo", writer);
|
||||
try testing.expectEqualStrings("foo: bar, baz\r\n", fbs.getWritten());
|
||||
}
|
||||
@@ -157,134 +157,120 @@ pub const BufferedConnection = struct {
|
||||
|
||||
/// A HTTP request originating from a client.
|
||||
pub const Request = struct {
|
||||
pub const Headers = struct {
|
||||
method: http.Method,
|
||||
target: []const u8,
|
||||
version: http.Version,
|
||||
content_length: ?u64 = null,
|
||||
transfer_encoding: ?http.TransferEncoding = null,
|
||||
transfer_compression: ?http.ContentEncoding = null,
|
||||
connection: http.Connection = .close,
|
||||
host: ?[]const u8 = null,
|
||||
pub const ParseError = Allocator.Error || error{
|
||||
ShortHttpStatusLine,
|
||||
BadHttpVersion,
|
||||
UnknownHttpMethod,
|
||||
HttpHeadersInvalid,
|
||||
HttpHeaderContinuationsUnsupported,
|
||||
HttpTransferEncodingUnsupported,
|
||||
HttpConnectionHeaderUnsupported,
|
||||
InvalidCharacter,
|
||||
};
|
||||
|
||||
pub const ParseError = error{
|
||||
ShortHttpStatusLine,
|
||||
BadHttpVersion,
|
||||
UnknownHttpMethod,
|
||||
HttpHeadersInvalid,
|
||||
HttpHeaderContinuationsUnsupported,
|
||||
HttpTransferEncodingUnsupported,
|
||||
HttpConnectionHeaderUnsupported,
|
||||
InvalidCharacter,
|
||||
pub fn parse(req: *Request, bytes: []const u8) !void {
|
||||
var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n");
|
||||
|
||||
const first_line = it.next() orelse return error.HttpHeadersInvalid;
|
||||
if (first_line.len < 10)
|
||||
return error.ShortHttpStatusLine;
|
||||
|
||||
const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid;
|
||||
const method_str = first_line[0..method_end];
|
||||
const method = std.meta.stringToEnum(http.Method, method_str) orelse return error.UnknownHttpMethod;
|
||||
|
||||
const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid;
|
||||
if (version_start == method_end) return error.HttpHeadersInvalid;
|
||||
|
||||
const version_str = first_line[version_start + 1 ..];
|
||||
if (version_str.len != 8) return error.HttpHeadersInvalid;
|
||||
const version: http.Version = switch (int64(version_str[0..8])) {
|
||||
int64("HTTP/1.0") => .@"HTTP/1.0",
|
||||
int64("HTTP/1.1") => .@"HTTP/1.1",
|
||||
else => return error.BadHttpVersion,
|
||||
};
|
||||
|
||||
pub fn parse(bytes: []const u8) !Headers {
|
||||
var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n");
|
||||
const target = first_line[method_end + 1 .. version_start];
|
||||
|
||||
const first_line = it.next() orelse return error.HttpHeadersInvalid;
|
||||
if (first_line.len < 10)
|
||||
return error.ShortHttpStatusLine;
|
||||
req.method = method;
|
||||
req.target = target;
|
||||
req.version = version;
|
||||
|
||||
const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid;
|
||||
const method_str = first_line[0..method_end];
|
||||
const method = std.meta.stringToEnum(http.Method, method_str) orelse return error.UnknownHttpMethod;
|
||||
while (it.next()) |line| {
|
||||
if (line.len == 0) return error.HttpHeadersInvalid;
|
||||
switch (line[0]) {
|
||||
' ', '\t' => return error.HttpHeaderContinuationsUnsupported,
|
||||
else => {},
|
||||
}
|
||||
|
||||
const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid;
|
||||
if (version_start == method_end) return error.HttpHeadersInvalid;
|
||||
var line_it = mem.tokenize(u8, line, ": ");
|
||||
const header_name = line_it.next() orelse return error.HttpHeadersInvalid;
|
||||
const header_value = line_it.rest();
|
||||
|
||||
const version_str = first_line[version_start + 1 ..];
|
||||
if (version_str.len != 8) return error.HttpHeadersInvalid;
|
||||
const version: http.Version = switch (int64(version_str[0..8])) {
|
||||
int64("HTTP/1.0") => .@"HTTP/1.0",
|
||||
int64("HTTP/1.1") => .@"HTTP/1.1",
|
||||
else => return error.BadHttpVersion,
|
||||
};
|
||||
try req.headers.append(header_name, header_value);
|
||||
|
||||
const target = first_line[method_end + 1 .. version_start];
|
||||
if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {
|
||||
if (req.content_length != null) return error.HttpHeadersInvalid;
|
||||
req.content_length = try std.fmt.parseInt(u64, header_value, 10);
|
||||
} else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
|
||||
// Transfer-Encoding: second, first
|
||||
// Transfer-Encoding: deflate, chunked
|
||||
var iter = mem.splitBackwards(u8, header_value, ",");
|
||||
|
||||
var headers: Headers = .{
|
||||
.method = method,
|
||||
.target = target,
|
||||
.version = version,
|
||||
};
|
||||
if (iter.next()) |first| {
|
||||
const trimmed = mem.trim(u8, first, " ");
|
||||
|
||||
while (it.next()) |line| {
|
||||
if (line.len == 0) return error.HttpHeadersInvalid;
|
||||
switch (line[0]) {
|
||||
' ', '\t' => return error.HttpHeaderContinuationsUnsupported,
|
||||
else => {},
|
||||
}
|
||||
|
||||
var line_it = mem.tokenize(u8, line, ": ");
|
||||
const header_name = line_it.next() orelse return error.HttpHeadersInvalid;
|
||||
const header_value = line_it.rest();
|
||||
if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {
|
||||
if (headers.content_length != null) return error.HttpHeadersInvalid;
|
||||
headers.content_length = try std.fmt.parseInt(u64, header_value, 10);
|
||||
} else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
|
||||
// Transfer-Encoding: second, first
|
||||
// Transfer-Encoding: deflate, chunked
|
||||
var iter = mem.splitBackwards(u8, header_value, ",");
|
||||
|
||||
if (iter.next()) |first| {
|
||||
const trimmed = mem.trim(u8, first, " ");
|
||||
|
||||
if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| {
|
||||
if (headers.transfer_encoding != null) return error.HttpHeadersInvalid;
|
||||
headers.transfer_encoding = te;
|
||||
} else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
|
||||
if (headers.transfer_compression != null) return error.HttpHeadersInvalid;
|
||||
headers.transfer_compression = ce;
|
||||
} else {
|
||||
return error.HttpTransferEncodingUnsupported;
|
||||
}
|
||||
}
|
||||
|
||||
if (iter.next()) |second| {
|
||||
if (headers.transfer_compression != null) return error.HttpTransferEncodingUnsupported;
|
||||
|
||||
const trimmed = mem.trim(u8, second, " ");
|
||||
|
||||
if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
|
||||
headers.transfer_compression = ce;
|
||||
} else {
|
||||
return error.HttpTransferEncodingUnsupported;
|
||||
}
|
||||
}
|
||||
|
||||
if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;
|
||||
} else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {
|
||||
if (headers.transfer_compression != null) return error.HttpHeadersInvalid;
|
||||
|
||||
const trimmed = mem.trim(u8, header_value, " ");
|
||||
|
||||
if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
|
||||
headers.transfer_compression = ce;
|
||||
if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| {
|
||||
if (req.transfer_encoding != null) return error.HttpHeadersInvalid;
|
||||
req.transfer_encoding = te;
|
||||
} else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
|
||||
if (req.transfer_compression != null) return error.HttpHeadersInvalid;
|
||||
req.transfer_compression = ce;
|
||||
} else {
|
||||
return error.HttpTransferEncodingUnsupported;
|
||||
}
|
||||
} else if (std.ascii.eqlIgnoreCase(header_name, "connection")) {
|
||||
if (std.ascii.eqlIgnoreCase(header_value, "keep-alive")) {
|
||||
headers.connection = .keep_alive;
|
||||
} else if (std.ascii.eqlIgnoreCase(header_value, "close")) {
|
||||
headers.connection = .close;
|
||||
}
|
||||
|
||||
if (iter.next()) |second| {
|
||||
if (req.transfer_compression != null) return error.HttpTransferEncodingUnsupported;
|
||||
|
||||
const trimmed = mem.trim(u8, second, " ");
|
||||
|
||||
if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
|
||||
req.transfer_compression = ce;
|
||||
} else {
|
||||
return error.HttpConnectionHeaderUnsupported;
|
||||
return error.HttpTransferEncodingUnsupported;
|
||||
}
|
||||
} else if (std.ascii.eqlIgnoreCase(header_name, "host")) {
|
||||
headers.host = header_value;
|
||||
}
|
||||
|
||||
if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;
|
||||
} else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {
|
||||
if (req.transfer_compression != null) return error.HttpHeadersInvalid;
|
||||
|
||||
const trimmed = mem.trim(u8, header_value, " ");
|
||||
|
||||
if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
|
||||
req.transfer_compression = ce;
|
||||
} else {
|
||||
return error.HttpTransferEncodingUnsupported;
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
|
||||
inline fn int64(array: *const [8]u8) u64 {
|
||||
return @bitCast(u64, array.*);
|
||||
}
|
||||
};
|
||||
inline fn int64(array: *const [8]u8) u64 {
|
||||
return @bitCast(u64, array.*);
|
||||
}
|
||||
|
||||
headers: Headers = undefined,
|
||||
method: http.Method,
|
||||
target: []const u8,
|
||||
version: http.Version,
|
||||
|
||||
content_length: ?u64 = null,
|
||||
transfer_encoding: ?http.TransferEncoding = null,
|
||||
transfer_compression: ?http.ContentEncoding = null,
|
||||
|
||||
headers: http.Headers = undefined,
|
||||
parser: proto.HeadersParser,
|
||||
compression: Compression = .none,
|
||||
};
|
||||
@@ -295,23 +281,17 @@ pub const Request = struct {
|
||||
/// Order of operations: accept -> wait -> do [ -> write -> finish][ -> reset /]
|
||||
/// \ -> read /
|
||||
pub const Response = struct {
|
||||
pub const Headers = struct {
|
||||
version: http.Version = .@"HTTP/1.1",
|
||||
status: http.Status = .ok,
|
||||
reason: ?[]const u8 = null,
|
||||
version: http.Version = .@"HTTP/1.1",
|
||||
status: http.Status = .ok,
|
||||
reason: ?[]const u8 = null,
|
||||
|
||||
server: ?[]const u8 = "zig (std.http)",
|
||||
connection: http.Connection = .keep_alive,
|
||||
transfer_encoding: RequestTransfer = .none,
|
||||
|
||||
custom: []const http.CustomHeader = &[_]http.CustomHeader{},
|
||||
};
|
||||
transfer_encoding: ResponseTransfer = .none,
|
||||
|
||||
server: *Server,
|
||||
address: net.Address,
|
||||
connection: BufferedConnection,
|
||||
|
||||
headers: Headers = .{},
|
||||
headers: http.Headers,
|
||||
request: Request,
|
||||
|
||||
/// Reset this response to its initial state. This must be called before handling a second request on the same connection.
|
||||
@@ -346,41 +326,54 @@ pub const Response = struct {
|
||||
var buffered = std.io.bufferedWriter(res.connection.writer());
|
||||
const w = buffered.writer();
|
||||
|
||||
try w.writeAll(@tagName(res.headers.version));
|
||||
try w.writeAll(@tagName(res.version));
|
||||
try w.writeByte(' ');
|
||||
try w.print("{d}", .{@enumToInt(res.headers.status)});
|
||||
try w.print("{d}", .{@enumToInt(res.status)});
|
||||
try w.writeByte(' ');
|
||||
if (res.headers.reason) |reason| {
|
||||
if (res.reason) |reason| {
|
||||
try w.writeAll(reason);
|
||||
} else if (res.headers.status.phrase()) |phrase| {
|
||||
} else if (res.status.phrase()) |phrase| {
|
||||
try w.writeAll(phrase);
|
||||
}
|
||||
try w.writeAll("\r\n");
|
||||
|
||||
if (res.headers.server) |server| {
|
||||
try w.writeAll("\r\nServer: ");
|
||||
try w.writeAll(server);
|
||||
if (!res.headers.contains("server")) {
|
||||
try w.writeAll("Server: zig (std.http)\r\n");
|
||||
}
|
||||
|
||||
if (res.headers.connection == .close) {
|
||||
try w.writeAll("\r\nConnection: close");
|
||||
if (!res.headers.contains("connection")) {
|
||||
try w.writeAll("Connection: keep-alive\r\n");
|
||||
}
|
||||
|
||||
const has_transfer_encoding = res.headers.contains("transfer-encoding");
|
||||
const has_content_length = res.headers.contains("content-length");
|
||||
|
||||
if (!has_transfer_encoding and !has_content_length) {
|
||||
switch (res.transfer_encoding) {
|
||||
.chunked => try w.writeAll("Transfer-Encoding: chunked\r\n"),
|
||||
.content_length => |content_length| try w.print("Content-Length: {d}\r\n", .{content_length}),
|
||||
.none => {},
|
||||
}
|
||||
} else {
|
||||
try w.writeAll("\r\nConnection: keep-alive");
|
||||
if (has_content_length) {
|
||||
const content_length = try std.fmt.parseInt(u64, res.headers.getFirstValue("content-length").?, 10);
|
||||
|
||||
res.transfer_encoding = .{ .content_length = content_length };
|
||||
} else if (has_transfer_encoding) {
|
||||
const transfer_encoding = res.headers.getFirstValue("content-length").?;
|
||||
if (std.mem.eql(u8, transfer_encoding, "chunked")) {
|
||||
res.transfer_encoding = .chunked;
|
||||
} else {
|
||||
return error.UnsupportedTransferEncoding;
|
||||
}
|
||||
} else {
|
||||
res.transfer_encoding = .none;
|
||||
}
|
||||
}
|
||||
|
||||
switch (res.headers.transfer_encoding) {
|
||||
.chunked => try w.writeAll("\r\nTransfer-Encoding: chunked"),
|
||||
.content_length => |content_length| try w.print("\r\nContent-Length: {d}", .{content_length}),
|
||||
.none => {},
|
||||
}
|
||||
try w.print("{}", .{res.headers});
|
||||
|
||||
for (res.headers.custom) |header| {
|
||||
try w.writeAll("\r\n");
|
||||
try w.writeAll(header.name);
|
||||
try w.writeAll(": ");
|
||||
try w.writeAll(header.value);
|
||||
}
|
||||
|
||||
try w.writeAll("\r\n\r\n");
|
||||
try w.writeAll("\r\n");
|
||||
|
||||
try buffered.flush();
|
||||
}
|
||||
@@ -419,22 +412,28 @@ pub const Response = struct {
|
||||
if (res.request.parser.state.isContent()) break;
|
||||
}
|
||||
|
||||
res.request.headers = try Request.Headers.parse(res.request.parser.header_bytes.items);
|
||||
res.request.headers = .{ .allocator = res.server.allocator, .owned = true };
|
||||
try res.request.parse(res.request.parser.header_bytes.items);
|
||||
|
||||
if (res.headers.connection == .keep_alive and res.request.headers.connection == .keep_alive) {
|
||||
const res_connection = res.headers.getFirstValue("connection");
|
||||
const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?);
|
||||
|
||||
const req_connection = res.request.headers.getFirstValue("connection");
|
||||
const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);
|
||||
if (res_keepalive and req_keepalive) {
|
||||
res.connection.conn.closing = false;
|
||||
} else {
|
||||
res.connection.conn.closing = true;
|
||||
}
|
||||
|
||||
if (res.request.headers.transfer_encoding) |te| {
|
||||
if (res.request.transfer_encoding) |te| {
|
||||
switch (te) {
|
||||
.chunked => {
|
||||
res.request.parser.next_chunk_length = 0;
|
||||
res.request.parser.state = .chunk_head_size;
|
||||
},
|
||||
}
|
||||
} else if (res.request.headers.content_length) |cl| {
|
||||
} else if (res.request.content_length) |cl| {
|
||||
res.request.parser.next_chunk_length = cl;
|
||||
|
||||
if (cl == 0) res.request.parser.done = true;
|
||||
@@ -443,7 +442,7 @@ pub const Response = struct {
|
||||
}
|
||||
|
||||
if (!res.request.parser.done) {
|
||||
if (res.request.headers.transfer_compression) |tc| switch (tc) {
|
||||
if (res.request.transfer_compression) |tc| switch (tc) {
|
||||
.compress => return error.CompressionNotSupported,
|
||||
.deflate => res.request.compression = .{
|
||||
.deflate = try std.compress.zlib.zlibStream(res.server.allocator, res.transferReader()),
|
||||
@@ -495,7 +494,7 @@ pub const Response = struct {
|
||||
|
||||
/// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent.
|
||||
pub fn write(res: *Response, bytes: []const u8) WriteError!usize {
|
||||
switch (res.headers.transfer_encoding) {
|
||||
switch (res.transfer_encoding) {
|
||||
.chunked => {
|
||||
try res.connection.writer().print("{x}\r\n", .{bytes.len});
|
||||
try res.connection.writeAll(bytes);
|
||||
@@ -525,7 +524,7 @@ pub const Response = struct {
|
||||
};
|
||||
|
||||
/// The mode of transport for responses.
|
||||
pub const RequestTransfer = union(enum) {
|
||||
pub const ResponseTransfer = union(enum) {
|
||||
content_length: u64,
|
||||
chunked: void,
|
||||
none: void,
|
||||
@@ -588,7 +587,11 @@ pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response {
|
||||
.stream = in.stream,
|
||||
.protocol = .plain,
|
||||
} },
|
||||
.headers = .{ .allocator = server.allocator },
|
||||
.request = .{
|
||||
.version = undefined,
|
||||
.method = undefined,
|
||||
.target = undefined,
|
||||
.parser = switch (options) {
|
||||
.dynamic => |max| proto.HeadersParser.initDynamic(max),
|
||||
.static => |buf| proto.HeadersParser.initStatic(buf),
|
||||
|
||||
@@ -479,7 +479,10 @@ fn fetchAndUnpack(
|
||||
};
|
||||
defer tmp_directory.closeAndFree(gpa);
|
||||
|
||||
var req = try http_client.request(uri, .{}, .{});
|
||||
var h = std.http.Headers{ .allocator = gpa };
|
||||
defer h.deinit();
|
||||
|
||||
var req = try http_client.request(uri, h, .{ .method = .GET });
|
||||
defer req.deinit();
|
||||
|
||||
try req.do();
|
||||
|
||||
Reference in New Issue
Block a user