Merge branch 'main' into agent

This commit is contained in:
Adrià Arrufat
2026-05-07 08:54:28 +02:00
13 changed files with 392 additions and 71 deletions

View File

@@ -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);
}
{

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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 });

View File

@@ -261,3 +261,96 @@
testing.expectEqual("6cad17e6f3f76680d6dd18ed043b75b4f6e1aa1d08b917294942e882fb6466c3510948c34af8b903ed0725b582b3b39c0e485ae2c1b7dfdb192ee38b79c782b6", await hash('sha-512', 'over 9000'));
});
</script>
<script>
// Helper for worker-based crypto tests. Spawns a worker, waits for it to
// signal readiness, then sends `kind` and forwards the worker's reply to
// `state.resolve`. Avoids racing worker startup, which matters on slow CI.
window.runCryptoInWorker = function(kind, state) {
const worker = new Worker('./crypto-worker.js');
worker.onmessage = (e) => {
if (e.data && e.data.ready) {
worker.postMessage({ kind });
return;
}
state.resolve(e.data);
};
};
</script>
<script id=worker-getRandomValues type=module>
{
const state = await testing.async();
runCryptoInWorker('getRandomValues', state);
await state.done((data) => {
testing.expectTrue(data.ok);
testing.expectEqual(true, data.same);
testing.expectEqual(true, data.looks_random);
});
}
</script>
<script id=worker-randomUUID type=module>
{
const state = await testing.async();
runCryptoInWorker('randomUUID', state);
await state.done((data) => {
testing.expectTrue(data.ok);
testing.expectEqual('string', data.type);
testing.expectEqual(36, data.length);
testing.expectEqual(true, data.valid);
});
}
</script>
<script id=worker-digest type=module>
{
const state = await testing.async();
runCryptoInWorker('digest', state);
await state.done((data) => {
testing.expectTrue(data.ok);
testing.expectEqual(
'1bc375bb92459685194dda18a4b835f4e2972ec1bde6d9ab3db53fcc584a6580',
data.hex,
);
});
}
</script>
<script id=worker-hmac-sign-verify type=module>
{
const state = await testing.async();
runCryptoInWorker('hmac', state);
await state.done((data) => {
testing.expectTrue(data.ok);
testing.expectEqual('object', data.key_type);
testing.expectEqual(128, data.raw_byte_length);
testing.expectEqual(true, data.is_array_buffer);
testing.expectEqual(true, data.verified);
});
}
</script>
<script id=worker-x25519-derive type=module>
{
const state = await testing.async();
runCryptoInWorker('x25519', state);
await state.done((data) => {
testing.expectTrue(data.ok);
testing.expectEqual('object', data.private_key_type);
testing.expectEqual('object', data.public_key_type);
testing.expectEqual(16, data.shared_byte_length);
});
}
</script>
<script id=worker-generateKey-rejects type=module>
{
const state = await testing.async();
runCryptoInWorker('generateKey-rejection', state);
await state.done((data) => {
testing.expectTrue(data.ok);
testing.expectEqual('SyntaxError', data.err_name);
});
}
</script>

View File

@@ -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) {

View File

@@ -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'));
});
}
</script>

View File

@@ -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 });

View File

@@ -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, .{});

View File

@@ -0,0 +1,83 @@
// Copyright (C) 2023-2026 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 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, .{});
};

View File

@@ -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 } });

View File

@@ -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);

View File

@@ -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,