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") ); }