diff --git a/src/Server.zig b/src/Server.zig index 0b5d8ef5..dd8e75ba 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -44,23 +44,22 @@ conns_mutex: std.Thread.Mutex = .{}, conns_pool: std.heap.MemoryPool(CDP), pub fn init(app: *App, address: net.Address) !*Server { - const json_version_response = try buildJSONVersionResponse(app); - errdefer app.allocator.free(json_version_response); - const self = try app.allocator.create(Server); errdefer app.allocator.destroy(self); self.* = .{ .app = app, .conns_pool = .init(app.allocator), - .json_version_response = json_version_response, + .json_version_response = "", }; errdefer self.conns_pool.deinit(); + // Bind first so /json/version can advertise the OS-assigned port (--port 0). var bound_address = address; try self.app.network.bind(&bound_address, self, onAccept); log.info(.app, "server running", .{ .address = bound_address }); + self.json_version_response = try buildJSONVersionResponse(app, bound_address.getPort()); return self; } @@ -237,10 +236,7 @@ fn unregisterConn(self: *Server, conn: *CDP) void { // Utils // -------- -fn buildJSONVersionResponse( - app: *const App, -) ![]const u8 { - const port = app.config.port(); +fn buildJSONVersionResponse(app: *const App, port: u16) ![]const u8 { const host = app.config.advertiseHost(); if (std.mem.eql(u8, host, "0.0.0.0")) { log.info(.cdp, "unreachable advertised host", .{ @@ -276,7 +272,7 @@ pub const milliTimestamp = @import("datetime.zig").milliTimestamp; const testing = @import("testing.zig"); test "server: buildJSONVersionResponse" { - const res = try buildJSONVersionResponse(testing.test_app); + const res = try buildJSONVersionResponse(testing.test_app, testing.test_app.config.port()); defer testing.test_app.allocator.free(res); // The response includes the build version, so check structure rather than exact bytes. @@ -509,7 +505,7 @@ test "server: get /json/version" { try testing.expect(std.mem.startsWith(u8, res1, "HTTP/1.1 200 OK\r\n")); try testing.expect(std.mem.indexOf(u8, res1, "\"Browser\": \"Lightpanda/") != null); try testing.expect(std.mem.indexOf(u8, res1, "\"Protocol-Version\": \"1.3\"") != null); - try testing.expect(std.mem.indexOf(u8, res1, "\"webSocketDebuggerUrl\": \"ws://127.0.0.1:9222/\"") != null); + try testing.expect(std.mem.indexOf(u8, res1, "\"webSocketDebuggerUrl\": \"ws://127.0.0.1:9583/\"") != null); } { diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index af9a1784..0fc1949c 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -407,6 +407,10 @@ pub fn deinit(self: *Frame) void { } const browser = page.session.browser; + + // don't abort pending frames. + browser.http_client.abortFrame(self._frame_id, .{}); + browser.env.destroyContext(self.js); // Must be after context is destroyed. A finalizer can reach into the *Worker @@ -417,9 +421,6 @@ pub fn deinit(self: *Frame) void { self._script_manager.base.shutdown = true; - // don't abort pending frames. - browser.http_client.abortFrame(self._frame_id, .{}); - self._script_manager.deinit(); self._style_manager.deinit(); diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index f2ec997e..6ffe578a 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -940,6 +940,7 @@ pub const PageJsApis = flattenTypes(&.{ // TODO: Expand this list to include all worker-appropriate APIs. pub const WorkerJsApis = flattenTypes(&.{ @import("../webapi/WorkerGlobalScope.zig"), + @import("../webapi/WorkerLocation.zig"), @import("../webapi/EventTarget.zig"), @import("../webapi/DOMException.zig"), @import("../webapi/net/URLSearchParams.zig"), @@ -949,6 +950,7 @@ pub const WorkerJsApis = flattenTypes(&.{ @import("../webapi/File.zig"), @import("../webapi/Console.zig"), @import("../webapi/Crypto.zig"), + @import("../webapi/SubtleCrypto.zig"), @import("../webapi/net/FormData.zig"), @import("../webapi/net/Headers.zig"), @import("../webapi/net/Request.zig"), @@ -977,7 +979,10 @@ pub const WorkerJsApis = flattenTypes(&.{ // to know about all possible types. Individual snapshots use their own // subsets (PageJsApis, WorkerSnapshot.JsApis). pub const JsApis = blk: { - const base = PageJsApis ++ [_]type{@import("../webapi/WorkerGlobalScope.zig").JsApi}; + const base = PageJsApis ++ [_]type{ + @import("../webapi/WorkerGlobalScope.zig").JsApi, + @import("../webapi/WorkerLocation.zig").JsApi, + }; if (lp.build_config.wpt_extensions == false) { break :blk base; } diff --git a/src/browser/tests/crypto-worker.js b/src/browser/tests/crypto-worker.js new file mode 100644 index 00000000..90d948d0 --- /dev/null +++ b/src/browser/tests/crypto-worker.js @@ -0,0 +1,92 @@ +// Exercises crypto APIs inside a worker. Posts 'ready' once the message +// handler is wired so the page knows it can send a command without racing +// worker startup. Receives the command, runs the crypto operation, and +// posts the result back. +self.onmessage = async function(e) { + const cmd = e.data; + try { + if (cmd.kind === 'getRandomValues') { + const ta = new Uint8Array(32); + const same = crypto.getRandomValues(ta) === ta; + const uniq = new Set(Array.from(ta)); + postMessage({ ok: true, same, looks_random: uniq.size > 8 }); + return; + } + + if (cmd.kind === 'randomUUID') { + const uuid = crypto.randomUUID(); + const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + postMessage({ ok: true, type: typeof uuid, length: uuid.length, valid: regex.test(uuid) }); + return; + } + + if (cmd.kind === 'digest') { + const buffer = await crypto.subtle.digest('sha-256', new TextEncoder().encode('over 9000')); + const hex = [...new Uint8Array(buffer)].map(x => x.toString(16).padStart(2, '0')).join(''); + postMessage({ ok: true, hex }); + return; + } + + if (cmd.kind === 'hmac') { + const key = await crypto.subtle.generateKey( + { name: 'HMAC', hash: { name: 'SHA-512' } }, + true, + ['sign', 'verify'], + ); + const raw = await crypto.subtle.exportKey('raw', key); + const encoder = new TextEncoder(); + const signature = await crypto.subtle.sign('HMAC', key, encoder.encode('Hello, world!')); + const verified = await crypto.subtle.verify( + { name: 'HMAC' }, + key, + signature, + encoder.encode('Hello, world!'), + ); + postMessage({ + ok: true, + key_type: typeof key, + raw_byte_length: raw.byteLength, + is_array_buffer: signature instanceof ArrayBuffer, + verified, + }); + return; + } + + if (cmd.kind === 'x25519') { + const { privateKey, publicKey } = await crypto.subtle.generateKey( + { name: 'X25519' }, + true, + ['deriveBits'], + ); + const sharedKey = await crypto.subtle.deriveBits( + { name: 'X25519', public: publicKey }, + privateKey, + 128, + ); + postMessage({ + ok: true, + private_key_type: typeof privateKey, + public_key_type: typeof publicKey, + shared_byte_length: sharedKey.byteLength, + }); + return; + } + + if (cmd.kind === 'generateKey-rejection') { + let err_name = null; + try { + await crypto.subtle.generateKey({ name: 'AES-CBC', length: 128 }, true, ['sign']); + } catch (err) { + err_name = err.name; + } + postMessage({ ok: true, err_name }); + return; + } + + postMessage({ ok: false, err: 'unknown command' }); + } catch (err) { + postMessage({ ok: false, err: String(err), stack: err.stack }); + } +}; + +postMessage({ ready: true }); diff --git a/src/browser/tests/crypto.html b/src/browser/tests/crypto.html index 59d6522e..f588b743 100644 --- a/src/browser/tests/crypto.html +++ b/src/browser/tests/crypto.html @@ -261,3 +261,96 @@ testing.expectEqual("6cad17e6f3f76680d6dd18ed043b75b4f6e1aa1d08b917294942e882fb6466c3510948c34af8b903ed0725b582b3b39c0e485ae2c1b7dfdb192ee38b79c782b6", await hash('sha-512', 'over 9000')); }); + + + + + + + + + + + + + + diff --git a/src/browser/tests/worker/api-worker.js b/src/browser/tests/worker/api-worker.js index edfea446..1f75d7e1 100644 --- a/src/browser/tests/worker/api-worker.js +++ b/src/browser/tests/worker/api-worker.js @@ -50,6 +50,15 @@ const blob_url_is_blob = blob_url.startsWith('blob:'); URL.revokeObjectURL(blob_url); + // self.location + const loc = self.location; + const loc_is_worker_location = loc instanceof WorkerLocation; + const loc_identity_stable = self.location === loc; + const loc_href = loc.href; + const loc_protocol = loc.protocol; + const loc_pathname = loc.pathname; + const loc_to_string = String(loc); + postMessage({ ok: true, results: { @@ -75,6 +84,12 @@ pre_aborted, pre_threw, blob_url_is_blob, + loc_is_worker_location, + loc_identity_stable, + loc_href, + loc_protocol, + loc_pathname, + loc_to_string, }, }); } catch (e) { diff --git a/src/browser/tests/worker/worker.html b/src/browser/tests/worker/worker.html index e660b492..2740d7c0 100644 --- a/src/browser/tests/worker/worker.html +++ b/src/browser/tests/worker/worker.html @@ -220,6 +220,14 @@ // URL.createObjectURL / revokeObjectURL testing.expectEqual(true, r.blob_url_is_blob); + + // WorkerLocation + testing.expectEqual(true, r.loc_is_worker_location); + testing.expectEqual(true, r.loc_identity_stable); + testing.expectTrue(r.loc_href.endsWith('api-worker.js')); + testing.expectEqual(r.loc_href, r.loc_to_string); + testing.expectEqual('http:', r.loc_protocol); + testing.expectTrue(r.loc_pathname.endsWith('api-worker.js')); }); } diff --git a/src/browser/webapi/SubtleCrypto.zig b/src/browser/webapi/SubtleCrypto.zig index f6b45ccf..3220352c 100644 --- a/src/browser/webapi/SubtleCrypto.zig +++ b/src/browser/webapi/SubtleCrypto.zig @@ -20,7 +20,6 @@ const std = @import("std"); const lp = @import("lightpanda"); const crypto = @import("../../sys/libcrypto.zig"); -const Frame = @import("../Frame.zig"); const js = @import("../js/js.zig"); const CryptoKey = @import("CryptoKey.zig"); @@ -34,6 +33,7 @@ const X25519 = @import("crypto/X25519.zig"); const log = lp.log; const String = lp.String; +const Execution = js.Execution; /// The SubtleCrypto interface of the Web Crypto API provides a number of low-level /// cryptographic functions. @@ -49,11 +49,11 @@ pub fn generateKey( algo: algorithm.Init, extractable: bool, key_usages: []const []const u8, - frame: *Frame, + exec: *const Execution, ) !js.Promise { - const local = frame.js.local.?; + const local = exec.context.local.?; switch (algo) { - .hmac_key_gen => |params| return HMAC.init(params, extractable, key_usages, frame), + .hmac_key_gen => |params| return HMAC.init(params, extractable, key_usages, exec), .aes_key_gen => |params| { AES.validate(params, key_usages) catch |err| { return local.rejectPromise(.{ .dom_exception = .{ .err = err } }); @@ -72,8 +72,8 @@ pub fn generateKey( }; log.warn(.not_implemented, "generateKey", .{ .name = params.name }); }, - .name => |js_name| return generateKeyFromName(try js_name.toSSO(false), extractable, key_usages, frame), - .object => |object| return generateKeyFromName(try object.name.toSSO(false), extractable, key_usages, frame), + .name => |js_name| return generateKeyFromName(try js_name.toSSO(false), extractable, key_usages, exec), + .object => |object| return generateKeyFromName(try object.name.toSSO(false), extractable, key_usages, exec), .invalid => return local.rejectPromise(.{ .type_error = "invalid algorithm" }), } @@ -84,10 +84,10 @@ fn generateKeyFromName( name: String, extractable: bool, key_usages: []const []const u8, - frame: *Frame, + exec: *const Execution, ) !js.Promise { - return _generateKeyFromName(name, extractable, key_usages, frame) catch |err| { - return frame.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = err } }); + return _generateKeyFromName(name, extractable, key_usages, exec) catch |err| { + return exec.context.local.?.rejectPromise(.{ .dom_exception = .{ .err = err } }); }; } @@ -95,10 +95,10 @@ fn _generateKeyFromName( name: String, extractable: bool, key_usages: []const []const u8, - frame: *Frame, + exec: *const Execution, ) !js.Promise { if (name.eql(comptime .wrap("X25519"))) { - return X25519.init(extractable, key_usages, frame); + return X25519.init(extractable, key_usages, exec); } { @@ -145,14 +145,15 @@ pub fn exportKey( _: *const SubtleCrypto, format: []const u8, key: *CryptoKey, - frame: *Frame, + exec: *const Execution, ) !js.Promise { + const local = exec.context.local.?; if (!key.canExportKey()) { - return frame.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }); + return local.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }); } if (std.mem.eql(u8, format, "raw")) { - return frame.js.local.?.resolvePromise(js.ArrayBuffer{ .values = key._key }); + return local.resolvePromise(js.ArrayBuffer{ .values = key._key }); } const is_unsupported = std.mem.eql(u8, format, "pkcs8") or @@ -160,10 +161,10 @@ pub fn exportKey( if (is_unsupported) { log.warn(.not_implemented, "SubtleCrypto.exportKey", .{ .format = format }); - return frame.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } }); + return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } }); } - return frame.js.local.?.rejectPromise(.{ .type_error = "invalid format" }); + return local.rejectPromise(.{ .type_error = "invalid format" }); } /// Derive a secret key from a master key. @@ -172,27 +173,28 @@ pub fn deriveBits( algo: algorithm.Derive, base_key: *const CryptoKey, // Private key. length: usize, - frame: *Frame, + exec: *const Execution, ) !js.Promise { + const local = exec.context.local.?; return switch (algo) { .ecdh_or_x25519 => |params| { const name = params.name; if (std.mem.eql(u8, name, "X25519")) { - const result = X25519.deriveBits(base_key, params.public, length, frame) catch |err| switch (err) { - error.InvalidAccessError => return frame.js.local.?.rejectPromise(.{ + const result = X25519.deriveBits(base_key, params.public, length, exec) catch |err| switch (err) { + error.InvalidAccessError => return local.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError }, }), else => return err, }; - return frame.js.local.?.resolvePromise(result); + return local.resolvePromise(result); } if (std.mem.eql(u8, name, "ECDH")) { log.warn(.not_implemented, "SubtleCrypto.deriveBits", .{ .name = name }); } - return frame.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } }); + return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } }); }, }; } @@ -204,14 +206,14 @@ pub fn sign( algo: algorithm.Sign, key: *CryptoKey, data: []const u8, // ArrayBuffer. - frame: *Frame, + exec: *const Execution, ) !js.Promise { return switch (key._type) { // Call sign for HMAC. - .hmac => return HMAC.sign(algo, key, data, frame), + .hmac => return HMAC.sign(algo, key, data, exec), else => { log.warn(.not_implemented, "SubtleCrypto.sign", .{ .key_type = key._type }); - return frame.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }); + return exec.context.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }); }, }; } @@ -223,33 +225,34 @@ pub fn verify( key: *const CryptoKey, signature: []const u8, // ArrayBuffer. data: []const u8, // ArrayBuffer. - frame: *Frame, + exec: *const Execution, ) !js.Promise { + const local = exec.context.local.?; if (!algo.isHMAC()) { - return frame.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }); + return local.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }); } return switch (key._type) { - .hmac => HMAC.verify(key, signature, data, frame), - else => frame.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }), + .hmac => HMAC.verify(key, signature, data, exec), + else => local.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }), }; } /// Generates a digest of the given data, using the specified hash function. -pub fn digest(_: *const SubtleCrypto, algo: []const u8, data: js.TypedArray(u8), frame: *Frame) !js.Promise { - const local = frame.js.local.?; +pub fn digest(_: *const SubtleCrypto, algo: []const u8, data: js.TypedArray(u8), exec: *const Execution) !js.Promise { + const local = exec.context.local.?; if (algo.len > 10) { return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } }); } - const normalized = std.ascii.upperString(&frame.buf, algo); + const normalized = std.ascii.upperString(exec.buf, algo); const digest_type = crypto.findDigest(normalized) catch { return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } }); }; const bytes = data.values; - const out = frame.buf[0..crypto.EVP_MAX_MD_SIZE]; + const out = exec.buf[0..crypto.EVP_MAX_MD_SIZE]; var out_size: c_uint = 0; const result = crypto.EVP_Digest(bytes.ptr, bytes.len, out, &out_size, digest_type, null); lp.assert(result == 1, "SubtleCrypto.digest", .{ .algo = algo }); diff --git a/src/browser/webapi/WorkerGlobalScope.zig b/src/browser/webapi/WorkerGlobalScope.zig index 1034f0e6..e9b8fe13 100644 --- a/src/browser/webapi/WorkerGlobalScope.zig +++ b/src/browser/webapi/WorkerGlobalScope.zig @@ -39,6 +39,7 @@ const Crypto = @import("Crypto.zig"); const Console = @import("Console.zig"); const Timers = @import("Timers.zig"); const EventTarget = @import("EventTarget.zig"); +const WorkerLocation = @import("WorkerLocation.zig"); const MessageEvent = @import("event/MessageEvent.zig"); const ErrorEvent = @import("event/ErrorEvent.zig"); const Fetch = @import("net/Fetch.zig"); @@ -98,6 +99,8 @@ _on_unhandled_rejection: ?JS.Function.Global = null, _on_message: ?JS.Function.Global = null, _on_messageerror: ?JS.Function.Global = null, +_location: WorkerLocation, + _timers: Timers = .{}, pub fn init(worker: *Worker, url: [:0]const u8) !*WorkerGlobalScope { @@ -125,6 +128,7 @@ pub fn init(worker: *Worker, url: [:0]const u8) !*WorkerGlobalScope { ._loader_id = worker._loader_id, ._event_manager = .init(arena), ._script_manager = undefined, + ._location = .{ ._url = url }, }); errdefer factory.destroy(self); @@ -218,6 +222,10 @@ pub fn getCrypto(self: *WorkerGlobalScope) *Crypto { return &self._crypto; } +pub fn getLocation(self: *WorkerGlobalScope) *WorkerLocation { + return &self._location; +} + pub fn getOnError(self: *const WorkerGlobalScope) ?JS.Function.Global { return self._on_error; } @@ -566,6 +574,7 @@ pub const JsApi = struct { pub const self = bridge.accessor(WorkerGlobalScope.getSelf, null, .{}); pub const console = bridge.accessor(WorkerGlobalScope.getConsole, null, .{}); pub const crypto = bridge.accessor(WorkerGlobalScope.getCrypto, null, .{}); + pub const location = bridge.accessor(WorkerGlobalScope.getLocation, null, .{}); pub const onerror = bridge.accessor(WorkerGlobalScope.getOnError, WorkerGlobalScope.setOnError, .{}); pub const onrejectionhandled = bridge.accessor(WorkerGlobalScope.getOnRejectionHandled, WorkerGlobalScope.setOnRejectionHandled, .{}); diff --git a/src/browser/webapi/WorkerLocation.zig b/src/browser/webapi/WorkerLocation.zig new file mode 100644 index 00000000..6de8e9fd --- /dev/null +++ b/src/browser/webapi/WorkerLocation.zig @@ -0,0 +1,83 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// 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 . + +const js = @import("../js/js.zig"); + +const U = @import("../URL.zig"); + +const WorkerLocation = @This(); + +// Workers can't navigate, so the URL is fixed for the lifetime of the worker. +_url: [:0]const u8, + +pub fn getProtocol(self: *const WorkerLocation) []const u8 { + return U.getProtocol(self._url); +} + +pub fn getHostname(self: *const WorkerLocation) []const u8 { + return U.getHostname(self._url); +} + +pub fn getHost(self: *const WorkerLocation) []const u8 { + return U.getHost(self._url); +} + +pub fn getPort(self: *const WorkerLocation) []const u8 { + return U.getPort(self._url); +} + +pub fn getPathname(self: *const WorkerLocation) []const u8 { + return U.getPathname(self._url); +} + +pub fn getSearch(self: *const WorkerLocation) []const u8 { + return U.getSearch(self._url); +} + +pub fn getHash(self: *const WorkerLocation) []const u8 { + return U.getHash(self._url); +} + +pub fn getOrigin(self: *const WorkerLocation, exec: *const js.Execution) ![]const u8 { + return (try U.getOrigin(exec.call_arena, self._url)) orelse "null"; +} + +pub fn toString(self: *const WorkerLocation) [:0]const u8 { + return self._url; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(WorkerLocation); + + pub const Meta = struct { + pub const name = "WorkerLocation"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const toString = bridge.function(WorkerLocation.toString, .{}); + pub const href = bridge.accessor(WorkerLocation.toString, null, .{}); + pub const origin = bridge.accessor(WorkerLocation.getOrigin, null, .{}); + pub const protocol = bridge.accessor(WorkerLocation.getProtocol, null, .{}); + pub const host = bridge.accessor(WorkerLocation.getHost, null, .{}); + pub const hostname = bridge.accessor(WorkerLocation.getHostname, null, .{}); + pub const port = bridge.accessor(WorkerLocation.getPort, null, .{}); + pub const pathname = bridge.accessor(WorkerLocation.getPathname, null, .{}); + pub const search = bridge.accessor(WorkerLocation.getSearch, null, .{}); + pub const hash = bridge.accessor(WorkerLocation.getHash, null, .{}); +}; diff --git a/src/browser/webapi/crypto/HMAC.zig b/src/browser/webapi/crypto/HMAC.zig index 9d0d4335..3ab0e507 100644 --- a/src/browser/webapi/crypto/HMAC.zig +++ b/src/browser/webapi/crypto/HMAC.zig @@ -22,19 +22,20 @@ const std = @import("std"); const lp = @import("lightpanda"); const crypto = @import("../../../sys/libcrypto.zig"); -const Frame = @import("../../Frame.zig"); const js = @import("../../js/js.zig"); const algorithm = @import("algorithm.zig"); const CryptoKey = @import("../CryptoKey.zig"); +const Execution = js.Execution; + pub fn init( params: algorithm.Init.HmacKeyGen, extractable: bool, key_usages: []const []const u8, - frame: *Frame, + exec: *const Execution, ) !js.Promise { - const local = frame.js.local.?; + const local = exec.context.local.?; // Per spec, an unrecognized hash is caught during algorithm normalization // and surfaces as NotSupportedError. const digest = crypto.findDigest(switch (params.hash) { @@ -74,14 +75,14 @@ pub fn init( }; // Should we reject this in promise too? - const key = try frame.arena.alloc(u8, block_size); - errdefer frame.arena.free(key); + const key = try exec.arena.alloc(u8, block_size); + errdefer exec.arena.free(key); // HMAC is simply CSPRNG. const res = crypto.RAND_bytes(key.ptr, key.len); lp.assert(res == 1, "HMAC.init", .{ .res = res }); - const crypto_key = try frame._factory.create(CryptoKey{ + const crypto_key = try exec._factory.create(CryptoKey{ ._type = .hmac, ._extractable = extractable, ._usages = mask, @@ -96,16 +97,16 @@ pub fn sign( algo: algorithm.Sign, crypto_key: *const CryptoKey, data: []const u8, - frame: *Frame, + exec: *const Execution, ) !js.Promise { - var resolver = frame.js.local.?.createPromiseResolver(); + var resolver = exec.context.local.?.createPromiseResolver(); if (!algo.isHMAC() or !crypto_key.canSign()) { resolver.rejectError("HMAC.sign", .{ .dom_exception = .{ .err = error.InvalidAccessError } }); return resolver.promise(); } - const buffer = try frame.call_arena.alloc(u8, crypto.EVP_MD_size(crypto_key.getDigest())); + const buffer = try exec.call_arena.alloc(u8, crypto.EVP_MD_size(crypto_key.getDigest())); var out_len: u32 = 0; // Try to sign. _ = crypto.HMAC( @@ -117,7 +118,7 @@ pub fn sign( buffer.ptr, &out_len, ) orelse { - frame.call_arena.free(buffer); + exec.call_arena.free(buffer); // Failure. resolver.rejectError("HMAC.sign", .{ .dom_exception = .{ .err = error.InvalidAccessError } }); return resolver.promise(); @@ -132,9 +133,9 @@ pub fn verify( crypto_key: *const CryptoKey, signature: []const u8, data: []const u8, - frame: *Frame, + exec: *const Execution, ) !js.Promise { - var resolver = frame.js.local.?.createPromiseResolver(); + var resolver = exec.context.local.?.createPromiseResolver(); if (!crypto_key.canVerify()) { resolver.rejectError("HMAC.verify", .{ .dom_exception = .{ .err = error.InvalidAccessError } }); diff --git a/src/browser/webapi/crypto/X25519.zig b/src/browser/webapi/crypto/X25519.zig index 968d1833..aaa1798c 100644 --- a/src/browser/webapi/crypto/X25519.zig +++ b/src/browser/webapi/crypto/X25519.zig @@ -22,21 +22,22 @@ const std = @import("std"); const lp = @import("lightpanda"); const crypto = @import("../../../sys/libcrypto.zig"); -const Frame = @import("../../Frame.zig"); const js = @import("../../js/js.zig"); const CryptoKey = @import("../CryptoKey.zig"); +const Execution = js.Execution; + pub fn init( extractable: bool, key_usages: []const []const u8, - frame: *Frame, + exec: *const Execution, ) !js.Promise { // This code has too many allocations here and there, might be nice to // gather them together with a single alloc call. Not sure if factory // pattern is suitable for it though. - const local = frame.js.local.?; + const local = exec.context.local.?; // Calculate usages; only matters for private key. // Only deriveKey() and deriveBits() be used for X25519. @@ -59,11 +60,11 @@ pub fn init( }); } - const public_value = try frame.arena.alloc(u8, crypto.X25519_PUBLIC_VALUE_LEN); - errdefer frame.arena.free(public_value); + const public_value = try exec.arena.alloc(u8, crypto.X25519_PUBLIC_VALUE_LEN); + errdefer exec.arena.free(public_value); - const private_key = try frame.arena.alloc(u8, crypto.X25519_PRIVATE_KEY_LEN); - errdefer frame.arena.free(private_key); + const private_key = try exec.arena.alloc(u8, crypto.X25519_PRIVATE_KEY_LEN); + errdefer exec.arena.free(private_key); // There's no info about whether this can fail; so I assume it cannot. crypto.X25519_keypair(@ptrCast(public_value), @ptrCast(private_key)); @@ -90,16 +91,16 @@ pub fn init( private_key.len, ) orelse return error.OutOfMemory; - const private = try frame._factory.create(CryptoKey{ + const private = try exec._factory.create(CryptoKey{ ._type = .x25519, ._extractable = extractable, ._usages = mask, ._key = private_key, ._vary = .{ .pkey = private_pkey }, }); - errdefer frame._factory.destroy(private); + errdefer exec._factory.destroy(private); - const public = try frame._factory.create(CryptoKey{ + const public = try exec._factory.create(CryptoKey{ ._type = .x25519, // Public keys are always extractable. ._extractable = true, @@ -116,7 +117,7 @@ pub fn deriveBits( private: *const CryptoKey, public: *const CryptoKey, length_in_bits: usize, - frame: *Frame, + exec: *const Execution, ) !js.ArrayBuffer { if (!private.canDeriveBits()) { return error.InvalidAccessError; @@ -137,8 +138,8 @@ pub fn deriveBits( return error.Internal; } - const derived_key = try frame.call_arena.alloc(u8, 32); - errdefer frame.call_arena.free(derived_key); + const derived_key = try exec.call_arena.alloc(u8, 32); + errdefer exec.call_arena.free(derived_key); var out_key_len: usize = derived_key.len; const result = crypto.EVP_PKEY_derive(ctx, derived_key.ptr, &out_key_len); diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 65068028..b714da1e 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -301,6 +301,20 @@ fn navigate(cmd: *CDP.Command) !void { const frame = session.currentFrame() orelse return error.FrameNotLoaded; const encoded_url = try URL.ensureEncoded(frame.call_arena, params.url, "UTF-8"); + + // Fast path: a freshly-created target whose root frame hasn't navigated + // yet has nothing to preserve across the HTTP round-trip. Skip the + // pending-Page allocation (which would create a V8 context just to + // throw the OLD blank one away at commit) and navigate the active + // frame in place. + if (frame._load_state == .waiting) { + return frame.navigate(encoded_url, .{ + .reason = .address_bar, + .cdp_id = cmd.input.id, + .kind = .{ .push = null }, + }); + } + try session.initiateRootNavigation(frame._frame_id, encoded_url, .{ .reason = .address_bar, .cdp_id = cmd.input.id,