mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 09:35:59 -04:00
Merge branch 'main' into agent
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
92
src/browser/tests/crypto-worker.js
Normal file
92
src/browser/tests/crypto-worker.js
Normal 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 });
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
83
src/browser/webapi/WorkerLocation.zig
Normal file
83
src/browser/webapi/WorkerLocation.zig
Normal 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, .{});
|
||||
};
|
||||
@@ -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 } });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user