105 Commits

Author SHA1 Message Date
Pierre Tachoire
02cd5e98f9 event_target: handle window target 2025-01-21 13:50:34 +01:00
Pierre Tachoire
a51c20068f netsurf: handle event target get type 2025-01-21 13:50:34 +01:00
Pierre Tachoire
99a3e4be3f upgrade vendor/netsurf/libdom 2025-01-21 13:50:33 +01:00
Pierre Tachoire
40c9355088 Merge pull request #355 from lightpanda-io/history
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
zig-test / demo-puppeteer (push) Has been cancelled
dom: history placeholder
2025-01-15 11:55:50 +01:00
Pierre Tachoire
8f1557254a typo fix 2025-01-15 11:45:30 +01:00
Pierre Tachoire
11d28b0bc3 dom: add placeholder for history interface 2025-01-15 11:45:30 +01:00
Pierre Tachoire
974cf780c0 dom: clean history file 2025-01-14 15:04:40 +01:00
Pierre Tachoire
73bb14e4a9 Merge pull request #285 from lightpanda-io/cdp-cdpcli
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
zig-test / demo-puppeteer (push) Has been cancelled
cdp: cdpcli compatibility
2025-01-13 18:16:40 +01:00
Pierre Tachoire
daf4236023 runtime: fix sessionid 2025-01-13 18:08:09 +01:00
Pierre Tachoire
4c9a24c64e start inspector when the js env starts 2025-01-13 10:53:38 +01:00
Pierre Tachoire
c149f65158 cdp: remove event dispateched by inspector 2025-01-13 10:53:36 +01:00
Pierre Tachoire
c5688c1bd3 cdp: display last message on cdp error 2025-01-13 10:53:35 +01:00
Pierre Tachoire
b276a15786 cdp: add target.detachFromTarget noop 2025-01-13 10:53:33 +01:00
Pierre Tachoire
2fed239ece browser: split page start from page navigate 2025-01-13 10:53:29 +01:00
Pierre Tachoire
8e2cb36597 cdp: fix some id inconsitency accross runtime messages 2025-01-13 10:49:48 +01:00
Pierre Tachoire
bcaace1c91 cdp: use identifiable hard coded ids 2025-01-13 10:47:51 +01:00
Pierre Tachoire
d664d07141 cdp: dispatch executionContextCreated on Runtime.enable 2025-01-13 10:47:42 +01:00
Pierre Tachoire
cb8b80c856 Merge pull request #345 from lightpanda-io/modules
browser: support for modules
2025-01-13 10:45:31 +01:00
Pierre Tachoire
d777d77b06 ci: update sig-v8 version 2025-01-13 10:36:52 +01:00
Pierre Tachoire
43678f8dc0 upgrade zig-js-runtime 2025-01-13 10:36:34 +01:00
Pierre Tachoire
5811577824 Merge pull request #354 from lightpanda-io/navigator-fix
Some checks are pending
wpt / web platform tests (push) Waiting to run
wpt / perf-fmt (push) Blocked by required conditions
zig-test / zig build dev (push) Waiting to run
zig-test / zig build release (push) Waiting to run
zig-test / zig test (push) Waiting to run
zig-test / perf-fmt (push) Blocked by required conditions
zig-test / demo-puppeteer (push) Blocked by required conditions
navigator: remove useless import
2025-01-13 09:37:14 +01:00
Pierre Tachoire
1587122efa navigator: remove useless import 2025-01-10 17:42:20 +01:00
Pierre Tachoire
48e7c8ad0f browser: implement fetch module 2025-01-10 16:48:45 +01:00
Pierre Tachoire
766f9798f6 browser: load module 2025-01-09 11:48:39 +01:00
Pierre Tachoire
680d634725 update zig-js-runtime 2025-01-09 11:48:39 +01:00
Pierre Tachoire
7ac945bf88 browser: refacto script 2025-01-09 11:48:34 +01:00
Pierre Tachoire
188d7a8558 Merge pull request #352 from utay/fix-shell-main
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
zig-test / demo-puppeteer (push) Has been cancelled
Fix shell main
2025-01-08 17:15:18 +01:00
Pierre Tachoire
ee561b0d1e Merge pull request #353 from lightpanda-io/cdp-enable-security
Cdp enable security
2025-01-08 16:55:31 +01:00
Pierre Tachoire
82bbe78e95 cdp: return correct result on Runtime 2025-01-08 16:46:05 +01:00
Pierre Tachoire
c761cd059b cdp: log errors on sendMessageToTarget 2025-01-08 16:17:29 +01:00
Pierre Tachoire
03e87155ca cdp: add security.enable 2025-01-08 16:17:20 +01:00
Yannick Utard
ea39cc52b1 Fix shell main 2025-01-08 15:28:19 +01:00
Pierre Tachoire
90fb90b186 Merge pull request #351 from lightpanda-io/ignore-blank
Some checks are pending
wpt / web platform tests (push) Waiting to run
wpt / perf-fmt (push) Blocked by required conditions
zig-test / zig build dev (push) Waiting to run
zig-test / zig build release (push) Waiting to run
zig-test / zig test (push) Waiting to run
zig-test / perf-fmt (push) Blocked by required conditions
zig-test / demo-puppeteer (push) Blocked by required conditions
browser: ignore about:blank navigation
2025-01-08 13:54:00 +01:00
Pierre Tachoire
5f8327eaf7 browser: ignore about:blank navigation 2025-01-08 13:44:41 +01:00
Pierre Tachoire
07869f3c48 Merge pull request #350 from lightpanda-io/cdp-enable
Cdp enable
2025-01-08 13:42:42 +01:00
Pierre Tachoire
8377eb02a5 cdp: add CSS.enable 2025-01-08 12:01:18 +01:00
Pierre Tachoire
3738e8eb44 cdp: add DOM.enable 2025-01-08 12:01:18 +01:00
Pierre Tachoire
4b000e44b3 cdp: add Inspector.enable 2025-01-08 12:01:18 +01:00
Pierre Tachoire
d6021d1702 Merge pull request #348 from lightpanda-io/dom-navigator
Dom navigator
2025-01-08 11:47:44 +01:00
Pierre Tachoire
b391eafc38 wpt: update tests 2025-01-08 11:38:11 +01:00
Pierre Tachoire
2af4545e10 dom: implement more navigator API 2025-01-08 11:03:26 +01:00
Pierre Tachoire
b96644d893 dom: implement navigatorLanguage 2025-01-08 11:03:26 +01:00
Pierre Tachoire
3cb77c0a32 dom: implement navigatorID 2025-01-08 11:03:25 +01:00
Pierre Tachoire
b3f7fb7be3 dom: implement navigator.userAgent 2025-01-08 11:03:11 +01:00
Pierre Tachoire
9fb51a1f29 Merge pull request #346 from lightpanda-io/target-created
cdp: add TargetCreated event on createTarget message
2025-01-08 10:10:18 +01:00
Pierre Tachoire
d78e8a725d cdp: remove useless parameter 2025-01-08 09:57:28 +01:00
Pierre Tachoire
7829bdbc95 Merge pull request #347 from lightpanda-io/send-message-to-target
cdp: add Target.sendMessageToTarget support
2025-01-07 16:21:41 +01:00
Pierre Tachoire
90ba6deba2 cdp: add Target.sendMessageToTarget support
see https://chromedevtools.github.io/devtools-protocol/tot/Target/#method-sendMessageToTarget
2025-01-07 16:00:44 +01:00
Pierre Tachoire
5fc763a738 cdp: add TargetCreated event on createTarget message 2025-01-07 10:34:47 +01:00
Pierre Tachoire
84614e903c Merge pull request #344 from lightpanda-io/test-e2e
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
zig-test / demo-puppeteer (push) Has been cancelled
Add end to end tests
2025-01-06 10:13:51 +01:00
Pierre Tachoire
c95d739347 ci: add test end to end
using puppeteer test from https://github.com/lightpanda-io/demo
2024-12-31 12:01:27 +01:00
Pierre Tachoire
c55849cef5 Merge pull request #343 from lightpanda-io/upgrade-zig-js
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
upgrade zig-js-runtime deps
2024-12-31 10:37:22 +01:00
Pierre Tachoire
88adb09417 upgrade zig-js-runtime deps 2024-12-31 10:09:48 +01:00
Pierre Tachoire
cb356ffca9 Merge pull request #341 from lightpanda-io/docker
fix docker port
2024-12-26 10:02:54 +01:00
Pierre Tachoire
2ba3f252ee fix docker port 2024-12-26 10:00:59 +01:00
Pierre Tachoire
ebe2c8e3dd Merge pull request #332 from lightpanda-io/verbose-option
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
add --verbose option
2024-12-17 09:41:58 +01:00
Francis Bouvier
ca2d63edcf Merge pull request #330 from lightpanda-io/contributing
add a CONTRIBUTING file
2024-12-16 15:54:17 +01:00
Francis Bouvier
489a1a1c37 Merge pull request #338 from lightpanda-io/README
README: be more precise on current status
2024-12-16 13:43:45 +01:00
Francis Bouvier
5cad13eea3 README: be more precise on current status
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-12-16 13:35:24 +01:00
Pierre Tachoire
a3e09e015c Merge pull request #329 from lightpanda-io/readme-getting-started
readme: add a quick start section
2024-12-16 09:34:10 +01:00
Pierre Tachoire
22a6accac1 readme: add a quick start section 2024-12-13 17:45:30 +01:00
Pierre Tachoire
277c97a959 Merge pull request #331 from lightpanda-io/continue-on-error
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
browser: don't stop processing page on error status code
2024-12-13 17:41:34 +01:00
Pierre Tachoire
c08bc3239a Merge pull request #334 from lightpanda-io/patch-1
Update README.md
2024-12-13 15:05:34 +01:00
katie-lpd
f798c7e0fb Update README.md 2024-12-13 14:45:02 +01:00
Pierre Tachoire
193780a88f cla: add katie-lpd to the allow list
Some checks are pending
wpt / web platform tests (push) Waiting to run
wpt / perf-fmt (push) Blocked by required conditions
zig-test / zig build dev (push) Waiting to run
zig-test / zig build release (push) Waiting to run
zig-test / zig test (push) Waiting to run
zig-test / perf-fmt (push) Blocked by required conditions
2024-12-13 14:33:57 +01:00
Pierre Tachoire
ab4973ab6c add --verbose option 2024-12-13 12:29:05 +01:00
Pierre Tachoire
f9da815e8f browser: don't stop processing page on error status code 2024-12-13 11:56:18 +01:00
Pierre Tachoire
59e187d59a add a CONTRIBUTING file 2024-12-13 11:28:14 +01:00
Francis Bouvier
3b018e2a6d Merge pull request #325 from lightpanda-io/await_promise
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
cdp: runtime, replace `"awaitPromise":true` only if present
2024-12-09 11:19:45 +01:00
Francis Bouvier
d4939c8260 Merge pull request #322 from lightpanda-io/cdp_msg_nullable_params
Some checks are pending
wpt / web platform tests (push) Waiting to run
wpt / perf-fmt (push) Blocked by required conditions
zig-test / zig build dev (push) Waiting to run
zig-test / zig build release (push) Waiting to run
zig-test / zig test (push) Waiting to run
zig-test / perf-fmt (push) Blocked by required conditions
cdp: handle nullable Type for params
2024-12-08 19:08:27 +01:00
Francis Bouvier
485352594c Merge pull request #324 from lightpanda-io/json_parse_unknown_field
cdp: do not throw error on json parse for unknown fields
2024-12-08 19:05:11 +01:00
Francis Bouvier
bcdcfee467 Merge pull request #323 from lightpanda-io/cdp_msg_size
Cdp msg size
2024-12-08 18:58:14 +01:00
Francis Bouvier
0217e3fcae cdp: runtime, replace "awaitPromise":true only if present
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-12-08 15:33:32 +01:00
Francis Bouvier
6e7d6421d5 cdp: do not throw error on json parse for unknown fields
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-12-08 15:27:12 +01:00
Francis Bouvier
913d3af938 cdp: increase msg size 16KB -> 256KB
And move header size encoding from 2 bytes -> 2 bytes

Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-12-04 23:06:55 +01:00
Francis Bouvier
53dd0a5e4c cdp: handle nullable Type for params
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-12-04 22:39:54 +01:00
Francis Bouvier
4b8c3cb188 Merge pull request #321 from lightpanda-io/README
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
README: more updates
2024-12-04 17:16:01 +01:00
Francis Bouvier
c9ca170d57 README: more updates
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-12-04 16:18:15 +01:00
Francis Bouvier
c6090dbc16 Update README
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-12-04 16:03:01 +01:00
Francis Bouvier
c787b6a1a5 Merge pull request #312 from lightpanda-io/fetch-polyfill
Fetch polyfill
2024-12-04 15:57:45 +01:00
Francis Bouvier
766aa0f60a Merge pull request #320 from lightpanda-io/http_json_version
websockets: add addr server info in Stream
2024-12-04 15:56:06 +01:00
Pierre Tachoire
fbe8835626 polyfill: use @embedfile to embed polyfill 2024-12-04 14:26:51 +01:00
Pierre Tachoire
4e2b35b585 tests: remove useless test file 2024-12-04 14:26:21 +01:00
Francis Bouvier
8ef79e348c websockets: add addr server info in Stream
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-12-04 12:07:52 +01:00
Pierre Tachoire
47eef392d1 add missing license header 2024-12-03 14:34:04 +01:00
Francis Bouvier
b846541ff6 Merge pull request #311 from lightpanda-io/msg_size_encode
Some checks are pending
wpt / web platform tests (push) Waiting to run
wpt / perf-fmt (push) Blocked by required conditions
zig-test / zig build dev (push) Waiting to run
zig-test / zig build release (push) Waiting to run
zig-test / zig test (push) Waiting to run
zig-test / perf-fmt (push) Blocked by required conditions
Msg size encode
2024-12-03 14:33:33 +01:00
Pierre Tachoire
adfffd2b08 polyfill: fetch: disable Arraybuffer usage 2024-12-02 18:00:32 +01:00
Pierre Tachoire
4138c6fe95 polyfill: first draft to polyfill fetch api 2024-12-02 18:00:32 +01:00
Francis Bouvier
3088c7a632 Merge pull request #318 from lightpanda-io/README
Update README
2024-11-29 18:41:10 +01:00
Francis Bouvier
21e7638ae1 Update README
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-29 18:40:26 +01:00
Francis Bouvier
ca0d90dbcf Merge pull request #310 from lightpanda-io/websockets
websocket: first implementation
2024-11-29 18:22:05 +01:00
Pierre Tachoire
8870045b0f Merge pull request #317 from lightpanda-io/upgrade-zig-async-io
upgrade zig-async-io
2024-11-29 15:22:13 +01:00
Pierre Tachoire
cbc8b2edf9 upgrade zig-async-io 2024-11-29 15:12:13 +01:00
Francis Bouvier
8f297b83c1 msg: rename MsgBuffer -> Buffer
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-29 15:07:52 +01:00
Francis Bouvier
b800d0eeb8 msg: fix len for msg.Buffer and encode msg size as binary header
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-29 15:07:52 +01:00
Francis Bouvier
d95462073a websockets: fix port default in help
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-29 15:06:22 +01:00
Francis Bouvier
95ac92b343 server: fix cancel
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-29 14:59:54 +01:00
Pierre Tachoire
4cfb317af6 Merge pull request #316 from lightpanda-io/innerText
dom: implement innerText
2024-11-28 17:55:14 +01:00
Pierre Tachoire
487aaffa94 dom: implement innerText 2024-11-28 17:47:41 +01:00
Pierre Tachoire
24fd7c7286 Merge pull request #314 from lightpanda-io/fix-xhr
xhr: fix invalid response with empty type
2024-11-28 16:34:30 +01:00
Pierre Tachoire
50e62d44ff xhr: fix invalid response with empty type 2024-11-28 16:29:09 +01:00
Francis Bouvier
760c082757 cli: wording mode -> opts
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-27 21:24:09 +01:00
Francis Bouvier
8449d5ab22 websocket: use Unix socket for internal server
And add an option for TCP only server

Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-27 21:24:09 +01:00
Francis Bouvier
27b50c46c3 Update websokets dep
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-27 21:24:09 +01:00
Francis Bouvier
325ecedf0b websocket: first implementation
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-27 21:24:09 +01:00
45 changed files with 2194 additions and 400 deletions

View File

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

View File

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

View File

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

4
.gitmodules vendored
View File

@@ -28,3 +28,7 @@
[submodule "vendor/zig-async-io"]
path = vendor/zig-async-io
url = git@github.com:lightpanda-io/zig-async-io.git
[submodule "vendor/websocket.zig"]
path = vendor/websocket.zig
url = git@github.com:lightpanda-io/websocket.zig.git
branch = lightpanda

10
CONTRIBUTING.md Normal file
View 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).

View File

@@ -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"]

View File

@@ -11,6 +11,7 @@ The following files are licensed under MIT:
```
src/http/Client.zig
src/polyfill/fetch.js
```
The following directories and their subdirectories are licensed under their

112
README.md
View File

@@ -2,7 +2,9 @@
<a href="https://lightpanda.io"><img src="https://cdn.lightpanda.io/assets/images/logo/lpd-logo.png" alt="Logo" height=170></a>
</p>
<h1 align="center">Lightpanda</h1>
<h1 align="center">Lightpanda Browser</h1>
<p align="center"><a href="https://lightpanda.io/">lightpanda.io</a></p>
<div align="center">
<br />
@@ -11,15 +13,15 @@
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 (12x less than Chrome)
- Blazingly fast & instant startup (64x faster than Chrome)
- Ultra-low memory footprint (9x less than Chrome)
- Exceptionally fast execution (11x faster than Chrome) & instant startup
<img width=500px src="https://cdn.lightpanda.io/assets/images/benchmark2.png">
<img width=500px src="https://cdn.lightpanda.io/assets/images/benchmark_2024-12-04.png">
See [benchmark details](https://github.com/lightpanda-io/demo).
@@ -27,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. Its 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. Its 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. Lets take a huge desktop application, hack it, and run it on the server, right? Hundreds of instance of Chrome if you use it at scale. Are you sure its 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 its such a good idea?
- Heavy on RAM and CPU, expensive to run
- Hard to package, deploy and maintain at scale
@@ -42,37 +44,103 @@ So if we need Javascript, why not use a real web browser. Lets 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 thats 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 thats we did:
- Not based on Chromium, Blink or WebKit
- Low-level system programming language (Zig) with optimisations in mind
- Opinionated, no rendering
- Opinionated: without graphical rendering
## Status
Lightpanda is still a work in progress and is currently at the Alpha stage.
Lightpanda is still a work in progress and is currently at a Beta stage.
Here are the key features we want to implement before releasing a Beta version:
:warning: You should expect most websites to fail or crash.
- [x] Loader
- [x] HTML parser and DOM tree
- [x] Javascript support
Here are the key features we have implemented:
- [x] HTTP loader
- [x] HTML parser and DOM tree (based on Netsurf libs)
- [x] Javascript support (v8)
- [x] Basic DOM APIs
- [x] Ajax
- [x] XHR API
- [ ] Fetch API
- [x] Fetch API
- [x] DOM dump
- [ ] Basic CDP server
- [x] Basic CDP/websockets server
We will not provide binary versions until we reach at least the Beta stage.
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.
## Build from sources
## Quick start
We do not provide yet binary versions of Lightpanda, you have to compile it from source.
### 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
### Prerequisites

View File

@@ -168,6 +168,11 @@ fn common(
.root_source_file = b.path("vendor/tls.zig/src/main.zig"),
});
step.root_module.addImport("tls", tlsmod);
const wsmod = b.addModule("websocket", .{
.root_source_file = b.path("vendor/websocket.zig/src/websocket.zig"),
});
step.root_module.addImport("websocket", wsmod);
}
fn moduleNetSurf(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module {

View File

@@ -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");
@@ -42,14 +43,19 @@ const FetchResult = @import("../http/Client.zig").Client.FetchResult;
const UserContext = @import("../user_context.zig").UserContext;
const HttpClient = @import("asyncio").Client;
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";
@@ -109,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,
@@ -120,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();
@@ -193,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();
@@ -252,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);
@@ -274,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
@@ -350,11 +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();
// inspector
if (self.session.inspector) |inspector| {
inspector.contextCreated(self.session.env, "", self.origin.?, auxData);
@@ -366,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
@@ -378,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);
@@ -393,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.
@@ -421,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;
}
@@ -445,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);
}
@@ -462,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);
}
@@ -486,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,
@@ -513,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;
}
@@ -547,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..];
@@ -563,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 });
}
}
};
};

View File

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

View File

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

View File

@@ -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,
@@ -193,18 +215,18 @@ pub fn sendEvent(
const resp = Resp{ .method = name, .params = params, .sessionId = sessionID };
const event_msg = try stringify(alloc, resp);
try server.sendAsync(ctx, event_msg);
try ctx.send(event_msg);
}
// Common
// ------
// 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
View 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
View 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
View 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);
}

View File

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

View File

@@ -323,7 +323,7 @@ fn navigate(
.loaderId = ctx.state.loaderID,
};
const res = try result(alloc, input.id, Resp, resp, input.sessionId);
try server.sendAsync(ctx, res);
try ctx.send(res);
// TODO: at this point do we need async the following actions to be async?
@@ -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,

View File

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

View File

@@ -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(
@@ -292,14 +309,14 @@ fn disposeBrowserContext(
// output
const res = try result(alloc, input.id, null, .{}, null);
try server.sendAsync(ctx, res);
try ctx.send(res);
return error.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,
};
@@ -378,7 +426,7 @@ fn closeTarget(
success: bool = true,
};
const res = try result(alloc, input.id, Resp, Resp{}, null);
try server.sendAsync(ctx, res);
try ctx.send(res);
// Inspector.detached event
const InspectorDetached = struct {
@@ -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);
}

View File

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

95
src/handler.zig Normal file
View File

@@ -0,0 +1,95 @@
// 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 ws = @import("websocket");
const Msg = @import("msg.zig").Msg;
const log = std.log.scoped(.handler);
pub const Stream = struct {
addr: std.net.Address,
socket: std.posix.socket_t = undefined,
ws_host: []const u8,
ws_port: u16,
ws_conn: *ws.Conn = undefined,
fn connectCDP(self: *Stream) !void {
const flags: u32 = std.posix.SOCK.STREAM;
const proto = blk: {
if (self.addr.any.family == std.posix.AF.UNIX) break :blk @as(u32, 0);
break :blk std.posix.IPPROTO.TCP;
};
const socket = try std.posix.socket(self.addr.any.family, flags, proto);
try std.posix.connect(
socket,
&self.addr.any,
self.addr.getOsSockLen(),
);
log.debug("connected to Stream server", .{});
self.socket = socket;
}
fn closeCDP(self: *const Stream) void {
const close_msg: []const u8 = .{ 5, 0, 0, 0 } ++ "close";
self.recv(close_msg) catch |err| {
log.err("stream close error: {any}", .{err});
};
std.posix.close(self.socket);
}
fn start(self: *Stream, ws_conn: *ws.Conn) !void {
try self.connectCDP();
self.ws_conn = ws_conn;
}
pub fn recv(self: *const Stream, data: []const u8) !void {
var pos: usize = 0;
while (pos < data.len) {
const len = try std.posix.write(self.socket, data[pos..]);
pos += len;
}
}
pub fn send(self: *const Stream, data: []const u8) !void {
return self.ws_conn.write(data);
}
};
pub const Handler = struct {
stream: *Stream,
pub fn init(_: ws.Handshake, ws_conn: *ws.Conn, stream: *Stream) !Handler {
try stream.start(ws_conn);
return .{ .stream = stream };
}
pub fn close(self: *Handler) void {
self.stream.closeCDP();
}
pub fn clientMessage(self: *Handler, data: []const u8) !void {
var header: [4]u8 = undefined;
Msg.setSize(data.len, &header);
try self.stream.recv(&header);
try self.stream.recv(data);
}
};

View File

@@ -26,6 +26,7 @@ const checkCases = jsruntime.test_utils.checkCases;
const Element = @import("../dom/element.zig").Element;
const URL = @import("../url/url.zig").URL;
const Node = @import("../dom/node.zig").Node;
// HTMLElement interfaces
pub const Interfaces = .{
@@ -117,6 +118,25 @@ pub const HTMLElement = struct {
pub fn get_style(_: *parser.ElementHTML) CSSProperties {
return .{};
}
pub fn get_innerText(e: *parser.ElementHTML) ![]const u8 {
const n = @as(*parser.Node, @ptrCast(e));
return try parser.nodeTextContent(n) orelse "";
}
pub fn set_innerText(e: *parser.ElementHTML, s: []const u8) !void {
const n = @as(*parser.Node, @ptrCast(e));
// create text node.
const doc = try parser.nodeOwnerDocument(n) orelse return error.NoDocument;
const t = try parser.documentCreateTextNode(doc, s);
// remove existing children.
try Node.removeChildren(n);
// attach the text node.
_ = try parser.nodeAppendChild(n, @as(*parser.Node, @ptrCast(t)));
}
};
// Deprecated HTMLElements in Chrome (2023/03/15)
@@ -1068,4 +1088,12 @@ pub fn testExecFn(
.{ .src = "script.async", .ex = "false" },
};
try checkCases(js_env, &script);
var innertext = [_]Case{
.{ .src = "const backup = document.getElementById('content')", .ex = "undefined" },
.{ .src = "document.getElementById('content').innerText = 'foo';", .ex = "foo" },
.{ .src = "document.getElementById('content').innerText", .ex = "foo" },
.{ .src = "document.getElementById('content').innerHTML = backup; true;", .ex = "true" },
};
try checkCases(js_env, &innertext);
}

128
src/html/history.zig Normal file
View 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);
}

View File

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

View File

@@ -25,11 +25,11 @@ 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");
const log = std.log.scoped(.window);
// https://dom.spec.whatwg.org/#interface-window-extensions
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
pub const Window = struct {
@@ -42,6 +42,7 @@ pub const Window = struct {
document: ?*parser.DocumentHTML = null,
target: []const u8,
history: History = .{},
storageShelf: ?*storage.Shelf = null,
@@ -50,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) },
};
}
@@ -68,8 +73,8 @@ pub const Window = struct {
return self;
}
pub fn _debug(_: *Window, str: []const u8) void {
log.debug("{s}", .{str});
pub fn get_navigator(self: *Window) *Navigator {
return &self.navigator;
}
pub fn get_self(self: *Window) *Window {
@@ -84,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;
}

View File

@@ -17,13 +17,15 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const posix = std.posix;
const builtin = @import("builtin");
const jsruntime = @import("jsruntime");
const websocket = @import("websocket");
const Browser = @import("browser/browser.zig").Browser;
const server = @import("server.zig");
const handler = @import("handler.zig");
const MaxSize = @import("msg.zig").MaxSize;
const parser = @import("netsurf");
const apiweb = @import("apiweb.zig");
@@ -32,101 +34,18 @@ pub const Types = jsruntime.reflect(apiweb.Interfaces);
pub const UserContext = apiweb.UserContext;
pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
// Simple blocking websocket connection model
// ie. 1 thread per ws connection without thread pool and epoll/kqueue
pub const websocket_blocking = true;
const log = std.log.scoped(.cli);
// Inspired by std.net.StreamServer in Zig < 0.12
pub const StreamServer = struct {
/// Copied from `Options` on `init`.
kernel_backlog: u31,
reuse_address: bool,
reuse_port: bool,
nonblocking: bool,
pub const std_options = .{
// Set the log level to info
.log_level = .debug,
/// `undefined` until `listen` returns successfully.
listen_address: std.net.Address,
sockfd: ?posix.socket_t,
pub const Options = struct {
/// How many connections the kernel will accept on the application's behalf.
/// If more than this many connections pool in the kernel, clients will start
/// seeing "Connection refused".
kernel_backlog: u31 = 128,
/// Enable SO.REUSEADDR on the socket.
reuse_address: bool = false,
/// Enable SO.REUSEPORT on the socket.
reuse_port: bool = false,
/// Non-blocking mode.
nonblocking: bool = false,
};
/// After this call succeeds, resources have been acquired and must
/// be released with `deinit`.
pub fn init(options: Options) StreamServer {
return StreamServer{
.sockfd = null,
.kernel_backlog = options.kernel_backlog,
.reuse_address = options.reuse_address,
.reuse_port = options.reuse_port,
.nonblocking = options.nonblocking,
.listen_address = undefined,
};
}
/// Release all resources. The `StreamServer` memory becomes `undefined`.
pub fn deinit(self: *StreamServer) void {
self.close();
self.* = undefined;
}
fn setSockOpt(fd: posix.socket_t, level: i32, option: u32, value: c_int) !void {
try posix.setsockopt(fd, level, option, &std.mem.toBytes(value));
}
pub fn listen(self: *StreamServer, address: std.net.Address) !void {
const sock_flags = posix.SOCK.STREAM | posix.SOCK.CLOEXEC;
var use_sock_flags: u32 = sock_flags;
if (self.nonblocking) use_sock_flags |= posix.SOCK.NONBLOCK;
const proto = if (address.any.family == posix.AF.UNIX) @as(u32, 0) else posix.IPPROTO.TCP;
const sockfd = try posix.socket(address.any.family, use_sock_flags, proto);
self.sockfd = sockfd;
errdefer {
posix.close(sockfd);
self.sockfd = null;
}
// socket options
if (self.reuse_address) {
try setSockOpt(sockfd, posix.SOL.SOCKET, posix.SO.REUSEADDR, 1);
}
if (@hasDecl(posix.SO, "REUSEPORT") and self.reuse_port) {
try setSockOpt(sockfd, posix.SOL.SOCKET, posix.SO.REUSEPORT, 1);
}
if (builtin.target.os.tag == .linux) { // posix.TCP not available on MacOS
// WARNING: disable Nagle's alogrithm to avoid latency issues
try setSockOpt(sockfd, posix.IPPROTO.TCP, posix.TCP.NODELAY, 1);
}
var socklen = address.getOsSockLen();
try posix.bind(sockfd, &address.any, socklen);
try posix.listen(sockfd, self.kernel_backlog);
try posix.getsockname(sockfd, &self.listen_address.any, &socklen);
}
/// Stop listening. It is still necessary to call `deinit` after stopping listening.
/// Calling `deinit` will automatically call `close`. It is safe to call `close` when
/// not listening.
pub fn close(self: *StreamServer) void {
if (self.sockfd) |fd| {
posix.close(fd);
self.sockfd = null;
self.listen_address = undefined;
}
}
// Define logFn to override the std implementation
.logFn = logFn,
};
const usage =
@@ -138,8 +57,9 @@ 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 "3245")
\\ --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)
\\
@@ -170,10 +90,11 @@ const CliMode = union(CliModeTag) {
host: []const u8 = Host,
port: u16 = Port,
timeout: u8 = Timeout,
tcp: bool = false, // undocumented TCP mode
// default options
const Host = "127.0.0.1";
const Port = 3245;
const Port = 9222;
const Timeout = 3; // in seconds
};
@@ -198,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;
@@ -235,6 +160,10 @@ const CliMode = union(CliModeTag) {
return printUsageExit(execname, 1);
}
}
if (std.mem.eql(u8, "--tcp", opt)) {
_server.tcp = true;
continue;
}
// unknown option
if (std.mem.startsWith(u8, opt, "--")) {
@@ -317,33 +246,70 @@ pub fn main() !void {
defer cli_mode.deinit();
switch (cli_mode) {
.server => |mode| {
.server => |opts| {
// server
var srv = StreamServer.init(.{
.reuse_address = true,
.reuse_port = true,
.nonblocking = true,
});
defer srv.deinit();
srv.listen(mode.addr) catch |err| {
log.err("address (host:port) {any}\n", .{err});
return printUsageExit(mode.execname, 1);
// Stream server
const addr = blk: {
if (opts.tcp) {
break :blk opts.addr;
} else {
const unix_path = "/tmp/lightpanda";
std.fs.deleteFileAbsolute(unix_path) catch {}; // file could not exists
break :blk try std.net.Address.initUnix(unix_path);
}
};
defer srv.close();
log.info("Server mode: listening on {s}:{d}...", .{ mode.host, mode.port });
const socket = server.listen(addr) catch |err| {
log.err("Server listen error: {any}\n", .{err});
return printUsageExit(opts.execname, 1);
};
defer std.posix.close(socket);
log.debug("Server opts: listening internally on {any}...", .{addr});
const timeout = std.time.ns_per_s * @as(u64, opts.timeout);
// loop
var loop = try jsruntime.Loop.init(alloc);
defer loop.deinit();
// listen
try server.listen(alloc, &loop, srv.sockfd.?, std.time.ns_per_s * @as(u64, mode.timeout));
// TCP server mode
if (opts.tcp) {
return server.handle(alloc, &loop, socket, null, timeout);
}
// start stream server in separate thread
var stream = handler.Stream{
.ws_host = opts.host,
.ws_port = opts.port,
.addr = addr,
};
const cdp_thread = try std.Thread.spawn(
.{ .allocator = alloc },
server.handle,
.{ alloc, &loop, socket, &stream, timeout },
);
// Websocket server
var ws = try websocket.Server(handler.Handler).init(alloc, .{
.port = opts.port,
.address = opts.host,
.max_message_size = MaxSize + 14, // overhead websocket
.max_conn = 1,
.handshake = .{
.timeout = 3,
.max_size = 1024,
// since we aren't using hanshake.headers
// we can set this to 0 to save a few bytes.
.max_headers = 0,
},
});
defer ws.deinit();
try ws.listen(&stream);
cdp_thread.join();
},
.fetch => |mode| {
log.debug("Fetch mode: url {s}, dump {any}", .{ mode.url, mode.dump });
.fetch => |opts| {
log.debug("Fetch mode: url {s}, dump {any}", .{ opts.url, opts.dump });
// vm
const vm = jsruntime.VM.init();
@@ -360,24 +326,41 @@ pub fn main() !void {
// page
const page = try browser.session.createPage();
try page.start(null);
defer page.end();
_ = page.navigate(mode.url, null) catch |err| switch (err) {
_ = page.navigate(opts.url, null) catch |err| switch (err) {
error.UnsupportedUriScheme, error.UriMissingHost => {
log.err("'{s}' is not a valid URL ({any})\n", .{ mode.url, err });
return printUsageExit(mode.execname, 1);
log.err("'{s}' is not a valid URL ({any})\n", .{ opts.url, err });
return printUsageExit(opts.execname, 1);
},
else => {
log.err("'{s}' fetching error ({any})s\n", .{ mode.url, err });
return printUsageExit(mode.execname, 1);
log.err("'{s}' fetching error ({any})s\n", .{ opts.url, err });
return printUsageExit(opts.execname, 1);
},
};
try page.wait();
// dump
if (mode.dump) {
if (opts.dump) {
try page.dump(std.io.getStdOut());
}
},
}
}
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);
}

View File

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

View File

@@ -18,44 +18,50 @@
const std = @import("std");
/// MsgBuffer returns messages from a raw text read stream,
/// according to the following format `<msg_size>:<msg>`.
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(u32, data[0..HeaderSize], .little);
}
pub fn setSize(len: usize, header: *[4]u8) void {
std.mem.writeInt(u32, header, @intCast(len), .little);
}
};
/// Buffer returns messages from a raw text read stream,
/// with the message size being encoded on the 2 first bytes (little endian)
/// It handles both:
/// - combined messages in one read
/// - single message in several reads (multipart)
/// It's safe (and a good practice) to reuse the same MsgBuffer
/// It's safe (and a good practice) to reuse the same Buffer
/// on several reads of the same stream.
pub const MsgBuffer = struct {
size: usize = 0,
pub const Buffer = struct {
buf: []u8,
size: usize = 0,
pos: usize = 0,
const MaxSize = 1024 * 1024; // 1MB
pub fn init(alloc: std.mem.Allocator, size: usize) std.mem.Allocator.Error!MsgBuffer {
const buf = try alloc.alloc(u8, size);
return .{ .buf = buf };
}
pub fn deinit(self: MsgBuffer, alloc: std.mem.Allocator) void {
alloc.free(self.buf);
}
fn isFinished(self: *MsgBuffer) bool {
fn isFinished(self: *const Buffer) bool {
return self.pos >= self.size;
}
fn isEmpty(self: MsgBuffer) bool {
fn isEmpty(self: *const Buffer) bool {
return self.size == 0 and self.pos == 0;
}
fn reset(self: *MsgBuffer) void {
fn reset(self: *Buffer) void {
self.size = 0;
self.pos = 0;
}
// read input
pub fn read(self: *MsgBuffer, alloc: std.mem.Allocator, input: []const u8) !struct {
pub fn read(self: *Buffer, input: []const u8) !struct {
msg: []const u8,
left: []const u8,
} {
@@ -64,11 +70,9 @@ pub const MsgBuffer = struct {
// msg size
var msg_size: usize = undefined;
if (self.isEmpty()) {
// parse msg size metadata
const size_pos = std.mem.indexOfScalar(u8, _input, ':') orelse return error.InputWithoutSize;
const size_str = _input[0..size_pos];
msg_size = try std.fmt.parseInt(u32, size_str, 10);
_input = _input[size_pos + 1 ..];
// decode msg size header
msg_size = Msg.getSize(_input);
_input = _input[HeaderSize..];
} else {
msg_size = self.size;
}
@@ -77,7 +81,7 @@ pub const MsgBuffer = struct {
const is_multipart = !self.isEmpty() or _input.len < msg_size;
if (is_multipart) {
// set msg size on empty MsgBuffer
// set msg size on empty Buffer
if (self.isEmpty()) {
self.size = msg_size;
}
@@ -90,19 +94,11 @@ pub const MsgBuffer = struct {
return error.MsgTooBig;
}
// check if the current input can fit in MsgBuffer
if (new_pos > self.buf.len) {
// we want to realloc at least:
// - a size big enough to fit the entire input (ie. new_pos)
// - a size big enough (ie. current msg size + starting buffer size)
// to avoid multiple reallocation
const new_size = @max(self.buf.len + self.size, new_pos);
// resize the MsgBuffer to fit
self.buf = try alloc.realloc(self.buf, new_size);
}
// copy the current input into MsgBuffer
@memcpy(self.buf[self.pos..new_pos], _input[0..]);
// copy the current input into Buffer
// NOTE: we could use @memcpy but it's not Thread-safe (alias problem)
// see https://www.openmymind.net/Zigs-memcpy-copyForwards-and-copyBackwards/
// Intead we just use std.mem.copyForwards
std.mem.copyForwards(u8, self.buf[self.pos..new_pos], _input[0..]);
// set the new cursor position
self.pos = new_pos;
@@ -120,47 +116,45 @@ pub const MsgBuffer = struct {
}
};
fn doTest(nb: *u8) void {
nb.* += 1;
}
test "MsgBuffer" {
test "Buffer" {
const Case = struct {
input: []const u8,
nb: u8,
};
const alloc = std.testing.allocator;
const cases = [_]Case{
// simple
.{ .input = "2:ok", .nb = 1 },
.{ .input = .{ 2, 0, 0, 0 } ++ "ok", .nb = 1 },
// combined
.{ .input = "2:ok3:foo7:bar2:ok", .nb = 3 }, // "bar2:ok" is a message, no need to escape "2:" here
.{ .input = .{ 2, 0, 0, 0 } ++ "ok" ++ .{ 3, 0, 0, 0 } ++ "foo", .nb = 2 },
// multipart
.{ .input = "9:multi", .nb = 0 },
.{ .input = .{ 9, 0, 0, 0 } ++ "multi", .nb = 0 },
.{ .input = "part", .nb = 1 },
// multipart & combined
.{ .input = "9:multi", .nb = 0 },
.{ .input = "part2: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:multi", .nb = 0 },
.{ .input = "part8: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: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:ok9:multi", .nb = 1 },
.{ .input = .{ 2, 0, 0, 0 } ++ "ok" ++ .{ 9, 0, 0, 0 } ++ "multi", .nb = 1 },
.{ .input = "part", .nb = 1 },
};
var msg_buf = try MsgBuffer.init(alloc, 10);
defer msg_buf.deinit(alloc);
var b: [MaxSize]u8 = undefined;
var buf = Buffer{ .buf = &b };
for (cases) |case| {
var nb: u8 = 0;
var input: []const u8 = case.input;
var input = case.input;
while (input.len > 0) {
const parts = msg_buf.read(alloc, input) catch |err| {
const parts = buf.read(input) catch |err| {
if (err == error.MsgMultipart) break; // go to the next case input
return err;
};

View File

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

671
src/polyfill/fetch.js Normal file
View File

@@ -0,0 +1,671 @@
// fetch.js code comes from
// https://github.com/JakeChampion/fetch/blob/main/fetch.js
//
// The original code source is available in MIT license.
//
// The script comes from the built version from npm.
// You can get the package with the command:
//
// wget $(npm view whatwg-fetch dist.tarball)
//
// The source is the content of `package/dist/fetch.umd.js` file.
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(factory((global.WHATWGFetch = {})));
}(this, (function (exports) { 'use strict';
/* eslint-disable no-prototype-builtins */
var g =
(typeof globalThis !== 'undefined' && globalThis) ||
(typeof self !== 'undefined' && self) ||
// eslint-disable-next-line no-undef
(typeof global !== 'undefined' && global) ||
{};
var support = {
searchParams: 'URLSearchParams' in g,
iterable: 'Symbol' in g && 'iterator' in Symbol,
blob:
'FileReader' in g &&
'Blob' in g &&
(function() {
try {
new Blob();
return true
} catch (e) {
return false
}
})(),
formData: 'FormData' in g,
// Arraybuffer is available but xhr doesn't implement it for now.
// arrayBuffer: 'ArrayBuffer' in g
arrayBuffer: false
};
function isDataView(obj) {
return obj && DataView.prototype.isPrototypeOf(obj)
}
if (support.arrayBuffer) {
var viewClasses = [
'[object Int8Array]',
'[object Uint8Array]',
'[object Uint8ClampedArray]',
'[object Int16Array]',
'[object Uint16Array]',
'[object Int32Array]',
'[object Uint32Array]',
'[object Float32Array]',
'[object Float64Array]'
];
var isArrayBufferView =
ArrayBuffer.isView ||
function(obj) {
return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1
};
}
function normalizeName(name) {
if (typeof name !== 'string') {
name = String(name);
}
if (/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(name) || name === '') {
throw new TypeError('Invalid character in header field name: "' + name + '"')
}
return name.toLowerCase()
}
function normalizeValue(value) {
if (typeof value !== 'string') {
value = String(value);
}
return value
}
// Build a destructive iterator for the value list
function iteratorFor(items) {
var iterator = {
next: function() {
var value = items.shift();
return {done: value === undefined, value: value}
}
};
if (support.iterable) {
iterator[Symbol.iterator] = function() {
return iterator
};
}
return iterator
}
function Headers(headers) {
this.map = {};
if (headers instanceof Headers) {
headers.forEach(function(value, name) {
this.append(name, value);
}, this);
} else if (Array.isArray(headers)) {
headers.forEach(function(header) {
if (header.length != 2) {
throw new TypeError('Headers constructor: expected name/value pair to be length 2, found' + header.length)
}
this.append(header[0], header[1]);
}, this);
} else if (headers) {
Object.getOwnPropertyNames(headers).forEach(function(name) {
this.append(name, headers[name]);
}, this);
}
}
Headers.prototype.append = function(name, value) {
name = normalizeName(name);
value = normalizeValue(value);
var oldValue = this.map[name];
this.map[name] = oldValue ? oldValue + ', ' + value : value;
};
Headers.prototype['delete'] = function(name) {
delete this.map[normalizeName(name)];
};
Headers.prototype.get = function(name) {
name = normalizeName(name);
return this.has(name) ? this.map[name] : null
};
Headers.prototype.has = function(name) {
return this.map.hasOwnProperty(normalizeName(name))
};
Headers.prototype.set = function(name, value) {
this.map[normalizeName(name)] = normalizeValue(value);
};
Headers.prototype.forEach = function(callback, thisArg) {
for (var name in this.map) {
if (this.map.hasOwnProperty(name)) {
callback.call(thisArg, this.map[name], name, this);
}
}
};
Headers.prototype.keys = function() {
var items = [];
this.forEach(function(value, name) {
items.push(name);
});
return iteratorFor(items)
};
Headers.prototype.values = function() {
var items = [];
this.forEach(function(value) {
items.push(value);
});
return iteratorFor(items)
};
Headers.prototype.entries = function() {
var items = [];
this.forEach(function(value, name) {
items.push([name, value]);
});
return iteratorFor(items)
};
if (support.iterable) {
Headers.prototype[Symbol.iterator] = Headers.prototype.entries;
}
function consumed(body) {
if (body._noBody) return
if (body.bodyUsed) {
return Promise.reject(new TypeError('Already read'))
}
body.bodyUsed = true;
}
function fileReaderReady(reader) {
return new Promise(function(resolve, reject) {
reader.onload = function() {
resolve(reader.result);
};
reader.onerror = function() {
reject(reader.error);
};
})
}
function readBlobAsArrayBuffer(blob) {
var reader = new FileReader();
var promise = fileReaderReady(reader);
reader.readAsArrayBuffer(blob);
return promise
}
function readBlobAsText(blob) {
var reader = new FileReader();
var promise = fileReaderReady(reader);
var match = /charset=([A-Za-z0-9_-]+)/.exec(blob.type);
var encoding = match ? match[1] : 'utf-8';
reader.readAsText(blob, encoding);
return promise
}
function readArrayBufferAsText(buf) {
var view = new Uint8Array(buf);
var chars = new Array(view.length);
for (var i = 0; i < view.length; i++) {
chars[i] = String.fromCharCode(view[i]);
}
return chars.join('')
}
function bufferClone(buf) {
if (buf.slice) {
return buf.slice(0)
} else {
var view = new Uint8Array(buf.byteLength);
view.set(new Uint8Array(buf));
return view.buffer
}
}
function Body() {
this.bodyUsed = false;
this._initBody = function(body) {
/*
fetch-mock wraps the Response object in an ES6 Proxy to
provide useful test harness features such as flush. However, on
ES5 browsers without fetch or Proxy support pollyfills must be used;
the proxy-pollyfill is unable to proxy an attribute unless it exists
on the object before the Proxy is created. This change ensures
Response.bodyUsed exists on the instance, while maintaining the
semantic of setting Request.bodyUsed in the constructor before
_initBody is called.
*/
// eslint-disable-next-line no-self-assign
this.bodyUsed = this.bodyUsed;
this._bodyInit = body;
if (!body) {
this._noBody = true;
this._bodyText = '';
} else if (typeof body === 'string') {
this._bodyText = body;
} else if (support.blob && Blob.prototype.isPrototypeOf(body)) {
this._bodyBlob = body;
} else if (support.formData && FormData.prototype.isPrototypeOf(body)) {
this._bodyFormData = body;
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
this._bodyText = body.toString();
} else if (support.arrayBuffer && support.blob && isDataView(body)) {
this._bodyArrayBuffer = bufferClone(body.buffer);
// IE 10-11 can't handle a DataView body.
this._bodyInit = new Blob([this._bodyArrayBuffer]);
} else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) {
this._bodyArrayBuffer = bufferClone(body);
} else {
this._bodyText = body = Object.prototype.toString.call(body);
}
if (!this.headers.get('content-type')) {
if (typeof body === 'string') {
this.headers.set('content-type', 'text/plain;charset=UTF-8');
} else if (this._bodyBlob && this._bodyBlob.type) {
this.headers.set('content-type', this._bodyBlob.type);
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
}
}
};
if (support.blob) {
this.blob = function() {
var rejected = consumed(this);
if (rejected) {
return rejected
}
if (this._bodyBlob) {
return Promise.resolve(this._bodyBlob)
} else if (this._bodyArrayBuffer) {
return Promise.resolve(new Blob([this._bodyArrayBuffer]))
} else if (this._bodyFormData) {
throw new Error('could not read FormData body as blob')
} else {
return Promise.resolve(new Blob([this._bodyText]))
}
};
}
this.arrayBuffer = function() {
if (this._bodyArrayBuffer) {
var isConsumed = consumed(this);
if (isConsumed) {
return isConsumed
} else if (ArrayBuffer.isView(this._bodyArrayBuffer)) {
return Promise.resolve(
this._bodyArrayBuffer.buffer.slice(
this._bodyArrayBuffer.byteOffset,
this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength
)
)
} else {
return Promise.resolve(this._bodyArrayBuffer)
}
} else if (support.blob) {
return this.blob().then(readBlobAsArrayBuffer)
} else {
throw new Error('could not read as ArrayBuffer')
}
};
this.text = function() {
var rejected = consumed(this);
if (rejected) {
return rejected
}
if (this._bodyBlob) {
return readBlobAsText(this._bodyBlob)
} else if (this._bodyArrayBuffer) {
return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer))
} else if (this._bodyFormData) {
throw new Error('could not read FormData body as text')
} else {
return Promise.resolve(this._bodyText)
}
};
if (support.formData) {
this.formData = function() {
return this.text().then(decode)
};
}
this.json = function() {
return this.text().then(JSON.parse)
};
return this
}
// HTTP methods whose capitalization should be normalized
var methods = ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE'];
function normalizeMethod(method) {
var upcased = method.toUpperCase();
return methods.indexOf(upcased) > -1 ? upcased : method
}
function Request(input, options) {
if (!(this instanceof Request)) {
throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.')
}
options = options || {};
var body = options.body;
if (input instanceof Request) {
if (input.bodyUsed) {
throw new TypeError('Already read')
}
this.url = input.url;
this.credentials = input.credentials;
if (!options.headers) {
this.headers = new Headers(input.headers);
}
this.method = input.method;
this.mode = input.mode;
this.signal = input.signal;
if (!body && input._bodyInit != null) {
body = input._bodyInit;
input.bodyUsed = true;
}
} else {
this.url = String(input);
}
this.credentials = options.credentials || this.credentials || 'same-origin';
if (options.headers || !this.headers) {
this.headers = new Headers(options.headers);
}
this.method = normalizeMethod(options.method || this.method || 'GET');
this.mode = options.mode || this.mode || null;
this.signal = options.signal || this.signal || (function () {
if ('AbortController' in g) {
var ctrl = new AbortController();
return ctrl.signal;
}
}());
this.referrer = null;
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
throw new TypeError('Body not allowed for GET or HEAD requests')
}
this._initBody(body);
if (this.method === 'GET' || this.method === 'HEAD') {
if (options.cache === 'no-store' || options.cache === 'no-cache') {
// Search for a '_' parameter in the query string
var reParamSearch = /([?&])_=[^&]*/;
if (reParamSearch.test(this.url)) {
// If it already exists then set the value with the current time
this.url = this.url.replace(reParamSearch, '$1_=' + new Date().getTime());
} else {
// Otherwise add a new '_' parameter to the end with the current time
var reQueryString = /\?/;
this.url += (reQueryString.test(this.url) ? '&' : '?') + '_=' + new Date().getTime();
}
}
}
}
Request.prototype.clone = function() {
return new Request(this, {body: this._bodyInit})
};
function decode(body) {
var form = new FormData();
body
.trim()
.split('&')
.forEach(function(bytes) {
if (bytes) {
var split = bytes.split('=');
var name = split.shift().replace(/\+/g, ' ');
var value = split.join('=').replace(/\+/g, ' ');
form.append(decodeURIComponent(name), decodeURIComponent(value));
}
});
return form
}
function parseHeaders(rawHeaders) {
var headers = new Headers();
// Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space
// https://tools.ietf.org/html/rfc7230#section-3.2
var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' ');
// Avoiding split via regex to work around a common IE11 bug with the core-js 3.6.0 regex polyfill
// https://github.com/github/fetch/issues/748
// https://github.com/zloirock/core-js/issues/751
preProcessedHeaders
.split('\r')
.map(function(header) {
return header.indexOf('\n') === 0 ? header.substr(1, header.length) : header
})
.forEach(function(line) {
var parts = line.split(':');
var key = parts.shift().trim();
if (key) {
var value = parts.join(':').trim();
try {
headers.append(key, value);
} catch (error) {
console.warn('Response ' + error.message);
}
}
});
return headers
}
Body.call(Request.prototype);
function Response(bodyInit, options) {
if (!(this instanceof Response)) {
throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.')
}
if (!options) {
options = {};
}
this.type = 'default';
this.status = options.status === undefined ? 200 : options.status;
if (this.status < 200 || this.status > 599) {
throw new RangeError("Failed to construct 'Response': The status provided (0) is outside the range [200, 599].")
}
this.ok = this.status >= 200 && this.status < 300;
this.statusText = options.statusText === undefined ? '' : '' + options.statusText;
this.headers = new Headers(options.headers);
this.url = options.url || '';
this._initBody(bodyInit);
}
Body.call(Response.prototype);
Response.prototype.clone = function() {
return new Response(this._bodyInit, {
status: this.status,
statusText: this.statusText,
headers: new Headers(this.headers),
url: this.url
})
};
Response.error = function() {
var response = new Response(null, {status: 200, statusText: ''});
response.ok = false;
response.status = 0;
response.type = 'error';
return response
};
var redirectStatuses = [301, 302, 303, 307, 308];
Response.redirect = function(url, status) {
if (redirectStatuses.indexOf(status) === -1) {
throw new RangeError('Invalid status code')
}
return new Response(null, {status: status, headers: {location: url}})
};
exports.DOMException = g.DOMException;
try {
new exports.DOMException();
} catch (err) {
exports.DOMException = function(message, name) {
this.message = message;
this.name = name;
var error = Error(message);
this.stack = error.stack;
};
exports.DOMException.prototype = Object.create(Error.prototype);
exports.DOMException.prototype.constructor = exports.DOMException;
}
function fetch(input, init) {
return new Promise(function(resolve, reject) {
var request = new Request(input, init);
if (request.signal && request.signal.aborted) {
return reject(new exports.DOMException('Aborted', 'AbortError'))
}
var xhr = new XMLHttpRequest();
function abortXhr() {
xhr.abort();
}
xhr.onload = function() {
var options = {
statusText: xhr.statusText,
headers: parseHeaders(xhr.getAllResponseHeaders() || '')
};
// This check if specifically for when a user fetches a file locally from the file system
// Only if the status is out of a normal range
if (request.url.indexOf('file://') === 0 && (xhr.status < 200 || xhr.status > 599)) {
options.status = 200;
} else {
options.status = xhr.status;
}
options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL');
var body = 'response' in xhr ? xhr.response : xhr.responseText;
setTimeout(function() {
resolve(new Response(body, options));
}, 0);
};
xhr.onerror = function() {
setTimeout(function() {
reject(new TypeError('Network request failed'));
}, 0);
};
xhr.ontimeout = function() {
setTimeout(function() {
reject(new TypeError('Network request timed out'));
}, 0);
};
xhr.onabort = function() {
setTimeout(function() {
reject(new exports.DOMException('Aborted', 'AbortError'));
}, 0);
};
function fixUrl(url) {
try {
return url === '' && g.location.href ? g.location.href : url
} catch (e) {
return url
}
}
xhr.open(request.method, fixUrl(request.url), true);
if (request.credentials === 'include') {
xhr.withCredentials = true;
} else if (request.credentials === 'omit') {
xhr.withCredentials = false;
}
if ('responseType' in xhr) {
if (support.blob) {
xhr.responseType = 'blob';
} else if (
support.arrayBuffer
) {
xhr.responseType = 'arraybuffer';
}
}
if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) {
var names = [];
Object.getOwnPropertyNames(init.headers).forEach(function(name) {
names.push(normalizeName(name));
xhr.setRequestHeader(name, normalizeValue(init.headers[name]));
});
request.headers.forEach(function(value, name) {
if (names.indexOf(name) === -1) {
xhr.setRequestHeader(name, value);
}
});
} else {
request.headers.forEach(function(value, name) {
xhr.setRequestHeader(name, value);
});
}
if (request.signal) {
request.signal.addEventListener('abort', abortXhr);
xhr.onreadystatechange = function() {
// DONE (success or failure)
if (xhr.readyState === 4) {
request.signal.removeEventListener('abort', abortXhr);
}
};
}
xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit);
})
}
fetch.polyfill = true;
if (!g.fetch) {
g.fetch = fetch;
g.Headers = Headers;
g.Request = Request;
g.Response = Response;
}
exports.Headers = Headers;
exports.Request = Request;
exports.Response = Response;
exports.fetch = fetch;
Object.defineProperty(exports, '__esModule', { value: true });
})));

55
src/polyfill/fetch.zig Normal file
View File

@@ -0,0 +1,55 @@
const std = @import("std");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
// fetch.js code comes from
// https://github.com/JakeChampion/fetch/blob/main/fetch.js
//
// The original code source is available in MIT license.
//
// The script comes from the built version from npm.
// You can get the package with the command:
//
// wget $(npm view whatwg-fetch dist.tarball)
//
// The source is the content of `package/dist/fetch.umd.js` file.
pub const source = @embedFile("fetch.js");
pub fn testExecFn(
alloc: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
try @import("polyfill.zig").load(alloc, js_env.*);
var fetch = [_]Case{
.{
.src =
\\var ok = false;
\\const request = new Request("https://httpbin.io/json");
\\fetch(request)
\\ .then((response) => { ok = response.ok; });
\\false;
,
.ex = "false",
},
// all events have been resolved.
.{ .src = "ok", .ex = "true" },
};
try checkCases(js_env, &fetch);
var fetch2 = [_]Case{
.{
.src =
\\var ok2 = false;
\\const request2 = new Request("https://httpbin.io/json");
\\(async function () { resp = await fetch(request2); ok2 = resp.ok; }());
\\false;
,
.ex = "false",
},
// all events have been resolved.
.{ .src = "ok2", .ex = "true" },
};
try checkCases(js_env, &fetch2);
}

56
src/polyfill/polyfill.zig Normal file
View File

@@ -0,0 +1,56 @@
// 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 Env = jsruntime.Env;
const fetch = @import("fetch.zig").fetch_polyfill;
const log = std.log.scoped(.polyfill);
const modules = [_]struct {
name: []const u8,
source: []const u8,
}{
.{ .name = "polyfill-fetch", .source = @import("fetch.zig").source },
};
pub fn load(alloc: std.mem.Allocator, env: Env) !void {
var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(env);
defer try_catch.deinit();
for (modules) |m| {
const res = env.exec(m.source, m.name) catch {
if (try try_catch.err(alloc, env)) |msg| {
defer alloc.free(msg);
log.err("load {s}: {s}", .{ m.name, msg });
}
return;
};
if (builtin.mode == .Debug) {
const msg = try res.toString(alloc, env);
defer alloc.free(msg);
log.debug("load {s}: {s}", .{ m.name, msg });
}
}
}

View File

@@ -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);
@@ -136,6 +136,9 @@ fn testsAllExecFn(
URLTestExecFn,
HTMLElementTestExecFn,
MutationObserverTestExecFn,
@import("polyfill/fetch.zig").testExecFn,
@import("html/navigator.zig").testExecFn,
@import("html/history.zig").testExecFn,
};
inline for (testFns) |testFn| {
@@ -358,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", .{});

View File

@@ -19,6 +19,8 @@
const std = @import("std");
const builtin = @import("builtin");
const Stream = @import("handler.zig").Stream;
const jsruntime = @import("jsruntime");
const Completion = jsruntime.IO.Completion;
const AcceptError = jsruntime.IO.AcceptError;
@@ -28,7 +30,8 @@ const CloseError = jsruntime.IO.CloseError;
const CancelError = jsruntime.IO.CancelError;
const TimeoutError = jsruntime.IO.TimeoutError;
const MsgBuffer = @import("msg.zig").MsgBuffer;
const MsgBuffer = @import("msg.zig").Buffer;
const MaxSize = @import("msg.zig").MaxSize;
const Browser = @import("browser/browser.zig").Browser;
const cdp = @import("cdp/cdp.zig");
@@ -49,6 +52,7 @@ const MaxStdOutSize = 512; // ensure debug msg are not too long
pub const Ctx = struct {
loop: *jsruntime.Loop,
stream: ?*Stream,
// internal fields
accept_socket: std.posix.socket_t,
@@ -117,8 +121,8 @@ pub const Ctx = struct {
std.debug.assert(completion == self.conn_completion);
const size = result catch |err| {
if (err == error.Canceled) {
log.debug("read canceled", .{});
if (self.isClosed() and err == error.FileDescriptorInvalid) {
log.debug("read has been canceled", .{});
return;
}
log.err("read error: {any}", .{err});
@@ -158,7 +162,7 @@ pub const Ctx = struct {
// read and execute input
var input: []const u8 = self.read_buf[0..size];
while (input.len > 0) {
const parts = self.msg_buf.read(self.alloc(), input) catch |err| {
const parts = self.msg_buf.read(input) catch |err| {
if (err == error.MsgMultipart) {
return;
} else {
@@ -171,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});
}
};
}
@@ -199,7 +204,7 @@ pub const Ctx = struct {
if (now.since(self.last_active.?) > self.timeout) {
// close current connection
log.debug("conn timeout, closing...", .{});
self.cancelAndClose();
self.close();
return;
}
@@ -213,19 +218,6 @@ pub const Ctx = struct {
);
}
fn cancelCbk(self: *Ctx, completion: *Completion, result: CancelError!void) void {
std.debug.assert(completion == self.accept_completion);
_ = result catch |err| {
log.err("cancel error: {any}", .{err});
self.err = err;
return;
};
log.debug("cancel done", .{});
self.close();
}
// shortcuts
// ---------
@@ -262,7 +254,7 @@ pub const Ctx = struct {
if (std.mem.eql(u8, cmd, "close")) {
// close connection
log.info("close cmd, closing conn...", .{});
self.cancelAndClose();
self.close();
return error.Closed;
}
@@ -283,30 +275,27 @@ pub const Ctx = struct {
// send result
if (!std.mem.eql(u8, res, "")) {
return sendAsync(self, res);
return self.send(res);
}
}
fn cancelAndClose(self: *Ctx) void {
if (isLinux) { // cancel is only available on Linux
self.loop.io.cancel(
*Ctx,
self,
Ctx.cancelCbk,
self.accept_completion,
self.conn_completion,
);
pub fn send(self: *Ctx, msg: []const u8) !void {
if (self.stream) |stream| {
// if we have a stream connection, just write on it
defer self.alloc().free(msg);
try stream.send(msg);
} else {
self.close();
// otherwise write asynchronously on the socket connection
return sendAsync(self, msg);
}
}
fn close(self: *Ctx) void {
std.posix.close(self.conn_socket);
// conn is closed
log.debug("connection closed", .{});
self.last_active = null;
std.posix.close(self.conn_socket);
log.debug("connection closed", .{});
// restart a new browser session in case of re-connect
if (!self.sessionNew) {
@@ -359,10 +348,10 @@ 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 sendAsync(ctx, s);
try ctx.send(s);
}
pub fn onInspectorResp(ctx_opaque: *anyopaque, _: u32, msg: []const u8) void {
@@ -422,16 +411,17 @@ const Send = struct {
pub fn sendAsync(ctx: *Ctx, msg: []const u8) !void {
const sd = try Send.init(ctx, msg);
ctx.loop.io.send(*Send, sd, Send.asyncCbk, &sd.completion, ctx.conn_socket, msg);
ctx.loop.io.send(*Send, sd, Send.asyncCbk, &sd.completion, ctx.conn_socket, sd.msg);
}
// Listen
// ------
// Listener and handler
// --------------------
pub fn listen(
pub fn handle(
alloc: std.mem.Allocator,
loop: *jsruntime.Loop,
server_socket: std.posix.socket_t,
stream: ?*Stream,
timeout: u64,
) anyerror!void {
@@ -446,8 +436,8 @@ pub fn listen(
// create buffers
var read_buf: [BufReadSize]u8 = undefined;
var msg_buf = try MsgBuffer.init(loop.alloc, BufReadSize * 256); // 256KB
defer msg_buf.deinit(loop.alloc);
var buf: [MaxSize]u8 = undefined;
var msg_buf = MsgBuffer{ .buf = &buf };
// create I/O completions
var accept_completion: Completion = undefined;
@@ -458,6 +448,7 @@ pub fn listen(
// for accepting connections and receving messages
var ctx = Ctx{
.loop = loop,
.stream = stream,
.browser = &browser,
.sessionNew = true,
.read_buf = &read_buf,
@@ -497,3 +488,43 @@ pub fn listen(
}
}
}
fn setSockOpt(fd: std.posix.socket_t, level: i32, option: u32, value: c_int) !void {
try std.posix.setsockopt(fd, level, option, &std.mem.toBytes(value));
}
fn isUnixSocket(addr: std.net.Address) bool {
return addr.any.family == std.posix.AF.UNIX;
}
pub fn listen(address: std.net.Address) !std.posix.socket_t {
// create socket
const flags = std.posix.SOCK.STREAM | std.posix.SOCK.CLOEXEC | std.posix.SOCK.NONBLOCK;
const proto = if (isUnixSocket(address)) @as(u32, 0) else std.posix.IPPROTO.TCP;
const sockfd = try std.posix.socket(address.any.family, flags, proto);
errdefer std.posix.close(sockfd);
// socket options
if (@hasDecl(std.posix.SO, "REUSEPORT")) {
try setSockOpt(sockfd, std.posix.SOL.SOCKET, std.posix.SO.REUSEPORT, 1);
} else {
try setSockOpt(sockfd, std.posix.SOL.SOCKET, std.posix.SO.REUSEADDR, 1);
}
if (!isUnixSocket(address)) {
if (builtin.target.os.tag == .linux) { // posix.TCP not available on MacOS
// WARNING: disable Nagle's alogrithm to avoid latency issues
try setSockOpt(sockfd, std.posix.IPPROTO.TCP, std.posix.TCP.NODELAY, 1);
}
}
// bind & listen
var socklen = address.getOsSockLen();
try std.posix.bind(sockfd, &address.any, socklen);
const kernel_backlog = 1; // default value is 128. Here we just want 1 connection
try std.posix.listen(sockfd, kernel_backlog);
var listen_address: std.net.Address = undefined;
try std.posix.getsockname(sockfd, &listen_address.any, &socklen);
return sockfd;
}

View File

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

View File

@@ -33,6 +33,8 @@ const Client = @import("asyncio").Client;
const Types = @import("../main_wpt.zig").Types;
const UserContext = @import("../main_wpt.zig").UserContext;
const polyfill = @import("../polyfill/polyfill.zig");
// runWPT parses the given HTML file, starts a js env and run the first script
// tags containing javascript sources.
// It loads first the js libs files.
@@ -74,6 +76,9 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const
try js_env.start();
defer js_env.stop();
// load polyfills
try polyfill.load(alloc, js_env);
// display console logs
defer {
const res = evalJS(js_env, alloc, "console.join('\\n');", "console") catch unreachable;
@@ -85,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);

View File

@@ -756,8 +756,10 @@ pub const XMLHttpRequest = struct {
// https://xhr.spec.whatwg.org/#the-response-attribute
pub fn get_response(self: *XMLHttpRequest, alloc: std.mem.Allocator) !?Response {
if (self.response_type == .Empty or self.response_type == .Text) {
if (self.state == LOADING or self.state == DONE) return .{ .Text = "" };
return .{ .Text = try self.get_responseText() };
if (self.state == LOADING or self.state == DONE) {
return .{ .Text = try self.get_responseText() };
}
return .{ .Text = "" };
}
// fastpath if response is previously parsed.
@@ -774,6 +776,7 @@ pub const XMLHttpRequest = struct {
// response object to a new ArrayBuffer object representing thiss
// received bytes. If this throws an exception, then set thiss
// response object to failure and return null.
log.err("response type ArrayBuffer not implemented", .{});
return null;
}
@@ -782,6 +785,7 @@ pub const XMLHttpRequest = struct {
// response object to a new Blob object representing thiss
// received bytes with type set to the result of get a final MIME
// type for this.
log.err("response type Blob not implemented", .{});
return null;
}
@@ -944,7 +948,7 @@ pub fn testExecFn(
.{ .src = "req.getResponseHeader('Content-Type')", .ex = "text/html; charset=utf-8" },
.{ .src = "req.getAllResponseHeaders().length > 64", .ex = "true" },
.{ .src = "req.responseText.length > 64", .ex = "true" },
.{ .src = "req.response", .ex = "" },
.{ .src = "req.response.length == req.responseText.length", .ex = "true" },
.{ .src = "req.responseXML instanceof Document", .ex = "true" },
};
try checkCases(js_env, &send);

1
vendor/websocket.zig vendored Submodule

Submodule vendor/websocket.zig added at 1b49626c78