├── .gitattributes ├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── build.zig ├── gyro.zzz └── src ├── client.zig ├── connection.zig ├── main.zig ├── request.zig ├── response.zig ├── socket.zig └── tests.zig /.gitattributes: -------------------------------------------------------------------------------- 1 | *.zig text eol=lf 2 | *.zzz text eol=lf 3 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: requestz 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | 8 | jobs: 9 | tests: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Setup Zig 15 | uses: goto-bus-stop/setup-zig@v1.3.0 16 | with: 17 | version: master 18 | - name: Setup Gyro 19 | uses: mattnite/setup-gyro@v1 20 | - run: gyro build test 21 | - run: zig fmt --check src build.zig 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build artifacts 2 | zig-cache/ 3 | 4 | # Gyro 5 | deps.zig 6 | .gyro 7 | gyro.lock 8 | zig-out/ 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The BSD Zero Clause License (0BSD) 2 | 3 | Copyright (c) 2020 ducdetronquito 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 10 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 12 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 13 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 14 | PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Requestz 2 | 3 | An HTTP client inspired by [httpx](https://github.com/encode/httpx) and [ureq](https://github.com/algesten/ureq). 4 | 5 | [![Build Status](https://api.travis-ci.org/ducdetronquito/requestz.svg?branch=master)](https://travis-ci.org/ducdetronquito/requestz) [![License](https://img.shields.io/badge/License-BSD%200--Clause-ff69b4.svg)](https://github.com/ducdetronquito/requestz#license) [![Requirements](https://img.shields.io/badge/zig-master_(19.08.2021)-orange)](https://ziglang.org/) 6 | 7 | ⚠️ I'm currently renovating an old house which does not allow me to work on [requestz](https://github.com/ducdetronquito/requestz/), [h11](https://github.com/ducdetronquito/h11/) and [http](https://github.com/ducdetronquito/http) anymore. Feel free to fork or borrow some ideas if there are any good ones :) 8 | 9 | ## Installation 10 | 11 | *requestz* is available on [astrolabe.pm](https://astrolabe.pm/) via [gyro](https://github.com/mattnite/gyro) 12 | 13 | ``` 14 | gyro add ducdetronquito/requestz 15 | ``` 16 | 17 | ## Usage 18 | 19 | Send a GET request 20 | ```zig 21 | const client = @import("requestz.zig").Client; 22 | 23 | var client = try Client.init(std.testing.allocator); 24 | defer client.deinit(); 25 | 26 | var response = try client.get("http://httpbin.org/get", .{}); 27 | defer response.deinit(); 28 | ``` 29 | 30 | Send a request with headers 31 | ```zig 32 | const Headers = @import("http").Headers; 33 | 34 | var headers = Headers.init(std.testing.allocator); 35 | defer headers.deinit(); 36 | try headers.append("Gotta-go", "Fast!"); 37 | 38 | var response = try client.get("http://httpbin.org/get", .{ .headers = headers.items() }); 39 | defer response.deinit(); 40 | ``` 41 | 42 | Send a request with compile-time headers 43 | ```zig 44 | var headers = .{ 45 | .{"Gotta-go", "Fast!"} 46 | }; 47 | 48 | var response = try client.get("http://httpbin.org/get", .{ .headers = headers }); 49 | defer response.deinit(); 50 | ``` 51 | 52 | Send binary data along with a POST request 53 | ```zig 54 | var response = try client.post("http://httpbin.org/post", .{ .content = "Gotta go fast!" }); 55 | defer response.deinit(); 56 | 57 | var tree = try response.json(); 58 | defer tree.deinit(); 59 | ``` 60 | 61 | Stream a response 62 | ```zig 63 | var response = try client.stream(.Get, "http://httpbin.org/", .{}); 64 | defer response.deinit(); 65 | 66 | while(true) { 67 | var buffer: [4096]u8 = undefined; 68 | var bytesRead = try response.read(&buffer); 69 | if (bytesRead == 0) { 70 | break; 71 | } 72 | std.debug.print("{}", .{buffer[0..bytesRead]}); 73 | } 74 | ``` 75 | 76 | Other standard HTTP method shortcuts: 77 | - `client.connect` 78 | - `client.delete` 79 | - `client.head` 80 | - `client.options` 81 | - `client.patch` 82 | - `client.put` 83 | - `client.trace` 84 | 85 | ## Dependencies 86 | 87 | - [h11](https://github.com/ducdetronquito/h11) 88 | - [http](https://github.com/ducdetronquito/http) 89 | - [iguanaTLS](https://github.com/alexnask/iguanaTLS) 90 | - [zig-network](https://github.com/MasterQ32/zig-network) 91 | 92 | ## License 93 | 94 | *requestz* is released under the [BSD Zero clause license](https://choosealicense.com/licenses/0bsd/). 🎉🍻 95 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const Builder = @import("std").build.Builder; 2 | const pkgs = @import("deps.zig").pkgs; 3 | 4 | pub fn build(b: *Builder) void { 5 | const mode = b.standardReleaseOptions(); 6 | const lib = b.addStaticLibrary("requestz", "src/main.zig"); 7 | pkgs.addAllTo(lib); 8 | lib.setBuildMode(mode); 9 | lib.install(); 10 | 11 | var main_tests = b.addTest("src/tests.zig"); 12 | pkgs.addAllTo(main_tests); 13 | main_tests.setBuildMode(mode); 14 | 15 | const test_step = b.step("test", "Run library tests"); 16 | test_step.dependOn(&main_tests.step); 17 | } 18 | -------------------------------------------------------------------------------- /gyro.zzz: -------------------------------------------------------------------------------- 1 | pkgs: 2 | requestz: 3 | version: 0.1.1 4 | root: src/main.zig 5 | description: HTTP client for Zig 🦎 6 | license: 0BSD 7 | source_url: "https://github.com/ducdetronquito/requestz" 8 | tags: http 9 | files: 10 | README.md 11 | LICENSE.txt 12 | src/*.zig 13 | deps: 14 | ducdetronquito/http: ^0.1.3 15 | ducdetronquito/h11: ^0.1.1 16 | iguanaTLS: 17 | src: 18 | github: 19 | user: alexnask 20 | repo: iguanaTLS 21 | ref: 0d39a361639ad5469f8e4dcdaea35446bbe54b48 22 | network: 23 | root: network.zig 24 | src: 25 | github: 26 | user: MasterQ32 27 | repo: zig-network 28 | ref: b9c91769d8ebd626c8e45b2abb05cbc28ccc50da 29 | -------------------------------------------------------------------------------- /src/client.zig: -------------------------------------------------------------------------------- 1 | const Allocator = std.mem.Allocator; 2 | const TcpConnection = @import("connection.zig").TcpConnection; 3 | const Method = @import("http").Method; 4 | const network = @import("network"); 5 | const Response = @import("response.zig").Response; 6 | const std = @import("std"); 7 | const StreamingResponse = @import("response.zig").StreamingResponse; 8 | const Uri = @import("http").Uri; 9 | 10 | pub const Client = struct { 11 | allocator: *Allocator, 12 | 13 | pub fn init(allocator: *Allocator) !Client { 14 | try network.init(); 15 | return Client{ .allocator = allocator }; 16 | } 17 | 18 | pub fn deinit(_: *Client) void { 19 | network.deinit(); 20 | } 21 | 22 | pub fn connect(self: Client, url: []const u8, args: anytype) !Response { 23 | return self.request(.Connect, url, args); 24 | } 25 | 26 | pub fn delete(self: Client, url: []const u8, args: anytype) !Response { 27 | return self.request(.Delete, url, args); 28 | } 29 | 30 | pub fn get(self: Client, url: []const u8, args: anytype) !Response { 31 | return self.request(.Get, url, args); 32 | } 33 | 34 | pub fn head(self: Client, url: []const u8, args: anytype) !Response { 35 | return self.request(.Head, url, args); 36 | } 37 | 38 | pub fn options(self: Client, url: []const u8, args: anytype) !Response { 39 | return self.request(.Options, url, args); 40 | } 41 | 42 | pub fn patch(self: Client, url: []const u8, args: anytype) !Response { 43 | return self.request(.Patch, url, args); 44 | } 45 | 46 | pub fn post(self: Client, url: []const u8, args: anytype) !Response { 47 | return self.request(.Post, url, args); 48 | } 49 | 50 | pub fn put(self: Client, url: []const u8, args: anytype) !Response { 51 | return self.request(.Put, url, args); 52 | } 53 | 54 | pub fn request(self: Client, method: Method, url: []const u8, args: anytype) !Response { 55 | const uri = try Uri.parse(url, false); 56 | 57 | var connection = try self.get_connection(uri); 58 | defer connection.deinit(); 59 | 60 | return connection.request(method, uri, args); 61 | } 62 | 63 | pub fn stream(self: Client, method: Method, url: []const u8, args: anytype) !StreamingResponse(TcpConnection) { 64 | const uri = try Uri.parse(url, false); 65 | 66 | var connection = try self.get_connection(uri); 67 | 68 | return connection.stream(method, uri, args); 69 | } 70 | 71 | pub fn trace(self: Client, url: []const u8, args: anytype) !Response { 72 | return self.request(.Trace, url, args); 73 | } 74 | 75 | fn get_connection(self: Client, uri: Uri) !*TcpConnection { 76 | return try TcpConnection.connect(self.allocator, uri); 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /src/connection.zig: -------------------------------------------------------------------------------- 1 | const Allocator = std.mem.Allocator; 2 | const h11 = @import("h11"); 3 | const Method = @import("http").Method; 4 | const TcpSocket = @import("socket.zig").TcpSocket; 5 | const Request = @import("request.zig").Request; 6 | const Response = @import("response.zig").Response; 7 | const std = @import("std"); 8 | const StreamingResponse = @import("response.zig").StreamingResponse; 9 | const tls = @import("iguanaTLS"); 10 | const Uri = @import("http").Uri; 11 | 12 | pub const TcpConnection = Connection(TcpSocket); 13 | 14 | pub const Protocol = enum { Http, Https }; 15 | 16 | pub fn Connection(comptime SocketType: type) type { 17 | return struct { 18 | const Self = @This(); 19 | const H11Client = h11.Client(Reader, Writer); 20 | const TlsContext = tls.Client(SocketType.Reader, SocketType.Writer, tls.ciphersuites.all, true); 21 | const Reader = std.io.Reader(*Self, ReaderError, read); 22 | const ReaderError = TlsContext.Reader.Error; 23 | const Writer = std.io.Writer(*Self, WriterError, write); 24 | const WriterError = SocketType.Writer.Error; 25 | 26 | allocator: *Allocator, 27 | protocol: Protocol, 28 | socket: SocketType, 29 | state: H11Client, 30 | tls_context: TlsContext = undefined, 31 | 32 | pub fn connect(allocator: *Allocator, uri: Uri) !*Self { 33 | var connection = try allocator.create(Self); 34 | connection.allocator = allocator; 35 | connection.protocol = .Http; 36 | connection.socket = try SocketType.connect(allocator, uri); 37 | connection.state = H11Client.init(allocator, connection.reader(), connection.writer()); 38 | 39 | if (std.mem.eql(u8, uri.scheme, "https")) { 40 | connection.protocol = .Https; 41 | connection.tls_context = try tls.client_connect(.{ 42 | .reader = connection.socket.reader(), 43 | .writer = connection.socket.writer(), 44 | .cert_verifier = .none, 45 | .temp_allocator = allocator, 46 | .ciphersuites = tls.ciphersuites.all, 47 | .protocols = &[_][]const u8{"http/1.1"}, 48 | }, uri.host.name); 49 | } 50 | 51 | return connection; 52 | } 53 | 54 | pub fn reader(self: *Self) Reader { 55 | return .{ .context = self }; 56 | } 57 | 58 | pub fn writer(self: *Self) Writer { 59 | return .{ .context = self }; 60 | } 61 | 62 | pub fn deinit(self: *Self) void { 63 | self.state.deinit(); 64 | if (self.protocol == .Https) { 65 | self.tls_context.close_notify() catch {}; 66 | } 67 | self.socket.close(); 68 | self.allocator.destroy(self); 69 | } 70 | 71 | pub fn read(self: *Self, buffer: []u8) ReaderError!usize { 72 | return switch (self.protocol) { 73 | .Http => self.socket.read(buffer), 74 | .Https => self.tls_context.read(buffer), 75 | }; 76 | } 77 | 78 | pub fn write(self: *Self, buffer: []const u8) WriterError!usize { 79 | return switch (self.protocol) { 80 | .Http => self.socket.write(buffer), 81 | .Https => self.tls_context.write(buffer), 82 | }; 83 | } 84 | 85 | pub fn request(self: *Self, method: Method, uri: Uri, options: anytype) !Response { 86 | var _request = try Request.init(self.allocator, method, uri, options); 87 | defer _request.deinit(); 88 | 89 | try self.sendRequest(_request); 90 | 91 | var response = try self.readResponse(); 92 | errdefer response.deinit(); 93 | var body = try self.readResponseBody(); 94 | 95 | return Response{ 96 | .allocator = self.allocator, 97 | .buffer = response.raw_bytes, 98 | .status = response.statusCode, 99 | .version = response.version, 100 | .headers = response.headers, 101 | .body = body, 102 | }; 103 | } 104 | 105 | pub fn stream(self: *Self, method: Method, uri: Uri, options: anytype) !StreamingResponse(Self) { 106 | var _request = try Request.init(self.allocator, method, uri, options); 107 | defer _request.deinit(); 108 | 109 | try self.sendRequest(_request); 110 | 111 | var response = try self.readResponse(); 112 | 113 | return StreamingResponse(Self){ 114 | .allocator = self.allocator, 115 | .buffer = response.raw_bytes, 116 | .connection = self, 117 | .status = response.statusCode, 118 | .version = response.version, 119 | .headers = response.headers, 120 | }; 121 | } 122 | 123 | fn sendRequest(self: *Self, _request: Request) !void { 124 | var request_event = try h11.Request.init(_request.method, _request.path, _request.version, _request.headers); 125 | 126 | try self.state.send(h11.Event{ .Request = request_event }); 127 | 128 | switch (_request.body) { 129 | .Empty => return, 130 | .ContentLength => |body| { 131 | try self.state.send(.{ .Data = h11.Data{ .bytes = body.content } }); 132 | }, 133 | } 134 | } 135 | 136 | fn readResponse(self: *Self) !h11.Response { 137 | var event = try self.state.nextEvent(.{}); 138 | return event.Response; 139 | } 140 | 141 | fn readResponseBody(self: *Self) ![]const u8 { 142 | var body = std.ArrayList(u8).init(self.allocator); 143 | errdefer body.deinit(); 144 | 145 | while (true) { 146 | var buffer: [4096]u8 = undefined; 147 | var event = try self.state.nextEvent(.{ .buffer = &buffer }); 148 | switch (event) { 149 | .Data => |data| try body.appendSlice(data.bytes), 150 | .EndOfMessage => return body.toOwnedSlice(), 151 | else => unreachable, 152 | } 153 | } 154 | } 155 | 156 | pub fn nextEvent(self: *Self, options: anytype) !h11.Event { 157 | return self.state.nextEvent(options); 158 | } 159 | }; 160 | } 161 | 162 | const ConnectionMock = Connection(SocketMock); 163 | const expect = std.testing.expect; 164 | const expectEqualStrings = std.testing.expectEqualStrings; 165 | const Headers = @import("http").Headers; 166 | const SocketMock = @import("socket.zig").SocketMock; 167 | 168 | test "Get" { 169 | const uri = try Uri.parse("http://httpbin.org/get", false); 170 | var connection = try ConnectionMock.connect(std.testing.allocator, uri); 171 | defer connection.deinit(); 172 | 173 | try connection.socket.target.has_received("HTTP/1.1 200 OK\r\nContent-Length: 14\r\nServer: gunicorn/19.9.0\r\n\r\n" ++ "Gotta Go Fast!"); 174 | 175 | var response = try connection.request(.Get, uri, .{}); 176 | defer response.deinit(); 177 | 178 | try expect(response.status == .Ok); 179 | try expect(response.version == .Http11); 180 | 181 | var headers = response.headers.items(); 182 | 183 | try expectEqualStrings(headers[0].name.raw(), "Content-Length"); 184 | try expectEqualStrings(headers[1].name.raw(), "Server"); 185 | 186 | try expect(response.body.len == 14); 187 | } 188 | 189 | test "Get with headers" { 190 | const uri = try Uri.parse("http://httpbin.org/get", false); 191 | 192 | var connection = try ConnectionMock.connect(std.testing.allocator, uri); 193 | defer connection.deinit(); 194 | try connection.socket.target.has_received("HTTP/1.1 200 OK\r\nContent-Length: 14\r\nServer: gunicorn/19.9.0\r\n\r\n" ++ "Gotta Go Fast!"); 195 | 196 | var headers = Headers.init(std.testing.allocator); 197 | defer headers.deinit(); 198 | try headers.append("Gotta-go", "Fast!"); 199 | 200 | var response = try connection.request(.Get, uri, .{ .headers = headers.items() }); 201 | defer response.deinit(); 202 | 203 | try expect(connection.socket.target.has_sent("GET /get HTTP/1.1\r\nHost: httpbin.org\r\nGotta-go: Fast!\r\n\r\n")); 204 | } 205 | 206 | test "Get with compile-time headers" { 207 | const uri = try Uri.parse("http://httpbin.org/get", false); 208 | 209 | var connection = try ConnectionMock.connect(std.testing.allocator, uri); 210 | defer connection.deinit(); 211 | try connection.socket.target.has_received("HTTP/1.1 200 OK\r\nContent-Length: 14\r\nServer: gunicorn/19.9.0\r\n\r\n" ++ "Gotta Go Fast!"); 212 | 213 | var headers = .{.{ "Gotta-go", "Fast!" }}; 214 | 215 | var response = try connection.request(.Get, uri, .{ .headers = headers }); 216 | defer response.deinit(); 217 | 218 | try expect(connection.socket.target.has_sent("GET /get HTTP/1.1\r\nHost: httpbin.org\r\nGotta-go: Fast!\r\n\r\n")); 219 | } 220 | 221 | test "Post binary data" { 222 | const uri = try Uri.parse("http://httpbin.org/post", false); 223 | 224 | var connection = try ConnectionMock.connect(std.testing.allocator, uri); 225 | defer connection.deinit(); 226 | try connection.socket.target.has_received("HTTP/1.1 200 OK\r\nContent-Length: 14\r\nServer: gunicorn/19.9.0\r\n\r\n" ++ "Gotta Go Fast!"); 227 | 228 | var response = try connection.request(.Post, uri, .{ .content = "Gotta go fast!" }); 229 | defer response.deinit(); 230 | 231 | try expect(connection.socket.target.has_sent("POST /post HTTP/1.1\r\nHost: httpbin.org\r\nContent-Length: 14\r\n\r\nGotta go fast!")); 232 | } 233 | 234 | test "Head request has no message body" { 235 | const uri = try Uri.parse("http://httpbin.org/head", false); 236 | var connection = try ConnectionMock.connect(std.testing.allocator, uri); 237 | defer connection.deinit(); 238 | 239 | try connection.socket.target.has_received("HTTP/1.1 200 OK\r\nContent-Length: 14\r\nServer: gunicorn/19.9.0\r\n\r\n"); 240 | 241 | var response = try connection.request(.Head, uri, .{}); 242 | defer response.deinit(); 243 | 244 | try expect(response.body.len == 0); 245 | } 246 | 247 | test "IP address and a port should be set in HOST headers" { 248 | const uri = try Uri.parse("http://127.0.0.1:8080/", false); 249 | 250 | var connection = try ConnectionMock.connect(std.testing.allocator, uri); 251 | defer connection.deinit(); 252 | 253 | try connection.socket.target.has_received("HTTP/1.1 200 OK\r\n\r\n"); 254 | 255 | var response = try connection.request(.Get, uri, .{}); 256 | defer response.deinit(); 257 | 258 | try expect(connection.socket.target.has_sent("GET / HTTP/1.1\r\nHost: 127.0.0.1:8080\r\n\r\n")); 259 | } 260 | 261 | test "Request a URI without path defaults to /" { 262 | const uri = try Uri.parse("http://httpbin.org", false); 263 | 264 | var connection = try ConnectionMock.connect(std.testing.allocator, uri); 265 | defer connection.deinit(); 266 | 267 | try connection.socket.target.has_received("HTTP/1.1 200 OK\r\n\r\n"); 268 | 269 | var response = try connection.request(.Get, uri, .{}); 270 | defer response.deinit(); 271 | 272 | try expect(connection.socket.target.has_sent("GET / HTTP/1.1\r\nHost: httpbin.org\r\n\r\n")); 273 | } 274 | 275 | test "Get a response in multiple socket read" { 276 | const uri = try Uri.parse("http://httpbin.org", false); 277 | 278 | var connection = try ConnectionMock.connect(std.heap.page_allocator, uri); 279 | defer connection.deinit(); 280 | 281 | try connection.socket.target.has_received("HTTP/1.1 200 OK\r\nContent-Length: 14\r\n\r\n"); 282 | try connection.socket.target.has_received("Gotta go fast!"); 283 | 284 | var response = try connection.request(.Get, uri, .{}); 285 | defer response.deinit(); 286 | 287 | try expect(response.status == .Ok); 288 | try expect(response.version == .Http11); 289 | 290 | var headers = response.headers.items(); 291 | 292 | try expectEqualStrings(headers[0].name.raw(), "Content-Length"); 293 | try expectEqualStrings(headers[0].value, "14"); 294 | 295 | try expect(response.body.len == 14); 296 | } 297 | 298 | test "Get a streaming response" { 299 | const uri = try Uri.parse("http://httpbin.org", false); 300 | 301 | var connection = try ConnectionMock.connect(std.heap.page_allocator, uri); 302 | 303 | try connection.socket.target.has_received("HTTP/1.1 200 OK\r\nContent-Length: 12288\r\n\r\n"); 304 | 305 | var body = "a" ** 12288; 306 | try connection.socket.target.has_received(body); 307 | 308 | var response = try connection.stream(.Get, uri, .{}); 309 | defer response.deinit(); 310 | 311 | try expect(response.status == .Ok); 312 | try expect(response.version == .Http11); 313 | 314 | var headers = response.headers.items(); 315 | try expectEqualStrings(headers[0].name.raw(), "Content-Length"); 316 | try expectEqualStrings(headers[0].value, "12288"); 317 | 318 | var result = std.ArrayList(u8).init(std.testing.allocator); 319 | defer result.deinit(); 320 | 321 | while (true) { 322 | var buffer: [4096]u8 = undefined; 323 | var bytesRead = try response.read(&buffer); 324 | if (bytesRead == 0) { 325 | break; 326 | } 327 | try result.appendSlice(buffer[0..bytesRead]); 328 | } 329 | 330 | try expectEqualStrings(result.items, body); 331 | } 332 | 333 | test "Get a chunk encoded response" { 334 | const uri = try Uri.parse("http://httpbin.org/get", false); 335 | var connection = try ConnectionMock.connect(std.testing.allocator, uri); 336 | defer connection.deinit(); 337 | 338 | try connection.socket.target.has_received("HTTP/1.1 200 OK\r\n" ++ "Transfer-Encoding: chunked\r\n\r\n" ++ "7\r\nMozilla\r\n" ++ "9\r\nDeveloper\r\n" ++ "7\r\nNetwork\r\n" ++ "0\r\n\r\n"); 339 | 340 | var response = try connection.request(.Get, uri, .{}); 341 | defer response.deinit(); 342 | 343 | try expect(response.status == .Ok); 344 | try expect(response.version == .Http11); 345 | 346 | var headers = response.headers.items(); 347 | 348 | try expectEqualStrings(headers[0].name.raw(), "Transfer-Encoding"); 349 | try expectEqualStrings(headers[0].value, "chunked"); 350 | 351 | try expectEqualStrings(response.body, "MozillaDeveloperNetwork"); 352 | } 353 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | pub const Client = @import("client.zig").Client; 2 | pub const Response = @import("response.zig").Response; 3 | -------------------------------------------------------------------------------- /src/request.zig: -------------------------------------------------------------------------------- 1 | const Allocator = std.mem.Allocator; 2 | const Header = @import("http").Header; 3 | const Headers = @import("http").Headers; 4 | const Method = @import("http").Method; 5 | const std = @import("std"); 6 | const Uri = @import("http").Uri; 7 | const Version = @import("http").Version; 8 | 9 | const BodyType = enum { 10 | ContentLength, 11 | Empty, 12 | }; 13 | 14 | const Body = union(BodyType) { 15 | ContentLength: struct { length: []const u8, content: []const u8 }, 16 | Empty: void, 17 | }; 18 | 19 | pub const Request = struct { 20 | allocator: *Allocator, 21 | headers: Headers, 22 | ip: ?[]const u8, 23 | method: Method, 24 | path: []const u8, 25 | uri: Uri, 26 | version: Version, 27 | body: Body, 28 | 29 | pub fn deinit(self: *Request) void { 30 | if (self.ip != null) { 31 | self.allocator.free(self.ip.?); 32 | } 33 | self.headers.deinit(); 34 | 35 | switch (self.body) { 36 | .ContentLength => |*body| { 37 | self.allocator.free(body.length); 38 | }, 39 | else => {}, 40 | } 41 | } 42 | 43 | pub fn init(allocator: *Allocator, method: Method, uri: Uri, options: anytype) !Request { 44 | var path = if (uri.path.len != 0) uri.path else "/"; 45 | var request = Request{ 46 | .allocator = allocator, 47 | .body = Body.Empty, 48 | .headers = Headers.init(allocator), 49 | .ip = null, 50 | .method = method, 51 | .path = path, 52 | .uri = uri, 53 | .version = Version.Http11, 54 | }; 55 | 56 | switch (request.uri.host) { 57 | .ip => |address| { 58 | request.ip = try std.fmt.allocPrint(allocator, "{}", .{address}); 59 | try request.headers.append("Host", request.ip.?); 60 | }, 61 | .name => |name| { 62 | try request.headers.append("Host", name); 63 | }, 64 | } 65 | 66 | if (@hasField(@TypeOf(options), "headers")) { 67 | var user_headers = getUserHeaders(options.headers); 68 | try request.headers._items.appendSlice(user_headers); 69 | } 70 | 71 | if (@hasField(@TypeOf(options), "version")) { 72 | request.options = options.version; 73 | } 74 | 75 | if (@hasField(@TypeOf(options), "content")) { 76 | var content_length = std.fmt.allocPrint(allocator, "{d}", .{options.content.len}) catch unreachable; 77 | try request.headers.append("Content-Length", content_length); 78 | request.body = Body{ .ContentLength = .{ 79 | .length = content_length, 80 | .content = options.content, 81 | } }; 82 | } 83 | 84 | return request; 85 | } 86 | 87 | fn getUserHeaders(user_headers: anytype) []Header { 88 | const typeof = @TypeOf(user_headers); 89 | const typeinfo = @typeInfo(typeof); 90 | 91 | return switch (typeinfo) { 92 | .Struct => Header.as_slice(user_headers), 93 | .Pointer => user_headers, 94 | else => { 95 | @compileError("Invalid headers type: You must provide either a http.Headers or an anonymous struct literal."); 96 | }, 97 | }; 98 | } 99 | }; 100 | 101 | const expect = std.testing.expect; 102 | const expectEqualStrings = std.testing.expectEqualStrings; 103 | 104 | test "Request" { 105 | const uri = try Uri.parse("http://ziglang.org/news/", false); 106 | var request = try Request.init(std.testing.allocator, .Get, uri, .{}); 107 | defer request.deinit(); 108 | 109 | try expect(request.method == .Get); 110 | try expect(request.version == .Http11); 111 | try expectEqualStrings(request.headers.items()[0].name.raw(), "Host"); 112 | try expectEqualStrings(request.headers.items()[0].value, "ziglang.org"); 113 | try expectEqualStrings(request.path, "/news/"); 114 | try expect(request.body == .Empty); 115 | } 116 | 117 | test "Request - Path defaults to /" { 118 | const uri = try Uri.parse("http://ziglang.org", false); 119 | var request = try Request.init(std.testing.allocator, .Get, uri, .{}); 120 | defer request.deinit(); 121 | 122 | try expectEqualStrings(request.path, "/"); 123 | } 124 | 125 | test "Request - With user headers" { 126 | const uri = try Uri.parse("http://ziglang.org/news/", false); 127 | 128 | var headers = Headers.init(std.testing.allocator); 129 | defer headers.deinit(); 130 | try headers.append("Gotta-go", "Fast!"); 131 | 132 | var request = try Request.init(std.testing.allocator, .Get, uri, .{ .headers = headers.items() }); 133 | defer request.deinit(); 134 | 135 | try expectEqualStrings(request.headers.items()[0].name.raw(), "Host"); 136 | try expectEqualStrings(request.headers.items()[0].value, "ziglang.org"); 137 | try expectEqualStrings(request.headers.items()[1].name.raw(), "Gotta-go"); 138 | try expectEqualStrings(request.headers.items()[1].value, "Fast!"); 139 | } 140 | 141 | test "Request - With compile time user headers" { 142 | const uri = try Uri.parse("http://ziglang.org/news/", false); 143 | 144 | var headers = .{.{ "Gotta-go", "Fast!" }}; 145 | var request = try Request.init(std.testing.allocator, .Get, uri, .{ .headers = headers }); 146 | defer request.deinit(); 147 | 148 | try expectEqualStrings(request.headers.items()[0].name.raw(), "Host"); 149 | try expectEqualStrings(request.headers.items()[0].value, "ziglang.org"); 150 | try expectEqualStrings(request.headers.items()[1].name.raw(), "Gotta-go"); 151 | try expectEqualStrings(request.headers.items()[1].value, "Fast!"); 152 | } 153 | 154 | test "Request - With IP address" { 155 | const uri = try Uri.parse("http://127.0.0.1:8080/", false); 156 | var request = try Request.init(std.testing.allocator, .Get, uri, .{}); 157 | defer request.deinit(); 158 | 159 | try expectEqualStrings(request.ip.?, "127.0.0.1:8080"); 160 | try expectEqualStrings(request.headers.items()[0].name.raw(), "Host"); 161 | try expectEqualStrings(request.headers.items()[0].value, "127.0.0.1:8080"); 162 | } 163 | 164 | test "Request - With content" { 165 | const uri = try Uri.parse("http://ziglang.org/news/", false); 166 | var request = try Request.init(std.testing.allocator, .Get, uri, .{ .content = "Gotta go fast!" }); 167 | defer request.deinit(); 168 | 169 | try expect(request.body == .ContentLength); 170 | try expectEqualStrings(request.headers.items()[0].name.raw(), "Host"); 171 | try expectEqualStrings(request.headers.items()[0].value, "ziglang.org"); 172 | try expectEqualStrings(request.headers.items()[1].name.raw(), "Content-Length"); 173 | try expectEqualStrings(request.headers.items()[1].value, "14"); 174 | try expectEqualStrings(request.body.ContentLength.length, "14"); 175 | try expectEqualStrings(request.body.ContentLength.content, "Gotta go fast!"); 176 | } 177 | -------------------------------------------------------------------------------- /src/response.zig: -------------------------------------------------------------------------------- 1 | const Allocator = std.mem.Allocator; 2 | const h11 = @import("h11"); 3 | const Headers = @import("http").Headers; 4 | const JsonParser = std.json.Parser; 5 | const StatusCode = @import("http").StatusCode; 6 | const std = @import("std"); 7 | const Version = @import("http").Version; 8 | const ValueTree = std.json.ValueTree; 9 | const Connection = @import("connection.zig").Connection; 10 | 11 | pub const Response = struct { 12 | allocator: *Allocator, 13 | buffer: []const u8, 14 | status: StatusCode, 15 | version: Version, 16 | headers: Headers, 17 | body: []const u8, 18 | 19 | pub fn deinit(self: *Response) void { 20 | self.headers.deinit(); 21 | self.allocator.free(self.buffer); 22 | self.allocator.free(self.body); 23 | } 24 | 25 | pub fn json(self: Response) !ValueTree { 26 | var parser = JsonParser.init(self.allocator, false); 27 | defer parser.deinit(); 28 | 29 | return try parser.parse(self.body); 30 | } 31 | }; 32 | 33 | pub fn StreamingResponse(comptime ConnectionType: type) type { 34 | return struct { 35 | const Self = @This(); 36 | allocator: *Allocator, 37 | buffer: []const u8, 38 | connection: *ConnectionType, 39 | headers: Headers, 40 | status: StatusCode, 41 | version: Version, 42 | 43 | pub fn deinit(self: *Self) void { 44 | self.allocator.free(self.buffer); 45 | self.headers.deinit(); 46 | self.connection.deinit(); 47 | } 48 | 49 | pub fn read(self: *Self, buffer: []u8) !usize { 50 | var event = try self.connection.nextEvent(.{ .buffer = buffer }); 51 | switch (event) { 52 | .Data => |data| return data.bytes.len, 53 | .EndOfMessage => return 0, 54 | else => unreachable, 55 | } 56 | } 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /src/socket.zig: -------------------------------------------------------------------------------- 1 | const Address = std.net.Address; 2 | const Allocator = std.mem.Allocator; 3 | const LinearFifo = std.fifo.LinearFifo; 4 | const network = @import("network"); 5 | const std = @import("std"); 6 | const tls = @import("iguanaTLS"); 7 | const Uri = @import("http").Uri; 8 | 9 | pub const TcpSocket = SocketWrapper(ZigNetwork); 10 | pub const SocketMock = SocketWrapper(NetworkMock); 11 | 12 | fn SocketWrapper(comptime Engine: type) type { 13 | return struct { 14 | target: Engine.Socket, 15 | tls_context: TlsContext = undefined, 16 | 17 | const Self = @This(); 18 | pub const Reader = std.io.Reader(Self, Engine.Socket.ReceiveError, read); 19 | pub const Writer = std.io.Writer(Self, Engine.Socket.SendError, write); 20 | const TlsContext = tls.Client(Engine.Socket.Reader, Engine.Socket.Writer, tls.ciphersuites.all, true); 21 | 22 | pub fn connect(allocator: *Allocator, uri: Uri) !Self { 23 | var defaultPort: u16 = if (std.mem.eql(u8, uri.scheme, "https")) 443 else 80; 24 | var port: u16 = uri.port orelse defaultPort; 25 | var socket = switch (uri.host) { 26 | .name => |host| try Self.connectToHost(allocator, host, port), 27 | .ip => |address| try Self.connectToAddress(allocator, address), 28 | }; 29 | 30 | return Self{ .target = socket }; 31 | } 32 | 33 | pub fn close(self: *Self) void { 34 | self.target.close(); 35 | } 36 | 37 | pub fn writer(self: Self) Writer { 38 | return .{ .context = self }; 39 | } 40 | 41 | pub fn reader(self: Self) Reader { 42 | return .{ .context = self }; 43 | } 44 | 45 | pub fn read(self: Self, buffer: []u8) !usize { 46 | return self.target.receive(buffer); 47 | } 48 | 49 | pub fn write(self: Self, buffer: []const u8) !usize { 50 | return self.target.send(buffer); 51 | } 52 | 53 | fn connectToHost(allocator: *Allocator, host: []const u8, port: u16) !Engine.Socket { 54 | return try Engine.connectToHost(allocator, host, port, .tcp); 55 | } 56 | 57 | fn connectToAddress(_: *Allocator, address: Address) !Engine.Socket { 58 | switch (address.any.family) { 59 | std.os.AF_INET => { 60 | const bytes = @ptrCast(*const [4]u8, &address.in.sa.addr); 61 | var ipv4 = network.Address{ .ipv4 = network.Address.IPv4.init(bytes[0], bytes[1], bytes[2], bytes[3]) }; 62 | var port = address.getPort(); 63 | var endpoint = network.EndPoint{ .address = ipv4, .port = port }; 64 | 65 | var socket = try Engine.Socket.create(.ipv4, .tcp); 66 | try socket.connect(endpoint); 67 | return socket; 68 | }, 69 | else => unreachable, 70 | } 71 | } 72 | }; 73 | } 74 | 75 | const ZigNetwork = struct { 76 | const Socket = network.Socket; 77 | 78 | fn connectToHost(allocator: *Allocator, host: []const u8, port: u16, protocol: network.Protocol) !Socket { 79 | return try network.connectToHost(allocator, host, port, protocol); 80 | } 81 | }; 82 | 83 | const NetworkMock = struct { 84 | const Socket = InMemorySocket; 85 | 86 | pub fn connectToHost(allocator: *Allocator, host: []const u8, port: u16, protocol: network.Protocol) !Socket { 87 | _ = allocator; 88 | _ = host; 89 | _ = port; 90 | _ = protocol; 91 | return try Socket.create(.{}, .{}); 92 | } 93 | }; 94 | 95 | const InMemorySocket = struct { 96 | const Context = struct { 97 | read_buffer: ReadBuffer, 98 | write_buffer: WriteBuffer, 99 | 100 | const ReadBuffer = LinearFifo(u8, .Dynamic); 101 | const WriteBuffer = std.ArrayList(u8); 102 | 103 | pub fn create() !*Context { 104 | var context = try std.mem.Allocator.create(std.testing.allocator, Context); 105 | context.read_buffer = ReadBuffer.init(std.testing.allocator); 106 | context.write_buffer = WriteBuffer.init(std.testing.allocator); 107 | return context; 108 | } 109 | 110 | pub fn deinit(self: *Context) void { 111 | self.read_buffer.deinit(); 112 | self.write_buffer.deinit(); 113 | } 114 | }; 115 | 116 | context: *Context, 117 | 118 | pub const Reader = std.io.Reader(InMemorySocket, ReceiveError, receive); 119 | pub const ReceiveError = anyerror; 120 | pub const Writer = std.io.Writer(InMemorySocket, SendError, send); 121 | pub const SendError = anyerror; 122 | 123 | pub fn create(address: anytype, protocol: anytype) !InMemorySocket { 124 | _ = address; 125 | _ = protocol; 126 | return InMemorySocket{ .context = try Context.create() }; 127 | } 128 | 129 | pub fn close(self: InMemorySocket) void { 130 | self.context.deinit(); 131 | std.mem.Allocator.destroy(std.testing.allocator, self.context); 132 | } 133 | 134 | pub fn connect(self: InMemorySocket, options: anytype) !void { 135 | _ = self; 136 | _ = options; 137 | } 138 | 139 | pub fn has_sent(self: InMemorySocket, data: []const u8) bool { 140 | return std.mem.eql(u8, self.context.write_buffer.items, data); 141 | } 142 | 143 | pub fn has_received(self: InMemorySocket, data: []const u8) !void { 144 | try self.context.read_buffer.write(data); 145 | } 146 | 147 | pub fn receive(self: InMemorySocket, dest: []u8) !usize { 148 | return self.context.read_buffer.read(dest); 149 | } 150 | 151 | pub fn send(self: InMemorySocket, bytes: []const u8) !usize { 152 | self.context.write_buffer.appendSlice(bytes) catch unreachable; 153 | return bytes.len; 154 | } 155 | 156 | pub fn reader(self: InMemorySocket) Reader { 157 | return .{ .context = self }; 158 | } 159 | 160 | pub fn writer(self: InMemorySocket) Writer { 161 | return .{ .context = self }; 162 | } 163 | }; 164 | -------------------------------------------------------------------------------- /src/tests.zig: -------------------------------------------------------------------------------- 1 | usingnamespace @import("client.zig"); 2 | usingnamespace @import("connection.zig"); 3 | usingnamespace @import("request.zig"); 4 | --------------------------------------------------------------------------------