From 94d2d28806f8268c042e2ec5fc38752574732c68 Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Tue, 1 Oct 2024 17:12:08 +0200 Subject: [PATCH] Redirect Runtime domain to JS engine Inspector Signed-off-by: Francis Bouvier --- src/browser/browser.zig | 35 ++++- src/cdp/cdp.zig | 8 +- src/cdp/page.zig | 47 +++++-- src/cdp/runtime.zig | 294 ++++++++++------------------------------ src/cdp/target.zig | 5 +- src/main.zig | 2 +- src/main_get.zig | 4 +- src/server.zig | 54 +++++++- 8 files changed, 195 insertions(+), 254 deletions(-) diff --git a/src/browser/browser.zig b/src/browser/browser.zig index 100b4498..5cf4f3f7 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -51,10 +51,9 @@ const log = std.log.scoped(.browser); pub const Browser = struct { session: *Session, - pub fn init(alloc: std.mem.Allocator, vm: jsruntime.VM) !Browser { + pub fn init(alloc: std.mem.Allocator) !Browser { // We want to ensure the caller initialised a VM, but the browser // doesn't use it directly... - _ = vm; return Browser{ .session = try Session.init(alloc, "about:blank"), @@ -91,6 +90,7 @@ pub const Session = struct { loader: Loader, env: Env = undefined, loop: Loop, + inspector: ?jsruntime.Inspector = null, window: Window, // TODO move the shed to the browser? storageShed: storage.Shed, @@ -122,6 +122,10 @@ pub const Session = struct { fn deinit(self: *Session) void { if (self.page) |page| page.end(); + if (self.inspector) |inspector| { + inspector.deinit(self.alloc); + } + self.env.deinit(); self.arena.deinit(); @@ -132,9 +136,25 @@ pub const Session = struct { self.alloc.destroy(self); } + pub fn setInspector( + self: *Session, + ctx: *anyopaque, + onResp: jsruntime.InspectorOnResponseFn, + onEvent: jsruntime.InspectorOnEventFn, + ) !void { + self.inspector = try jsruntime.Inspector.init(self.alloc, self.env, ctx, onResp, onEvent); + self.env.setInspector(self.inspector.?); + } + pub fn createPage(self: *Session) !Page { return Page.init(self.alloc, self); } + + pub fn callInspector(self: *Session, msg: []const u8) void { + if (self.inspector) |inspector| { + inspector.send(msg, self.env); + } + } }; // Page navigates to an url. @@ -219,7 +239,7 @@ pub const Page = struct { } // spec reference: https://html.spec.whatwg.org/#document-lifecycle - pub fn navigate(self: *Page, uri: []const u8) !void { + pub fn navigate(self: *Page, uri: []const u8, auxData: ?[]const u8) !void { const alloc = self.arena.allocator(); log.debug("starting GET {s}", .{uri}); @@ -280,7 +300,7 @@ pub const Page = struct { log.debug("header content-type: {s}", .{ct.?}); const mime = try Mime.parse(ct.?); if (mime.eql(Mime.HTML)) { - try self.loadHTMLDoc(req.reader(), mime.charset orelse "utf-8"); + try self.loadHTMLDoc(req.reader(), mime.charset orelse "utf-8", auxData); } else { log.info("non-HTML document: {s}", .{ct.?}); @@ -290,7 +310,7 @@ pub const Page = struct { } // https://html.spec.whatwg.org/#read-html - fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8) !void { + fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8, auxData: ?[]const u8) !void { const alloc = self.arena.allocator(); // start netsurf memory arena. @@ -327,6 +347,11 @@ pub const Page = struct { log.debug("start js env", .{}); try self.session.env.start(); + // inspector + if (self.session.inspector) |inspector| { + inspector.contextCreated(self.session.env, "", self.origin.?, auxData); + } + // replace the user context document with the new one. try self.session.env.setUserContext(.{ .document = html_doc, diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 89f1bf3e..87f1477b 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -16,6 +16,7 @@ const performance = @import("performance.zig").performance; pub const Error = error{ UnknonwDomain, UnknownMethod, + NoResponse, }; pub fn isCdpError(err: anyerror) ?Error { @@ -85,7 +86,7 @@ pub fn do( .Target => target(alloc, id, iter.next().?, &scanner, ctx), .Page => page(alloc, id, iter.next().?, &scanner, ctx), .Log => log(alloc, id, iter.next().?, &scanner, ctx), - .Runtime => runtime(alloc, id, iter.next().?, &scanner, ctx), + .Runtime => runtime(alloc, id, iter.next().?, &scanner, s, ctx), .Network => network(alloc, id, iter.next().?, &scanner, ctx), .Emulation => emulation(alloc, id, iter.next().?, &scanner, ctx), .Fetch => fetch(alloc, id, iter.next().?, &scanner, ctx), @@ -199,7 +200,7 @@ fn getParams( key: []const u8, ) !?T { - // check key key is "params" + // check key is "params" if (!std.mem.eql(u8, "params", key)) return null; // skip "params" if not requested @@ -285,7 +286,8 @@ pub fn getMsg( // Common // ------ -pub const SessionID = "9559320D92474062597D9875C664CAC0"; +pub const BrowserSessionID = "9559320D92474062597D9875C664CAC0"; +pub const ContextSessionID = "4FDC2CB760A23A220497A05C95417CF4"; pub const URLBase = "chrome://newtab/"; pub const FrameID = "90D14BBD8AED408A0467AC93100BCDBE"; pub const LoaderID = "CFC8BED824DD2FD56CF1EF33C965C79C"; diff --git a/src/cdp/page.zig b/src/cdp/page.zig index 0f9d06bc..38b3b8f4 100644 --- a/src/cdp/page.zig +++ b/src/cdp/page.zig @@ -144,13 +144,38 @@ fn createIsolatedWorld( alloc: std.mem.Allocator, id: ?u16, scanner: *std.json.Scanner, - _: *Ctx, + ctx: *Ctx, ) ![]const u8 { - const msg = try getMsg(alloc, void, scanner); + + // input + const Params = struct { + frameId: []const u8, + worldName: []const u8, + grantUniveralAccess: bool, + }; + const msg = try getMsg(alloc, Params, scanner); + std.debug.assert(msg.sessionID != null); + const params = msg.params.?; + + // noop executionContextCreated event + try Runtime.executionContextCreated( + alloc, + ctx, + 0, + "", + params.worldName, + "7102379147004877974.3265385113993241162", + .{ + .isDefault = false, + .type = "isolated", + .frameId = params.frameId, + }, + msg.sessionID, + ); // output const Resp = struct { - executionContextId: u8 = 2, + executionContextId: u8 = 0, }; return result(alloc, id orelse msg.id.?, Resp, .{}, msg.sessionID); @@ -230,20 +255,14 @@ fn navigate( // Launch navigate var p = try ctx.browser.currentSession().createPage(); - _ = try p.navigate(params.url); - - // Send create runtime context event ctx.state.executionContextId += 1; - try Runtime.executionContextCreated( + const auxData = try std.fmt.allocPrint( alloc, - ctx, - ctx.state.executionContextId, - "http://127.0.0.1:1234", // TODO: real domain - "", - "7102379147004877974.3265385113993241162", - .{ .frameId = ctx.state.frameID }, - msg.sessionID, + "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", + .{ctx.state.frameID}, ); + defer alloc.free(auxData); + _ = try p.navigate(params.url, auxData); // frameNavigated event const FrameNavigated = struct { diff --git a/src/cdp/runtime.zig b/src/cdp/runtime.zig index b817dde0..8554cdcc 100644 --- a/src/cdp/runtime.zig +++ b/src/cdp/runtime.zig @@ -16,6 +16,7 @@ const Methods = enum { evaluate, addBinding, callFunctionOn, + releaseObject, }; pub fn runtime( @@ -23,44 +24,83 @@ pub fn runtime( id: ?u16, action: []const u8, scanner: *std.json.Scanner, + s: []const u8, ctx: *Ctx, ) ![]const u8 { const method = std.meta.stringToEnum(Methods, action) orelse + // NOTE: we could send it anyway to the JS runtime but it's good to check it return error.UnknownMethod; return switch (method) { - .enable => enable(alloc, id, scanner, ctx), .runIfWaitingForDebugger => runIfWaitingForDebugger(alloc, id, scanner, ctx), - .evaluate => evaluate(alloc, id, scanner, ctx), - .addBinding => addBinding(alloc, id, scanner, ctx), - .callFunctionOn => callFunctionOn(alloc, id, scanner, ctx), + else => sendInspector(alloc, method, id, s, scanner, ctx), }; } -fn enable( +fn sendInspector( alloc: std.mem.Allocator, - id: ?u16, + method: Methods, + _id: ?u16, + s: []const u8, scanner: *std.json.Scanner, - _: *Ctx, + ctx: *Ctx, ) ![]const u8 { - // input - const msg = try getMsg(alloc, void, scanner); + // save script in file at debug mode + if (std.log.defaultLogEnabled(.debug)) { - // output - // const uniqueID = "1367118932354479079.-1471398151593995849"; - // const mainCtx = try executionContextCreated( - // alloc, - // 1, - // cdp.URLBase, - // "", - // uniqueID, - // .{}, - // sessionID, - // ); - // std.log.debug("res {s}", .{mainCtx}); - // try server.sendAsync(ctx, mainCtx); + // input + var script: ?[]const u8 = null; + var id: u16 = undefined; - return result(alloc, id orelse msg.id.?, null, null, msg.sessionID); + if (method == .evaluate) { + const Params = struct { + expression: []const u8, + contextId: ?u8 = null, + returnByValue: ?bool = null, + awaitPromise: ?bool = null, + userGesture: ?bool = null, + }; + + const msg = try getMsg(alloc, Params, scanner); + const params = msg.params.?; + script = params.expression; + id = _id orelse msg.id.?; + } else if (method == .callFunctionOn) { + const Params = struct { + functionDeclaration: []const u8, + objectId: ?[]const u8 = null, + executionContextId: ?u8 = null, + arguments: ?[]struct { + value: ?[]const u8 = null, + objectId: ?[]const u8 = null, + } = null, + returnByValue: ?bool = null, + awaitPromise: ?bool = null, + userGesture: ?bool = null, + }; + + const msg = try getMsg(alloc, Params, scanner); + const params = msg.params.?; + script = params.functionDeclaration; + id = _id orelse msg.id.?; + } + + if (script) |src| { + try cdp.dumpFile(alloc, id, src); + } + } + + // remove awaitPromise true params + // TODO: delete when Promise are correctly handled by zig-js-runtime + if (method == .callFunctionOn or method == .evaluate) { + const buf = try alloc.alloc(u8, s.len + 1); + defer alloc.free(buf); + _ = std.mem.replace(u8, s, "\"awaitPromise\":true", "\"awaitPromise\":false", buf); + ctx.sendInspector(buf); + } else { + ctx.sendInspector(s); + } + return ""; } pub const AuxData = struct { @@ -69,14 +109,6 @@ pub const AuxData = struct { frameId: []const u8 = cdp.FrameID, }; -const ExecutionContextDescription = struct { - id: u64, - origin: []const u8, - name: []const u8, - uniqueId: []const u8, - auxData: ?AuxData = null, -}; - pub fn executionContextCreated( alloc: std.mem.Allocator, ctx: *Ctx, @@ -88,7 +120,13 @@ pub fn executionContextCreated( sessionID: ?[]const u8, ) !void { const Params = struct { - context: ExecutionContextDescription, + context: struct { + id: u64, + origin: []const u8, + name: []const u8, + uniqueId: []const u8, + auxData: ?AuxData = null, + }, }; const params = Params{ .context = .{ @@ -112,195 +150,3 @@ fn runIfWaitingForDebugger( return result(alloc, id orelse msg.id.?, null, null, msg.sessionID); } - -fn evaluate( - alloc: std.mem.Allocator, - _id: ?u16, - scanner: *std.json.Scanner, - ctx: *Ctx, -) ![]const u8 { - - // ensure a page has been previously created - if (ctx.browser.currentSession().page == null) return error.CDPNoPage; - - // input - const Params = struct { - expression: []const u8, - contextId: ?u8 = null, - returnByValue: ?bool = null, - awaitPromise: ?bool = null, - userGesture: ?bool = null, - }; - - const msg = try getMsg(alloc, Params, scanner); - std.debug.assert(msg.sessionID != null); - const params = msg.params.?; - const id = _id orelse msg.id.?; - - // save script in file at debug mode - std.log.debug("script {d} length: {d}", .{ id, params.expression.len }); - if (std.log.defaultLogEnabled(.debug)) { - try cdp.dumpFile(alloc, id, params.expression); - } - - // evaluate the script in the context of the current page - const session = ctx.browser.currentSession(); - // TODO: should we use instead the allocator of the page? - // the following code does not work with session.page.?.arena.allocator() as alloc - const res = try runtimeEvaluate(session.alloc, id, session.env, params.expression, "cdp"); - - // check result - const res_type = try res.typeOf(session.env); - - // TODO: Resp should depends on JS result returned by the JS engine - const Resp = struct { - result: struct { - type: []const u8, - subtype: ?[]const u8 = null, - className: ?[]const u8 = null, - description: ?[]const u8 = null, - objectId: ?[]const u8 = null, - }, - }; - var resp = Resp{ - .result = .{ - .type = @tagName(res_type), - }, - }; - if (res_type == .object) { - resp.result.className = "Object"; - resp.result.description = "Object"; - resp.result.objectId = "-9051357107442861868.3.2"; - } - return result(alloc, id, Resp, resp, msg.sessionID); -} - -fn addBinding( - alloc: std.mem.Allocator, - _id: ?u16, - scanner: *std.json.Scanner, - ctx: *Ctx, -) ![]const u8 { - - // input - const Params = struct { - name: []const u8, - executionContextId: ?u8 = null, - }; - const msg = try getMsg(alloc, Params, scanner); - const id = _id orelse msg.id.?; - const params = msg.params.?; - if (params.executionContextId) |contextId| { - std.debug.assert(contextId == ctx.state.executionContextId); - } - - const script = try std.fmt.allocPrint(alloc, "globalThis['{s}'] = {{}};", .{params.name}); - defer alloc.free(script); - - const session = ctx.browser.currentSession(); - _ = try runtimeEvaluate(session.alloc, id, session.env, script, "addBinding"); - - return result(alloc, id, null, null, msg.sessionID); -} - -fn callFunctionOn( - alloc: std.mem.Allocator, - _id: ?u16, - scanner: *std.json.Scanner, - ctx: *Ctx, -) ![]const u8 { - - // input - const Params = struct { - functionDeclaration: []const u8, - objectId: ?[]const u8 = null, - executionContextId: ?u8 = null, - arguments: ?[]struct { - value: ?[]const u8 = null, - } = null, - returnByValue: ?bool = null, - awaitPromise: ?bool = null, - userGesture: ?bool = null, - }; - const msg = try getMsg(alloc, Params, scanner); - const id = _id orelse msg.id.?; - const params = msg.params.?; - std.debug.assert(params.objectId != null or params.executionContextId != null); - if (params.executionContextId) |contextID| { - std.debug.assert(contextID == ctx.state.executionContextId); - } - const name = "callFunctionOn"; - - // save script in file at debug mode - std.log.debug("{s} script id {d}, length: {d}", .{ name, id, params.functionDeclaration.len }); - if (std.log.defaultLogEnabled(.debug)) { - try cdp.dumpFile(alloc, id, params.functionDeclaration); - } - - // parse function - if (!std.mem.startsWith(u8, params.functionDeclaration, "function ")) { - return error.CDPRuntimeCallFunctionOnNotFunction; - } - const pos = std.mem.indexOfScalar(u8, params.functionDeclaration, '('); - if (pos == null) return error.CDPRuntimeCallFunctionOnWrongFunction; - var function = params.functionDeclaration[9..pos.?]; - function = try std.fmt.allocPrint(alloc, "{s}(", .{function}); - defer alloc.free(function); - if (params.arguments) |args| { - for (args, 0..) |arg, i| { - if (i > 0) { - function = try std.fmt.allocPrint(alloc, "{s}, ", .{function}); - } - if (arg.value) |value| { - function = try std.fmt.allocPrint(alloc, "{s}\"{s}\"", .{ function, value }); - } else { - function = try std.fmt.allocPrint(alloc, "{s}undefined", .{function}); - } - } - } - function = try std.fmt.allocPrint(alloc, "{s});", .{function}); - std.log.debug("{s} id {d}, function parsed: {s}", .{ name, id, function }); - - const session = ctx.browser.currentSession(); - // TODO: should we use the page's allocator instead of the session's allocator? - // the following code does not work with session.page.?.arena.allocator() as alloc - - // first evaluate the function declaration - _ = try runtimeEvaluate(session.alloc, id, session.env, params.functionDeclaration, name); - - // then call the function on the arguments - _ = try runtimeEvaluate(session.alloc, id, session.env, function, name); - - return result(alloc, id, null, "{\"type\":\"undefined\"}", msg.sessionID); -} - -// caller is the owner of JSResult returned -fn runtimeEvaluate( - alloc: std.mem.Allocator, - id: u16, - env: jsruntime.Env, - script: []const u8, - comptime name: []const u8, -) !jsruntime.JSValue { - - // try catch - var try_catch: jsruntime.TryCatch = undefined; - try_catch.init(env); - defer try_catch.deinit(); - - // script exec - const res = env.execWait(script, name) catch { - if (try try_catch.err(alloc, env)) |err_msg| { - defer alloc.free(err_msg); - std.log.err("'{s}' id {d}, result: {s}", .{ name, id, err_msg }); - } - return error.CDPRuntimeEvaluate; - }; - - if (builtin.mode == .Debug) { - const res_msg = try res.toString(alloc, env); - defer alloc.free(res_msg); - std.log.debug("'{s}' id {d}, result: {s}", .{ name, id, res_msg }); - } - return res; -} diff --git a/src/cdp/target.zig b/src/cdp/target.zig index bc2de491..b77b0249 100644 --- a/src/cdp/target.zig +++ b/src/cdp/target.zig @@ -86,7 +86,7 @@ fn setAutoAttach( if (msg.sessionID == null) { const attached = AttachToTarget{ - .sessionId = cdp.SessionID, + .sessionId = cdp.BrowserSessionID, .targetInfo = .{ .targetId = PageTargetID, .title = "New Incognito tab", @@ -160,7 +160,6 @@ fn getBrowserContexts( } const ContextID = "22648B09EDCCDD11109E2D4FEFBE4F89"; -const ContextSessionID = "4FDC2CB760A23A220497A05C95417CF4"; fn createBrowserContext( alloc: std.mem.Allocator, @@ -218,7 +217,7 @@ fn createTarget( // send attachToTarget event const attached = AttachToTarget{ - .sessionId = ContextSessionID, + .sessionId = cdp.ContextSessionID, .targetInfo = .{ .targetId = ctx.state.frameID, .title = "", diff --git a/src/main.zig b/src/main.zig index c6363b32..1c9de927 100644 --- a/src/main.zig +++ b/src/main.zig @@ -161,7 +161,7 @@ pub fn main() !void { defer srv.close(); std.debug.print("Listening on: {s}...\n", .{socket_path}); - var browser = try Browser.init(arena.allocator(), vm); + var browser = try Browser.init(arena.allocator()); defer browser.deinit(); try server.listen(&browser, srv.sockfd.?); diff --git a/src/main_get.zig b/src/main_get.zig index 6f455553..ac646be6 100644 --- a/src/main_get.zig +++ b/src/main_get.zig @@ -80,13 +80,13 @@ pub fn main() !void { const vm = jsruntime.VM.init(); defer vm.deinit(); - var browser = try Browser.init(allocator, vm); + var browser = try Browser.init(allocator); defer browser.deinit(); var page = try browser.currentSession().createPage(); defer page.deinit(); - try page.navigate(url); + try page.navigate(url, null); defer page.end(); try page.wait(); diff --git a/src/server.zig b/src/server.zig index c53f21e3..401e6524 100644 --- a/src/server.zig +++ b/src/server.zig @@ -70,16 +70,20 @@ pub const Cmd = struct { } // shortcuts - fn alloc(self: *Cmd) std.mem.Allocator { + inline fn alloc(self: *Cmd) std.mem.Allocator { // TODO: should we return the allocator from the page instead? return self.browser.currentSession().alloc; } - fn loop(self: *Cmd) public.Loop { + inline fn loop(self: *Cmd) public.Loop { // TODO: pointer instead? return self.browser.currentSession().loop; } + inline fn env(self: Cmd) public.Env { + return self.browser.currentSession().env; + } + fn do(self: *Cmd, cmd: []const u8) anyerror!void { const res = try cdp.do(self.alloc(), cmd, self); @@ -89,6 +93,49 @@ pub const Cmd = struct { return sendAsync(self, res); } } + + // Inspector + + pub fn sendInspector(self: *Cmd, msg: []const u8) void { + if (self.env().getInspector()) |inspector| { + inspector.send(self.env(), msg); + } + } + + pub fn onInspectorResp(cmd_opaque: *anyopaque, _: u32, msg: []const u8) void { + std.log.debug("onResp biz fn called: {s}", .{msg}); + const aligned = @as(*align(@alignOf(Cmd)) anyopaque, @alignCast(cmd_opaque)); + const self = @as(*Cmd, @ptrCast(aligned)); + + const tpl = "{s},\"sessionId\":\"{s}\"}}"; + const msg_open = msg[0 .. msg.len - 1]; // remove closing bracket + const s = std.fmt.allocPrint( + self.alloc(), + tpl, + .{ msg_open, cdp.ContextSessionID }, + ) catch unreachable; + defer self.alloc().free(s); + + sendSync(self, s) catch unreachable; + } + + pub fn onInspectorNotif(cmd_opaque: *anyopaque, msg: []const u8) void { + std.log.debug("onNotif biz fn called: {s}", .{msg}); + const aligned = @as(*align(@alignOf(Cmd)) anyopaque, @alignCast(cmd_opaque)); + const self = @as(*Cmd, @ptrCast(aligned)); + + const tpl = "{s},\"sessionId\":\"{s}\"}}"; + const msg_open = msg[0 .. msg.len - 1]; // remove closing bracket + const s = std.fmt.allocPrint( + self.alloc(), + tpl, + .{ msg_open, cdp.ContextSessionID }, + ) catch unreachable; + defer self.alloc().free(s); + std.log.debug("event: {s}", .{s}); + + sendSync(self, s) catch unreachable; + } }; // I/O Send @@ -195,6 +242,9 @@ pub fn listen(browser: *Browser, socket: std.posix.socket_t) anyerror!void { .buf = &ctxInput, .msg_buf = &msg_buf, }; + const cmd_opaque = @as(*anyopaque, @ptrCast(&cmd)); + try browser.currentSession().setInspector(cmd_opaque, Cmd.onInspectorResp, Cmd.onInspectorNotif); + var accept = Accept{ .cmd = &cmd, .socket = socket,