summaryrefslogtreecommitdiff
path: root/src/http.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/http.zig')
-rw-r--r--src/http.zig392
1 files changed, 392 insertions, 0 deletions
diff --git a/src/http.zig b/src/http.zig
new file mode 100644
index 0000000..49b4e71
--- /dev/null
+++ b/src/http.zig
@@ -0,0 +1,392 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const split = std.mem.split;
+const net = std.net;
+const expect = std.testing.expect;
+const expectError = std.testing.expectError;
+const StringHashMap = std.StringHashMap;
+
+pub const HttpError = error {
+ ParsingError
+};
+
+/// HttpStatus
+pub const HttpStatus = struct{
+ protocol: []const u8,
+ status: usize,
+ status_msg: []const u8,
+};
+
+pub const HttpStatusParser = struct{
+ status: HttpStatus,
+ rawline: []u8,
+ allocator: Allocator,
+
+ const Self = @This();
+ pub fn init(allocator: Allocator) Self{
+ return Self{
+ .status = undefined, // Can I do this?
+ .rawline = "",
+ .allocator = allocator,
+ };
+ }
+ /// Takes a reader and consumes one status line of HTTP response allocating
+ /// it's own copy of the line. Must call `deinit()` to clean the
+ /// allocations.
+ /// Fills it's `status` field with the results of that.
+ pub fn parseReader(self: *Self, reader: anytype) !void{
+ self.rawline = try reader.readUntilDelimiterAlloc(self.allocator, '\r', 1000);
+ // Drop \n
+ _ = try reader.readByte();
+
+ var components = split(u8, self.rawline, " ");
+
+ const p = if(components.next()) |p| p else return error.ParsingError;
+ const s = if(components.next()) |s| s else return error.ParsingError;
+ const sm = components.rest();
+ if (sm.len == 0) return error.ParsingError;
+ self.status.protocol = p;
+ self.status.status = try std.fmt.parseUnsigned(usize, s, 10);
+ self.status.status_msg = sm;
+ }
+ pub fn deinit(self: *Self) void{
+ self.allocator.free(self.rawline);
+ }
+};
+
+
+test "200 OK is parsed correctly in an HttpStatus" {
+ const status_line = "HTTP/1.1 200 OK\r\n";
+ const allocator = std.testing.allocator;
+
+ var fis = std.io.fixedBufferStream(status_line);
+ const reader = fis.reader();
+
+ var status = HttpStatusParser.init(allocator);
+ defer status.deinit();
+ try status.parseReader(reader);
+
+ try expect( std.mem.eql(u8, status.status.protocol, "HTTP/1.1") );
+ try expect( status.status.status == 200 );
+ try expect( std.mem.eql(u8, status.status.status_msg, "OK") );
+
+}
+
+test "306 Switch Proxy is parsed correctly in an HttpStatus" {
+ const status_line = "HTTP/1.1 306 Switch Proxy\r\n";
+ const allocator = std.testing.allocator;
+
+ var fis = std.io.fixedBufferStream(status_line);
+ const reader = fis.reader();
+
+ var status = HttpStatusParser.init(allocator);
+ defer status.deinit();
+ try status.parseReader(reader);
+
+ try expect( std.mem.eql(u8, status.status.protocol, "HTTP/1.1") );
+ try expect( status.status.status == 306 );
+ try expect( std.mem.eql(u8, status.status.status_msg, "Switch Proxy") );
+}
+
+test "Broken statusline is detected correctly in an HttpStatus" {
+ const status_line = "HTTP/1.1 306 \r\n";
+ const allocator = std.testing.allocator;
+ var fis = std.io.fixedBufferStream(status_line);
+ const reader = fis.reader();
+
+ var status = HttpStatusParser.init(allocator);
+ defer status.deinit();
+ try expectError( error.ParsingError, status.parseReader(reader) );
+}
+
+
+/// HttpHeaders
+
+pub const HttpHeaders = struct{
+ values: StringHashMap([]const u8),
+ owned_data: std.ArrayList([]u8),
+ allocator: Allocator,
+
+ const Self = @This();
+ pub fn init(allocator: Allocator) Self{
+ return Self{
+ .values = StringHashMap([]const u8).init(allocator),
+ .allocator = allocator,
+ .owned_data = std.ArrayList([]u8).init(allocator),
+ };
+ }
+ pub fn get(self: *Self, k: []const u8) !?[]const u8{
+ var key:[]u8 = try self.allocator.alloc(u8, k.len);
+ defer self.allocator.free(key);
+ for (key) |*char, index|{
+ char.* = std.ascii.toLower(k[index]);
+ }
+ return self.values.get(key);
+ }
+ pub fn put(self: *Self, k: []const u8, v: []const u8) !void{
+ // Own key
+ var key:[]u8 = try self.allocator.alloc(u8, k.len);
+ for (key) |*char,index|{
+ char.* = std.ascii.toLower(char.*);
+ char.* = std.ascii.toLower(k[index]);
+ }
+ try self.owned_data.append(key);
+
+ // Own value
+ var value:[]u8 = try self.allocator.alloc(u8, v.len);
+ std.mem.copy(u8, value, v);
+ try self.owned_data.append(value);
+
+ // Store header
+ try self.values.put(key, value);
+ }
+ pub fn deinit(self: *Self) void{
+ self.values.deinit();
+ for (self.owned_data.items) |item|{
+ self.allocator.free(item);
+ }
+ self.owned_data.deinit();
+ }
+};
+
+pub const HttpHeaderParser = struct{
+ headers: HttpHeaders,
+ allocator: Allocator,
+
+ const Self = @This();
+ pub fn init(allocator: Allocator) Self{
+ return Self{
+ .headers = HttpHeaders.init(allocator),
+ .allocator = allocator,
+ };
+ }
+ pub fn parseReader(self: *Self, reader: anytype) !void{
+ while(true) {
+ var line:[100000]u8 = undefined;
+ var slice = try reader.readUntilDelimiter(&line, '\r');
+ // Drop \n
+ _ = try reader.readByte();
+
+ if(slice.len == 0){
+ return; // Finished header block
+ }
+ var hit = std.mem.split(u8, slice, ":");
+ const key = if (hit.next()) |k| k else return error.ParsingError;
+ const val = hit.rest();
+ if (val.len == 0){
+ return error.ParsingError;
+ }
+ var spaces:usize = 0;
+ for(val) |ch|{
+ if(ch == ' '){
+ spaces = spaces + 1;
+ } else {
+ break;
+ }
+ } else {
+ return error.ParsingError;
+ }
+
+ try self.headers.put(key, val[spaces..]);
+ }
+ }
+ pub fn deinit(self: *Self) void{
+ self.headers.deinit();
+ }
+};
+
+
+
+test "Header block is parsed correctly, is case-insensitive and ignores spaces after :" {
+ const allocator = std.testing.allocator;
+
+ const block =
+ "Host: api.open-meteo.com\r\n" ++
+ "Transfer-Encoding: chunked\r\n" ++
+ "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/12.0\r\n" ++
+ "\r\n";
+ var fis = std.io.fixedBufferStream(block);
+ const reader = fis.reader();
+
+ var headers = HttpHeaderParser.init(allocator);
+ defer headers.deinit();
+
+ try headers.parseReader(reader);
+ if (try headers.headers.get("HOST")) |host| {
+ try expect( std.mem.eql(u8, host, "api.open-meteo.com") );
+ }
+ if (try headers.headers.get("host")) |host| {
+ try expect( std.mem.eql(u8, host, "api.open-meteo.com") );
+ }
+ if (try headers.headers.get("Transfer-Encoding")) |tencoding| {
+ try expect( std.mem.eql(u8, tencoding, "chunked") );
+ }
+ if (try headers.headers.get("User-Agent")) |useragent| {
+ try expect( std.mem.eql(u8, useragent,
+ "Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/12.0") );
+ }
+}
+
+test "Header line with no semicolon is a parsing error" {
+ const allocator = std.testing.allocator;
+
+ const block =
+ "Host api.open-meteo.com\r\n" ++
+ "Transfer-Encoding: chunked\r\n" ++
+ "\r\n";
+ var fis = std.io.fixedBufferStream(block);
+ const reader = fis.reader();
+
+ var headers = HttpHeaderParser.init(allocator);
+ defer headers.deinit();
+
+ try expectError(error.ParsingError, headers.parseReader(reader));
+}
+
+
+/// HttpBody
+
+pub const HttpBodyParser = struct {
+ data: []u8,
+ allocator: Allocator,
+
+ const Self = @This();
+ pub fn init(allocator: Allocator) !Self{
+ return Self{
+ .data = try allocator.create([0]u8),
+ .allocator = allocator,
+ };
+ }
+ pub fn deinit(self: *Self) void{
+ self.allocator.free(self.data);
+ }
+ pub fn parseChunkedReader(self: *Self, body_reader: anytype) !void{
+ var cursor:usize = 0;
+ while (true){
+ var buffer:[100]u8 = undefined;
+ // Read chunk size line
+ var size = try body_reader.readUntilDelimiter(&buffer, '\r');
+ _ = try body_reader.readByte(); // Drop \n
+ // Parse the number
+ const chunk_size = try std.fmt.parseUnsigned(usize, size, 16);
+ // If chunk is empty finish
+ if( chunk_size == 0 ){ return; }
+
+ // Reallocate the data to fit the incoming chunk
+ self.data = try self.allocator.realloc(self.data, cursor + chunk_size);
+ // Read all the bytes of the chunk
+ for( self.data[cursor..cursor+chunk_size] ) |*byte|{
+ byte.* = try body_reader.readByte();
+ }
+ // Drop \r\n
+ _ = try body_reader.readByte();
+ _ = try body_reader.readByte();
+ cursor = cursor + chunk_size;
+ }
+ }
+ pub fn parseFixedSizeReader(self: *Self, body_reader: anytype, size: usize) !void{
+ self.allocator.free(self.data);
+ self.data = try body_reader.readAllAlloc(self.allocator, size);
+ }
+};
+
+test "Parse chunked body from Reader"{
+ const chunked_body =
+ "7\r\n" ++
+ "Mozilla\r\n" ++
+ "12\r\n" ++
+ " Developer Network\r\n" ++
+ "0\r\n" ++
+ "\r\n".*;
+ var fis = std.io.fixedBufferStream(chunked_body);
+ const reader = fis.reader();
+
+ const allocator = std.testing.allocator;
+ var body = try HttpBodyParser.init(allocator);
+ defer body.deinit();
+
+ try body.parseChunkedReader(reader);
+
+ try expect( std.mem.eql(u8, "Mozilla Developer Network", body.data) );
+}
+
+/// HttpResponse
+
+pub const HttpResponseParser = struct{
+ statusparser: HttpStatusParser,
+ headerparser: HttpHeaderParser,
+ bodyparser: HttpBodyParser,
+
+ const Self = @This();
+ pub fn init(allocator: Allocator) !Self{
+ return Self{
+ .statusparser = HttpStatusParser.init(allocator),
+ .headerparser = HttpHeaderParser.init(allocator),
+ .bodyparser = try HttpBodyParser.init(allocator),
+ };
+ }
+ pub fn parseReader(self: *Self, reader: anytype) !void{
+ try self.statusparser.parseReader(reader);
+ try self.headerparser.parseReader(reader);
+ if(try self.headerparser.headers.get("Transfer-Encoding")) |te|{
+ if( std.mem.containsAtLeast(u8, te, 1, "chunked") ) {
+ try self.bodyparser.parseChunkedReader(reader);
+ }
+ } else {
+ if(try self.headerparser.headers.get("Content-Length")) |cl|{
+ const content_length = try std.fmt.parseUnsigned(usize, cl, 10);
+ try self.bodyparser.parseFixedSizeReader(reader, content_length);
+ }
+ }
+ }
+ pub fn deinit(self: *Self) void{
+ self.statusparser.deinit();
+ self.headerparser.deinit();
+ self.bodyparser.deinit();
+ }
+
+ pub fn body(self: Self) []u8{
+ return self.bodyparser.data;
+ }
+ pub fn headers(self: Self) HttpHeaders{
+ return self.headerparser.headers;
+ }
+ pub fn status(self: Self) usize{
+ return self.statusparser.status.status;
+ }
+ pub fn statusMsg(self: Self) []const u8{
+ return self.statusparser.status.status_msg;
+ }
+ pub fn protocol(self: Self) []const u8{
+ return self.statusparser.status.protocol;
+ }
+};
+
+// Full answer parser
+
+test "Parse full response correctly" {
+ const responsedata =
+ "HTTP/1.1 200 OK\r\n" ++
+ "Content-Type: text/plain\r\n" ++
+ "Transfer-Encoding: chunked\r\n" ++
+ "\r\n" ++
+ "7\r\n" ++
+ "Mozilla\r\n" ++
+ "12\r\n" ++
+ " Developer Network\r\n" ++
+ "0\r\n" ++
+ "\r\n";
+
+ var fis = std.io.fixedBufferStream(responsedata);
+ const reader = fis.reader();
+
+ const allocator = std.testing.allocator;
+ var response = try HttpResponseParser.init(allocator);
+ defer response.deinit();
+
+ try response.parseReader(reader);
+
+ try expect( std.mem.eql(u8, response.body(), "Mozilla Developer Network") );
+ try expect( response.status() == 200 );
+ try expect( std.mem.eql(u8, response.statusMsg(), "OK") );
+}