mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-28 22:53:28 +00:00
Compare commits
76 Commits
beta
...
event-targ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02cd5e98f9 | ||
|
|
a51c20068f | ||
|
|
99a3e4be3f | ||
|
|
40c9355088 | ||
|
|
8f1557254a | ||
|
|
11d28b0bc3 | ||
|
|
974cf780c0 | ||
|
|
73bb14e4a9 | ||
|
|
daf4236023 | ||
|
|
4c9a24c64e | ||
|
|
c149f65158 | ||
|
|
c5688c1bd3 | ||
|
|
b276a15786 | ||
|
|
2fed239ece | ||
|
|
8e2cb36597 | ||
|
|
bcaace1c91 | ||
|
|
d664d07141 | ||
|
|
cb8b80c856 | ||
|
|
d777d77b06 | ||
|
|
43678f8dc0 | ||
|
|
5811577824 | ||
|
|
1587122efa | ||
|
|
48e7c8ad0f | ||
|
|
766f9798f6 | ||
|
|
680d634725 | ||
|
|
7ac945bf88 | ||
|
|
188d7a8558 | ||
|
|
ee561b0d1e | ||
|
|
82bbe78e95 | ||
|
|
c761cd059b | ||
|
|
03e87155ca | ||
|
|
ea39cc52b1 | ||
|
|
90fb90b186 | ||
|
|
5f8327eaf7 | ||
|
|
07869f3c48 | ||
|
|
8377eb02a5 | ||
|
|
3738e8eb44 | ||
|
|
4b000e44b3 | ||
|
|
d6021d1702 | ||
|
|
b391eafc38 | ||
|
|
2af4545e10 | ||
|
|
b96644d893 | ||
|
|
3cb77c0a32 | ||
|
|
b3f7fb7be3 | ||
|
|
9fb51a1f29 | ||
|
|
d78e8a725d | ||
|
|
7829bdbc95 | ||
|
|
90ba6deba2 | ||
|
|
5fc763a738 | ||
|
|
84614e903c | ||
|
|
c95d739347 | ||
|
|
c55849cef5 | ||
|
|
88adb09417 | ||
|
|
cb356ffca9 | ||
|
|
2ba3f252ee | ||
|
|
ebe2c8e3dd | ||
|
|
ca2d63edcf | ||
|
|
489a1a1c37 | ||
|
|
5cad13eea3 | ||
|
|
a3e09e015c | ||
|
|
22a6accac1 | ||
|
|
277c97a959 | ||
|
|
c08bc3239a | ||
|
|
f798c7e0fb | ||
|
|
193780a88f | ||
|
|
ab4973ab6c | ||
|
|
f9da815e8f | ||
|
|
59e187d59a | ||
|
|
3b018e2a6d | ||
|
|
d4939c8260 | ||
|
|
485352594c | ||
|
|
bcdcfee467 | ||
|
|
0217e3fcae | ||
|
|
6e7d6421d5 | ||
|
|
913d3af938 | ||
|
|
53dd0a5e4c |
2
.github/actions/install/action.yml
vendored
2
.github/actions/install/action.yml
vendored
@@ -17,7 +17,7 @@ inputs:
|
||||
zig-v8:
|
||||
description: 'zig v8 version to install'
|
||||
required: false
|
||||
default: 'v0.1.9'
|
||||
default: 'v0.1.11'
|
||||
v8:
|
||||
description: 'v8 version to install'
|
||||
required: false
|
||||
|
||||
2
.github/workflows/cla.yml
vendored
2
.github/workflows/cla.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
path-to-document: 'https://github.com/lightpanda-io/browser/blob/main/CLA.md'
|
||||
# branch should not be protected
|
||||
branch: 'main'
|
||||
allowlist: krichprollsch,francisbouvier
|
||||
allowlist: krichprollsch,francisbouvier,katie-lpd
|
||||
|
||||
remote-organization-name: lightpanda-io
|
||||
remote-repository-name: cla
|
||||
|
||||
35
.github/workflows/zig-test.yml
vendored
35
.github/workflows/zig-test.yml
vendored
@@ -56,6 +56,14 @@ jobs:
|
||||
- name: zig build debug
|
||||
run: zig build -Dengine=v8
|
||||
|
||||
- name: upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lightpanda-build-dev
|
||||
path: |
|
||||
zig-out/bin/lightpanda
|
||||
retention-days: 1
|
||||
|
||||
zig-build-release:
|
||||
name: zig build release
|
||||
|
||||
@@ -131,3 +139,30 @@ jobs:
|
||||
|
||||
- name: format and send json result
|
||||
run: /perf-fmt bench-browser ${{ github.sha }} bench.json
|
||||
|
||||
demo-puppeteer:
|
||||
name: demo-puppeteer
|
||||
needs: zig-build-dev
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'lightpanda-io/demo'
|
||||
fetch-depth: 0
|
||||
|
||||
- run: npm install
|
||||
|
||||
- name: download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: lightpanda-build-dev
|
||||
|
||||
- run: chmod a+x ./lightpanda
|
||||
|
||||
- name: run puppeteer
|
||||
run: |
|
||||
python3 -m http.server 1234 -d ./public &
|
||||
./lightpanda &
|
||||
RUNS=2 npm run bench-puppeteer-cdp
|
||||
|
||||
10
CONTRIBUTING.md
Normal file
10
CONTRIBUTING.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Contributing
|
||||
|
||||
Lightpanda accepts pull requests through GitHub.
|
||||
|
||||
You have to sign our [CLA](CLA.md) during your first pull request process
|
||||
otherwise we're not able to accept your contributions.
|
||||
|
||||
The process signature uses the [CLA assistant
|
||||
lite](https://github.com/marketplace/actions/cla-assistant-lite). You can see
|
||||
an example of the process in [#303](https://github.com/lightpanda-io/browser/pull/303).
|
||||
@@ -74,4 +74,6 @@ COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.
|
||||
|
||||
COPY --from=0 /browser/zig-out/bin/lightpanda /bin/lightpanda
|
||||
|
||||
CMD ["/bin/lightpanda", "--host", "0.0.0.0", "--port", "3245"]
|
||||
EXPOSE 9222/tcp
|
||||
|
||||
CMD ["/bin/lightpanda", "--host", "0.0.0.0", "--port", "9222"]
|
||||
|
||||
88
README.md
88
README.md
@@ -13,13 +13,13 @@
|
||||
Lightpanda is the open-source browser made for headless usage:
|
||||
|
||||
- Javascript execution
|
||||
- Support of the Web APIs (partial, WIP)
|
||||
- Support of Web APIs (partial, WIP)
|
||||
- Compatible with Playwright, Puppeteer through CDP (WIP)
|
||||
|
||||
Fast scraping and web automation with minimal memory footprint:
|
||||
|
||||
- Ultra-low memory footprint (9x less than Chrome)
|
||||
- Blazingly fast execution (11x faster than Chrome) & instant startup
|
||||
- Exceptionally fast execution (11x faster than Chrome) & instant startup
|
||||
|
||||
<img width=500px src="https://cdn.lightpanda.io/assets/images/benchmark_2024-12-04.png">
|
||||
|
||||
@@ -29,14 +29,14 @@ See [benchmark details](https://github.com/lightpanda-io/demo).
|
||||
|
||||
### Javascript execution is mandatory for the modern web
|
||||
|
||||
Back in the good old times, grabbing a webpage was as easy as making an HTTP request, cURL-like. It’s not possible anymore, because Javascript is everywhere, like it or not:
|
||||
In the good old days, scraping a webpage was as easy as making an HTTP request, cURL-like. It’s not possible anymore, because Javascript is everywhere, like it or not:
|
||||
|
||||
- Ajax, Single Page App, Infinite loading, “click to display”, instant search, etc.
|
||||
- Ajax, Single Page App, infinite loading, “click to display”, instant search, etc.
|
||||
- JS web frameworks: React, Vue, Angular & others
|
||||
|
||||
### Chrome is not the right tool
|
||||
|
||||
So if we need Javascript, why not use a real web browser. Let’s take a huge desktop application, hack it, and run it on the server, right? Hundreds or thousands of instances of Chrome if you use it at scale. Are you sure it’s such a good idea?
|
||||
If we need Javascript, why not use a real web browser? Take a huge desktop application, hack it, and run it on the server. Hundreds or thousands of instances of Chrome if you use it at scale. Are you sure it’s such a good idea?
|
||||
|
||||
- Heavy on RAM and CPU, expensive to run
|
||||
- Hard to package, deploy and maintain at scale
|
||||
@@ -44,20 +44,22 @@ So if we need Javascript, why not use a real web browser. Let’s take a huge de
|
||||
|
||||
### Lightpanda is built for performance
|
||||
|
||||
If we want both Javascript and performance, for a real headless browser, we need to start from scratch. Not yet another iteration of Chromium, really from a blank page. Crazy right? But that’s we did:
|
||||
If we want both Javascript and performance in a true headless browser, we need to start from scratch. Not another iteration of Chromium, really from a blank page. Crazy right? But that’s we did:
|
||||
|
||||
- Not based on Chromium, Blink or WebKit
|
||||
- Low-level system programming language (Zig) with optimisations in mind
|
||||
- Opinionated, without graphical rendering
|
||||
- Opinionated: without graphical rendering
|
||||
|
||||
## Status
|
||||
|
||||
Lightpanda is still a work in progress and is currently at a Beta stage.
|
||||
|
||||
:warning: You should expect most websites to fail or crash.
|
||||
|
||||
Here are the key features we have implemented:
|
||||
|
||||
- [x] HTTP loader
|
||||
- [x] HTML parser and DOM tree
|
||||
- [x] HTML parser and DOM tree (based on Netsurf libs)
|
||||
- [x] Javascript support (v8)
|
||||
- [x] Basic DOM APIs
|
||||
- [x] Ajax
|
||||
@@ -66,13 +68,77 @@ Here are the key features we have implemented:
|
||||
- [x] DOM dump
|
||||
- [x] Basic CDP/websockets server
|
||||
|
||||
NOTE: There are hundreds of Web APIs. Developing a browser, even just for headless mode, is a huge task. It's more about coverage than a _working/not working_ binary situation.
|
||||
NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.
|
||||
|
||||
You can also follow the progress of our Javascript support in our dedicated [zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime#development) project.
|
||||
|
||||
## Install
|
||||
## Quick start
|
||||
|
||||
We do provide [nighly builds](https://github.com/lightpanda-io/browser/releases/tag/nightly) for Linux x86_64 and MacOS aarch64.
|
||||
### Install from the nightly builds
|
||||
|
||||
You can download the last binary from the [nightly
|
||||
builds](https://github.com/lightpanda-io/browser/releases/tag/nightly) for
|
||||
Linux x86_64 and MacOS aarch64.
|
||||
|
||||
```console
|
||||
# Download the binary
|
||||
$ wget https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-x86_64-linux
|
||||
$ chmod a+x ./lightpanda-x86_64-linux
|
||||
$ ./lightpanda-x86_64-linux -h
|
||||
usage: ./lightpanda-x86_64-linux [options] [URL]
|
||||
|
||||
start Lightpanda browser
|
||||
|
||||
* if an url is provided the browser will fetch the page and exit
|
||||
* otherwhise the browser starts a CDP server
|
||||
|
||||
-h, --help Print this help message and exit.
|
||||
--host Host of the CDP server (default "127.0.0.1")
|
||||
--port Port of the CDP server (default "9222")
|
||||
--timeout Timeout for incoming connections of the CDP server (in seconds, default "3")
|
||||
--dump Dump document in stdout (fetch mode only)
|
||||
```
|
||||
|
||||
### Dump an URL
|
||||
|
||||
```console
|
||||
$ ./lightpanda-x86_64-linux --dump https://lightpanda.io
|
||||
info(browser): GET https://lightpanda.io/ http.Status.ok
|
||||
info(browser): fetch script https://api.website.lightpanda.io/js/script.js: http.Status.ok
|
||||
info(browser): eval remote https://api.website.lightpanda.io/js/script.js: TypeError: Cannot read properties of undefined (reading 'pushState')
|
||||
<!DOCTYPE html>
|
||||
```
|
||||
|
||||
### Start a CDP server
|
||||
|
||||
```console
|
||||
$ ./lightpanda-x86_64-linux --host 127.0.0.1 --port 9222
|
||||
info(websocket): starting blocking worker to listen on 127.0.0.1:9222
|
||||
info(server): accepting new conn...
|
||||
```
|
||||
|
||||
Once the CDP server started, you can run a Puppeteer script by configuring the
|
||||
`browserWSEndpoint`.
|
||||
|
||||
```js
|
||||
'use scrict'
|
||||
|
||||
import puppeteer from 'puppeteer-core';
|
||||
|
||||
// use browserWSEndpoint to pass the Lightpanda's CDP server address.
|
||||
const browser = await puppeteer.connect({
|
||||
browserWSEndpoint: "ws://127.0.0.1:9222",
|
||||
});
|
||||
|
||||
// The rest of your script remains the same.
|
||||
const context = await browser.createBrowserContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto('https://wikipedia.com/');
|
||||
|
||||
await page.close();
|
||||
await context.close();
|
||||
```
|
||||
|
||||
## Build from sources
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ const Mime = @import("mime.zig");
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Loop = jsruntime.Loop;
|
||||
const Env = jsruntime.Env;
|
||||
const Module = jsruntime.Module;
|
||||
|
||||
const apiweb = @import("../apiweb.zig");
|
||||
|
||||
@@ -46,12 +47,15 @@ const polyfill = @import("../polyfill/polyfill.zig");
|
||||
|
||||
const log = std.log.scoped(.browser);
|
||||
|
||||
pub const user_agent = "Lightpanda/1.0";
|
||||
|
||||
// Browser is an instance of the browser.
|
||||
// You can create multiple browser instances.
|
||||
// A browser contains only one session.
|
||||
// TODO allow multiple sessions per browser.
|
||||
pub const Browser = struct {
|
||||
session: Session = undefined,
|
||||
agent: []const u8 = user_agent,
|
||||
|
||||
const uri = "about:blank";
|
||||
|
||||
@@ -111,7 +115,7 @@ pub const Session = struct {
|
||||
.uri = uri,
|
||||
.alloc = alloc,
|
||||
.arena = std.heap.ArenaAllocator.init(alloc),
|
||||
.window = Window.create(null),
|
||||
.window = Window.create(null, .{ .agent = user_agent }),
|
||||
.loader = Loader.init(alloc),
|
||||
.storageShed = storage.Shed.init(alloc),
|
||||
.httpClient = undefined,
|
||||
@@ -122,6 +126,21 @@ pub const Session = struct {
|
||||
try self.env.load(&self.jstypes);
|
||||
}
|
||||
|
||||
fn fetchModule(ctx: *anyopaque, referrer: ?jsruntime.Module, specifier: []const u8) !jsruntime.Module {
|
||||
_ = referrer;
|
||||
|
||||
const self: *Session = @ptrCast(@alignCast(ctx));
|
||||
|
||||
if (self.page == null) return error.NoPage;
|
||||
|
||||
log.debug("fetch module: specifier: {s}", .{specifier});
|
||||
const alloc = self.arena.allocator();
|
||||
const body = try self.page.?.fetchData(alloc, specifier);
|
||||
defer alloc.free(body);
|
||||
|
||||
return self.env.compileModule(body, specifier);
|
||||
}
|
||||
|
||||
fn deinit(self: *Session) void {
|
||||
if (self.page) |*p| p.end();
|
||||
|
||||
@@ -195,6 +214,31 @@ pub const Page = struct {
|
||||
};
|
||||
}
|
||||
|
||||
// start js env.
|
||||
// - auxData: extra data forwarded to the Inspector
|
||||
// see Inspector.contextCreated
|
||||
pub fn start(self: *Page, auxData: ?[]const u8) !void {
|
||||
// start JS env
|
||||
log.debug("start js env", .{});
|
||||
try self.session.env.start();
|
||||
|
||||
// register the module loader
|
||||
try self.session.env.setModuleLoadFn(self.session, Session.fetchModule);
|
||||
|
||||
// add global objects
|
||||
log.debug("setup global env", .{});
|
||||
try self.session.env.bindGlobal(&self.session.window);
|
||||
|
||||
// load polyfills
|
||||
try polyfill.load(self.arena.allocator(), self.session.env);
|
||||
|
||||
// inspector
|
||||
if (self.session.inspector) |inspector| {
|
||||
log.debug("inspector context created", .{});
|
||||
inspector.contextCreated(self.session.env, "", self.origin orelse "://", auxData);
|
||||
}
|
||||
}
|
||||
|
||||
// reset js env and mem arena.
|
||||
pub fn end(self: *Page) void {
|
||||
self.session.env.stop();
|
||||
@@ -254,6 +298,11 @@ pub const Page = struct {
|
||||
|
||||
log.debug("starting GET {s}", .{uri});
|
||||
|
||||
// if the uri is about:blank, nothing to do.
|
||||
if (std.mem.eql(u8, "about:blank", uri)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// own the url
|
||||
if (self.rawuri) |prev| alloc.free(prev);
|
||||
self.rawuri = try alloc.dupe(u8, uri);
|
||||
@@ -276,18 +325,15 @@ pub const Page = struct {
|
||||
|
||||
const req = resp.req;
|
||||
|
||||
log.info("GET {any} {d}", .{ self.uri, req.response.status });
|
||||
log.info("GET {any} {d}", .{ self.uri, @intFromEnum(req.response.status) });
|
||||
|
||||
// TODO handle redirection
|
||||
if (req.response.status != .ok) {
|
||||
log.debug("{?} {d} {s}", .{
|
||||
req.response.version,
|
||||
req.response.status,
|
||||
req.response.reason,
|
||||
// TODO log headers
|
||||
});
|
||||
return error.BadStatusCode;
|
||||
}
|
||||
log.debug("{?} {d} {s}", .{
|
||||
req.response.version,
|
||||
@intFromEnum(req.response.status),
|
||||
req.response.reason,
|
||||
// TODO log headers
|
||||
});
|
||||
|
||||
// TODO handle charset
|
||||
// https://html.spec.whatwg.org/#content-type
|
||||
@@ -352,14 +398,6 @@ pub const Page = struct {
|
||||
|
||||
// https://html.spec.whatwg.org/#read-html
|
||||
|
||||
// start JS env
|
||||
// TODO load the js env concurrently with the HTML parsing.
|
||||
log.debug("start js env", .{});
|
||||
try self.session.env.start();
|
||||
|
||||
// load polyfills
|
||||
try polyfill.load(alloc, self.session.env);
|
||||
|
||||
// inspector
|
||||
if (self.session.inspector) |inspector| {
|
||||
inspector.contextCreated(self.session.env, "", self.origin.?, auxData);
|
||||
@@ -371,10 +409,6 @@ pub const Page = struct {
|
||||
.httpClient = &self.session.httpClient,
|
||||
});
|
||||
|
||||
// add global objects
|
||||
log.debug("setup global env", .{});
|
||||
try self.session.env.bindGlobal(&self.session.window);
|
||||
|
||||
// browse the DOM tree to retrieve scripts
|
||||
// TODO execute the synchronous scripts during the HTL parsing.
|
||||
// TODO fetch the script resources concurrently but execute them in the
|
||||
@@ -383,7 +417,7 @@ pub const Page = struct {
|
||||
// sasync stores scripts which can be run asynchronously.
|
||||
// for now they are just run after the non-async one in order to
|
||||
// dispatch DOMContentLoaded the sooner as possible.
|
||||
var sasync = std.ArrayList(*parser.Element).init(alloc);
|
||||
var sasync = std.ArrayList(Script).init(alloc);
|
||||
defer sasync.deinit();
|
||||
|
||||
const root = parser.documentToNode(doc);
|
||||
@@ -398,21 +432,10 @@ pub const Page = struct {
|
||||
}
|
||||
|
||||
const e = parser.nodeToElement(next.?);
|
||||
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e)));
|
||||
|
||||
// ignore non-script tags
|
||||
if (tag != .script) continue;
|
||||
|
||||
// ignore non-js script.
|
||||
// > type
|
||||
// > Attribute is not set (default), an empty string, or a JavaScript MIME
|
||||
// > type indicates that the script is a "classic script", containing
|
||||
// > JavaScript code.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type
|
||||
const stype = try parser.elementGetAttribute(e, "type");
|
||||
if (!isJS(stype)) {
|
||||
continue;
|
||||
}
|
||||
const script = try Script.init(e) orelse continue;
|
||||
if (script.kind == .unknown) continue;
|
||||
|
||||
// Ignore the defer attribute b/c we analyze all script
|
||||
// after the document has been parsed.
|
||||
@@ -426,8 +449,8 @@ pub const Page = struct {
|
||||
// > then the classic script will be fetched in parallel to
|
||||
// > parsing and evaluated as soon as it is available.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#async
|
||||
if (try parser.elementGetAttribute(e, "async") != null) {
|
||||
try sasync.append(e);
|
||||
if (script.isasync) {
|
||||
try sasync.append(script);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -450,7 +473,7 @@ pub const Page = struct {
|
||||
// > page.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#notes
|
||||
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(e));
|
||||
self.evalScript(e) catch |err| log.warn("evaljs: {any}", .{err});
|
||||
self.evalScript(script) catch |err| log.warn("evaljs: {any}", .{err});
|
||||
try parser.documentHTMLSetCurrentScript(html_doc, null);
|
||||
}
|
||||
|
||||
@@ -467,9 +490,9 @@ pub const Page = struct {
|
||||
_ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, html_doc), evt);
|
||||
|
||||
// eval async scripts.
|
||||
for (sasync.items) |e| {
|
||||
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(e));
|
||||
self.evalScript(e) catch |err| log.warn("evaljs: {any}", .{err});
|
||||
for (sasync.items) |s| {
|
||||
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(s.element));
|
||||
self.evalScript(s) catch |err| log.warn("evaljs: {any}", .{err});
|
||||
try parser.documentHTMLSetCurrentScript(html_doc, null);
|
||||
}
|
||||
|
||||
@@ -491,15 +514,15 @@ pub const Page = struct {
|
||||
// evalScript evaluates the src in priority.
|
||||
// if no src is present, we evaluate the text source.
|
||||
// https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model
|
||||
fn evalScript(self: *Page, e: *parser.Element) !void {
|
||||
fn evalScript(self: *Page, s: Script) !void {
|
||||
const alloc = self.arena.allocator();
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
|
||||
const opt_src = try parser.elementGetAttribute(e, "src");
|
||||
const opt_src = try parser.elementGetAttribute(s.element, "src");
|
||||
if (opt_src) |src| {
|
||||
log.debug("starting GET {s}", .{src});
|
||||
|
||||
self.fetchScript(src) catch |err| {
|
||||
self.fetchScript(s) catch |err| {
|
||||
switch (err) {
|
||||
FetchError.BadStatusCode => return err,
|
||||
|
||||
@@ -518,26 +541,10 @@ pub const Page = struct {
|
||||
return;
|
||||
}
|
||||
|
||||
var try_catch: jsruntime.TryCatch = undefined;
|
||||
try_catch.init(self.session.env);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const opt_text = try parser.nodeTextContent(parser.elementToNode(e));
|
||||
// TODO handle charset attribute
|
||||
const opt_text = try parser.nodeTextContent(parser.elementToNode(s.element));
|
||||
if (opt_text) |text| {
|
||||
// TODO handle charset attribute
|
||||
const res = self.session.env.exec(text, "") catch {
|
||||
if (try try_catch.err(alloc, self.session.env)) |msg| {
|
||||
defer alloc.free(msg);
|
||||
log.info("eval inline {s}: {s}", .{ text, msg });
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
if (builtin.mode == .Debug) {
|
||||
const msg = try res.toString(alloc, self.session.env);
|
||||
defer alloc.free(msg);
|
||||
log.debug("eval inline {s}", .{msg});
|
||||
}
|
||||
try s.eval(alloc, self.session.env, text);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -552,12 +559,9 @@ pub const Page = struct {
|
||||
JsErr,
|
||||
};
|
||||
|
||||
// fetchScript senf a GET request to the src and execute the script
|
||||
// received.
|
||||
fn fetchScript(self: *Page, src: []const u8) !void {
|
||||
const alloc = self.arena.allocator();
|
||||
|
||||
log.debug("starting fetch script {s}", .{src});
|
||||
// the caller owns the returned string
|
||||
fn fetchData(self: *Page, alloc: std.mem.Allocator, src: []const u8) ![]const u8 {
|
||||
log.debug("starting fetch {s}", .{src});
|
||||
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var b: []u8 = buffer[0..];
|
||||
@@ -568,46 +572,91 @@ pub const Page = struct {
|
||||
|
||||
const resp = fetchres.req.response;
|
||||
|
||||
log.info("fetch script {any}: {d}", .{ u, resp.status });
|
||||
log.info("fetch {any}: {d}", .{ u, resp.status });
|
||||
|
||||
if (resp.status != .ok) return FetchError.BadStatusCode;
|
||||
|
||||
// TODO check content-type
|
||||
const body = try fetchres.req.reader().readAllAlloc(alloc, 16 * 1024 * 1024);
|
||||
defer alloc.free(body);
|
||||
|
||||
// check no body
|
||||
if (body.len == 0) return FetchError.NoBody;
|
||||
|
||||
var try_catch: jsruntime.TryCatch = undefined;
|
||||
try_catch.init(self.session.env);
|
||||
defer try_catch.deinit();
|
||||
return body;
|
||||
}
|
||||
|
||||
const res = self.session.env.exec(body, src) catch {
|
||||
if (try try_catch.err(alloc, self.session.env)) |msg| {
|
||||
defer alloc.free(msg);
|
||||
log.info("eval remote {s}: {s}", .{ src, msg });
|
||||
}
|
||||
return FetchError.JsErr;
|
||||
// fetchScript senf a GET request to the src and execute the script
|
||||
// received.
|
||||
fn fetchScript(self: *Page, s: Script) !void {
|
||||
const alloc = self.arena.allocator();
|
||||
const body = try self.fetchData(alloc, s.src);
|
||||
defer alloc.free(body);
|
||||
|
||||
try s.eval(alloc, self.session.env, body);
|
||||
}
|
||||
|
||||
const Script = struct {
|
||||
element: *parser.Element,
|
||||
kind: Kind,
|
||||
isasync: bool,
|
||||
|
||||
src: []const u8,
|
||||
|
||||
const Kind = enum {
|
||||
unknown,
|
||||
javascript,
|
||||
module,
|
||||
};
|
||||
|
||||
if (builtin.mode == .Debug) {
|
||||
const msg = try res.toString(alloc, self.session.env);
|
||||
defer alloc.free(msg);
|
||||
log.debug("eval remote {s}: {s}", .{ src, msg });
|
||||
fn init(e: *parser.Element) !?Script {
|
||||
// ignore non-script tags
|
||||
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e)));
|
||||
if (tag != .script) return null;
|
||||
|
||||
return .{
|
||||
.element = e,
|
||||
.kind = kind(try parser.elementGetAttribute(e, "type")),
|
||||
.isasync = try parser.elementGetAttribute(e, "async") != null,
|
||||
|
||||
.src = try parser.elementGetAttribute(e, "src") orelse "inline",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// > type
|
||||
// > Attribute is not set (default), an empty string, or a JavaScript MIME
|
||||
// > type indicates that the script is a "classic script", containing
|
||||
// > JavaScript code.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type
|
||||
fn isJS(stype: ?[]const u8) bool {
|
||||
if (stype == null or stype.?.len == 0) return true;
|
||||
if (std.mem.eql(u8, stype.?, "application/javascript")) return true;
|
||||
if (!std.mem.eql(u8, stype.?, "module")) return true;
|
||||
// > type
|
||||
// > Attribute is not set (default), an empty string, or a JavaScript MIME
|
||||
// > type indicates that the script is a "classic script", containing
|
||||
// > JavaScript code.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type
|
||||
fn kind(stype: ?[]const u8) Kind {
|
||||
if (stype == null or stype.?.len == 0) return .javascript;
|
||||
if (std.mem.eql(u8, stype.?, "application/javascript")) return .javascript;
|
||||
if (std.mem.eql(u8, stype.?, "module")) return .module;
|
||||
|
||||
return false;
|
||||
}
|
||||
return .unknown;
|
||||
}
|
||||
|
||||
fn eval(self: Script, alloc: std.mem.Allocator, env: Env, body: []const u8) !void {
|
||||
var try_catch: jsruntime.TryCatch = undefined;
|
||||
try_catch.init(env);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const res = switch (self.kind) {
|
||||
.unknown => return error.UnknownScript,
|
||||
.javascript => env.exec(body, self.src),
|
||||
.module => env.module(body, self.src),
|
||||
} catch {
|
||||
if (try try_catch.err(alloc, env)) |msg| {
|
||||
defer alloc.free(msg);
|
||||
log.info("eval script {s}: {s}", .{ self.src, msg });
|
||||
}
|
||||
return FetchError.JsErr;
|
||||
};
|
||||
|
||||
if (builtin.mode == .Debug) {
|
||||
const msg = try res.toString(alloc, env);
|
||||
defer alloc.free(msg);
|
||||
log.debug("eval script {s}: {s}", .{ self.src, msg });
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
const std = @import("std");
|
||||
const Client = @import("../http/Client.zig");
|
||||
|
||||
const user_agent = "Lightpanda.io/1.0";
|
||||
const user_agent = @import("browser.zig").user_agent;
|
||||
|
||||
pub const Loader = struct {
|
||||
client: Client,
|
||||
|
||||
@@ -112,7 +112,7 @@ fn getWindowForTarget(
|
||||
const Params = struct {
|
||||
targetId: ?[]const u8 = null,
|
||||
};
|
||||
const input = try Input(Params).get(alloc, msg);
|
||||
const input = try Input(?Params).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
std.debug.assert(input.sessionId != null);
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "browser.getWindowForTarget" });
|
||||
|
||||
@@ -31,6 +31,11 @@ const emulation = @import("emulation.zig").emulation;
|
||||
const fetch = @import("fetch.zig").fetch;
|
||||
const performance = @import("performance.zig").performance;
|
||||
const IncomingMessage = @import("msg.zig").IncomingMessage;
|
||||
const Input = @import("msg.zig").Input;
|
||||
const inspector = @import("inspector.zig").inspector;
|
||||
const dom = @import("dom.zig").dom;
|
||||
const css = @import("css.zig").css;
|
||||
const security = @import("security.zig").security;
|
||||
|
||||
const log_cdp = std.log.scoped(.cdp);
|
||||
|
||||
@@ -59,9 +64,13 @@ const Domains = enum {
|
||||
Log,
|
||||
Runtime,
|
||||
Network,
|
||||
DOM,
|
||||
CSS,
|
||||
Inspector,
|
||||
Emulation,
|
||||
Fetch,
|
||||
Performance,
|
||||
Security,
|
||||
};
|
||||
|
||||
// The caller is responsible for calling `free` on the returned slice.
|
||||
@@ -69,12 +78,20 @@ pub fn do(
|
||||
alloc: std.mem.Allocator,
|
||||
s: []const u8,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
) anyerror![]const u8 {
|
||||
|
||||
// incoming message parser
|
||||
var msg = IncomingMessage.init(alloc, s);
|
||||
defer msg.deinit();
|
||||
|
||||
return dispatch(alloc, &msg, ctx);
|
||||
}
|
||||
|
||||
pub fn dispatch(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
ctx: *Ctx,
|
||||
) anyerror![]const u8 {
|
||||
const method = try msg.getMethod();
|
||||
|
||||
// retrieve domain from method
|
||||
@@ -85,21 +102,26 @@ pub fn do(
|
||||
// select corresponding domain
|
||||
const action = iter.next() orelse return error.BadMethod;
|
||||
return switch (domain) {
|
||||
.Browser => browser(alloc, &msg, action, ctx),
|
||||
.Target => target(alloc, &msg, action, ctx),
|
||||
.Page => page(alloc, &msg, action, ctx),
|
||||
.Log => log(alloc, &msg, action, ctx),
|
||||
.Runtime => runtime(alloc, &msg, action, ctx),
|
||||
.Network => network(alloc, &msg, action, ctx),
|
||||
.Emulation => emulation(alloc, &msg, action, ctx),
|
||||
.Fetch => fetch(alloc, &msg, action, ctx),
|
||||
.Performance => performance(alloc, &msg, action, ctx),
|
||||
.Browser => browser(alloc, msg, action, ctx),
|
||||
.Target => target(alloc, msg, action, ctx),
|
||||
.Page => page(alloc, msg, action, ctx),
|
||||
.Log => log(alloc, msg, action, ctx),
|
||||
.Runtime => runtime(alloc, msg, action, ctx),
|
||||
.Network => network(alloc, msg, action, ctx),
|
||||
.DOM => dom(alloc, msg, action, ctx),
|
||||
.CSS => css(alloc, msg, action, ctx),
|
||||
.Inspector => inspector(alloc, msg, action, ctx),
|
||||
.Emulation => emulation(alloc, msg, action, ctx),
|
||||
.Fetch => fetch(alloc, msg, action, ctx),
|
||||
.Performance => performance(alloc, msg, action, ctx),
|
||||
.Security => security(alloc, msg, action, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
pub const State = struct {
|
||||
executionContextId: u32 = 0,
|
||||
contextID: ?[]const u8 = null,
|
||||
sessionID: ?[]const u8 = null,
|
||||
frameID: []const u8 = FrameID,
|
||||
url: []const u8 = URLBase,
|
||||
securityOrigin: []const u8 = URLBase,
|
||||
@@ -200,11 +222,11 @@ pub fn sendEvent(
|
||||
// ------
|
||||
|
||||
// TODO: hard coded IDs
|
||||
pub const BrowserSessionID = "9559320D92474062597D9875C664CAC0";
|
||||
pub const ContextSessionID = "4FDC2CB760A23A220497A05C95417CF4";
|
||||
pub const BrowserSessionID = "BROWSERSESSIONID597D9875C664CAC0";
|
||||
pub const ContextSessionID = "CONTEXTSESSIONID0497A05C95417CF4";
|
||||
pub const URLBase = "chrome://newtab/";
|
||||
pub const FrameID = "90D14BBD8AED408A0467AC93100BCDBE";
|
||||
pub const LoaderID = "CFC8BED824DD2FD56CF1EF33C965C79C";
|
||||
pub const LoaderID = "LOADERID24DD2FD56CF1EF33C965C79C";
|
||||
pub const FrameID = "FRAMEIDD8AED408A0467AC93100BCDBE";
|
||||
|
||||
pub const TimestampEvent = struct {
|
||||
timestamp: f64,
|
||||
|
||||
59
src/cdp/css.zig
Normal file
59
src/cdp/css.zig
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (C) 2023-2024 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 std = @import("std");
|
||||
|
||||
const server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const cdp = @import("cdp.zig");
|
||||
const result = cdp.result;
|
||||
const IncomingMessage = @import("msg.zig").IncomingMessage;
|
||||
const Input = @import("msg.zig").Input;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
enable,
|
||||
};
|
||||
|
||||
pub fn css(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
action: []const u8,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
const method = std.meta.stringToEnum(Methods, action) orelse
|
||||
return error.UnknownMethod;
|
||||
|
||||
return switch (method) {
|
||||
.enable => enable(alloc, msg, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
fn enable(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const input = try Input(void).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "inspector.enable" });
|
||||
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
59
src/cdp/dom.zig
Normal file
59
src/cdp/dom.zig
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (C) 2023-2024 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 std = @import("std");
|
||||
|
||||
const server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const cdp = @import("cdp.zig");
|
||||
const result = cdp.result;
|
||||
const IncomingMessage = @import("msg.zig").IncomingMessage;
|
||||
const Input = @import("msg.zig").Input;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
enable,
|
||||
};
|
||||
|
||||
pub fn dom(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
action: []const u8,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
const method = std.meta.stringToEnum(Methods, action) orelse
|
||||
return error.UnknownMethod;
|
||||
|
||||
return switch (method) {
|
||||
.enable => enable(alloc, msg, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
fn enable(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const input = try Input(void).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "inspector.enable" });
|
||||
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
59
src/cdp/inspector.zig
Normal file
59
src/cdp/inspector.zig
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (C) 2023-2024 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 std = @import("std");
|
||||
|
||||
const server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const cdp = @import("cdp.zig");
|
||||
const result = cdp.result;
|
||||
const IncomingMessage = @import("msg.zig").IncomingMessage;
|
||||
const Input = @import("msg.zig").Input;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
enable,
|
||||
};
|
||||
|
||||
pub fn inspector(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
action: []const u8,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
const method = std.meta.stringToEnum(Methods, action) orelse
|
||||
return error.UnknownMethod;
|
||||
|
||||
return switch (method) {
|
||||
.enable => enable(alloc, msg, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
fn enable(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const input = try Input(void).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "inspector.enable" });
|
||||
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
@@ -130,7 +130,8 @@ pub const IncomingMessage = struct {
|
||||
// asking for getParams, we don't know how to parse them.
|
||||
fn scanParams(self: *IncomingMessage) !void {
|
||||
const tt = try self.scanner.peekNextTokenType();
|
||||
if (tt != .object_begin) return error.InvalidParams;
|
||||
// accept object begin or null JSON value.
|
||||
if (tt != .object_begin and tt != .null) return error.InvalidParams;
|
||||
try self.scanner.skipValue();
|
||||
self.params_skip = true;
|
||||
}
|
||||
@@ -146,10 +147,19 @@ pub const IncomingMessage = struct {
|
||||
return error.SkippedParams;
|
||||
}
|
||||
|
||||
try self.scanUntil("params");
|
||||
self.scanUntil("params") catch |err| {
|
||||
// handle nullable type
|
||||
if (@typeInfo(T) == .Optional) {
|
||||
if (err == error.InvalidToken or err == error.EndOfDocument) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return err;
|
||||
};
|
||||
|
||||
// parse "params"
|
||||
const options = std.json.ParseOptions{
|
||||
.ignore_unknown_fields = true,
|
||||
.max_value_len = self.scanner.input.len,
|
||||
.allocate = .alloc_always,
|
||||
};
|
||||
@@ -250,3 +260,34 @@ test "read incoming message with null session id" {
|
||||
try std.testing.expectEqual(1, try msg.getId());
|
||||
}
|
||||
}
|
||||
|
||||
test "message with nullable params" {
|
||||
const T = struct {
|
||||
bar: []const u8,
|
||||
};
|
||||
|
||||
// nullable type, params is present => value
|
||||
const not_null =
|
||||
\\{"id": 1,"method":"foo","params":{"bar":"baz"}}
|
||||
;
|
||||
var msg = IncomingMessage.init(std.testing.allocator, not_null);
|
||||
defer msg.deinit();
|
||||
const input = try Input(?T).get(std.testing.allocator, &msg);
|
||||
defer input.deinit();
|
||||
try std.testing.expectEqualStrings(input.params.?.bar, "baz");
|
||||
|
||||
// nullable type, params is not present => null
|
||||
const is_null =
|
||||
\\{"id": 1,"method":"foo","sessionId":"AAA"}
|
||||
;
|
||||
var msg_null = IncomingMessage.init(std.testing.allocator, is_null);
|
||||
defer msg_null.deinit();
|
||||
const input_null = try Input(?T).get(std.testing.allocator, &msg_null);
|
||||
defer input_null.deinit();
|
||||
try std.testing.expectEqual(null, input_null.params);
|
||||
try std.testing.expectEqualStrings("AAA", input_null.sessionId.?);
|
||||
|
||||
// not nullable type, params is not present => error
|
||||
const params_or_error = msg_null.getParams(std.testing.allocator, T);
|
||||
try std.testing.expectError(error.EndOfDocument, params_or_error);
|
||||
}
|
||||
|
||||
@@ -331,8 +331,9 @@ fn navigate(
|
||||
// TODO: noop event, we have no env context at this point, is it necesarry?
|
||||
try sendEvent(alloc, ctx, "Runtime.executionContextsCleared", void, {}, input.sessionId);
|
||||
|
||||
// Launch navigate
|
||||
const p = try ctx.browser.session.createPage();
|
||||
// Launch navigate, the page must have been created by a
|
||||
// target.createTarget.
|
||||
var p = ctx.browser.session.page orelse return error.NoPage;
|
||||
ctx.state.executionContextId += 1;
|
||||
const auxData = try std.fmt.allocPrint(
|
||||
alloc,
|
||||
|
||||
@@ -28,6 +28,7 @@ const result = cdp.result;
|
||||
const IncomingMessage = @import("msg.zig").IncomingMessage;
|
||||
const Input = @import("msg.zig").Input;
|
||||
const stringify = cdp.stringify;
|
||||
const target = @import("target.zig");
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
@@ -116,17 +117,25 @@ fn sendInspector(
|
||||
}
|
||||
}
|
||||
|
||||
ctx.state.sessionID = msg.sessionId;
|
||||
|
||||
// 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, msg.json.len + 1);
|
||||
defer alloc.free(buf);
|
||||
_ = std.mem.replace(u8, msg.json, "\"awaitPromise\":true", "\"awaitPromise\":false", buf);
|
||||
ctx.sendInspector(buf);
|
||||
} else {
|
||||
ctx.sendInspector(msg.json);
|
||||
if (std.mem.indexOf(u8, msg.json, "\"awaitPromise\":true")) |_| {
|
||||
const buf = try alloc.alloc(u8, msg.json.len + 1);
|
||||
defer alloc.free(buf);
|
||||
_ = std.mem.replace(u8, msg.json, "\"awaitPromise\":true", "\"awaitPromise\":false", buf);
|
||||
ctx.sendInspector(buf);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
|
||||
ctx.sendInspector(msg.json);
|
||||
|
||||
if (msg.id == null) return "";
|
||||
|
||||
return result(alloc, msg.id.?, null, null, msg.sessionId);
|
||||
}
|
||||
|
||||
pub const AuxData = struct {
|
||||
|
||||
59
src/cdp/security.zig
Normal file
59
src/cdp/security.zig
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (C) 2023-2024 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 std = @import("std");
|
||||
|
||||
const server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const cdp = @import("cdp.zig");
|
||||
const result = cdp.result;
|
||||
const IncomingMessage = @import("msg.zig").IncomingMessage;
|
||||
const Input = @import("msg.zig").Input;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
enable,
|
||||
};
|
||||
|
||||
pub fn security(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
action: []const u8,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
const method = std.meta.stringToEnum(Methods, action) orelse
|
||||
return error.UnknownMethod;
|
||||
|
||||
return switch (method) {
|
||||
.enable => enable(alloc, msg, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
fn enable(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const input = try Input(void).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "security.enable" });
|
||||
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
@@ -38,6 +38,8 @@ const Methods = enum {
|
||||
disposeBrowserContext,
|
||||
createTarget,
|
||||
closeTarget,
|
||||
sendMessageToTarget,
|
||||
detachFromTarget,
|
||||
};
|
||||
|
||||
pub fn target(
|
||||
@@ -58,13 +60,15 @@ pub fn target(
|
||||
.disposeBrowserContext => disposeBrowserContext(alloc, msg, ctx),
|
||||
.createTarget => createTarget(alloc, msg, ctx),
|
||||
.closeTarget => closeTarget(alloc, msg, ctx),
|
||||
.sendMessageToTarget => sendMessageToTarget(alloc, msg, ctx),
|
||||
.detachFromTarget => detachFromTarget(alloc, msg, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: hard coded IDs
|
||||
const PageTargetID = "CFCD6EC01573CF29BB638E9DC0F52DDC";
|
||||
const BrowserTargetID = "2d2bdef9-1c95-416f-8c0e-83f3ab73a30c";
|
||||
const BrowserContextID = "65618675CB7D3585A95049E9DFE95EA9";
|
||||
pub const PageTargetID = "PAGETARGETIDB638E9DC0F52DDC";
|
||||
pub const BrowserTargetID = "browser9-targ-et6f-id0e-83f3ab73a30c";
|
||||
pub const BrowserContextID = "BROWSERCONTEXTIDA95049E9DFE95EA9";
|
||||
|
||||
// TODO: noop method
|
||||
fn setDiscoverTargets(
|
||||
@@ -95,6 +99,19 @@ const AttachToTarget = struct {
|
||||
waitingForDebugger: bool = false,
|
||||
};
|
||||
|
||||
const TargetCreated = struct {
|
||||
sessionId: []const u8,
|
||||
targetInfo: struct {
|
||||
targetId: []const u8,
|
||||
type: []const u8 = "page",
|
||||
title: []const u8,
|
||||
url: []const u8,
|
||||
attached: bool = true,
|
||||
canAccessOpener: bool = false,
|
||||
browserContextId: []const u8,
|
||||
},
|
||||
};
|
||||
|
||||
const TargetFilter = struct {
|
||||
type: ?[]const u8 = null,
|
||||
exclude: ?bool = null,
|
||||
@@ -123,7 +140,7 @@ fn setAutoAttach(
|
||||
.sessionId = cdp.BrowserSessionID,
|
||||
.targetInfo = .{
|
||||
.targetId = PageTargetID,
|
||||
.title = "New Incognito tab",
|
||||
.title = "about:blank",
|
||||
.url = cdp.URLBase,
|
||||
.browserContextId = BrowserContextID,
|
||||
},
|
||||
@@ -156,8 +173,8 @@ fn attachToTarget(
|
||||
const attached = AttachToTarget{
|
||||
.sessionId = cdp.BrowserSessionID,
|
||||
.targetInfo = .{
|
||||
.targetId = PageTargetID,
|
||||
.title = "New Incognito tab",
|
||||
.targetId = input.params.targetId,
|
||||
.title = "about:blank",
|
||||
.url = cdp.URLBase,
|
||||
.browserContextId = BrowserContextID,
|
||||
},
|
||||
@@ -170,7 +187,7 @@ fn attachToTarget(
|
||||
sessionId: []const u8,
|
||||
};
|
||||
const output = SessionId{
|
||||
.sessionId = input.sessionId orelse BrowserContextID,
|
||||
.sessionId = input.sessionId orelse cdp.BrowserSessionID,
|
||||
};
|
||||
return result(alloc, input.id, SessionId, output, null);
|
||||
}
|
||||
@@ -184,7 +201,7 @@ fn getTargetInfo(
|
||||
const Params = struct {
|
||||
targetId: ?[]const u8 = null,
|
||||
};
|
||||
const input = try Input(Params).get(alloc, msg);
|
||||
const input = try Input(?Params).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "target.getTargetInfo" });
|
||||
|
||||
@@ -237,7 +254,7 @@ fn getBrowserContexts(
|
||||
return result(alloc, input.id, Resp, resp, null);
|
||||
}
|
||||
|
||||
const ContextID = "22648B09EDCCDD11109E2D4FEFBE4F89";
|
||||
const ContextID = "CONTEXTIDDCCDD11109E2D4FEFBE4F89";
|
||||
|
||||
// TODO: noop method
|
||||
fn createBrowserContext(
|
||||
@@ -298,8 +315,8 @@ fn disposeBrowserContext(
|
||||
}
|
||||
|
||||
// TODO: hard coded IDs
|
||||
const TargetID = "57356548460A8F29706A2ADF14316298";
|
||||
const LoaderID = "DD4A76F842AA389647D702B4D805F49A";
|
||||
const TargetID = "TARGETID460A8F29706A2ADF14316298";
|
||||
const LoaderID = "LOADERID42AA389647D702B4D805F49A";
|
||||
|
||||
fn createTarget(
|
||||
alloc: std.mem.Allocator,
|
||||
@@ -327,15 +344,46 @@ fn createTarget(
|
||||
ctx.state.securityOrigin = "://";
|
||||
ctx.state.secureContextType = "InsecureScheme";
|
||||
ctx.state.loaderID = LoaderID;
|
||||
ctx.state.sessionID = msg.sessionId;
|
||||
|
||||
// TODO stop the previous page instead?
|
||||
if (ctx.browser.session.page != null) return error.pageAlreadyExists;
|
||||
|
||||
// create the page
|
||||
const p = try ctx.browser.session.createPage();
|
||||
ctx.state.executionContextId += 1;
|
||||
// start the js env
|
||||
const auxData = try std.fmt.allocPrint(
|
||||
alloc,
|
||||
// NOTE: we assume this is the default web page
|
||||
"{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}",
|
||||
.{ctx.state.frameID},
|
||||
);
|
||||
defer alloc.free(auxData);
|
||||
try p.start(auxData);
|
||||
|
||||
// send targetCreated event
|
||||
const created = TargetCreated{
|
||||
.sessionId = cdp.ContextSessionID,
|
||||
.targetInfo = .{
|
||||
.targetId = ctx.state.frameID,
|
||||
.title = "about:blank",
|
||||
.url = ctx.state.url,
|
||||
.browserContextId = input.params.browserContextId orelse ContextID,
|
||||
.attached = true,
|
||||
},
|
||||
};
|
||||
try cdp.sendEvent(alloc, ctx, "Target.targetCreated", TargetCreated, created, input.sessionId);
|
||||
|
||||
// send attachToTarget event
|
||||
const attached = AttachToTarget{
|
||||
.sessionId = cdp.ContextSessionID,
|
||||
.targetInfo = .{
|
||||
.targetId = ctx.state.frameID,
|
||||
.title = "",
|
||||
.title = "about:blank",
|
||||
.url = ctx.state.url,
|
||||
.browserContextId = input.params.browserContextId orelse ContextID,
|
||||
.attached = true,
|
||||
},
|
||||
.waitingForDebugger = true,
|
||||
};
|
||||
@@ -410,5 +458,66 @@ fn closeTarget(
|
||||
null,
|
||||
);
|
||||
|
||||
if (ctx.browser.session.page != null) ctx.browser.session.page.?.end();
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
fn sendMessageToTarget(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const Params = struct {
|
||||
message: []const u8,
|
||||
sessionId: []const u8,
|
||||
};
|
||||
const input = try Input(Params).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s} ({s})", .{ input.id, "target.sendMessageToTarget", input.params.message });
|
||||
|
||||
// get the wrapped message.
|
||||
var wmsg = IncomingMessage.init(alloc, input.params.message);
|
||||
defer wmsg.deinit();
|
||||
|
||||
const res = cdp.dispatch(alloc, &wmsg, ctx) catch |e| {
|
||||
log.err("send message {d} ({s}): {any}", .{ input.id, input.params.message, e });
|
||||
// TODO dispatch error correctly.
|
||||
return e;
|
||||
};
|
||||
|
||||
// receivedMessageFromTarget event
|
||||
const ReceivedMessageFromTarget = struct {
|
||||
message: []const u8,
|
||||
sessionId: []const u8,
|
||||
};
|
||||
try cdp.sendEvent(
|
||||
alloc,
|
||||
ctx,
|
||||
"Target.receivedMessageFromTarget",
|
||||
ReceivedMessageFromTarget,
|
||||
.{
|
||||
.message = res,
|
||||
.sessionId = input.params.sessionId,
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
// noop
|
||||
fn detachFromTarget(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const input = try Input(void).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "target.detachFromTarget" });
|
||||
|
||||
// output
|
||||
return result(alloc, input.id, bool, true, input.sessionId);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ const EventHandler = @import("../events/event.zig").EventHandler;
|
||||
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
const Nod = @import("node.zig");
|
||||
const Window = @import("../html/window.zig").Window;
|
||||
|
||||
// EventTarget interfaces
|
||||
pub const Union = Nod.Union;
|
||||
@@ -40,9 +41,10 @@ pub const EventTarget = struct {
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
pub fn toInterface(et: *parser.EventTarget) !Union {
|
||||
// NOTE: for now we state that all EventTarget are Nodes
|
||||
// TODO: handle other types (eg. Window)
|
||||
return Nod.Node.toInterface(@as(*parser.Node, @ptrCast(et)));
|
||||
return switch (try parser.eventTargetGetType(et)) {
|
||||
.window => .{ .Window = @as(*Window, @ptrCast(et)) },
|
||||
.node => Nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))),
|
||||
};
|
||||
}
|
||||
|
||||
// JS funcs
|
||||
|
||||
@@ -49,7 +49,7 @@ pub const Stream = struct {
|
||||
}
|
||||
|
||||
fn closeCDP(self: *const Stream) void {
|
||||
const close_msg: []const u8 = .{ 5, 0 } ++ "close";
|
||||
const close_msg: []const u8 = .{ 5, 0, 0, 0 } ++ "close";
|
||||
self.recv(close_msg) catch |err| {
|
||||
log.err("stream close error: {any}", .{err});
|
||||
};
|
||||
@@ -87,7 +87,7 @@ pub const Handler = struct {
|
||||
}
|
||||
|
||||
pub fn clientMessage(self: *Handler, data: []const u8) !void {
|
||||
var header: [2]u8 = undefined;
|
||||
var header: [4]u8 = undefined;
|
||||
Msg.setSize(data.len, &header);
|
||||
try self.stream.recv(&header);
|
||||
try self.stream.recv(data);
|
||||
|
||||
128
src/html/history.zig
Normal file
128
src/html/history.zig
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright (C) 2023-2024 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 std = @import("std");
|
||||
|
||||
const builtin = @import("builtin");
|
||||
const jsruntime = @import("jsruntime");
|
||||
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
|
||||
pub const History = struct {
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
const ScrollRestorationMode = enum {
|
||||
auto,
|
||||
manual,
|
||||
};
|
||||
|
||||
scrollRestoration: ScrollRestorationMode = .auto,
|
||||
state: std.json.Value = .null,
|
||||
|
||||
// count tracks the history length until we implement correctly pushstate.
|
||||
count: u32 = 0,
|
||||
|
||||
pub fn get_length(self: *History) u32 {
|
||||
// TODO return the real history length value.
|
||||
return self.count;
|
||||
}
|
||||
|
||||
pub fn get_scrollRestoration(self: *History) []const u8 {
|
||||
return switch (self.scrollRestoration) {
|
||||
.auto => "auto",
|
||||
.manual => "manual",
|
||||
};
|
||||
}
|
||||
|
||||
pub fn set_scrollRestoration(self: *History, mode: []const u8) void {
|
||||
if (std.mem.eql(u8, "manual", mode)) self.scrollRestoration = .manual;
|
||||
if (std.mem.eql(u8, "auto", mode)) self.scrollRestoration = .auto;
|
||||
}
|
||||
|
||||
pub fn get_state(self: *History) std.json.Value {
|
||||
return self.state;
|
||||
}
|
||||
|
||||
// TODO implement the function
|
||||
// data must handle any argument. We could expect a std.json.Value but
|
||||
// https://github.com/lightpanda-io/zig-js-runtime/issues/267 is missing.
|
||||
pub fn _pushState(self: *History, data: []const u8, _: ?[]const u8, url: ?[]const u8) void {
|
||||
self.count += 1;
|
||||
_ = url;
|
||||
_ = data;
|
||||
}
|
||||
|
||||
// TODO implement the function
|
||||
// data must handle any argument. We could expect a std.json.Value but
|
||||
// https://github.com/lightpanda-io/zig-js-runtime/issues/267 is missing.
|
||||
pub fn _replaceState(self: *History, data: []const u8, _: ?[]const u8, url: ?[]const u8) void {
|
||||
_ = self;
|
||||
_ = url;
|
||||
_ = data;
|
||||
}
|
||||
|
||||
// TODO implement the function
|
||||
pub fn _go(self: *History, delta: ?i32) void {
|
||||
_ = self;
|
||||
_ = delta;
|
||||
}
|
||||
|
||||
// TODO implement the function
|
||||
pub fn _back(self: *History) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
// TODO implement the function
|
||||
pub fn _forward(self: *History) void {
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var history = [_]Case{
|
||||
.{ .src = "history.scrollRestoration", .ex = "auto" },
|
||||
.{ .src = "history.scrollRestoration = 'manual'", .ex = "manual" },
|
||||
.{ .src = "history.scrollRestoration = 'foo'", .ex = "foo" },
|
||||
.{ .src = "history.scrollRestoration", .ex = "manual" },
|
||||
.{ .src = "history.scrollRestoration = 'auto'", .ex = "auto" },
|
||||
.{ .src = "history.scrollRestoration", .ex = "auto" },
|
||||
|
||||
.{ .src = "history.state", .ex = "null" },
|
||||
|
||||
.{ .src = "history.pushState({}, null, '')", .ex = "undefined" },
|
||||
|
||||
.{ .src = "history.replaceState({}, null, '')", .ex = "undefined" },
|
||||
|
||||
.{ .src = "history.go()", .ex = "undefined" },
|
||||
.{ .src = "history.go(1)", .ex = "undefined" },
|
||||
.{ .src = "history.go(-1)", .ex = "undefined" },
|
||||
|
||||
.{ .src = "history.forward()", .ex = "undefined" },
|
||||
|
||||
.{ .src = "history.back()", .ex = "undefined" },
|
||||
};
|
||||
try checkCases(js_env, &history);
|
||||
}
|
||||
@@ -21,6 +21,8 @@ const generate = @import("../generate.zig");
|
||||
const HTMLDocument = @import("document.zig").HTMLDocument;
|
||||
const HTMLElem = @import("elements.zig");
|
||||
const Window = @import("window.zig").Window;
|
||||
const Navigator = @import("navigator.zig").Navigator;
|
||||
const History = @import("history.zig").History;
|
||||
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
HTMLDocument,
|
||||
@@ -28,4 +30,6 @@ pub const Interfaces = generate.Tuple(.{
|
||||
HTMLElem.HTMLMediaElement,
|
||||
HTMLElem.Interfaces,
|
||||
Window,
|
||||
Navigator,
|
||||
History,
|
||||
});
|
||||
|
||||
102
src/html/navigator.zig
Normal file
102
src/html/navigator.zig
Normal file
@@ -0,0 +1,102 @@
|
||||
// Copyright (C) 2023-2024 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 std = @import("std");
|
||||
|
||||
const builtin = @import("builtin");
|
||||
const jsruntime = @import("jsruntime");
|
||||
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/system-state.html#navigator
|
||||
pub const Navigator = struct {
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
agent: []const u8 = "Lightpanda/1.0",
|
||||
version: []const u8 = "1.0",
|
||||
vendor: []const u8 = "",
|
||||
platform: []const u8 = std.fmt.comptimePrint("{any} {any}", .{ builtin.os.tag, builtin.cpu.arch }),
|
||||
|
||||
language: []const u8 = "en-US",
|
||||
|
||||
pub fn get_userAgent(self: *Navigator) []const u8 {
|
||||
return self.agent;
|
||||
}
|
||||
pub fn get_appCodeName(_: *Navigator) []const u8 {
|
||||
return "Mozilla";
|
||||
}
|
||||
pub fn get_appName(_: *Navigator) []const u8 {
|
||||
return "Netscape";
|
||||
}
|
||||
pub fn get_appVersion(self: *Navigator) []const u8 {
|
||||
return self.version;
|
||||
}
|
||||
pub fn get_platform(self: *Navigator) []const u8 {
|
||||
return self.platform;
|
||||
}
|
||||
pub fn get_product(_: *Navigator) []const u8 {
|
||||
return "Gecko";
|
||||
}
|
||||
pub fn get_productSub(_: *Navigator) []const u8 {
|
||||
return "20030107";
|
||||
}
|
||||
pub fn get_vendor(self: *Navigator) []const u8 {
|
||||
return self.vendor;
|
||||
}
|
||||
pub fn get_vendorSub(_: *Navigator) []const u8 {
|
||||
return "";
|
||||
}
|
||||
pub fn get_language(self: *Navigator) []const u8 {
|
||||
return self.language;
|
||||
}
|
||||
// TODO wait for arrays.
|
||||
//pub fn get_languages(self: *Navigator) [][]const u8 {
|
||||
// return .{self.language};
|
||||
//}
|
||||
pub fn get_online(_: *Navigator) bool {
|
||||
return true;
|
||||
}
|
||||
pub fn _registerProtocolHandler(_: *Navigator, scheme: []const u8, url: []const u8) void {
|
||||
_ = scheme;
|
||||
_ = url;
|
||||
}
|
||||
pub fn _unregisterProtocolHandler(_: *Navigator, scheme: []const u8, url: []const u8) void {
|
||||
_ = scheme;
|
||||
_ = url;
|
||||
}
|
||||
|
||||
pub fn get_cookieEnabled(_: *Navigator) bool {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var navigator = [_]Case{
|
||||
.{ .src = "navigator.userAgent", .ex = "Lightpanda/1.0" },
|
||||
.{ .src = "navigator.appVersion", .ex = "1.0" },
|
||||
.{ .src = "navigator.language", .ex = "en-US" },
|
||||
};
|
||||
try checkCases(js_env, &navigator);
|
||||
}
|
||||
@@ -25,6 +25,8 @@ const CallbackArg = jsruntime.CallbackArg;
|
||||
const Loop = jsruntime.Loop;
|
||||
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const Navigator = @import("navigator.zig").Navigator;
|
||||
const History = @import("history.zig").History;
|
||||
|
||||
const storage = @import("../storage/storage.zig");
|
||||
|
||||
@@ -40,6 +42,7 @@ pub const Window = struct {
|
||||
|
||||
document: ?*parser.DocumentHTML = null,
|
||||
target: []const u8,
|
||||
history: History = .{},
|
||||
|
||||
storageShelf: ?*storage.Shelf = null,
|
||||
|
||||
@@ -48,9 +51,13 @@ pub const Window = struct {
|
||||
timeoutid: u32 = 0,
|
||||
timeoutids: [512]u64 = undefined,
|
||||
|
||||
pub fn create(target: ?[]const u8) Window {
|
||||
navigator: Navigator,
|
||||
|
||||
pub fn create(target: ?[]const u8, navigator: ?Navigator) Window {
|
||||
return Window{
|
||||
.target = target orelse "",
|
||||
.navigator = navigator orelse .{},
|
||||
.base = .{ .et_type = @intFromEnum(parser.EventTargetType.window) },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,6 +73,10 @@ pub const Window = struct {
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn get_navigator(self: *Window) *Navigator {
|
||||
return &self.navigator;
|
||||
}
|
||||
|
||||
pub fn get_self(self: *Window) *Window {
|
||||
return self;
|
||||
}
|
||||
@@ -78,6 +89,10 @@ pub const Window = struct {
|
||||
return self.document;
|
||||
}
|
||||
|
||||
pub fn get_history(self: *Window) *History {
|
||||
return &self.history;
|
||||
}
|
||||
|
||||
pub fn get_name(self: *Window) []const u8 {
|
||||
return self.target;
|
||||
}
|
||||
|
||||
30
src/main.zig
30
src/main.zig
@@ -40,6 +40,14 @@ pub const websocket_blocking = true;
|
||||
|
||||
const log = std.log.scoped(.cli);
|
||||
|
||||
pub const std_options = .{
|
||||
// Set the log level to info
|
||||
.log_level = .debug,
|
||||
|
||||
// Define logFn to override the std implementation
|
||||
.logFn = logFn,
|
||||
};
|
||||
|
||||
const usage =
|
||||
\\usage: {s} [options] [URL]
|
||||
\\
|
||||
@@ -49,6 +57,7 @@ const usage =
|
||||
\\ * otherwhise the browser starts a CDP server
|
||||
\\
|
||||
\\ -h, --help Print this help message and exit.
|
||||
\\ --verbose Display all logs. By default only info, warn and err levels are displayed.
|
||||
\\ --host Host of the CDP server (default "127.0.0.1")
|
||||
\\ --port Port of the CDP server (default "9222")
|
||||
\\ --timeout Timeout for incoming connections of the CDP server (in seconds, default "3")
|
||||
@@ -110,6 +119,10 @@ const CliMode = union(CliModeTag) {
|
||||
if (std.mem.eql(u8, "-h", opt) or std.mem.eql(u8, "--help", opt)) {
|
||||
return printUsageExit(execname, 0);
|
||||
}
|
||||
if (std.mem.eql(u8, "--verbose", opt)) {
|
||||
verbose = true;
|
||||
continue;
|
||||
}
|
||||
if (std.mem.eql(u8, "--dump", opt)) {
|
||||
_fetch.dump = true;
|
||||
continue;
|
||||
@@ -313,6 +326,8 @@ pub fn main() !void {
|
||||
|
||||
// page
|
||||
const page = try browser.session.createPage();
|
||||
try page.start(null);
|
||||
defer page.end();
|
||||
|
||||
_ = page.navigate(opts.url, null) catch |err| switch (err) {
|
||||
error.UnsupportedUriScheme, error.UriMissingHost => {
|
||||
@@ -334,3 +349,18 @@ pub fn main() !void {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var verbose: bool = builtin.mode == .Debug; // In debug mode, force verbose.
|
||||
fn logFn(
|
||||
comptime level: std.log.Level,
|
||||
comptime scope: @Type(.EnumLiteral),
|
||||
comptime format: []const u8,
|
||||
args: anytype,
|
||||
) void {
|
||||
if (!verbose) {
|
||||
// hide all messages with level greater of equal to debug level.
|
||||
if (@intFromEnum(level) >= @intFromEnum(std.log.Level.debug)) return;
|
||||
}
|
||||
// default std log function.
|
||||
std.log.defaultLog(level, scope, format, args);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ fn execJS(
|
||||
defer storageShelf.deinit();
|
||||
|
||||
// alias global as self and window
|
||||
var window = Window.create(null);
|
||||
var window = Window.create(null, null);
|
||||
window.replaceDocument(doc);
|
||||
window.setStorageShelf(&storageShelf);
|
||||
try js_env.bindGlobal(window);
|
||||
|
||||
31
src/msg.zig
31
src/msg.zig
@@ -18,17 +18,20 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
pub const MsgSize = 16 * 1204; // 16KB
|
||||
pub const HeaderSize = 2;
|
||||
pub const HeaderSize = 4;
|
||||
pub const MsgSize = 256 * 1204; // 256KB
|
||||
// NOTE: Theorically we could go up to 4GB with a 4 bytes binary encoding
|
||||
// but we prefer to put a lower hard limit for obvious memory size reasons.
|
||||
|
||||
pub const MaxSize = HeaderSize + MsgSize;
|
||||
|
||||
pub const Msg = struct {
|
||||
pub fn getSize(data: []const u8) usize {
|
||||
return std.mem.readInt(u16, data[0..HeaderSize], .little);
|
||||
return std.mem.readInt(u32, data[0..HeaderSize], .little);
|
||||
}
|
||||
|
||||
pub fn setSize(len: usize, header: *[2]u8) void {
|
||||
std.mem.writeInt(u16, header, @intCast(len), .little);
|
||||
pub fn setSize(len: usize, header: *[4]u8) void {
|
||||
std.mem.writeInt(u32, header, @intCast(len), .little);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -121,26 +124,26 @@ test "Buffer" {
|
||||
|
||||
const cases = [_]Case{
|
||||
// simple
|
||||
.{ .input = .{ 2, 0 } ++ "ok", .nb = 1 },
|
||||
.{ .input = .{ 2, 0, 0, 0 } ++ "ok", .nb = 1 },
|
||||
// combined
|
||||
.{ .input = .{ 2, 0 } ++ "ok" ++ .{ 3, 0 } ++ "foo", .nb = 2 },
|
||||
.{ .input = .{ 2, 0, 0, 0 } ++ "ok" ++ .{ 3, 0, 0, 0 } ++ "foo", .nb = 2 },
|
||||
// multipart
|
||||
.{ .input = .{ 9, 0 } ++ "multi", .nb = 0 },
|
||||
.{ .input = .{ 9, 0, 0, 0 } ++ "multi", .nb = 0 },
|
||||
.{ .input = "part", .nb = 1 },
|
||||
// multipart & combined
|
||||
.{ .input = .{ 9, 0 } ++ "multi", .nb = 0 },
|
||||
.{ .input = "part" ++ .{ 2, 0 } ++ "ok", .nb = 2 },
|
||||
.{ .input = .{ 9, 0, 0, 0 } ++ "multi", .nb = 0 },
|
||||
.{ .input = "part" ++ .{ 2, 0, 0, 0 } ++ "ok", .nb = 2 },
|
||||
// multipart & combined with other multipart
|
||||
.{ .input = .{ 9, 0 } ++ "multi", .nb = 0 },
|
||||
.{ .input = "part" ++ .{ 8, 0 } ++ "co", .nb = 1 },
|
||||
.{ .input = .{ 9, 0, 0, 0 } ++ "multi", .nb = 0 },
|
||||
.{ .input = "part" ++ .{ 8, 0, 0, 0 } ++ "co", .nb = 1 },
|
||||
.{ .input = "mbined", .nb = 1 },
|
||||
// several multipart
|
||||
.{ .input = .{ 23, 0 } ++ "multi", .nb = 0 },
|
||||
.{ .input = .{ 23, 0, 0, 0 } ++ "multi", .nb = 0 },
|
||||
.{ .input = "several", .nb = 0 },
|
||||
.{ .input = "complex", .nb = 0 },
|
||||
.{ .input = "part", .nb = 1 },
|
||||
// combined & multipart
|
||||
.{ .input = .{ 2, 0 } ++ "ok" ++ .{ 9, 0 } ++ "multi", .nb = 1 },
|
||||
.{ .input = .{ 2, 0, 0, 0 } ++ "ok" ++ .{ 9, 0, 0, 0 } ++ "multi", .nb = 1 },
|
||||
.{ .input = "part", .nb = 1 },
|
||||
};
|
||||
|
||||
|
||||
@@ -560,6 +560,11 @@ fn eventListenerGetData(lst: *EventListener) ?*anyopaque {
|
||||
}
|
||||
|
||||
// EventTarget
|
||||
pub const EventTargetType = enum(u4) {
|
||||
node = c.DOM_EVENT_TARGET_NODE,
|
||||
window = 2,
|
||||
};
|
||||
|
||||
pub const EventTarget = c.dom_event_target;
|
||||
|
||||
pub fn eventTargetToNode(et: *EventTarget) *Node {
|
||||
@@ -801,6 +806,14 @@ pub fn eventTargetDispatchEvent(et: *EventTarget, event: *Event) !bool {
|
||||
return res;
|
||||
}
|
||||
|
||||
pub fn eventTargetGetType(et: *EventTarget) !EventTargetType {
|
||||
var res: c.dom_event_target_type = undefined;
|
||||
const err = eventTargetVtable(et).dom_event_target_get_type.?(et, &res);
|
||||
try DOMErr(err);
|
||||
|
||||
return @enumFromInt(res);
|
||||
}
|
||||
|
||||
pub fn eventTargetTBaseFieldName(comptime T: type) ?[]const u8 {
|
||||
std.debug.assert(@inComptime());
|
||||
switch (@typeInfo(T)) {
|
||||
@@ -824,8 +837,10 @@ pub const EventTargetTBase = extern struct {
|
||||
.remove_event_listener = remove_event_listener,
|
||||
.add_event_listener = add_event_listener,
|
||||
.iter_event_listener = iter_event_listener,
|
||||
.dom_event_target_get_type = dom_event_target_get_type,
|
||||
},
|
||||
eti: c.dom_event_target_internal = c.dom_event_target_internal{ .listeners = null },
|
||||
et_type: c.dom_event_target_type = @intFromEnum(EventTargetType.node),
|
||||
|
||||
pub fn add_event_listener(et: [*c]c.dom_event_target, t: [*c]c.dom_string, l: ?*c.struct_dom_event_listener, capture: bool) callconv(.C) c.dom_exception {
|
||||
const self = @as(*Self, @ptrCast(et));
|
||||
@@ -858,6 +873,17 @@ pub const EventTargetTBase = extern struct {
|
||||
const self = @as(*Self, @ptrCast(et));
|
||||
return c._dom_event_target_iter_event_listener(self.eti, t, capture, cur, next, l);
|
||||
}
|
||||
|
||||
pub fn dom_event_target_get_type(
|
||||
et: [*c]c.dom_event_target,
|
||||
res: [*c]c.dom_event_target_type,
|
||||
) callconv(.C) c.dom_exception {
|
||||
const self = @as(*Self, @ptrCast(et));
|
||||
|
||||
res.* = self.et_type;
|
||||
|
||||
return c.DOM_NO_ERR;
|
||||
}
|
||||
};
|
||||
|
||||
// NodeType
|
||||
|
||||
@@ -96,7 +96,7 @@ fn testExecFn(
|
||||
});
|
||||
|
||||
// alias global as self and window
|
||||
var window = Window.create(null);
|
||||
var window = Window.create(null, null);
|
||||
|
||||
window.replaceDocument(doc);
|
||||
window.setStorageShelf(&storageShelf);
|
||||
@@ -137,6 +137,8 @@ fn testsAllExecFn(
|
||||
HTMLElementTestExecFn,
|
||||
MutationObserverTestExecFn,
|
||||
@import("polyfill/fetch.zig").testExecFn,
|
||||
@import("html/navigator.zig").testExecFn,
|
||||
@import("html/history.zig").testExecFn,
|
||||
};
|
||||
|
||||
inline for (testFns) |testFn| {
|
||||
@@ -359,7 +361,7 @@ test "bug document html parsing #4" {
|
||||
}
|
||||
|
||||
test "Window is a libdom event target" {
|
||||
var window = Window.create(null);
|
||||
var window = Window.create(null, null);
|
||||
|
||||
const event = try parser.eventCreate();
|
||||
try parser.eventInit(event, "foo", .{});
|
||||
|
||||
@@ -175,6 +175,7 @@ pub const Ctx = struct {
|
||||
self.do(parts.msg) catch |err| {
|
||||
if (err != error.Closed) {
|
||||
log.err("do error: {any}", .{err});
|
||||
log.debug("last msg: {s}", .{parts.msg});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -347,7 +348,7 @@ pub const Ctx = struct {
|
||||
const s = try std.fmt.allocPrint(
|
||||
allocator,
|
||||
tpl,
|
||||
.{ msg_open, cdp.ContextSessionID },
|
||||
.{ msg_open, ctx.state.sessionID orelse cdp.ContextSessionID },
|
||||
);
|
||||
|
||||
try ctx.send(s);
|
||||
|
||||
@@ -173,7 +173,7 @@ pub const Bottle = struct {
|
||||
// > context of another document.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event
|
||||
//
|
||||
// So for now, we won't impement the feature.
|
||||
// So for now, we won't implement the feature.
|
||||
}
|
||||
|
||||
pub fn _removeItem(self: *Bottle, k: []const u8) !void {
|
||||
|
||||
@@ -90,7 +90,7 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const
|
||||
}
|
||||
|
||||
// setup global env vars.
|
||||
var window = Window.create(null);
|
||||
var window = Window.create(null, null);
|
||||
window.replaceDocument(html_doc);
|
||||
window.setStorageShelf(&storageShelf);
|
||||
try js_env.bindGlobal(&window);
|
||||
|
||||
Submodule tests/wpt updated: e0a721a3b8...ea16a1e361
2
vendor/netsurf/libdom
vendored
2
vendor/netsurf/libdom
vendored
Submodule vendor/netsurf/libdom updated: 279398bebb...fe6c9b8d40
2
vendor/zig-js-runtime
vendored
2
vendor/zig-js-runtime
vendored
Submodule vendor/zig-js-runtime updated: a244e75fd1...1b1b3431ff
Reference in New Issue
Block a user