zig/lib/std/http/Client.zig

142 lines
4.4 KiB
Zig

const std = @import("../std.zig");
const assert = std.debug.assert;
const http = std.http;
const net = std.net;
const Client = @This();
allocator: std.mem.Allocator,
headers: std.ArrayListUnmanaged(u8) = .{},
active_requests: usize = 0,
pub const Request = struct {
client: *Client,
stream: net.Stream,
headers: std.ArrayListUnmanaged(u8) = .{},
tls: std.crypto.Tls,
protocol: Protocol,
pub const Protocol = enum { http, https };
pub const Options = struct {
family: Family = .any,
protocol: Protocol = .https,
method: http.Method = .GET,
host: []const u8 = "localhost",
path: []const u8 = "/",
port: u16 = 0,
pub const Family = enum { any, ip4, ip6 };
};
pub fn deinit(req: *Request) void {
req.client.active_requests -= 1;
req.headers.deinit(req.client.allocator);
req.* = undefined;
}
pub fn addHeader(req: *Request, name: []const u8, value: []const u8) !void {
const gpa = req.client.allocator;
// Ensure an extra +2 for the \r\n in end()
try req.headers.ensureUnusedCapacity(gpa, name.len + value.len + 6);
req.headers.appendSliceAssumeCapacity(name);
req.headers.appendSliceAssumeCapacity(": ");
req.headers.appendSliceAssumeCapacity(value);
req.headers.appendSliceAssumeCapacity("\r\n");
}
pub fn end(req: *Request) !void {
req.headers.appendSliceAssumeCapacity("\r\n");
switch (req.protocol) {
.http => {
try req.stream.writeAll(req.headers.items);
},
.https => {
try req.tls.writeAll(req.stream, req.headers.items);
},
}
}
pub fn read(req: *Request, buffer: []u8) !usize {
switch (req.protocol) {
.http => return req.stream.read(buffer),
.https => return req.tls.read(req.stream, buffer),
}
}
pub fn readAll(req: *Request, buffer: []u8) !usize {
return readAtLeast(req, buffer, buffer.len);
}
pub fn readAtLeast(req: *Request, buffer: []u8, len: usize) !usize {
var index: usize = 0;
while (index < len) {
const amt = try req.read(buffer[index..]);
if (amt == 0) {
switch (req.protocol) {
.http => break,
.https => if (req.tls.eof) break,
}
}
index += amt;
}
return index;
}
};
pub fn deinit(client: *Client) void {
assert(client.active_requests == 0);
client.headers.deinit(client.allocator);
client.* = undefined;
}
pub fn request(client: *Client, options: Request.Options) !Request {
var req: Request = .{
.client = client,
.stream = try net.tcpConnectToHost(client.allocator, options.host, options.port),
.protocol = options.protocol,
.tls = undefined,
};
client.active_requests += 1;
errdefer req.deinit();
switch (options.protocol) {
.http => {},
.https => {
req.tls = try std.crypto.Tls.init(req.stream, options.host);
},
}
try req.headers.ensureUnusedCapacity(
client.allocator,
@tagName(options.method).len +
1 +
options.path.len +
" HTTP/1.1\r\nHost: ".len +
options.host.len +
"\r\nUpgrade-Insecure-Requests: 1\r\n".len +
client.headers.items.len +
2, // for the \r\n at the end of headers
);
req.headers.appendSliceAssumeCapacity(@tagName(options.method));
req.headers.appendSliceAssumeCapacity(" ");
req.headers.appendSliceAssumeCapacity(options.path);
req.headers.appendSliceAssumeCapacity(" HTTP/1.1\r\nHost: ");
req.headers.appendSliceAssumeCapacity(options.host);
switch (options.protocol) {
.https => req.headers.appendSliceAssumeCapacity("\r\nUpgrade-Insecure-Requests: 1\r\n"),
.http => req.headers.appendSliceAssumeCapacity("\r\n"),
}
req.headers.appendSliceAssumeCapacity(client.headers.items);
return req;
}
pub fn addHeader(client: *Client, name: []const u8, value: []const u8) !void {
const gpa = client.allocator;
try client.headers.ensureUnusedCapacity(gpa, name.len + value.len + 4);
client.headers.appendSliceAssumeCapacity(name);
client.headers.appendSliceAssumeCapacity(": ");
client.headers.appendSliceAssumeCapacity(value);
client.headers.appendSliceAssumeCapacity("\r\n");
}