mirror of
				https://github.com/lightpanda-io/browser.git
				synced 2025-10-30 15:41:48 +00:00 
			
		
		
		
	Re-enable telemetry
Start work on supporting navigation events (clicks, form submission).
This commit is contained in:
		| @@ -16,271 +16,307 @@ | ||||
| // You should have received a copy of the GNU Affero General Public License | ||||
| // along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
| 
 | ||||
| pub const c = @cImport({ | ||||
|     @cInclude("curl/curl.h"); | ||||
| }); | ||||
| 
 | ||||
| const ENABLE_DEBUG = false; | ||||
| 
 | ||||
| const std = @import("std"); | ||||
| const log = @import("../log.zig"); | ||||
| const builtin = @import("builtin"); | ||||
| const errors = @import("errors.zig"); | ||||
| const Http = @import("Http.zig"); | ||||
| 
 | ||||
| const c = Http.c; | ||||
| 
 | ||||
| const Allocator = std.mem.Allocator; | ||||
| const ArenaAllocator = std.heap.ArenaAllocator; | ||||
| 
 | ||||
| pub fn init() !void { | ||||
|     try errorCheck(c.curl_global_init(c.CURL_GLOBAL_SSL)); | ||||
|     if (comptime ENABLE_DEBUG) { | ||||
|         std.debug.print("curl version: {s}\n\n", .{c.curl_version()}); | ||||
|     } | ||||
| } | ||||
| const errorCheck = Http.errorCheck; | ||||
| const errorMCheck = Http.errorMCheck; | ||||
| 
 | ||||
| pub fn deinit() void { | ||||
|     c.curl_global_cleanup(); | ||||
| } | ||||
| pub const Method = Http.Method; | ||||
| 
 | ||||
| pub const Client = struct { | ||||
|     active: usize, | ||||
|     multi: *c.CURLM, | ||||
|     handles: Handles, | ||||
|     queue: RequestQueue, | ||||
|     allocator: Allocator, | ||||
|     transfer_pool: std.heap.MemoryPool(Transfer), | ||||
|     queue_node_pool: std.heap.MemoryPool(RequestQueue.Node), | ||||
|     //@newhttp | ||||
|     http_proxy: ?std.Uri = null, | ||||
| // This is loosely tied to a browser Page. Loading all the <scripts>, doing | ||||
| // XHR requests, and loading imports all happens through here. Sine the app | ||||
| // currently supports 1 browser and 1 page at-a-time, we only have 1 Client and | ||||
| // re-use it from page to page. This allows us better re-use of the various | ||||
| // buffers/caches (including keepalive connections) that libcurl has. | ||||
| // | ||||
| // The app has other secondary http needs, like telemetry. While we want to | ||||
| // share some things (namely the ca blob, and maybe some configuration | ||||
| // (TODO: ??? should proxy settings be global ???)), we're able to do call | ||||
| // client.abort() to abort the transfers being made by a page, without impacting | ||||
| // those other http requests. | ||||
| pub const Client = @This(); | ||||
| 
 | ||||
|     const RequestQueue = std.DoublyLinkedList(Request); | ||||
| active: usize, | ||||
| multi: *c.CURLM, | ||||
| handles: Handles, | ||||
| queue: RequestQueue, | ||||
| allocator: Allocator, | ||||
| transfer_pool: std.heap.MemoryPool(Transfer), | ||||
| queue_node_pool: std.heap.MemoryPool(RequestQueue.Node), | ||||
| //@newhttp | ||||
| http_proxy: ?std.Uri = null, | ||||
| 
 | ||||
|     const Opts = struct { | ||||
|         timeout_ms: u31 = 0, | ||||
|         max_redirects: u8 = 10, | ||||
|         connect_timeout_ms: u31 = 5000, | ||||
|         max_concurrent_transfers: u8 = 5, | ||||
| const RequestQueue = std.DoublyLinkedList(Request); | ||||
| 
 | ||||
| pub fn init(allocator: Allocator, ca_blob: c.curl_blob, opts: Http.Opts) !*Client { | ||||
|     var transfer_pool = std.heap.MemoryPool(Transfer).init(allocator); | ||||
|     errdefer transfer_pool.deinit(); | ||||
| 
 | ||||
|     var queue_node_pool = std.heap.MemoryPool(RequestQueue.Node).init(allocator); | ||||
|     errdefer queue_node_pool.deinit(); | ||||
| 
 | ||||
|     const client = try allocator.create(Client); | ||||
|     errdefer allocator.destroy(client); | ||||
| 
 | ||||
|     const multi = c.curl_multi_init() orelse return error.FailedToInitializeMulti; | ||||
|     errdefer _ = c.curl_multi_cleanup(multi); | ||||
| 
 | ||||
|     var handles = try Handles.init(allocator, client, ca_blob, opts); | ||||
|     errdefer handles.deinit(allocator, multi); | ||||
| 
 | ||||
|     client.* = .{ | ||||
|         .queue = .{}, | ||||
|         .active = 0, | ||||
|         .multi = multi, | ||||
|         .handles = handles, | ||||
|         .allocator = allocator, | ||||
|         .transfer_pool = transfer_pool, | ||||
|         .queue_node_pool = queue_node_pool, | ||||
|     }; | ||||
|     pub fn init(allocator: Allocator, opts: Opts) !*Client { | ||||
|         var transfer_pool = std.heap.MemoryPool(Transfer).init(allocator); | ||||
|         errdefer transfer_pool.deinit(); | ||||
| 
 | ||||
|         var queue_node_pool = std.heap.MemoryPool(RequestQueue.Node).init(allocator); | ||||
|         errdefer queue_node_pool.deinit(); | ||||
|     return client; | ||||
| } | ||||
| 
 | ||||
|         const client = try allocator.create(Client); | ||||
|         errdefer allocator.destroy(client); | ||||
| pub fn deinit(self: *Client) void { | ||||
|     self.handles.deinit(self.allocator, self.multi); | ||||
|     _ = c.curl_multi_cleanup(self.multi); | ||||
| 
 | ||||
|         var handles = try Handles.init(allocator, client, opts); | ||||
|         errdefer handles.deinit(allocator); | ||||
|     self.transfer_pool.deinit(); | ||||
|     self.queue_node_pool.deinit(); | ||||
|     self.allocator.destroy(self); | ||||
| } | ||||
| 
 | ||||
|         const multi = c.curl_multi_init() orelse return error.FailedToInitializeMulti; | ||||
|         errdefer _ = c.curl_multi_cleanup(multi); | ||||
| pub fn abort(self: *Client) void { | ||||
|     self.handles.abort(self.multi); | ||||
| 
 | ||||
|         client.* = .{ | ||||
|             .queue = .{}, | ||||
|             .active = 0, | ||||
|             .multi = multi, | ||||
|             .handles = handles, | ||||
|             .allocator = allocator, | ||||
|             .transfer_pool = transfer_pool, | ||||
|             .queue_node_pool = queue_node_pool, | ||||
|         }; | ||||
|         return client; | ||||
|     var n = self.queue.first; | ||||
|     while (n) |node| { | ||||
|         n = node.next; | ||||
|         self.queue_node_pool.destroy(node); | ||||
|     } | ||||
|     self.queue = .{}; | ||||
|     self.active = 0; | ||||
| 
 | ||||
|     pub fn deinit(self: *Client) void { | ||||
|         self.handles.deinit(self.allocator); | ||||
|         _ = c.curl_multi_cleanup(self.multi); | ||||
| 
 | ||||
|         self.transfer_pool.deinit(); | ||||
|         self.queue_node_pool.deinit(); | ||||
|         self.allocator.destroy(self); | ||||
|     } | ||||
| 
 | ||||
|     pub fn tick(self: *Client, timeout_ms: usize) !void { | ||||
|         var handles = &self.handles.available; | ||||
|         while (true) { | ||||
|             if (handles.first == null) { | ||||
|                 break; | ||||
|             } | ||||
|             const queue_node = self.queue.popFirst() orelse break; | ||||
| 
 | ||||
|             defer self.queue_node_pool.destroy(queue_node); | ||||
| 
 | ||||
|             const handle = handles.popFirst().?.data; | ||||
|             try self.makeRequest(handle, queue_node.data); | ||||
|         } | ||||
| 
 | ||||
|         try self.perform(@intCast(timeout_ms)); | ||||
|     } | ||||
| 
 | ||||
|     pub fn request(self: *Client, req: Request) !void { | ||||
|         if (self.handles.getFreeHandle()) |handle| { | ||||
|             return self.makeRequest(handle, req); | ||||
|         } | ||||
| 
 | ||||
|         const node = try self.queue_node_pool.create(); | ||||
|         node.data = req; | ||||
|         self.queue.append(node); | ||||
|     } | ||||
| 
 | ||||
|     fn makeRequest(self: *Client, handle: *Handle, req: Request) !void { | ||||
|         const easy = handle.easy; | ||||
| 
 | ||||
|         const header_list = blk: { | ||||
|             errdefer self.handles.release(handle); | ||||
|             try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_URL, req.url.ptr)); | ||||
|             switch (req.method) { | ||||
|                 .GET => try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_HTTPGET, @as(c_long, 1))), | ||||
|                 .POST => try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_HTTPPOST, @as(c_long, 1))), | ||||
|                 .PUT => try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CUSTOMREQUEST, "put")), | ||||
|                 .DELETE => try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CUSTOMREQUEST, "delete")), | ||||
|                 .HEAD => try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CUSTOMREQUEST, "head")), | ||||
|                 .OPTIONS => try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CUSTOMREQUEST, "options")), | ||||
|             } | ||||
| 
 | ||||
|             const header_list = c.curl_slist_append(null, "User-Agent: Lightpanda/1.0"); | ||||
|             errdefer c.curl_slist_free_all(header_list); | ||||
| 
 | ||||
|             try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_HTTPHEADER, header_list)); | ||||
| 
 | ||||
|             break :blk header_list; | ||||
|         }; | ||||
| 
 | ||||
|         { | ||||
|             errdefer self.handles.release(handle); | ||||
| 
 | ||||
|             const transfer = try self.transfer_pool.create(); | ||||
|             transfer.* = .{ | ||||
|                 .id = 0, | ||||
|                 .req = req, | ||||
|                 .ctx = req.ctx, | ||||
|                 .handle = handle, | ||||
|                 ._request_header_list = header_list, | ||||
|             }; | ||||
|             errdefer self.transfer_pool.destroy(transfer); | ||||
|             try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PRIVATE, transfer)); | ||||
| 
 | ||||
|             try errorMCheck(c.curl_multi_add_handle(self.multi, easy)); | ||||
|             if (req.start_callback) |cb| { | ||||
|                 cb(transfer) catch |err| { | ||||
|                     try errorMCheck(c.curl_multi_remove_handle(self.multi, easy)); | ||||
|                     return err; | ||||
|                 }; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         self.active += 1; | ||||
|         return self.perform(0); | ||||
|     } | ||||
| 
 | ||||
|     fn perform(self: *Client, timeout_ms: c_int) !void { | ||||
|         const multi = self.multi; | ||||
| 
 | ||||
|     // Maybe a bit of overkill | ||||
|     // We can remove some (all?) of these once we're confident its right. | ||||
|     std.debug.assert(self.handles.in_use.first == null); | ||||
|     std.debug.assert(self.handles.available.len == self.handles.handles.len); | ||||
|     if (builtin.mode == .Debug) { | ||||
|         var running: c_int = undefined; | ||||
|         try errorMCheck(c.curl_multi_perform(multi, &running)); | ||||
|         std.debug.assert(c.curl_multi_perform(self.multi, &running) == c.CURLE_OK); | ||||
|         std.debug.assert(running == 0); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|         if (running > 0 and timeout_ms > 0) { | ||||
|             try errorMCheck(c.curl_multi_poll(multi, null, 0, timeout_ms, null)); | ||||
| pub fn tick(self: *Client, timeout_ms: usize) !void { | ||||
|     var handles = &self.handles.available; | ||||
|     while (true) { | ||||
|         if (handles.first == null) { | ||||
|             break; | ||||
|         } | ||||
|         const queue_node = self.queue.popFirst() orelse break; | ||||
| 
 | ||||
|         defer self.queue_node_pool.destroy(queue_node); | ||||
| 
 | ||||
|         const handle = handles.popFirst().?.data; | ||||
|         try self.makeRequest(handle, queue_node.data); | ||||
|     } | ||||
| 
 | ||||
|     try self.perform(@intCast(timeout_ms)); | ||||
| } | ||||
| 
 | ||||
| pub fn request(self: *Client, req: Request) !void { | ||||
|     if (self.handles.getFreeHandle()) |handle| { | ||||
|         return self.makeRequest(handle, req); | ||||
|     } | ||||
| 
 | ||||
|     const node = try self.queue_node_pool.create(); | ||||
|     node.data = req; | ||||
|     self.queue.append(node); | ||||
| } | ||||
| 
 | ||||
| fn makeRequest(self: *Client, handle: *Handle, req: Request) !void { | ||||
|     const easy = handle.easy; | ||||
| 
 | ||||
|     const header_list = blk: { | ||||
|         errdefer self.handles.release(handle); | ||||
|         try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_URL, req.url.ptr)); | ||||
| 
 | ||||
|         try Http.setMethod(easy, req.method); | ||||
|         if (req.body) |b| { | ||||
|             try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_POSTFIELDS, b.ptr)); | ||||
|             try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_POSTFIELDSIZE, @as(c_long, @intCast(b.len)))); | ||||
|         } | ||||
| 
 | ||||
|         while (true) { | ||||
|             var remaining: c_int = undefined; | ||||
|             const msg: *c.CURLMsg = c.curl_multi_info_read(multi, &remaining) orelse break; | ||||
|             if (msg.msg == c.CURLMSG_DONE) { | ||||
|                 self.active -= 1; | ||||
|                 const easy = msg.easy_handle.?; | ||||
|                 const transfer = try Transfer.fromEasy(easy); | ||||
|                 defer { | ||||
|                     self.handles.release(transfer.handle); | ||||
|                     transfer.deinit(); | ||||
|                     self.transfer_pool.destroy(transfer); | ||||
|                 } | ||||
|         var header_list = c.curl_slist_append(null, "User-Agent: Lightpanda/1.0"); | ||||
|         errdefer c.curl_slist_free_all(header_list); | ||||
| 
 | ||||
|                 if (errorCheck(msg.data.result)) { | ||||
|                     transfer.req.done_callback(transfer) catch |err| transfer.onError(err); | ||||
|                 } else |err| { | ||||
|                     transfer.onError(err); | ||||
|                 } | ||||
|         if (req.content_type) |ct| { | ||||
|             header_list = c.curl_slist_append(header_list, ct); | ||||
|         } | ||||
| 
 | ||||
|                 try errorMCheck(c.curl_multi_remove_handle(multi, easy)); | ||||
|             } | ||||
|         try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_HTTPHEADER, header_list)); | ||||
| 
 | ||||
|             if (remaining == 0) { | ||||
|                 break; | ||||
|             } | ||||
|         break :blk header_list; | ||||
|     }; | ||||
| 
 | ||||
|     { | ||||
|         errdefer self.handles.release(handle); | ||||
| 
 | ||||
|         const transfer = try self.transfer_pool.create(); | ||||
|         transfer.* = .{ | ||||
|             .id = 0, | ||||
|             .req = req, | ||||
|             .ctx = req.ctx, | ||||
|             .handle = handle, | ||||
|             ._request_header_list = header_list, | ||||
|         }; | ||||
|         errdefer self.transfer_pool.destroy(transfer); | ||||
|         try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PRIVATE, transfer)); | ||||
| 
 | ||||
|         try errorMCheck(c.curl_multi_add_handle(self.multi, easy)); | ||||
|         if (req.start_callback) |cb| { | ||||
|             cb(transfer) catch |err| { | ||||
|                 try errorMCheck(c.curl_multi_remove_handle(self.multi, easy)); | ||||
|                 return err; | ||||
|             }; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
|     self.active += 1; | ||||
|     return self.perform(0); | ||||
| } | ||||
| 
 | ||||
| fn perform(self: *Client, timeout_ms: c_int) !void { | ||||
|     const multi = self.multi; | ||||
| 
 | ||||
|     var running: c_int = undefined; | ||||
|     try errorMCheck(c.curl_multi_perform(multi, &running)); | ||||
| 
 | ||||
|     if (running > 0 and timeout_ms > 0) { | ||||
|         try errorMCheck(c.curl_multi_poll(multi, null, 0, timeout_ms, null)); | ||||
|     } | ||||
| 
 | ||||
|     while (true) { | ||||
|         var remaining: c_int = undefined; | ||||
|         const msg: *c.CURLMsg = c.curl_multi_info_read(multi, &remaining) orelse break; | ||||
|         if (msg.msg == c.CURLMSG_DONE) { | ||||
|             const easy = msg.easy_handle.?; | ||||
| 
 | ||||
|             const transfer = try Transfer.fromEasy(easy); | ||||
| 
 | ||||
|             const ctx = transfer.ctx; | ||||
|             const done_callback = transfer.req.done_callback; | ||||
|             const error_callback = transfer.req.error_callback; | ||||
|             // release it ASAP so that it's avaiable (since some done_callbacks | ||||
|             // will load more resources). | ||||
|             self.endTransfer(transfer); | ||||
| 
 | ||||
|             if (errorCheck(msg.data.result)) { | ||||
|                 done_callback(ctx) catch |err| error_callback(ctx, err); | ||||
|             } else |err| { | ||||
|                 error_callback(ctx, err); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (remaining == 0) { | ||||
|             break; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| fn endTransfer(self: *Client, transfer: *Transfer) void { | ||||
|     const handle = transfer.handle; | ||||
| 
 | ||||
|     transfer.deinit(); | ||||
|     self.transfer_pool.destroy(transfer); | ||||
| 
 | ||||
|     errorMCheck(c.curl_multi_remove_handle(self.multi, handle.easy)) catch |err| { | ||||
|         log.fatal(.http, "Failed to abort", .{ .err = err }); | ||||
|     }; | ||||
| 
 | ||||
|     self.handles.release(handle); | ||||
|     self.active -= 1; | ||||
| } | ||||
| 
 | ||||
| const Handles = struct { | ||||
|     handles: []Handle, | ||||
|     available: FreeList, | ||||
|     cert_arena: ArenaAllocator, | ||||
|     in_use: HandleList, | ||||
|     available: HandleList, | ||||
| 
 | ||||
|     const FreeList = std.DoublyLinkedList(*Handle); | ||||
|     const HandleList = std.DoublyLinkedList(*Handle); | ||||
| 
 | ||||
|     fn init(allocator: Allocator, client: *Client, opts: Client.Opts) !Handles { | ||||
|     fn init(allocator: Allocator, client: *Client, ca_blob: c.curl_blob, opts: Http.Opts) !Handles { | ||||
|         const count = opts.max_concurrent_transfers; | ||||
|         std.debug.assert(count > 0); | ||||
| 
 | ||||
|         const handles = try allocator.alloc(Handle, count); | ||||
|         errdefer allocator.free(handles); | ||||
| 
 | ||||
|         var initialized_count: usize = 0; | ||||
|         errdefer cleanup(allocator, handles[0..initialized_count]); | ||||
| 
 | ||||
|         var cert_arena = ArenaAllocator.init(allocator); | ||||
|         errdefer cert_arena.deinit(); | ||||
|         const ca_blob = try @import("ca_certs.zig").load(allocator, cert_arena.allocator()); | ||||
| 
 | ||||
|         var available: FreeList = .{}; | ||||
|         var available: HandleList = .{}; | ||||
|         for (0..count) |i| { | ||||
|             const node = try allocator.create(FreeList.Node); | ||||
|             errdefer allocator.destroy(node); | ||||
|             const easy = c.curl_easy_init() orelse return error.FailedToInitializeEasy; | ||||
|             errdefer _ = c.curl_easy_cleanup(easy); | ||||
| 
 | ||||
|             handles[i] = .{ | ||||
|                 .node = node, | ||||
|                 .easy = easy, | ||||
|                 .client = client, | ||||
|                 .easy = undefined, | ||||
|                 .node = undefined, | ||||
|             }; | ||||
|             try handles[i].init(ca_blob, opts); | ||||
|             initialized_count += 1; | ||||
|             try handles[i].configure(ca_blob, opts); | ||||
| 
 | ||||
|             node.data = &handles[i]; | ||||
|             available.append(node); | ||||
|             handles[i].node.data = &handles[i]; | ||||
|             available.append(&handles[i].node); | ||||
|         } | ||||
| 
 | ||||
|         return .{ | ||||
|             .in_use = .{}, | ||||
|             .handles = handles, | ||||
|             .available = available, | ||||
|             .cert_arena = cert_arena, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     fn deinit(self: *Handles, allocator: Allocator) void { | ||||
|         cleanup(allocator, self.handles); | ||||
|     fn deinit(self: *Handles, allocator: Allocator, multi: *c.CURLM) void { | ||||
|         self.abort(multi); | ||||
|         for (self.handles) |*h| { | ||||
|             _ = c.curl_easy_cleanup(h.easy); | ||||
|         } | ||||
|         allocator.free(self.handles); | ||||
|         self.cert_arena.deinit(); | ||||
|     } | ||||
| 
 | ||||
|     // Done line this so that cleanup can be called from init with a partial state | ||||
|     fn cleanup(allocator: Allocator, handles: []Handle) void { | ||||
|         for (handles) |*h| { | ||||
|             _ = c.curl_easy_cleanup(h.easy); | ||||
|             allocator.destroy(h.node); | ||||
|     fn abort(self: *Handles, multi: *c.CURLM) void { | ||||
|         while (self.in_use.first) |node| { | ||||
|             const handle = node.data; | ||||
|             errorMCheck(c.curl_multi_remove_handle(multi, handle.easy)) catch |err| { | ||||
|                 log.err(.http, "remove handle", .{ .err = err }); | ||||
|             }; | ||||
|             self.release(handle); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn getFreeHandle(self: *Handles) ?*Handle { | ||||
|         if (self.available.popFirst()) |handle| { | ||||
|             return handle.data; | ||||
|         if (self.available.popFirst()) |node| { | ||||
|             node.prev = null; | ||||
|             node.next = null; | ||||
|             self.in_use.append(node); | ||||
|             return node.data; | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     fn release(self: *Handles, handle: *Handle) void { | ||||
|         self.available.append(handle.node); | ||||
|         const node = &handle.node; | ||||
|         self.in_use.remove(node); | ||||
|         node.prev = null; | ||||
|         node.next = null; | ||||
|         self.available.append(node); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| @@ -289,17 +325,13 @@ const Handles = struct { | ||||
| const Handle = struct { | ||||
|     easy: *c.CURL, | ||||
|     client: *Client, | ||||
|     node: *Handles.FreeList.Node, | ||||
|     node: Handles.HandleList.Node, | ||||
|     error_buffer: [c.CURL_ERROR_SIZE:0]u8 = undefined, | ||||
| 
 | ||||
|     // Is called by Handles when already partially initialized. Done like this | ||||
|     // so that we have a stable pointer to error_buffer. | ||||
|     fn init(self: *Handle, ca_blob: c.curl_blob, opts: Client.Opts) !void { | ||||
|         const easy = c.curl_easy_init() orelse return error.FailedToInitializeEasy; | ||||
|         errdefer _ = c.curl_easy_cleanup(easy); | ||||
| 
 | ||||
|         self.easy = easy; | ||||
| 
 | ||||
|     fn configure(self: *Handle, ca_blob: c.curl_blob, opts: Http.Opts) !void { | ||||
|         const easy = self.easy; | ||||
|         try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_ERRORBUFFER, &self.error_buffer)); | ||||
| 
 | ||||
|         // timeouts | ||||
| @@ -323,7 +355,7 @@ const Handle = struct { | ||||
|         try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CAINFO_BLOB, ca_blob)); | ||||
| 
 | ||||
|         // debug | ||||
|         if (comptime ENABLE_DEBUG) { | ||||
|         if (comptime Http.ENABLE_DEBUG) { | ||||
|             try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_VERBOSE, @as(c_long, 1))); | ||||
|         } | ||||
|     } | ||||
| @@ -332,6 +364,9 @@ const Handle = struct { | ||||
| pub const Request = struct { | ||||
|     method: Method, | ||||
|     url: [:0]const u8, | ||||
|     body: ?[]const u8 = null, | ||||
|     content_type: ?[:0]const u8 = null, | ||||
| 
 | ||||
|     // arbitrary data that can be associated with this request | ||||
|     ctx: *anyopaque = undefined, | ||||
| 
 | ||||
| @@ -339,8 +374,8 @@ pub const Request = struct { | ||||
|     header_callback: ?*const fn (req: *Transfer, header: []const u8) anyerror!void = null, | ||||
|     header_done_callback: *const fn (req: *Transfer) anyerror!void, | ||||
|     data_callback: *const fn (req: *Transfer, data: []const u8) anyerror!void, | ||||
|     done_callback: *const fn (req: *Transfer) anyerror!void, | ||||
|     error_callback: *const fn (req: *Transfer, err: anyerror) void, | ||||
|     done_callback: *const fn (ctx: *anyopaque) anyerror!void, | ||||
|     error_callback: *const fn (ctx: *anyopaque, err: anyerror) void, | ||||
| }; | ||||
| 
 | ||||
| pub const Transfer = struct { | ||||
| @@ -368,14 +403,10 @@ pub const Transfer = struct { | ||||
|         return writer.print("[{d}] {s} {s}", .{ self.id, @tagName(req.method), req.url }); | ||||
|     } | ||||
| 
 | ||||
|     fn onError(self: *Transfer, err: anyerror) void { | ||||
|         self.req.error_callback(self, err); | ||||
|     } | ||||
| 
 | ||||
|     pub fn setBody(self: *Transfer, body: []const u8) !void { | ||||
|         const easy = self.handle.easy; | ||||
|         try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_POSTFIELDSIZE, @as(c_long, @intCast(body.len)))); | ||||
|         try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_POSTFIELDS, body.ptr)); | ||||
|         try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_POSTFIELDSIZE, @as(c_long, @intCast(body.len)))); | ||||
|     } | ||||
| 
 | ||||
|     pub fn addHeader(self: *Transfer, value: [:0]const u8) !void { | ||||
| @@ -383,12 +414,7 @@ pub const Transfer = struct { | ||||
|     } | ||||
| 
 | ||||
|     pub fn abort(self: *Transfer) void { | ||||
|         var client = self.handle.client; | ||||
|         errorMCheck(c.curl_multi_remove_handle(client.multi, self.handle.easy)) catch |err| { | ||||
|             log.err(.http, "Failed to abort", .{ .err = err }); | ||||
|         }; | ||||
|         client.active -= 1; | ||||
|         self.deinit(); | ||||
|         self.handle.client.endTransfer(self); | ||||
|     } | ||||
| 
 | ||||
|     fn headerCallback(buffer: [*]const u8, header_count: usize, buf_len: usize, data: *anyopaque) callconv(.c) usize { | ||||
| @@ -410,7 +436,7 @@ pub const Transfer = struct { | ||||
|                 if (transfer._redirecting) { | ||||
|                     return buf_len; | ||||
|                 } | ||||
|                 transfer.onError(error.InvalidResponseLine); | ||||
|                 log.debug(.http, "invalid response line", .{ .line = header }); | ||||
|                 return 0; | ||||
|             } | ||||
|             const version_start: usize = if (header[5] == '2') 7 else 9; | ||||
| @@ -421,7 +447,7 @@ pub const Transfer = struct { | ||||
|             std.debug.assert(version_end < 13); | ||||
| 
 | ||||
|             const status = std.fmt.parseInt(u16, header[version_start..version_end], 10) catch { | ||||
|                 transfer.onError(error.InvalidResponseStatus); | ||||
|                 log.debug(.http, "invalid status code", .{ .line = header }); | ||||
|                 return 0; | ||||
|             }; | ||||
| 
 | ||||
| @@ -433,7 +459,7 @@ pub const Transfer = struct { | ||||
| 
 | ||||
|             var url: [*c]u8 = undefined; | ||||
|             errorCheck(c.curl_easy_getinfo(handle.easy, c.CURLINFO_EFFECTIVE_URL, &url)) catch |err| { | ||||
|                 transfer.onError(err); | ||||
|                 log.err(.http, "failed to get URL", .{ .err = err }); | ||||
|                 return 0; | ||||
|             }; | ||||
| 
 | ||||
| @@ -511,41 +537,3 @@ pub const Header = struct { | ||||
|         return self._content_type[0..self._content_type_len]; | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| fn errorCheck(code: c.CURLcode) errors.Error!void { | ||||
|     if (code == c.CURLE_OK) { | ||||
|         return; | ||||
|     } | ||||
|     return errors.fromCode(code); | ||||
| } | ||||
| 
 | ||||
| fn errorMCheck(code: c.CURLMcode) errors.Multi!void { | ||||
|     if (code == c.CURLM_OK) { | ||||
|         return; | ||||
|     } | ||||
|     if (code == c.CURLM_CALL_MULTI_PERFORM) { | ||||
|         // should we can client.perform() here? | ||||
|         // or just wait until the next time we naturally call it? | ||||
|         return; | ||||
|     } | ||||
|     return errors.fromMCode(code); | ||||
| } | ||||
| 
 | ||||
| pub const Method = enum { | ||||
|     GET, | ||||
|     PUT, | ||||
|     POST, | ||||
|     DELETE, | ||||
|     HEAD, | ||||
|     OPTIONS, | ||||
| }; | ||||
| 
 | ||||
| pub const ProxyType = enum { | ||||
|     forward, | ||||
|     connect, | ||||
| }; | ||||
| 
 | ||||
| pub const ProxyAuth = union(enum) { | ||||
|     basic: struct { user_pass: []const u8 }, | ||||
|     bearer: struct { token: []const u8 }, | ||||
| }; | ||||
							
								
								
									
										269
									
								
								src/http/Http.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										269
									
								
								src/http/Http.zig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,269 @@ | ||||
| // Copyright (C) 2023-2025  Lightpanda (Selecy SAS) | ||||
| // | ||||
| // Francis Bouvier <francis@lightpanda.io> | ||||
| // Pierre Tachoire <pierre@lightpanda.io> | ||||
| // | ||||
| // This program is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU Affero General Public License as | ||||
| // published by the Free Software Foundation, either version 3 of the | ||||
| // License, or (at your option) any later version. | ||||
| // | ||||
| // This program is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| // GNU Affero General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU Affero General Public License | ||||
| // along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
|  | ||||
| const std = @import("std"); | ||||
|  | ||||
| pub const c = @cImport({ | ||||
|     @cInclude("curl/curl.h"); | ||||
| }); | ||||
| const errors = @import("errors.zig"); | ||||
| const Client = @import("Client.zig"); | ||||
|  | ||||
| const Allocator = std.mem.Allocator; | ||||
| const ArenaAllocator = std.heap.ArenaAllocator; | ||||
|  | ||||
| pub const ENABLE_DEBUG = false; | ||||
|  | ||||
| // Client.zig does the bulk of the work and is loosely tied to a browser Page. | ||||
| // But we still need something above Client.zig for the "utility" http stuff | ||||
| // we need to do, like telemetry. The most important thing we want from this | ||||
| // is to be able to share the ca_blob, which can be quite large - loading it | ||||
| // once for all http connections is a win. | ||||
| const Http = @This(); | ||||
|  | ||||
| opts: Opts, | ||||
| client: *Client, | ||||
| ca_blob: ?c.curl_blob, | ||||
| cert_arena: ArenaAllocator, | ||||
|  | ||||
| pub fn init(allocator: Allocator, opts: Opts) !Http { | ||||
|     try errorCheck(c.curl_global_init(c.CURL_GLOBAL_SSL)); | ||||
|     errdefer c.curl_global_cleanup(); | ||||
|  | ||||
|     if (comptime ENABLE_DEBUG) { | ||||
|         std.debug.print("curl version: {s}\n\n", .{c.curl_version()}); | ||||
|     } | ||||
|  | ||||
|     var cert_arena = ArenaAllocator.init(allocator); | ||||
|     errdefer cert_arena.deinit(); | ||||
|     const ca_blob = try loadCerts(allocator, cert_arena.allocator()); | ||||
|  | ||||
|     var client = try Client.init(allocator, ca_blob, opts); | ||||
|     errdefer client.deinit(); | ||||
|  | ||||
|     return .{ | ||||
|         .opts = opts, | ||||
|         .client = client, | ||||
|         .ca_blob = ca_blob, | ||||
|         .cert_arena = cert_arena, | ||||
|     }; | ||||
| } | ||||
|  | ||||
| pub fn deinit(self: *Http) void { | ||||
|     self.client.deinit(); | ||||
|     c.curl_global_cleanup(); | ||||
|     self.cert_arena.deinit(); | ||||
| } | ||||
|  | ||||
| pub fn newConnection(self: *Http) !Connection { | ||||
|     return Connection.init(self.ca_blob, self.opts); | ||||
| } | ||||
|  | ||||
| pub const Connection = struct { | ||||
|     easy: *c.CURL, | ||||
|  | ||||
|     // Is called by Handles when already partially initialized. Done like this | ||||
|     // so that we have a stable pointer to error_buffer. | ||||
|     pub fn init(ca_blob_: ?c.curl_blob, opts: Opts) !Connection { | ||||
|         const easy = c.curl_easy_init() orelse return error.FailedToInitializeEasy; | ||||
|         errdefer _ = c.curl_easy_cleanup(easy); | ||||
|  | ||||
|         // timeouts | ||||
|         try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_TIMEOUT_MS, @as(c_long, @intCast(opts.timeout_ms)))); | ||||
|         try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CONNECTTIMEOUT_MS, @as(c_long, @intCast(opts.connect_timeout_ms)))); | ||||
|  | ||||
|         // redirect behavior | ||||
|         try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_MAXREDIRS, @as(c_long, @intCast(opts.max_redirects)))); | ||||
|         try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_FOLLOWLOCATION, @as(c_long, 2))); | ||||
|         try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_REDIR_PROTOCOLS_STR, "HTTP,HTTPS")); // remove FTP and FTPS from the default | ||||
|  | ||||
|         // tls | ||||
|         // try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYHOST, @as(c_long, 0))); | ||||
|         // try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYPEER, @as(c_long, 0))); | ||||
|         if (ca_blob_) |ca_blob| { | ||||
|             try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CAINFO_BLOB, ca_blob)); | ||||
|         } | ||||
|  | ||||
|         // debug | ||||
|         if (comptime Http.ENABLE_DEBUG) { | ||||
|             try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_VERBOSE, @as(c_long, 1))); | ||||
|         } | ||||
|  | ||||
|         return .{ | ||||
|             .easy = easy, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     pub fn deinit(self: *const Connection) void { | ||||
|         c.curl_easy_cleanup(self.easy); | ||||
|     } | ||||
|  | ||||
|     pub fn setURL(self: *const Connection, url: [:0]const u8) !void { | ||||
|         try errorCheck(c.curl_easy_setopt(self.easy, c.CURLOPT_URL, url.ptr)); | ||||
|     } | ||||
|  | ||||
|     pub fn setMethod(self: *const Connection, method: Method) !void { | ||||
|         try Http.setMethod(self.easy, method); | ||||
|     } | ||||
|  | ||||
|     pub fn setBody(self: *const Connection, body: []const u8) !void { | ||||
|         const easy = self.easy; | ||||
|         try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_POSTFIELDSIZE, @as(c_long, @intCast(body.len)))); | ||||
|         try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_POSTFIELDS, body.ptr)); | ||||
|     } | ||||
|  | ||||
|     pub fn request(self: *const Connection) !u16 { | ||||
|         try errorCheck(c.curl_easy_perform(self.easy)); | ||||
|         var http_code: c_long = undefined; | ||||
|         try errorCheck(c.curl_easy_getinfo(self.easy, c.CURLINFO_RESPONSE_CODE, &http_code)); | ||||
|         if (http_code < 0 or http_code > std.math.maxInt(u16)) { | ||||
|             return 0; | ||||
|         } | ||||
|         return @intCast(http_code); | ||||
|     } | ||||
| }; | ||||
|  | ||||
| // used by Connection and Handle | ||||
| pub fn setMethod(easy: *c.CURL, method: Method) !void { | ||||
|     switch (method) { | ||||
|         .GET => try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_HTTPGET, @as(c_long, 1))), | ||||
|         .POST => try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_HTTPPOST, @as(c_long, 1))), | ||||
|         .PUT => try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CUSTOMREQUEST, "put")), | ||||
|         .DELETE => try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CUSTOMREQUEST, "delete")), | ||||
|         .HEAD => try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CUSTOMREQUEST, "head")), | ||||
|         .OPTIONS => try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CUSTOMREQUEST, "options")), | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn errorCheck(code: c.CURLcode) errors.Error!void { | ||||
|     if (code == c.CURLE_OK) { | ||||
|         return; | ||||
|     } | ||||
|     return errors.fromCode(code); | ||||
| } | ||||
|  | ||||
| pub fn errorMCheck(code: c.CURLMcode) errors.Multi!void { | ||||
|     if (code == c.CURLM_OK) { | ||||
|         return; | ||||
|     } | ||||
|     if (code == c.CURLM_CALL_MULTI_PERFORM) { | ||||
|         // should we can client.perform() here? | ||||
|         // or just wait until the next time we naturally call it? | ||||
|         return; | ||||
|     } | ||||
|     return errors.fromMCode(code); | ||||
| } | ||||
|  | ||||
| pub const Opts = struct { | ||||
|     timeout_ms: u31 = 0, | ||||
|     max_redirects: u8 = 10, | ||||
|     connect_timeout_ms: u31 = 5000, | ||||
|     max_concurrent_transfers: u8 = 5, | ||||
| }; | ||||
|  | ||||
| pub const Method = enum { | ||||
|     GET, | ||||
|     PUT, | ||||
|     POST, | ||||
|     DELETE, | ||||
|     HEAD, | ||||
|     OPTIONS, | ||||
| }; | ||||
|  | ||||
| pub const ProxyType = enum { | ||||
|     forward, | ||||
|     connect, | ||||
| }; | ||||
|  | ||||
| pub const ProxyAuth = union(enum) { | ||||
|     basic: struct { user_pass: []const u8 }, | ||||
|     bearer: struct { token: []const u8 }, | ||||
| }; | ||||
|  | ||||
| // TODO: on BSD / Linux, we could just read the PEM file directly. | ||||
| // This whole rescan + decode is really just needed for MacOS. On Linux | ||||
| // bundle.rescan does find the .pem file(s) which could be in a few different | ||||
| // places, so it's still useful, just not efficient. | ||||
| fn loadCerts(allocator: Allocator, arena: Allocator) !c.curl_blob { | ||||
|     var bundle: std.crypto.Certificate.Bundle = .{}; | ||||
|     try bundle.rescan(allocator); | ||||
|     defer bundle.deinit(allocator); | ||||
|  | ||||
|     var it = bundle.map.valueIterator(); | ||||
|     const bytes = bundle.bytes.items; | ||||
|  | ||||
|     const encoder = std.base64.standard.Encoder; | ||||
|     var arr: std.ArrayListUnmanaged(u8) = .empty; | ||||
|  | ||||
|     const encoded_size = encoder.calcSize(bytes.len); | ||||
|     const buffer_size = encoded_size + | ||||
|         (bundle.map.count() * 75) + // start / end per certificate + extra, just in case | ||||
|         (encoded_size / 64) // newline per 64 characters | ||||
|     ; | ||||
|     try arr.ensureTotalCapacity(arena, buffer_size); | ||||
|     var writer = arr.writer(arena); | ||||
|  | ||||
|     while (it.next()) |index| { | ||||
|         const cert = try std.crypto.Certificate.der.Element.parse(bytes, index.*); | ||||
|  | ||||
|         try writer.writeAll("-----BEGIN CERTIFICATE-----\n"); | ||||
|         var line_writer = LineWriter{ .inner = writer }; | ||||
|         try encoder.encodeWriter(&line_writer, bytes[index.*..cert.slice.end]); | ||||
|         try writer.writeAll("\n-----END CERTIFICATE-----\n"); | ||||
|     } | ||||
|  | ||||
|     // Final encoding should not be larger than our initial size estimate | ||||
|     std.debug.assert(buffer_size > arr.items.len); | ||||
|  | ||||
|     return .{ | ||||
|         .len = arr.items.len, | ||||
|         .data = arr.items.ptr, | ||||
|         .flags = 0, | ||||
|     }; | ||||
| } | ||||
|  | ||||
| // Wraps lines @ 64 columns. A PEM is basically a base64 encoded DER (which is | ||||
| // what Zig has), with lines wrapped at 64 characters and with a basic header | ||||
| // and footer | ||||
| const LineWriter = struct { | ||||
|     col: usize = 0, | ||||
|     inner: std.ArrayListUnmanaged(u8).Writer, | ||||
|  | ||||
|     pub fn writeAll(self: *LineWriter, data: []const u8) !void { | ||||
|         var writer = self.inner; | ||||
|  | ||||
|         var col = self.col; | ||||
|         const len = 64 - col; | ||||
|  | ||||
|         var remain = data; | ||||
|         if (remain.len > len) { | ||||
|             col = 0; | ||||
|             try writer.writeAll(data[0..len]); | ||||
|             try writer.writeByte('\n'); | ||||
|             remain = data[len..]; | ||||
|         } | ||||
|  | ||||
|         while (remain.len > 64) { | ||||
|             try writer.writeAll(remain[0..64]); | ||||
|             try writer.writeByte('\n'); | ||||
|             remain = data[len..]; | ||||
|         } | ||||
|         try writer.writeAll(remain); | ||||
|         self.col = col + remain.len; | ||||
|     } | ||||
| }; | ||||
| @@ -1,93 +0,0 @@ | ||||
| // Copyright (C) 2023-2025  Lightpanda (Selecy SAS) | ||||
| // | ||||
| // Francis Bouvier <francis@lightpanda.io> | ||||
| // Pierre Tachoire <pierre@lightpanda.io> | ||||
| // | ||||
| // This program is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU Affero General Public License as | ||||
| // published by the Free Software Foundation, either version 3 of the | ||||
| // License, or (at your option) any later version. | ||||
| // | ||||
| // This program is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| // GNU Affero General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU Affero General Public License | ||||
| // along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
|  | ||||
| const std = @import("std"); | ||||
| const c = @import("client.zig").c; | ||||
|  | ||||
| const Allocator = std.mem.Allocator; | ||||
|  | ||||
| // TODO: on BSD / Linux, we could just read the PEM file directly. | ||||
| // This whole rescan + decode is really just needed for MacOS. On Linux | ||||
| // bundle.rescan does find the .pem file(s) which could be in a few different | ||||
| // places, so it's still useful, just not efficient. | ||||
| pub fn load(allocator: Allocator, arena: Allocator) !c.curl_blob { | ||||
|     var bundle: std.crypto.Certificate.Bundle = .{}; | ||||
|     try bundle.rescan(allocator); | ||||
|     defer bundle.deinit(allocator); | ||||
|  | ||||
|     var it = bundle.map.valueIterator(); | ||||
|     const bytes = bundle.bytes.items; | ||||
|  | ||||
|     const encoder = std.base64.standard.Encoder; | ||||
|     var arr: std.ArrayListUnmanaged(u8) = .empty; | ||||
|  | ||||
|     const encoded_size = encoder.calcSize(bytes.len); | ||||
|     const buffer_size = encoded_size + | ||||
|         (bundle.map.count() * 75) + // start / end per certificate + extra, just in case | ||||
|         (encoded_size / 64) // newline per 64 characters | ||||
|     ; | ||||
|     try arr.ensureTotalCapacity(arena, buffer_size); | ||||
|     var writer = arr.writer(arena); | ||||
|  | ||||
|     while (it.next()) |index| { | ||||
|         const cert = try std.crypto.Certificate.der.Element.parse(bytes, index.*); | ||||
|  | ||||
|         try writer.writeAll("-----BEGIN CERTIFICATE-----\n"); | ||||
|         var line_writer = LineWriter{ .inner = writer }; | ||||
|         try encoder.encodeWriter(&line_writer, bytes[index.*..cert.slice.end]); | ||||
|         try writer.writeAll("\n-----END CERTIFICATE-----\n"); | ||||
|     } | ||||
|  | ||||
|     // Final encoding should not be larger than our initial size estimate | ||||
|     std.debug.assert(buffer_size > arr.items.len); | ||||
|  | ||||
|     return .{ | ||||
|         .len = arr.items.len, | ||||
|         .data = arr.items.ptr, | ||||
|         .flags = 0, | ||||
|     }; | ||||
| } | ||||
|  | ||||
| // Wraps lines @ 64 columns | ||||
| const LineWriter = struct { | ||||
|     col: usize = 0, | ||||
|     inner: std.ArrayListUnmanaged(u8).Writer, | ||||
|  | ||||
|     pub fn writeAll(self: *LineWriter, data: []const u8) !void { | ||||
|         var writer = self.inner; | ||||
|  | ||||
|         var col = self.col; | ||||
|         const len = 64 - col; | ||||
|  | ||||
|         var remain = data; | ||||
|         if (remain.len > len) { | ||||
|             col = 0; | ||||
|             try writer.writeAll(data[0..len]); | ||||
|             try writer.writeByte('\n'); | ||||
|             remain = data[len..]; | ||||
|         } | ||||
|  | ||||
|         while (remain.len > 64) { | ||||
|             try writer.writeAll(remain[0..64]); | ||||
|             try writer.writeByte('\n'); | ||||
|             remain = data[len..]; | ||||
|         } | ||||
|         try writer.writeAll(remain); | ||||
|         self.col = col + remain.len; | ||||
|     } | ||||
| }; | ||||
| @@ -17,7 +17,7 @@ | ||||
| // along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
|  | ||||
| const std = @import("std"); | ||||
| const c = @import("client.zig").c; | ||||
| const c = @import("Http.zig").c; | ||||
|  | ||||
| pub const Error = error{ | ||||
|     UnsupportedProtocol, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Karl Seguin
					Karl Seguin