diff --git a/src/cdp/browser.zig b/src/cdp/browser.zig new file mode 100644 index 00000000..ff20c329 --- /dev/null +++ b/src/cdp/browser.zig @@ -0,0 +1,77 @@ +const std = @import("std"); + +const server = @import("../server.zig"); +const Ctx = server.CmdContext; +const SendFn = server.SendFn; +const result = @import("cdp.zig").result; +const getParams = @import("cdp.zig").getParams; + +const BrowserMethods = enum { + getVersion, + setDownloadBehavior, +}; + +pub fn browser( + alloc: std.mem.Allocator, + id: u64, + action: []const u8, + scanner: *std.json.Scanner, + ctx: *Ctx, + comptime sendFn: SendFn, +) ![]const u8 { + const method = std.meta.stringToEnum(BrowserMethods, action) orelse + return error.UnknownBrowserMethod; + return switch (method) { + .getVersion => browserGetVersion(alloc, id, scanner, ctx, sendFn), + .setDownloadBehavior => browserSetDownloadBehavior(alloc, id, scanner, ctx, sendFn), + }; +} + +const ProtocolVersion = "1.3"; +const Product = "Chrome/124.0.6367.29"; +const Revision = "@9e6ded5ac1ff5e38d930ae52bd9aec09bd1a68e4"; +const UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"; +const JsVersion = "12.4.254.8"; + +fn browserGetVersion( + alloc: std.mem.Allocator, + id: u64, + _: *std.json.Scanner, + _: *Ctx, + comptime _: SendFn, +) ![]const u8 { + const Res = struct { + protocolVersion: []const u8, + product: []const u8, + revision: []const u8, + userAgent: []const u8, + jsVersion: []const u8, + }; + + const res = Res{ + .protocolVersion = ProtocolVersion, + .product = Product, + .revision = Revision, + .userAgent = UserAgent, + .jsVersion = JsVersion, + }; + return result(alloc, id, Res, res); +} + +fn browserSetDownloadBehavior( + alloc: std.mem.Allocator, + id: u64, + scanner: *std.json.Scanner, + _: *Ctx, + comptime _: SendFn, +) ![]const u8 { + const Params = struct { + behavior: []const u8, + browserContextId: ?[]const u8 = null, + downloadPath: ?[]const u8 = null, + eventsEnabled: ?bool = null, + }; + const params = try getParams(alloc, Params, scanner); + std.log.debug("params {any}", .{params}); + return result(alloc, id, null, null); +} diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig new file mode 100644 index 00000000..7d33bc66 --- /dev/null +++ b/src/cdp/cdp.zig @@ -0,0 +1,87 @@ +const std = @import("std"); + +const server = @import("../server.zig"); +const Ctx = server.CmdContext; +const SendFn = server.SendFn; +const browser = @import("browser.zig").browser; +const target = @import("target.zig").target; + +const Domains = enum { + Browser, + Target, +}; + +// The caller is responsible for calling `free` on the returned slice. +pub fn do( + alloc: std.mem.Allocator, + s: []const u8, + ctx: *Ctx, + comptime sendFn: SendFn, +) ![]const u8 { + var scanner = std.json.Scanner.initCompleteInput(alloc, s); + defer scanner.deinit(); + + std.debug.assert(try scanner.next() == .object_begin); + + try checkKey("id", (try scanner.next()).string); + const id = try std.fmt.parseUnsigned(u64, (try scanner.next()).number, 10); + + try checkKey("method", (try scanner.next()).string); + const method = (try scanner.next()).string; + + std.log.debug("cmd: id {any}, method {s}\n", .{ id, method }); + + var iter = std.mem.splitScalar(u8, method, '.'); + const domain = std.meta.stringToEnum(Domains, iter.first()) orelse + return error.UnknonwDomain; + + return switch (domain) { + .Browser => browser(alloc, id, iter.next().?, &scanner, ctx, sendFn), + .Target => target(alloc, id, iter.next().?, &scanner, ctx, sendFn), + }; +} + +// Utils +// ----- + +fn checkKey(key: []const u8, token: []const u8) !void { + if (!std.mem.eql(u8, key, token)) return error.WrongToken; +} + +const resultNull = "{{\"id\": {d}, \"result\": {{}}}}"; + +pub fn result( + alloc: std.mem.Allocator, + id: u64, + comptime T: ?type, + res: anytype, +) ![]const u8 { + if (T == null) return try std.fmt.allocPrint(alloc, resultNull, .{id}); + + const Resp = struct { + id: u64, + result: T.?, + }; + const resp = Resp{ .id = id, .result = res }; + + var out = std.ArrayList(u8).init(alloc); + defer out.deinit(); + + try std.json.stringify(resp, .{}, out.writer()); + const ret = try alloc.alloc(u8, out.items.len); + @memcpy(ret, out.items); + return ret; +} + +pub fn getParams( + alloc: std.mem.Allocator, + comptime T: type, + scanner: *std.json.Scanner, +) !T { + try checkKey("params", (try scanner.next()).string); + const options = std.json.ParseOptions{ + .max_value_len = scanner.input.len, + .allocate = .alloc_if_needed, + }; + return std.json.innerParse(T, alloc, scanner, options); +} diff --git a/src/cdp/target.zig b/src/cdp/target.zig new file mode 100644 index 00000000..cf861086 --- /dev/null +++ b/src/cdp/target.zig @@ -0,0 +1,100 @@ +const std = @import("std"); + +const server = @import("../server.zig"); +const Ctx = server.CmdContext; +const SendFn = server.SendFn; +const result = @import("cdp.zig").result; +const getParams = @import("cdp.zig").getParams; + +const TargetMethods = enum { + setAutoAttach, + // attachedToTarget, + // getTargetInfo, +}; + +pub fn target( + alloc: std.mem.Allocator, + id: u64, + action: []const u8, + scanner: *std.json.Scanner, + ctx: *Ctx, + comptime sendFn: SendFn, +) ![]const u8 { + const method = std.meta.stringToEnum(TargetMethods, action) orelse + return error.UnknownTargetMethod; + return switch (method) { + .setAutoAttach => tagetSetAutoAttach(alloc, id, scanner, ctx, sendFn), + // .getTargetInfo => tagetGetTargetInfo(alloc, id, scanner), + }; +} + +fn tagetSetAutoAttach( + alloc: std.mem.Allocator, + id: u64, + scanner: *std.json.Scanner, + _: *Ctx, + comptime _: SendFn, +) ![]const u8 { + const Params = struct { + autoAttach: bool, + waitForDebuggerOnStart: bool, + flatten: ?bool = null, + }; + const params = try getParams(alloc, Params, scanner); + std.log.debug("params {any}", .{params}); + return result(alloc, id, null, null); +} + +const TargetID = "CFCD6EC01573CF29BB638E9DC0F52DDC"; + +fn tagetGetTargetInfo( + alloc: std.mem.Allocator, + id: u64, + scanner: *std.json.Scanner, + _: *Ctx, + comptime _: SendFn, +) ![]const u8 { + _ = scanner; + + const TargetInfo = struct { + targetId: []const u8, + type: []const u8, + title: []const u8, + url: []const u8, + attached: bool, + canAccessOpener: bool, + + browserContextId: ?[]const u8 = null, + }; + const targetInfo = TargetInfo{ + .targetId = TargetID, + .type = "page", + }; + _ = targetInfo; + return result(alloc, id, null, null); +} + +// fn tagetGetTargetInfo( +// alloc: std.mem.Allocator, +// id: u64, +// scanner: *std.json.Scanner, +// ) ![]const u8 { +// _ = scanner; + +// const TargetInfo = struct { +// targetId: []const u8, +// type: []const u8, +// title: []const u8, +// url: []const u8, +// attached: bool, +// canAccessOpener: bool, + +// browserContextId: ?[]const u8 = null, +// }; +// const targetInfo = TargetInfo{ +// .targetId = TargetID, +// .type = "page", +// }; +// _ = targetInfo; +// return result(alloc, id, null, null); +// } diff --git a/src/main.zig b/src/main.zig index 40b513fe..5575f815 100644 --- a/src/main.zig +++ b/src/main.zig @@ -20,57 +20,16 @@ const std = @import("std"); const jsruntime = @import("jsruntime"); +const server = @import("server.zig"); + const parser = @import("netsurf"); const apiweb = @import("apiweb.zig"); -const Window = @import("html/window.zig").Window; pub const Types = jsruntime.reflect(apiweb.Interfaces); pub const UserContext = apiweb.UserContext; const socket_path = "/tmp/browsercore-server.sock"; -var doc: *parser.DocumentHTML = undefined; -var server: std.net.Server = undefined; - -fn execJS( - alloc: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - // start JS env - try js_env.start(); - defer js_env.stop(); - - // alias global as self and window - var window = Window.create(null); - window.replaceDocument(doc); - try js_env.bindGlobal(window); - - // try catch - var try_catch: jsruntime.TryCatch = undefined; - try_catch.init(js_env.*); - defer try_catch.deinit(); - - while (true) { - - // read cmd - const conn = try server.accept(); - var buf: [100]u8 = undefined; - const read = try conn.stream.read(&buf); - const cmd = buf[0..read]; - std.debug.print("<- {s}\n", .{cmd}); - if (std.mem.eql(u8, cmd, "exit")) { - break; - } - - const res = try js_env.exec(cmd, "cdp"); - const res_str = try res.toString(alloc, js_env.*); - defer alloc.free(res_str); - std.debug.print("-> {s}\n", .{res_str}); - - _ = try conn.stream.write(res_str); - } -} - pub fn main() !void { // create v8 vm @@ -81,18 +40,6 @@ pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); - try parser.init(); - defer parser.deinit(); - - // document - const file = try std.fs.cwd().openFile("test.html", .{}); - defer file.close(); - - doc = try parser.documentHTMLParse(file.reader(), "UTF-8"); - defer parser.documentHTMLClose(doc) catch |err| { - std.debug.print("documentHTMLClose error: {s}\n", .{@errorName(err)}); - }; - // remove socket file of internal server // reuse_address (SO_REUSEADDR flag) does not seems to work on unix socket // see: https://gavv.net/articles/unix-socket-reuse/ @@ -105,9 +52,15 @@ pub fn main() !void { // server const addr = try std.net.Address.initUnix(socket_path); - server = try addr.listen(.{}); - defer server.deinit(); + var srv = std.net.StreamServer.init(.{ + .reuse_address = true, + .reuse_port = true, + .force_nonblocking = true, + }); + defer srv.deinit(); + try srv.listen(addr); std.debug.print("Listening on: {s}...\n", .{socket_path}); + server.socket_fd = srv.sockfd.?; - try jsruntime.loadEnv(&arena, null, execJS); + try jsruntime.loadEnv(&arena, server.execJS); } diff --git a/src/server.zig b/src/server.zig new file mode 100644 index 00000000..47d3abba --- /dev/null +++ b/src/server.zig @@ -0,0 +1,231 @@ +const std = @import("std"); + +const public = @import("jsruntime"); + +const Window = @import("html/window.zig").Window; + +const cdp = @import("cdp/cdp.zig"); +pub var socket_fd: std.os.socket_t = undefined; + +// I/O input command context +pub const CmdContext = struct { + alloc: std.mem.Allocator, + js_env: *public.Env, + socket: std.os.socket_t, + completion: *public.IO.Completion, + buf: []u8, + close: bool = false, + + try_catch: public.TryCatch, + + // cmds: cdp.Cmds, +}; + +fn respCallback( + ctx: *CmdContext, + _: *public.IO.Completion, + result: public.IO.SendError!usize, +) void { + _ = result catch |err| { + ctx.close = true; + std.debug.print("send error: {s}\n", .{@errorName(err)}); + return; + }; + std.log.debug("send ok", .{}); +} + +pub const SendFn = (fn (*CmdContext, []const u8) anyerror!void); + +fn osSend(ctx: *CmdContext, msg: []const u8) !void { + const s = try std.os.write(ctx.socket, msg); + std.log.debug("send ok {d}", .{s}); +} + +fn loopSend(ctx: *CmdContext, msg: []const u8) !void { + ctx.js_env.nat_ctx.loop.io.send( + *CmdContext, + ctx, + respCallback, + ctx.completion, + ctx.socket, + msg, + ); +} + +// I/O input command callback +fn cmdCallback( + ctx: *CmdContext, + completion: *public.IO.Completion, + result: public.IO.RecvError!usize, +) void { + const size = result catch |err| { + ctx.close = true; + std.debug.print("recv error: {s}\n", .{@errorName(err)}); + return; + }; + + const input = ctx.buf[0..size]; + + // close on exit command + if (std.mem.eql(u8, input, "exit")) { + ctx.close = true; + return; + } + + // continue receving messages asynchronously + defer { + ctx.js_env.nat_ctx.loop.io.recv( + *CmdContext, + ctx, + cmdCallback, + completion, + ctx.socket, + ctx.buf, + ); + } + + std.debug.print("input {s}\n", .{input}); + const res = cdp.do(ctx.alloc, input, ctx, osSend) catch |err| { + std.log.debug("error: {any}\n", .{err}); + loopSend(ctx, "{}") catch unreachable; + // TODO: return proper error + return; + }; + defer ctx.alloc.free(res); + std.log.debug("res {s}", .{res}); + + osSend(ctx, res) catch unreachable; + + // ctx.js_env.nat_ctx.loop.io.send( + // *CmdContext, + // ctx, + // respCallback, + // completion, + // ctx.socket, + // res, + // ); + + // JS execute + // const res = ctx.js_env.exec( + // ctx.alloc, + // input, + // "shell.js", + // ctx.try_catch, + // ) catch |err| { + // ctx.close = true; + // std.debug.print("JS exec error: {s}\n", .{@errorName(err)}); + // return; + // }; + // defer res.deinit(ctx.alloc); + + // // JS print result + // if (res.success) { + // if (std.mem.eql(u8, res.result, "undefined")) { + // std.debug.print("<- \x1b[38;5;242m{s}\x1b[0m\n", .{res.result}); + // } else { + // std.debug.print("<- \x1b[33m{s}\x1b[0m\n", .{res.result}); + // } + // } else { + // std.debug.print("{s}\n", .{res.result}); + // } + + // acknowledge to repl result has been printed + // _ = std.os.write(ctx.socket, "ok") catch unreachable; +} + +// I/O connection context +const ConnContext = struct { + socket: std.os.socket_t, + + cmdContext: *CmdContext, +}; + +// I/O connection callback +fn connCallback( + ctx: *ConnContext, + completion: *public.IO.Completion, + result: public.IO.AcceptError!std.os.socket_t, +) void { + ctx.cmdContext.socket = result catch |err| @panic(@errorName(err)); + + // launch receving messages asynchronously + ctx.cmdContext.js_env.nat_ctx.loop.io.recv( + *CmdContext, + ctx.cmdContext, + cmdCallback, + completion, + ctx.cmdContext.socket, + ctx.cmdContext.buf, + ); +} + +pub fn execJS( + alloc: std.mem.Allocator, + js_env: *public.Env, +) anyerror!void { + + // start JS env + try js_env.start(alloc); + defer js_env.stop(); + + // alias global as self + try js_env.attachObject(try js_env.getGlobal(), "self", null); + + // alias global as self and window + const window = Window.create(null); + // window.replaceDocument(doc); TODO + try js_env.bindGlobal(window); + + // add console object + const console = public.Console{}; + try js_env.addObject(console, "console"); + + // JS try cache + var try_catch = public.TryCatch.init(js_env.*); + defer try_catch.deinit(); + + // create I/O contexts and callbacks + // for accepting connections and receving messages + var completion: public.IO.Completion = undefined; + var input: [1024]u8 = undefined; + var cmd_ctx = CmdContext{ + .alloc = alloc, + .js_env = js_env, + .socket = undefined, + .buf = &input, + .try_catch = try_catch, + .completion = &completion, + // .cmds = .{}, + }; + var conn_ctx = ConnContext{ + .socket = socket_fd, + .cmdContext = &cmd_ctx, + }; + + // launch accepting connection asynchronously on internal server + const loop = js_env.nat_ctx.loop; + loop.io.accept( + *ConnContext, + &conn_ctx, + connCallback, + &completion, + socket_fd, + ); + + // infinite loop on I/O events, either: + // - cmd from incoming connection on server socket + // - JS callbacks events from scripts + while (true) { + try loop.io.tick(); + if (loop.cbk_error) { + if (try try_catch.exception(alloc, js_env.*)) |msg| { + std.debug.print("\n\rUncaught {s}\n\r", .{msg}); + alloc.free(msg); + } + loop.cbk_error = false; + } + if (cmd_ctx.close) { + break; + } + } +}