diff options
author | Ekaitz Zarraga <ekaitz@elenq.tech> | 2023-04-09 22:28:36 +0200 |
---|---|---|
committer | Ekaitz Zarraga <ekaitz@elenq.tech> | 2023-04-09 22:28:36 +0200 |
commit | 57982c29668ffb45fb0b7a574fee34261978453a (patch) | |
tree | a06dca8e2e9252f78365afc3c153f05fef545e58 /src/http.zig |
Some http random parser and stuff
Diffstat (limited to 'src/http.zig')
-rw-r--r-- | src/http.zig | 392 |
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") ); +} |