diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index d9d7b803..4c0a28f9 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -13,7 +13,7 @@ inputs: zig-v8: description: 'zig v8 version to install' required: false - default: 'v0.3.2' + default: 'v0.3.3' v8: description: 'v8 version to install' required: false diff --git a/Dockerfile b/Dockerfile index e30fbdbe..f106905a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM debian:stable-slim ARG MINISIG=0.12 ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U ARG V8=14.0.365.4 -ARG ZIG_V8=v0.3.2 +ARG ZIG_V8=v0.3.3 ARG TARGETPLATFORM RUN apt-get update -yq && \ diff --git a/build.zig.zon b/build.zig.zon index b7f9cf3b..9a28408b 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,8 +5,8 @@ .minimum_zig_version = "0.15.2", .dependencies = .{ .v8 = .{ - .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.2.tar.gz", - .hash = "v8-0.0.0-xddH6wx-BABNgL7YIDgbnFgKZuXZ68yZNngNSrV6OjrY", + .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.3.tar.gz", + .hash = "v8-0.0.0-xddH6yx3BAAGD9jSoq_ttt_bk9MectTU44s_HZxxE5LD", }, // .v8 = .{ .path = "../zig-v8-fork" }, .brotli = .{ diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index edb6baee..0d8b87cd 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -265,13 +265,15 @@ pub fn blob(_: *const Factory, arena: Allocator, child: anytype) !*@TypeOf(child return chain.get(1); } -pub fn abstractRange(self: *Factory, child: anytype, page: *Page) !*@TypeOf(child) { - const allocator = self._slab.allocator(); - const chain = try PrototypeChain(&.{ AbstractRange, @TypeOf(child) }).allocate(allocator); +pub fn abstractRange(_: *const Factory, arena: Allocator, child: anytype, page: *Page) !*@TypeOf(child) { + const chain = try PrototypeChain(&.{ AbstractRange, @TypeOf(child) }).allocate(arena); const doc = page.document.asNode(); const abstract_range = chain.get(0); abstract_range.* = AbstractRange{ + ._rc = 0, + ._arena = arena, + ._page_id = page.id, ._type = unionInit(AbstractRange.Type, chain.get(1)), ._end_offset = 0, ._start_offset = 0, diff --git a/src/browser/HttpClient.zig b/src/browser/HttpClient.zig index 98292efc..f716904c 100644 --- a/src/browser/HttpClient.zig +++ b/src/browser/HttpClient.zig @@ -66,9 +66,18 @@ active: usize, // 'networkAlmostIdle' Page.lifecycleEvent in CDP). intercepted: usize, -// Our easy handles, managed by a curl multi. +// Our curl multi handle. handles: Net.Handles, +// Connections currently in this client's curl_multi. +in_use: std.DoublyLinkedList = .{}, + +// Connections that failed to be removed from curl_multi during perform. +dirty: std.DoublyLinkedList = .{}, + +// Whether we're currently inside a curl_multi_perform call. +performing: bool = false, + // Use to generate the next request ID next_request_id: u32 = 0, @@ -88,8 +97,8 @@ pending_robots_queue: std.StringHashMapUnmanaged(std.ArrayList(Request)) = .empt // request. These wil come and go with each request. transfer_pool: std.heap.MemoryPool(Transfer), -// only needed for CDP which can change the proxy and then restore it. When -// restoring, this originally-configured value is what it goes to. +// The current proxy. CDP can change it, restoreOriginalProxy restores +// from config. http_proxy: ?[:0]const u8 = null, // track if the client use a proxy for connections. @@ -97,6 +106,9 @@ http_proxy: ?[:0]const u8 = null, // CDP. use_proxy: bool, +// Current TLS verification state, applied per-connection in makeRequest. +tls_verify: bool = true, + cdp_client: ?CDPClient = null, // libcurl can monitor arbitrary sockets, this lets us use libcurl to poll @@ -126,13 +138,8 @@ pub fn init(allocator: Allocator, network: *Network) !*Client { const client = try allocator.create(Client); errdefer allocator.destroy(client); - var handles = try Net.Handles.init(allocator, network.ca_blob, network.config); - errdefer handles.deinit(allocator); - - // Set transfer callbacks on each connection. - for (handles.connections) |*conn| { - try conn.setCallbacks(Transfer.headerCallback, Transfer.dataCallback); - } + var handles = try Net.Handles.init(network.config); + errdefer handles.deinit(); const http_proxy = network.config.httpProxy(); @@ -145,6 +152,7 @@ pub fn init(allocator: Allocator, network: *Network) !*Client { .network = network, .http_proxy = http_proxy, .use_proxy = http_proxy != null, + .tls_verify = network.config.tlsVerifyHost(), .transfer_pool = transfer_pool, }; @@ -153,7 +161,7 @@ pub fn init(allocator: Allocator, network: *Network) !*Client { pub fn deinit(self: *Client) void { self.abort(); - self.handles.deinit(self.allocator); + self.handles.deinit(); self.transfer_pool.deinit(); @@ -182,14 +190,14 @@ pub fn abortFrame(self: *Client, frame_id: u32) void { // but abort can avoid the frame_id check at comptime. fn _abort(self: *Client, comptime abort_all: bool, frame_id: u32) void { { - var q = &self.handles.in_use; + var q = &self.in_use; var n = q.first; while (n) |node| { n = node.next; const conn: *Net.Connection = @fieldParentPtr("node", node); var transfer = Transfer.fromConnection(conn) catch |err| { // Let's cleanup what we can - self.handles.remove(conn); + self.removeConn(conn); log.err(.http, "get private info", .{ .err = err, .source = "abort" }); continue; }; @@ -226,8 +234,7 @@ fn _abort(self: *Client, comptime abort_all: bool, frame_id: u32) void { } if (comptime IS_DEBUG and abort_all) { - std.debug.assert(self.handles.in_use.first == null); - std.debug.assert(self.handles.available.len() == self.handles.connections.len); + std.debug.assert(self.in_use.first == null); const running = self.handles.perform() catch |err| { lp.assert(false, "multi perform in abort", .{ .err = err }); @@ -237,15 +244,12 @@ fn _abort(self: *Client, comptime abort_all: bool, frame_id: u32) void { } pub fn tick(self: *Client, timeout_ms: u32) !PerformStatus { - while (true) { - if (self.handles.hasAvailable() == false) { + while (self.queue.popFirst()) |queue_node| { + const conn = self.network.getConnection() orelse { + self.queue.prepend(queue_node); break; - } - const queue_node = self.queue.popFirst() orelse break; + }; const transfer: *Transfer = @fieldParentPtr("_node", queue_node); - - // we know this exists, because we checked hasAvailable() above - const conn = self.handles.get().?; try self.makeRequest(conn, transfer); } return self.perform(@intCast(timeout_ms)); @@ -529,8 +533,8 @@ fn waitForInterceptedResponse(self: *Client, transfer: *Transfer) !bool { fn process(self: *Client, transfer: *Transfer) !void { // libcurl doesn't allow recursive calls, if we're in a `perform()` operation // then we _have_ to queue this. - if (self.handles.performing == false) { - if (self.handles.get()) |conn| { + if (self.performing == false) { + if (self.network.getConnection()) |conn| { return self.makeRequest(conn, transfer); } } @@ -644,10 +648,7 @@ fn requestFailed(transfer: *Transfer, err: anyerror, comptime execute_callback: // can be changed at any point in the easy's lifecycle. pub fn changeProxy(self: *Client, proxy: [:0]const u8) !void { try self.ensureNoActiveConnection(); - - for (self.handles.connections) |*conn| { - try conn.setProxy(proxy.ptr); - } + self.http_proxy = proxy; self.use_proxy = true; } @@ -656,31 +657,21 @@ pub fn changeProxy(self: *Client, proxy: [:0]const u8) !void { pub fn restoreOriginalProxy(self: *Client) !void { try self.ensureNoActiveConnection(); - const proxy = if (self.http_proxy) |p| p.ptr else null; - for (self.handles.connections) |*conn| { - try conn.setProxy(proxy); - } - self.use_proxy = proxy != null; + self.http_proxy = self.network.config.httpProxy(); + self.use_proxy = self.http_proxy != null; } // Enable TLS verification on all connections. -pub fn enableTlsVerify(self: *Client) !void { +pub fn setTlsVerify(self: *Client, verify: bool) !void { // Remove inflight connections check on enable TLS b/c chromiumoxide calls // the command during navigate and Curl seems to accept it... - for (self.handles.connections) |*conn| { - try conn.setTlsVerify(true, self.use_proxy); - } -} - -// Disable TLS verification on all connections. -pub fn disableTlsVerify(self: *Client) !void { - // Remove inflight connections check on disable TLS b/c chromiumoxide calls - // the command during navigate and Curl seems to accept it... - - for (self.handles.connections) |*conn| { - try conn.setTlsVerify(false, self.use_proxy); + var it = self.in_use.first; + while (it) |node| : (it = node.next) { + const conn: *Net.Connection = @fieldParentPtr("node", node); + try conn.setTlsVerify(verify, self.use_proxy); } + self.tls_verify = verify; } fn makeRequest(self: *Client, conn: *Net.Connection, transfer: *Transfer) anyerror!void { @@ -691,9 +682,14 @@ fn makeRequest(self: *Client, conn: *Net.Connection, transfer: *Transfer) anyerr errdefer { transfer._conn = null; transfer.deinit(); - self.handles.isAvailable(conn); + self.releaseConn(conn); } + // Set callbacks and per-client settings on the pooled connection. + try conn.setCallbacks(Transfer.headerCallback, Transfer.dataCallback); + try conn.setProxy(self.http_proxy); + try conn.setTlsVerify(self.tls_verify, self.use_proxy); + try conn.setURL(req.url); try conn.setMethod(req.method); if (req.body) |b| { @@ -728,10 +724,12 @@ fn makeRequest(self: *Client, conn: *Net.Connection, transfer: *Transfer) anyerr // fails BEFORE `curl_multi_add_handle` succeeds, the we still need to do // cleanup. But if things fail after `curl_multi_add_handle`, we expect // perfom to pickup the failure and cleanup. + self.in_use.append(&conn.node); self.handles.add(conn) catch |err| { transfer._conn = null; transfer.deinit(); - self.handles.isAvailable(conn); + self.in_use.remove(&conn.node); + self.releaseConn(conn); return err; }; @@ -752,7 +750,22 @@ pub const PerformStatus = enum { }; fn perform(self: *Client, timeout_ms: c_int) !PerformStatus { - const running = try self.handles.perform(); + const running = blk: { + self.performing = true; + defer self.performing = false; + + break :blk try self.handles.perform(); + }; + + // Process dirty connections — return them to Runtime pool. + while (self.dirty.popFirst()) |node| { + const conn: *Net.Connection = @fieldParentPtr("node", node); + self.handles.remove(conn) catch |err| { + log.fatal(.http, "multi remove handle", .{ .err = err, .src = "perform" }); + @panic("multi_remove_handle"); + }; + self.releaseConn(conn); + } // We're potentially going to block for a while until we get data. Process // whatever messages we have waiting ahead of time. @@ -871,11 +884,26 @@ fn processMessages(self: *Client) !bool { fn endTransfer(self: *Client, transfer: *Transfer) void { const conn = transfer._conn.?; - self.handles.remove(conn); + self.removeConn(conn); transfer._conn = null; self.active -= 1; } +fn removeConn(self: *Client, conn: *Net.Connection) void { + self.in_use.remove(&conn.node); + if (self.handles.remove(conn)) { + self.releaseConn(conn); + } else |_| { + // Can happen if we're in a perform() call, so we'll queue this + // for cleanup later. + self.dirty.append(&conn.node); + } +} + +fn releaseConn(self: *Client, conn: *Net.Connection) void { + self.network.releaseConnection(conn); +} + fn ensureNoActiveConnection(self: *const Client) !void { if (self.active > 0) { return error.InflightConnection; @@ -1023,7 +1051,7 @@ pub const Transfer = struct { fn deinit(self: *Transfer) void { self.req.headers.deinit(); if (self._conn) |conn| { - self.client.handles.remove(conn); + self.client.removeConn(conn); } self.arena.deinit(); self.client.transfer_pool.destroy(self); @@ -1093,7 +1121,7 @@ pub const Transfer = struct { requestFailed(self, err, true); const client = self.client; - if (self._performing or client.handles.performing) { + if (self._performing or client.performing) { // We're currently in a curl_multi_perform. We cannot call endTransfer // as that calls curl_multi_remove_handle, and you can't do that // from a curl callback. Instead, we flag this transfer and all of @@ -1258,6 +1286,16 @@ pub const Transfer = struct { if (buf_len < 3) { // could be \r\n or \n. + // We get the last header line. + if (transfer._redirecting) { + // parse and set cookies for the redirection. + redirectionCookies(transfer, &conn) catch |err| { + if (comptime IS_DEBUG) { + log.debug(.http, "redirection cookies", .{ .err = err }); + } + return 0; + }; + } return buf_len; } @@ -1324,38 +1362,22 @@ pub const Transfer = struct { transfer.bytes_received += buf_len; } - if (buf_len > 2) { - if (transfer._auth_challenge != null) { - // try to parse auth challenge. - if (std.ascii.startsWithIgnoreCase(header, "WWW-Authenticate") or - std.ascii.startsWithIgnoreCase(header, "Proxy-Authenticate")) - { - const ac = AuthChallenge.parse( - transfer._auth_challenge.?.status, - header, - ) catch |err| { - // We can't parse the auth challenge - log.err(.http, "parse auth challenge", .{ .err = err, .header = header }); - // Should we cancel the request? I don't think so. - return buf_len; - }; - transfer._auth_challenge = ac; - } + if (transfer._auth_challenge != null) { + // try to parse auth challenge. + if (std.ascii.startsWithIgnoreCase(header, "WWW-Authenticate") or + std.ascii.startsWithIgnoreCase(header, "Proxy-Authenticate")) + { + const ac = AuthChallenge.parse( + transfer._auth_challenge.?.status, + header, + ) catch |err| { + // We can't parse the auth challenge + log.err(.http, "parse auth challenge", .{ .err = err, .header = header }); + // Should we cancel the request? I don't think so. + return buf_len; + }; + transfer._auth_challenge = ac; } - return buf_len; - } - - // Starting here, we get the last header line. - - if (transfer._redirecting) { - // parse and set cookies for the redirection. - redirectionCookies(transfer, &conn) catch |err| { - if (comptime IS_DEBUG) { - log.debug(.http, "redirection cookies", .{ .err = err }); - } - return 0; - }; - return buf_len; } return buf_len; diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 2e727695..f0c914ad 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -80,6 +80,8 @@ pub const BUF_SIZE = 1024; const Page = @This(); +id: u32, + // This is the "id" of the frame. It can be re-used from page-to-page, e.g. // when navigating. _frame_id: u32, @@ -254,6 +256,7 @@ pub fn init(self: *Page, frame_id: u32, session: *Session, parent: ?*Page) !void })).asDocument(); self.* = .{ + .id = session.nextPageId(), .js = undefined, .parent = parent, .arena = session.page_arena, @@ -404,6 +407,18 @@ pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool { return std.mem.startsWith(u8, url, current_origin); } +/// Look up a blob URL in this page's registry, walking up the parent chain. +pub fn lookupBlobUrl(self: *Page, url: []const u8) ?*Blob { + var current: ?*Page = self; + while (current) |page| { + if (page._blob_urls.get(url)) |blob| { + return blob; + } + current = page.parent; + } + return null; +} + pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !void { lp.assert(self._load_state == .waiting, "page.renavigate", .{}); const session = self._session; @@ -419,12 +434,17 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi .type = self._type, }); - // if the url is about:blank, we load an empty HTML document in the - // page and dispatch the events. - if (std.mem.eql(u8, "about:blank", request_url)) { - self.url = "about:blank"; + // Handle synthetic navigations: about:blank and blob: URLs + const is_about_blank = std.mem.eql(u8, "about:blank", request_url); + const is_blob = !is_about_blank and std.mem.startsWith(u8, request_url, "blob:"); - if (self.parent) |parent| { + if (is_about_blank or is_blob) { + self.url = if (is_about_blank) "about:blank" else try self.arena.dupeZ(u8, request_url); + + if (is_blob) { + // strip out blob: + self.origin = try URL.getOrigin(self.arena, request_url[5.. :0]); + } else if (self.parent) |parent| { self.origin = parent.origin; } else { self.origin = null; @@ -435,10 +455,22 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi // It's important to force a reset during the following navigation. self._parse_state = .complete; - self.document.injectBlank(self) catch |err| { - log.err(.browser, "inject blank", .{ .err = err }); - return error.InjectBlankFailed; - }; + // Content injection + if (is_blob) { + const blob = self.lookupBlobUrl(request_url) orelse { + log.warn(.js, "invalid blob", .{ .url = request_url }); + return error.BlobNotFound; + }; + const parse_arena = try self.getArena(.{ .debug = "Page.parseBlob" }); + defer self.releaseArena(parse_arena); + var parser = Parser.init(parse_arena, self.document.asNode(), self); + parser.parse(blob._slice); + } else { + self.document.injectBlank(self) catch |err| { + log.err(.browser, "inject blank", .{ .err = err }); + return error.InjectBlankFailed; + }; + } self.documentIsComplete(); session.notification.dispatch(.page_navigate, &.{ @@ -452,7 +484,7 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi // Record telemetry for navigation session.browser.app.telemetry.record(.{ .navigate = .{ - .tls = false, // about:blank is not TLS + .tls = false, // about:blank and blob: are not TLS .proxy = session.browser.app.config.httpProxy() != null, }, }); diff --git a/src/browser/Session.zig b/src/browser/Session.zig index 529f0847..fea56a87 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -84,6 +84,7 @@ queued_navigation: std.ArrayList(*Page), // about:blank navigations (which may add to queued_navigation). queued_queued_navigation: std.ArrayList(*Page), +page_id_gen: u32, frame_id_gen: u32, pub fn init(self: *Session, browser: *Browser, notification: *Notification) !void { @@ -103,6 +104,7 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi .page_arena = page_arena, .factory = Factory.init(page_arena), .history = .{}, + .page_id_gen = 0, .frame_id_gen = 0, // The prototype (EventTarget) for Navigation is created when a Page is created. .navigation = .{ ._proto = undefined }, @@ -297,9 +299,24 @@ pub const WaitResult = enum { cdp_socket, }; -pub fn findPage(self: *Session, frame_id: u32) ?*Page { +pub fn findPageByFrameId(self: *Session, frame_id: u32) ?*Page { const page = self.currentPage() orelse return null; - return if (page._frame_id == frame_id) page else null; + return findPageBy(page, "_frame_id", frame_id); +} + +pub fn findPageById(self: *Session, id: u32) ?*Page { + const page = self.currentPage() orelse return null; + return findPageBy(page, "id", id); +} + +fn findPageBy(page: *Page, comptime field: []const u8, id: u32) ?*Page { + if (@field(page, field) == id) return page; + for (page.frames.items) |f| { + if (findPageBy(f, field, id)) |found| { + return found; + } + } + return null; } pub fn wait(self: *Session, wait_ms: u32) WaitResult { @@ -636,3 +653,9 @@ pub fn nextFrameId(self: *Session) u32 { self.frame_id_gen = id; return id; } + +pub fn nextPageId(self: *Session) u32 { + const id = self.page_id_gen +% 1; + self.page_id_gen = id; + return id; +} diff --git a/src/browser/URL.zig b/src/browser/URL.zig index 19b87333..f8d7dd90 100644 --- a/src/browser/URL.zig +++ b/src/browser/URL.zig @@ -277,6 +277,11 @@ pub fn isCompleteHTTPUrl(url: []const u8) bool { return false; } + // blob: and data: URLs are complete but don't follow scheme:// pattern + if (std.mem.startsWith(u8, url, "blob:") or std.mem.startsWith(u8, url, "data:")) { + return true; + } + // Check if there's a scheme (protocol) ending with :// const colon_pos = std.mem.indexOfScalar(u8, url, ':') orelse return false; diff --git a/src/browser/tests/canvas/canvas_rendering_context_2d.html b/src/browser/tests/canvas/canvas_rendering_context_2d.html index a6765d88..e065b81f 100644 --- a/src/browser/tests/canvas/canvas_rendering_context_2d.html +++ b/src/browser/tests/canvas/canvas_rendering_context_2d.html @@ -89,6 +89,41 @@ } + + + + + + diff --git a/src/browser/tests/page/blob.html b/src/browser/tests/page/blob.html new file mode 100644 index 00000000..434c1ce6 --- /dev/null +++ b/src/browser/tests/page/blob.html @@ -0,0 +1,41 @@ + + + + + + + diff --git a/src/browser/webapi/AbstractRange.zig b/src/browser/webapi/AbstractRange.zig index e766ac29..d80fc115 100644 --- a/src/browser/webapi/AbstractRange.zig +++ b/src/browser/webapi/AbstractRange.zig @@ -19,15 +19,22 @@ const std = @import("std"); const js = @import("../js/js.zig"); +const Session = @import("../Session.zig"); + const Node = @import("Node.zig"); const Range = @import("Range.zig"); +const Allocator = std.mem.Allocator; +const IS_DEBUG = @import("builtin").mode == .Debug; + const AbstractRange = @This(); pub const _prototype_root = true; +_rc: u8, _type: Type, - +_page_id: u32, +_arena: Allocator, _end_offset: u32, _start_offset: u32, _end_container: *Node, @@ -36,6 +43,27 @@ _start_container: *Node, // Intrusive linked list node for tracking live ranges on the Page. _range_link: std.DoublyLinkedList.Node = .{}, +pub fn acquireRef(self: *AbstractRange) void { + self._rc += 1; +} + +pub fn deinit(self: *AbstractRange, shutdown: bool, session: *Session) void { + _ = shutdown; + const rc = self._rc; + if (comptime IS_DEBUG) { + std.debug.assert(rc != 0); + } + + if (rc == 1) { + if (session.findPageById(self._page_id)) |page| { + page._live_ranges.remove(&self._range_link); + } + session.releaseArena(self._arena); + return; + } + self._rc = rc - 1; +} + pub const Type = union(enum) { range: *Range, // TODO: static_range: *StaticRange, @@ -310,6 +338,8 @@ pub const JsApi = struct { pub const name = "AbstractRange"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; + pub const weak = true; + pub const finalizer = bridge.finalizer(AbstractRange.deinit); }; pub const startContainer = bridge.accessor(AbstractRange.getStartContainer, null, .{}); diff --git a/src/browser/webapi/ImageData.zig b/src/browser/webapi/ImageData.zig index abb5abd5..e25e2dd6 100644 --- a/src/browser/webapi/ImageData.zig +++ b/src/browser/webapi/ImageData.zig @@ -52,7 +52,7 @@ pub const ConstructorSettings = struct { /// ``` /// /// We currently support only the first 2. -pub fn constructor( +pub fn init( width: u32, height: u32, maybe_settings: ?ConstructorSettings, @@ -106,7 +106,7 @@ pub const JsApi = struct { pub var class_id: bridge.ClassId = undefined; }; - pub const constructor = bridge.constructor(ImageData.constructor, .{ .dom_exception = true }); + pub const constructor = bridge.constructor(ImageData.init, .{ .dom_exception = true }); pub const colorSpace = bridge.property("srgb", .{ .template = false, .readonly = true }); pub const pixelFormat = bridge.property("rgba-unorm8", .{ .template = false, .readonly = true }); diff --git a/src/browser/webapi/Range.zig b/src/browser/webapi/Range.zig index 66ce1c93..fd02ce25 100644 --- a/src/browser/webapi/Range.zig +++ b/src/browser/webapi/Range.zig @@ -21,22 +21,31 @@ const String = @import("../../string.zig").String; const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); +const Session = @import("../Session.zig"); const Node = @import("Node.zig"); const DocumentFragment = @import("DocumentFragment.zig"); const AbstractRange = @import("AbstractRange.zig"); const DOMRect = @import("DOMRect.zig"); +const Allocator = std.mem.Allocator; + const Range = @This(); _proto: *AbstractRange, -pub fn asAbstractRange(self: *Range) *AbstractRange { - return self._proto; +pub fn init(page: *Page) !*Range { + const arena = try page.getArena(.{ .debug = "Range" }); + errdefer page.releaseArena(arena); + return page._factory.abstractRange(arena, Range{ ._proto = undefined }, page); } -pub fn init(page: *Page) !*Range { - return page._factory.abstractRange(Range{ ._proto = undefined }, page); +pub fn deinit(self: *Range, shutdown: bool, session: *Session) void { + self._proto.deinit(shutdown, session); +} + +pub fn asAbstractRange(self: *Range) *AbstractRange { + return self._proto; } pub fn setStart(self: *Range, node: *Node, offset: u32) !void { @@ -309,7 +318,10 @@ pub fn intersectsNode(self: *const Range, node: *Node) bool { } pub fn cloneRange(self: *const Range, page: *Page) !*Range { - const clone = try page._factory.abstractRange(Range{ ._proto = undefined }, page); + const arena = try page.getArena(.{ .debug = "Range.clone" }); + errdefer page.releaseArena(arena); + + const clone = try page._factory.abstractRange(arena, Range{ ._proto = undefined }, page); clone._proto._end_offset = self._proto._end_offset; clone._proto._start_offset = self._proto._start_offset; clone._proto._end_container = self._proto._end_container; @@ -687,6 +699,8 @@ pub const JsApi = struct { pub const name = "Range"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; + pub const weak = true; + pub const finalizer = bridge.finalizer(Range.deinit); }; // Constants for compareBoundaryPoints diff --git a/src/browser/webapi/Selection.zig b/src/browser/webapi/Selection.zig index a13390a3..7c261823 100644 --- a/src/browser/webapi/Selection.zig +++ b/src/browser/webapi/Selection.zig @@ -21,6 +21,8 @@ const log = @import("../../log.zig"); const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); +const Session = @import("../Session.zig"); + const Range = @import("Range.zig"); const AbstractRange = @import("AbstractRange.zig"); const Node = @import("Node.zig"); @@ -37,13 +39,22 @@ _direction: SelectionDirection = .none, pub const init: Selection = .{}; +pub fn deinit(self: *Selection, shutdown: bool, session: *Session) void { + if (self._range) |r| { + r.deinit(shutdown, session); + self._range = null; + } +} + fn dispatchSelectionChangeEvent(page: *Page) !void { const event = try Event.init("selectionchange", .{}, page); try page._event_manager.dispatch(page.document.asEventTarget(), event); } fn isInTree(self: *const Selection) bool { - if (self._range == null) return false; + if (self._range == null) { + return false; + } const anchor_node = self.getAnchorNode() orelse return false; const focus_node = self.getFocusNode() orelse return false; return anchor_node.isConnected() and focus_node.isConnected(); @@ -104,21 +115,33 @@ pub fn getIsCollapsed(self: *const Selection) bool { } pub fn getRangeCount(self: *const Selection) u32 { - if (self._range == null) return 0; - if (!self.isInTree()) return 0; + if (self._range == null) { + return 0; + } + if (!self.isInTree()) { + return 0; + } return 1; } pub fn getType(self: *const Selection) []const u8 { - if (self._range == null) return "None"; - if (!self.isInTree()) return "None"; - if (self.getIsCollapsed()) return "Caret"; + if (self._range == null) { + return "None"; + } + if (!self.isInTree()) { + return "None"; + } + if (self.getIsCollapsed()) { + return "Caret"; + } return "Range"; } pub fn addRange(self: *Selection, range: *Range, page: *Page) !void { - if (self._range != null) return; + if (self._range != null) { + return; + } // Only add the range if its root node is in the document associated with this selection const start_node = range.asAbstractRange().getStartContainer(); @@ -126,22 +149,25 @@ pub fn addRange(self: *Selection, range: *Range, page: *Page) !void { return; } - self._range = range; + self.setRange(range, page); try dispatchSelectionChangeEvent(page); } pub fn removeRange(self: *Selection, range: *Range, page: *Page) !void { - if (self._range == range) { - self._range = null; - try dispatchSelectionChangeEvent(page); - return; - } else { + const existing_range = self._range orelse return error.NotFound; + if (existing_range != range) { return error.NotFound; } + self.setRange(null, page); + try dispatchSelectionChangeEvent(page); } pub fn removeAllRanges(self: *Selection, page: *Page) !void { - self._range = null; + if (self._range == null) { + return; + } + + self.setRange(null, page); self._direction = .none; try dispatchSelectionChangeEvent(page); } @@ -157,7 +183,7 @@ pub fn collapseToEnd(self: *Selection, page: *Page) !void { try new_range.setStart(last_node, last_offset); try new_range.setEnd(last_node, last_offset); - self._range = new_range; + self.setRange(new_range, page); self._direction = .none; try dispatchSelectionChangeEvent(page); } @@ -173,7 +199,7 @@ pub fn collapseToStart(self: *Selection, page: *Page) !void { try new_range.setStart(first_node, first_offset); try new_range.setEnd(first_node, first_offset); - self._range = new_range; + self.setRange(new_range, page); self._direction = .none; try dispatchSelectionChangeEvent(page); } @@ -255,7 +281,7 @@ pub fn extend(self: *Selection, node: *Node, _offset: ?u32, page: *Page) !void { }, } - self._range = new_range; + self.setRange(new_range, page); try dispatchSelectionChangeEvent(page); } @@ -560,7 +586,8 @@ fn applyModify(self: *Selection, alter: ModifyAlter, new_node: *Node, new_offset const new_range = try Range.init(page); try new_range.setStart(new_node, new_offset); try new_range.setEnd(new_node, new_offset); - self._range = new_range; + + self.setRange(new_range, page); self._direction = .none; try dispatchSelectionChangeEvent(page); }, @@ -582,7 +609,7 @@ pub fn selectAllChildren(self: *Selection, parent: *Node, page: *Page) !void { const child_count = parent.getChildrenCount(); try range.setEnd(parent, @intCast(child_count)); - self._range = range; + self.setRange(range, page); self._direction = .forward; try dispatchSelectionChangeEvent(page); } @@ -630,7 +657,7 @@ pub fn setBaseAndExtent( }, } - self._range = range; + self.setRange(range, page); try dispatchSelectionChangeEvent(page); } @@ -656,7 +683,7 @@ pub fn collapse(self: *Selection, _node: ?*Node, _offset: ?u32, page: *Page) !vo try range.setStart(node, offset); try range.setEnd(node, offset); - self._range = range; + self.setRange(range, page); self._direction = .none; try dispatchSelectionChangeEvent(page); } @@ -666,6 +693,16 @@ pub fn toString(self: *const Selection, page: *Page) ![]const u8 { return try range.toString(page); } +fn setRange(self: *Selection, new_range: ?*Range, page: *Page) void { + if (self._range) |existing| { + existing.deinit(false, page._session); + } + if (new_range) |nr| { + nr.asAbstractRange().acquireRef(); + } + self._range = new_range; +} + pub const JsApi = struct { pub const bridge = js.Bridge(Selection); @@ -673,6 +710,7 @@ pub const JsApi = struct { pub const name = "Selection"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; + pub const finalizer = bridge.finalizer(Selection.deinit); }; pub const anchorNode = bridge.accessor(Selection.getAnchorNode, null, .{}); diff --git a/src/browser/webapi/URL.zig b/src/browser/webapi/URL.zig index fda7d2a5..e9263319 100644 --- a/src/browser/webapi/URL.zig +++ b/src/browser/webapi/URL.zig @@ -249,6 +249,8 @@ pub fn createObjectURL(blob: *Blob, page: *Page) ![]const u8 { .{ page.origin orelse "null", uuid_buf }, ); try page._blob_urls.put(page.arena, blob_url, blob); + // prevent GC from cleaning up the blob while it's in the registry + page.js.strongRef(blob); return blob_url; } @@ -258,8 +260,10 @@ pub fn revokeObjectURL(url: []const u8, page: *Page) void { return; } - // Remove from registry (no-op if not found) - _ = page._blob_urls.remove(url); + // Remove from registry and release strong ref (no-op if not found) + if (page._blob_urls.fetchRemove(url)) |entry| { + page.js.weakRef(entry.value); + } } pub const JsApi = struct { diff --git a/src/browser/webapi/canvas/CanvasRenderingContext2D.zig b/src/browser/webapi/canvas/CanvasRenderingContext2D.zig index 056100ee..a2127e5b 100644 --- a/src/browser/webapi/canvas/CanvasRenderingContext2D.zig +++ b/src/browser/webapi/canvas/CanvasRenderingContext2D.zig @@ -64,15 +64,30 @@ pub fn createImageData( switch (width_or_image_data) { .width => |width| { const height = maybe_height orelse return error.TypeError; - return ImageData.constructor(width, height, maybe_settings, page); + return ImageData.init(width, height, maybe_settings, page); }, .image_data => |image_data| { - return ImageData.constructor(image_data._width, image_data._height, null, page); + return ImageData.init(image_data._width, image_data._height, null, page); }, } } pub fn putImageData(_: *const CanvasRenderingContext2D, _: *ImageData, _: f64, _: f64, _: ?f64, _: ?f64, _: ?f64, _: ?f64) void {} + +pub fn getImageData( + _: *const CanvasRenderingContext2D, + _: i32, // sx + _: i32, // sy + sw: i32, + sh: i32, + page: *Page, +) !*ImageData { + if (sw <= 0 or sh <= 0) { + return error.IndexSizeError; + } + return ImageData.init(@intCast(sw), @intCast(sh), null, page); +} + pub fn save(_: *CanvasRenderingContext2D) void {} pub fn restore(_: *CanvasRenderingContext2D) void {} pub fn scale(_: *CanvasRenderingContext2D, _: f64, _: f64) void {} @@ -125,6 +140,7 @@ pub const JsApi = struct { pub const createImageData = bridge.function(CanvasRenderingContext2D.createImageData, .{ .dom_exception = true }); pub const putImageData = bridge.function(CanvasRenderingContext2D.putImageData, .{ .noop = true }); + pub const getImageData = bridge.function(CanvasRenderingContext2D.getImageData, .{ .dom_exception = true }); pub const save = bridge.function(CanvasRenderingContext2D.save, .{ .noop = true }); pub const restore = bridge.function(CanvasRenderingContext2D.restore, .{ .noop = true }); pub const scale = bridge.function(CanvasRenderingContext2D.scale, .{ .noop = true }); diff --git a/src/browser/webapi/canvas/OffscreenCanvasRenderingContext2D.zig b/src/browser/webapi/canvas/OffscreenCanvasRenderingContext2D.zig index 1a11bd68..faea97f0 100644 --- a/src/browser/webapi/canvas/OffscreenCanvasRenderingContext2D.zig +++ b/src/browser/webapi/canvas/OffscreenCanvasRenderingContext2D.zig @@ -63,15 +63,30 @@ pub fn createImageData( switch (width_or_image_data) { .width => |width| { const height = maybe_height orelse return error.TypeError; - return ImageData.constructor(width, height, maybe_settings, page); + return ImageData.init(width, height, maybe_settings, page); }, .image_data => |image_data| { - return ImageData.constructor(image_data._width, image_data._height, null, page); + return ImageData.init(image_data._width, image_data._height, null, page); }, } } pub fn putImageData(_: *const OffscreenCanvasRenderingContext2D, _: *ImageData, _: f64, _: f64, _: ?f64, _: ?f64, _: ?f64, _: ?f64) void {} + +pub fn getImageData( + _: *const OffscreenCanvasRenderingContext2D, + _: i32, // sx + _: i32, // sy + sw: i32, + sh: i32, + page: *Page, +) !*ImageData { + if (sw <= 0 or sh <= 0) { + return error.IndexSizeError; + } + return ImageData.init(@intCast(sw), @intCast(sh), null, page); +} + pub fn save(_: *OffscreenCanvasRenderingContext2D) void {} pub fn restore(_: *OffscreenCanvasRenderingContext2D) void {} pub fn scale(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64) void {} @@ -124,6 +139,7 @@ pub const JsApi = struct { pub const createImageData = bridge.function(OffscreenCanvasRenderingContext2D.createImageData, .{ .dom_exception = true }); pub const putImageData = bridge.function(OffscreenCanvasRenderingContext2D.putImageData, .{ .noop = true }); + pub const getImageData = bridge.function(OffscreenCanvasRenderingContext2D.getImageData, .{ .dom_exception = true }); pub const save = bridge.function(OffscreenCanvasRenderingContext2D.save, .{ .noop = true }); pub const restore = bridge.function(OffscreenCanvasRenderingContext2D.restore, .{ .noop = true }); pub const scale = bridge.function(OffscreenCanvasRenderingContext2D.scale, .{ .noop = true }); diff --git a/src/cdp/domains/accessibility.zig b/src/cdp/domains/accessibility.zig index f8e8df30..864c52b5 100644 --- a/src/cdp/domains/accessibility.zig +++ b/src/cdp/domains/accessibility.zig @@ -53,8 +53,8 @@ fn getFullAXTree(cmd: anytype) !void { const frame_id = params.frameId orelse { break :blk session.currentPage() orelse return error.PageNotLoaded; }; - const page_id = try id.toPageId(.frame_id, frame_id); - break :blk session.findPage(page_id) orelse { + const page_frame_id = try id.toPageId(.frame_id, frame_id); + break :blk session.findPageByFrameId(page_frame_id) orelse { return cmd.sendError(-32000, "Frame with the given id does not belong to the target.", .{}); }; }; diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index df1d37c2..5150a8e5 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -502,9 +502,9 @@ fn getFrameOwner(cmd: anytype) !void { })) orelse return error.InvalidParams; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const page_id = try id.toPageId(.frame_id, params.frameId); + const page_frame_id = try id.toPageId(.frame_id, params.frameId); - const page = bc.session.findPage(page_id) orelse { + const page = bc.session.findPageByFrameId(page_frame_id) orelse { return cmd.sendError(-32000, "Frame with the given id does not belong to the target.", .{}); }; diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index a2a36bbe..c04ee33b 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -238,7 +238,7 @@ pub fn httpRequestStart(bc: anytype, msg: *const Notification.RequestStart) !voi const transfer = msg.transfer; const req = &transfer.req; const frame_id = req.frame_id; - const page = bc.session.findPage(frame_id) orelse return; + const page = bc.session.findPageByFrameId(frame_id) orelse return; // Modify request with extra CDP headers for (bc.extra_headers.items) |extra| { diff --git a/src/cdp/domains/security.zig b/src/cdp/domains/security.zig index 830cd591..0ebfedae 100644 --- a/src/cdp/domains/security.zig +++ b/src/cdp/domains/security.zig @@ -35,12 +35,7 @@ fn setIgnoreCertificateErrors(cmd: anytype) !void { ignore: bool, })) orelse return error.InvalidParams; - if (params.ignore) { - try cmd.cdp.browser.http_client.disableTlsVerify(); - } else { - try cmd.cdp.browser.http_client.enableTlsVerify(); - } - + try cmd.cdp.browser.http_client.setTlsVerify(!params.ignore); return cmd.sendResult(null, .{}); } diff --git a/src/network/Runtime.zig b/src/network/Runtime.zig index af441a6a..b3cd6a06 100644 --- a/src/network/Runtime.zig +++ b/src/network/Runtime.zig @@ -43,6 +43,10 @@ config: *const Config, ca_blob: ?net_http.Blob, robot_store: RobotStore, +connections: []net_http.Connection, +available: std.DoublyLinkedList = .{}, +conn_mutex: std.Thread.Mutex = .{}, + pollfds: []posix.pollfd, listener: ?Listener = null, @@ -191,11 +195,23 @@ pub fn init(allocator: Allocator, config: *const Config) !Runtime { ca_blob = try loadCerts(allocator); } + const count: usize = config.httpMaxConcurrent(); + const connections = try allocator.alloc(net_http.Connection, count); + errdefer allocator.free(connections); + + var available: std.DoublyLinkedList = .{}; + for (0..count) |i| { + connections[i] = try net_http.Connection.init(ca_blob, config); + available.append(&connections[i].node); + } + return .{ .allocator = allocator, .config = config, .ca_blob = ca_blob, .robot_store = RobotStore.init(allocator), + .connections = connections, + .available = available, .pollfds = pollfds, .wakeup_pipe = pipe, }; @@ -216,6 +232,11 @@ pub fn deinit(self: *Runtime) void { self.allocator.free(data[0..ca_blob.len]); } + for (self.connections) |*conn| { + conn.deinit(); + } + self.allocator.free(self.connections); + self.robot_store.deinit(); globalDeinit(); @@ -310,6 +331,25 @@ pub fn stop(self: *Runtime) void { _ = posix.write(self.wakeup_pipe[1], &.{1}) catch {}; } +pub fn getConnection(self: *Runtime) ?*net_http.Connection { + self.conn_mutex.lock(); + defer self.conn_mutex.unlock(); + + const node = self.available.popFirst() orelse return null; + return @fieldParentPtr("node", node); +} + +pub fn releaseConnection(self: *Runtime, conn: *net_http.Connection) void { + conn.reset() catch |err| { + lp.assert(false, "couldn't reset curl easy", .{ .err = err }); + }; + + self.conn_mutex.lock(); + defer self.conn_mutex.unlock(); + + self.available.append(&conn.node); +} + pub fn newConnection(self: *Runtime) !net_http.Connection { return net_http.Connection.init(self.ca_blob, self.config); } diff --git a/src/network/http.zig b/src/network/http.zig index 28fd7736..b0f70375 100644 --- a/src/network/http.zig +++ b/src/network/http.zig @@ -237,7 +237,7 @@ pub const ResponseHead = struct { pub const Connection = struct { easy: *libcurl.Curl, - node: Handles.HandleList.Node = .{}, + node: std.DoublyLinkedList.Node = .{}, pub fn init( ca_blob_: ?libcurl.CurlBlob, @@ -385,8 +385,16 @@ pub const Connection = struct { try libcurl.curl_easy_setopt(self.easy, .write_function, data_cb); } - pub fn setProxy(self: *const Connection, proxy: ?[*:0]const u8) !void { - try libcurl.curl_easy_setopt(self.easy, .proxy, proxy); + pub fn reset(self: *const Connection) !void { + try libcurl.curl_easy_setopt(self.easy, .header_data, null); + try libcurl.curl_easy_setopt(self.easy, .header_function, null); + try libcurl.curl_easy_setopt(self.easy, .write_data, null); + try libcurl.curl_easy_setopt(self.easy, .write_function, null); + try libcurl.curl_easy_setopt(self.easy, .proxy, null); + } + + pub fn setProxy(self: *const Connection, proxy: ?[:0]const u8) !void { + try libcurl.curl_easy_setopt(self.easy, .proxy, if (proxy) |p| p.ptr else null); } pub fn setTlsVerify(self: *const Connection, verify: bool, use_proxy: bool) !void { @@ -467,111 +475,32 @@ pub const Connection = struct { }; pub const Handles = struct { - connections: []Connection, - dirty: HandleList, - in_use: HandleList, - available: HandleList, multi: *libcurl.CurlM, - performing: bool = false, - - pub const HandleList = std.DoublyLinkedList; - - pub fn init( - allocator: Allocator, - ca_blob: ?libcurl.CurlBlob, - config: *const Config, - ) !Handles { - const count: usize = config.httpMaxConcurrent(); - if (count == 0) return error.InvalidMaxConcurrent; + pub fn init(config: *const Config) !Handles { const multi = libcurl.curl_multi_init() orelse return error.FailedToInitializeMulti; errdefer libcurl.curl_multi_cleanup(multi) catch {}; try libcurl.curl_multi_setopt(multi, .max_host_connections, config.httpMaxHostOpen()); - const connections = try allocator.alloc(Connection, count); - errdefer allocator.free(connections); - - var available: HandleList = .{}; - for (0..count) |i| { - connections[i] = try Connection.init(ca_blob, config); - available.append(&connections[i].node); - } - - return .{ - .dirty = .{}, - .in_use = .{}, - .connections = connections, - .available = available, - .multi = multi, - }; + return .{ .multi = multi }; } - pub fn deinit(self: *Handles, allocator: Allocator) void { - for (self.connections) |*conn| { - conn.deinit(); - } - allocator.free(self.connections); + pub fn deinit(self: *Handles) void { libcurl.curl_multi_cleanup(self.multi) catch {}; } - pub fn hasAvailable(self: *const Handles) bool { - return self.available.first != null; - } - - pub fn get(self: *Handles) ?*Connection { - if (self.available.popFirst()) |node| { - self.in_use.append(node); - return @as(*Connection, @fieldParentPtr("node", node)); - } - return null; - } - pub fn add(self: *Handles, conn: *const Connection) !void { try libcurl.curl_multi_add_handle(self.multi, conn.easy); } - pub fn remove(self: *Handles, conn: *Connection) void { - if (libcurl.curl_multi_remove_handle(self.multi, conn.easy)) { - self.isAvailable(conn); - } else |err| { - // can happen if we're in a perform() call, so we'll queue this - // for cleanup later. - const node = &conn.node; - self.in_use.remove(node); - self.dirty.append(node); - log.warn(.http, "multi remove handle", .{ .err = err }); - } - } - - pub fn isAvailable(self: *Handles, conn: *Connection) void { - const node = &conn.node; - self.in_use.remove(node); - self.available.append(node); + pub fn remove(self: *Handles, conn: *const Connection) !void { + try libcurl.curl_multi_remove_handle(self.multi, conn.easy); } pub fn perform(self: *Handles) !c_int { - self.performing = true; - defer self.performing = false; - - const multi = self.multi; var running: c_int = undefined; try libcurl.curl_multi_perform(self.multi, &running); - - { - const list = &self.dirty; - while (list.first) |node| { - list.remove(node); - const conn: *Connection = @fieldParentPtr("node", node); - if (libcurl.curl_multi_remove_handle(multi, conn.easy)) { - self.available.append(node); - } else |err| { - log.fatal(.http, "multi remove handle", .{ .err = err, .src = "perform" }); - @panic("multi_remove_handle"); - } - } - } - return running; } diff --git a/src/sys/libcurl.zig b/src/sys/libcurl.zig index f13e999a..b93ecc77 100644 --- a/src/sys/libcurl.zig +++ b/src/sys/libcurl.zig @@ -590,7 +590,10 @@ pub fn curl_easy_setopt(easy: *Curl, comptime option: CurlOption, value: anytype .header_data, .write_data, => blk: { - const ptr: *anyopaque = @ptrCast(value); + const ptr: ?*anyopaque = switch (@typeInfo(@TypeOf(value))) { + .null => null, + else => @ptrCast(value), + }; break :blk c.curl_easy_setopt(easy, opt, ptr); },