mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 15:13:28 +00:00
Compare commits
315 Commits
x86-backen
...
event-targ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02cd5e98f9 | ||
|
|
a51c20068f | ||
|
|
99a3e4be3f | ||
|
|
40c9355088 | ||
|
|
8f1557254a | ||
|
|
11d28b0bc3 | ||
|
|
974cf780c0 | ||
|
|
73bb14e4a9 | ||
|
|
daf4236023 | ||
|
|
4c9a24c64e | ||
|
|
c149f65158 | ||
|
|
c5688c1bd3 | ||
|
|
b276a15786 | ||
|
|
2fed239ece | ||
|
|
8e2cb36597 | ||
|
|
bcaace1c91 | ||
|
|
d664d07141 | ||
|
|
cb8b80c856 | ||
|
|
d777d77b06 | ||
|
|
43678f8dc0 | ||
|
|
5811577824 | ||
|
|
1587122efa | ||
|
|
48e7c8ad0f | ||
|
|
766f9798f6 | ||
|
|
680d634725 | ||
|
|
7ac945bf88 | ||
|
|
188d7a8558 | ||
|
|
ee561b0d1e | ||
|
|
82bbe78e95 | ||
|
|
c761cd059b | ||
|
|
03e87155ca | ||
|
|
ea39cc52b1 | ||
|
|
90fb90b186 | ||
|
|
5f8327eaf7 | ||
|
|
07869f3c48 | ||
|
|
8377eb02a5 | ||
|
|
3738e8eb44 | ||
|
|
4b000e44b3 | ||
|
|
d6021d1702 | ||
|
|
b391eafc38 | ||
|
|
2af4545e10 | ||
|
|
b96644d893 | ||
|
|
3cb77c0a32 | ||
|
|
b3f7fb7be3 | ||
|
|
9fb51a1f29 | ||
|
|
d78e8a725d | ||
|
|
7829bdbc95 | ||
|
|
90ba6deba2 | ||
|
|
5fc763a738 | ||
|
|
84614e903c | ||
|
|
c95d739347 | ||
|
|
c55849cef5 | ||
|
|
88adb09417 | ||
|
|
cb356ffca9 | ||
|
|
2ba3f252ee | ||
|
|
ebe2c8e3dd | ||
|
|
ca2d63edcf | ||
|
|
489a1a1c37 | ||
|
|
5cad13eea3 | ||
|
|
a3e09e015c | ||
|
|
22a6accac1 | ||
|
|
277c97a959 | ||
|
|
c08bc3239a | ||
|
|
f798c7e0fb | ||
|
|
193780a88f | ||
|
|
ab4973ab6c | ||
|
|
f9da815e8f | ||
|
|
59e187d59a | ||
|
|
3b018e2a6d | ||
|
|
d4939c8260 | ||
|
|
485352594c | ||
|
|
bcdcfee467 | ||
|
|
0217e3fcae | ||
|
|
6e7d6421d5 | ||
|
|
913d3af938 | ||
|
|
53dd0a5e4c | ||
|
|
4b8c3cb188 | ||
|
|
c9ca170d57 | ||
|
|
c6090dbc16 | ||
|
|
c787b6a1a5 | ||
|
|
766aa0f60a | ||
|
|
fbe8835626 | ||
|
|
4e2b35b585 | ||
|
|
8ef79e348c | ||
|
|
47eef392d1 | ||
|
|
b846541ff6 | ||
|
|
adfffd2b08 | ||
|
|
4138c6fe95 | ||
|
|
3088c7a632 | ||
|
|
21e7638ae1 | ||
|
|
ca0d90dbcf | ||
|
|
8870045b0f | ||
|
|
cbc8b2edf9 | ||
|
|
8f297b83c1 | ||
|
|
b800d0eeb8 | ||
|
|
d95462073a | ||
|
|
95ac92b343 | ||
|
|
4cfb317af6 | ||
|
|
487aaffa94 | ||
|
|
24fd7c7286 | ||
|
|
50e62d44ff | ||
|
|
760c082757 | ||
|
|
8449d5ab22 | ||
|
|
27b50c46c3 | ||
|
|
325ecedf0b | ||
|
|
8f7a8c0ee1 | ||
|
|
7dbb55da06 | ||
|
|
4b90b70534 | ||
|
|
d6f1843ef3 | ||
|
|
5cf176927a | ||
|
|
eae21a034e | ||
|
|
06855cdfa3 | ||
|
|
45c7af9769 | ||
|
|
f9ddd5c368 | ||
|
|
6bf4dc887e | ||
|
|
4cc31bb41f | ||
|
|
88e32505a3 | ||
|
|
5b5d28f7c1 | ||
|
|
1a2fd9a584 | ||
|
|
de286dd78e | ||
|
|
70752027f1 | ||
|
|
395eb3e8ad | ||
|
|
e1137274fb | ||
|
|
bc716ef0ad | ||
|
|
d2d2e851b0 | ||
|
|
9149b60136 | ||
|
|
18ab0c8199 | ||
|
|
7fed1f3015 | ||
|
|
6809bb5393 | ||
|
|
fadf3f609a | ||
|
|
8d2d089803 | ||
|
|
f6ad95c647 | ||
|
|
0788e22dd5 | ||
|
|
de260693dc | ||
|
|
f60fcbec04 | ||
|
|
4f99407462 | ||
|
|
38813a20a7 | ||
|
|
2cd1e927f7 | ||
|
|
5094942560 | ||
|
|
82c37fc71b | ||
|
|
8ba911c8dd | ||
|
|
ac77453139 | ||
|
|
8a25545cac | ||
|
|
ed3a464843 | ||
|
|
1854074f64 | ||
|
|
ec5de2fce0 | ||
|
|
3af34d11ca | ||
|
|
eed7b7186d | ||
|
|
d5e7ebdc63 | ||
|
|
3ecfa6aca8 | ||
|
|
625c1741c6 | ||
|
|
f6f5ec5eb3 | ||
|
|
c74feb9c3a | ||
|
|
0d76f80223 | ||
|
|
1e64513c16 | ||
|
|
64779acf32 | ||
|
|
c3a3ac19f4 | ||
|
|
b9bae3f66d | ||
|
|
2a2486cbe0 | ||
|
|
0813d99b44 | ||
|
|
491e89d102 | ||
|
|
f01558251c | ||
|
|
8665d0420b | ||
|
|
cf0636ca63 | ||
|
|
46d0aa6f9e | ||
|
|
b9e2be2052 | ||
|
|
b3054d68bf | ||
|
|
60adf0a9c3 | ||
|
|
be5d7022cc | ||
|
|
d1951b286c | ||
|
|
dcdef2f640 | ||
|
|
7afe74310f | ||
|
|
826f82610e | ||
|
|
5d7796b95d | ||
|
|
b3ac313cc7 | ||
|
|
b281ba7754 | ||
|
|
10994b202b | ||
|
|
2aeac1bdeb | ||
|
|
8508c21080 | ||
|
|
20dd140c31 | ||
|
|
486c19079a | ||
|
|
f30501ca3c | ||
|
|
e67e6e267b | ||
|
|
8dc757ddf3 | ||
|
|
b64f7d013d | ||
|
|
62ec936f1e | ||
|
|
8d83dfad45 | ||
|
|
e450072f45 | ||
|
|
7f08d08a78 | ||
|
|
b0634cd871 | ||
|
|
462485bfcb | ||
|
|
2311765289 | ||
|
|
7bc7da5499 | ||
|
|
b712a4771e | ||
|
|
8e05f09fc8 | ||
|
|
84c49fbe34 | ||
|
|
7750956c7b | ||
|
|
ea9af210f9 | ||
|
|
efca71510a | ||
|
|
cbf6348055 | ||
|
|
ec680593b0 | ||
|
|
fd6c25daaa | ||
|
|
4b495f213f | ||
|
|
7ad03fb548 | ||
|
|
17c641845e | ||
|
|
e53b9d984b | ||
|
|
28593d93ff | ||
|
|
fa4920bd94 | ||
|
|
eaf5c6f86f | ||
|
|
0d89b98bad | ||
|
|
bf56345e48 | ||
|
|
2bc58bebce | ||
|
|
c564702eac | ||
|
|
9400dd799e | ||
|
|
ff0bbc3f96 | ||
|
|
15414f5ee4 | ||
|
|
f9b097794f | ||
|
|
a2f65eb540 | ||
|
|
cea38a10e9 | ||
|
|
c8a91d4cf6 | ||
|
|
b0ff325125 | ||
|
|
c35c09db60 | ||
|
|
49adb61146 | ||
|
|
76a9034668 | ||
|
|
4c225e515d | ||
|
|
9c913b2e6c | ||
|
|
5ab1d2a8a5 | ||
|
|
2f3a581859 | ||
|
|
8bdd2a14e8 | ||
|
|
1675f69582 | ||
|
|
94d2d28806 | ||
|
|
82a5e50056 | ||
|
|
46062e185a | ||
|
|
6929141210 | ||
|
|
cce36c5fbd | ||
|
|
2518287326 | ||
|
|
aefab86501 | ||
|
|
30679d18ee | ||
|
|
95c0ff6f39 | ||
|
|
4d6f59ecb8 | ||
|
|
4b5668f4fd | ||
|
|
44a5fa011a | ||
|
|
14a3a662fd | ||
|
|
41409031fd | ||
|
|
ea410c8ced | ||
|
|
aca64eedca | ||
|
|
0f8b47b598 | ||
|
|
5eae15889d | ||
|
|
9319e4a7f1 | ||
|
|
4d756b5bfc | ||
|
|
409969621d | ||
|
|
7abb7277c9 | ||
|
|
9120b9c1de | ||
|
|
08c11ac41f | ||
|
|
cecc03e1ed | ||
|
|
7d67d131c2 | ||
|
|
1929eed8ac | ||
|
|
ad8c9fac2b | ||
|
|
fa82160265 | ||
|
|
dc1456f4e8 | ||
|
|
3ad19dffa1 | ||
|
|
bfb9db235e | ||
|
|
c57e50c5b9 | ||
|
|
bafdca3ffa | ||
|
|
ba12945e5b | ||
|
|
96906df64b | ||
|
|
3396c70b67 | ||
|
|
28d5c682cd | ||
|
|
7a03562a33 | ||
|
|
4a31dd8aa3 | ||
|
|
1b1b7cdfb0 | ||
|
|
9e13ffb8ff | ||
|
|
ed38705efd | ||
|
|
1a1cd0353c | ||
|
|
4f0b071c59 | ||
|
|
9ce574a1f0 | ||
|
|
c54b50eb0c | ||
|
|
aec7455151 | ||
|
|
c7ba567d7f | ||
|
|
fc1b3d5397 | ||
|
|
508741c367 | ||
|
|
f02de77295 | ||
|
|
9974b56607 | ||
|
|
0506a7bb53 | ||
|
|
06f161c423 | ||
|
|
69f5bb9ed3 | ||
|
|
490eb40028 | ||
|
|
43a558f5ae | ||
|
|
e4ae2df1a4 | ||
|
|
1620138421 | ||
|
|
e59fc903f2 | ||
|
|
4d8cdc6dc8 | ||
|
|
21afa1f4b3 | ||
|
|
05c5d06df5 | ||
|
|
9e8b765f7a | ||
|
|
36dbc28bde | ||
|
|
26eda90f7e | ||
|
|
211fa3d947 | ||
|
|
67bbd9957d | ||
|
|
aff2250504 | ||
|
|
86b1c851c0 | ||
|
|
0a03dcb465 | ||
|
|
e073e3388d | ||
|
|
626fae0da0 | ||
|
|
a708a7f387 | ||
|
|
b1242207a9 | ||
|
|
980571073d | ||
|
|
5e1fe656e8 | ||
|
|
ffbfd36502 | ||
|
|
e908cb0ec4 | ||
|
|
95a64b7696 | ||
|
|
cfd6fc9532 | ||
|
|
defab0c774 | ||
|
|
babac692d5 | ||
|
|
c57bb9ef72 |
4
.github/actions/install/action.yml
vendored
4
.github/actions/install/action.yml
vendored
@@ -17,7 +17,7 @@ inputs:
|
||||
zig-v8:
|
||||
description: 'zig v8 version to install'
|
||||
required: false
|
||||
default: 'v0.1.6'
|
||||
default: 'v0.1.11'
|
||||
v8:
|
||||
description: 'v8 version to install'
|
||||
required: false
|
||||
@@ -47,7 +47,7 @@ runs:
|
||||
cache-name: cache-v8
|
||||
with:
|
||||
path: ${{ inputs.cache-dir }}/v8
|
||||
key: libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}.a
|
||||
key: libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}_${{ inputs.zig-v8 }}.a
|
||||
|
||||
- if: ${{ steps.cache-v8.outputs.cache-hit != 'true' }}
|
||||
shell: bash
|
||||
|
||||
12
.github/workflows/build.yml
vendored
12
.github/workflows/build.yml
vendored
@@ -16,13 +16,12 @@ jobs:
|
||||
ARCH: x86_64
|
||||
OS: linux
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GH_CI_PAT }}
|
||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
||||
submodules: recursive
|
||||
|
||||
@@ -32,13 +31,13 @@ jobs:
|
||||
run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8 -Dcpu=x86_64
|
||||
|
||||
- name: Rename binary
|
||||
run: mv zig-out/bin/browsercore-get lightpanda-get-${{ env.ARCH }}-${{ env.OS }}
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
|
||||
- name: Upload the build
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: lightpanda-get-${{ env.ARCH }}-${{ env.OS }}
|
||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
tag: nightly
|
||||
|
||||
build-macos-aarch64:
|
||||
@@ -52,7 +51,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GH_CI_PAT }}
|
||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
||||
submodules: recursive
|
||||
|
||||
@@ -65,11 +63,11 @@ jobs:
|
||||
run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8
|
||||
|
||||
- name: Rename binary
|
||||
run: mv zig-out/bin/browsercore-get lightpanda-get-${{ env.ARCH }}-${{ env.OS }}
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
|
||||
- name: Upload the build
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: lightpanda-get-${{ env.ARCH }}-${{ env.OS }}
|
||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
tag: nightly
|
||||
|
||||
32
.github/workflows/cla.yml
vendored
Normal file
32
.github/workflows/cla.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: "CLA Assistant"
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_target:
|
||||
types: [opened,closed,synchronize]
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
|
||||
jobs:
|
||||
CLAAssistant:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "CLA Assistant"
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||
uses: contributor-assistant/github-action@v2.6.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_GH_PAT }}
|
||||
with:
|
||||
path-to-signatures: 'signatures/browser/version1/cla.json'
|
||||
path-to-document: 'https://github.com/lightpanda-io/browser/blob/main/CLA.md'
|
||||
# branch should not be protected
|
||||
branch: 'main'
|
||||
allowlist: krichprollsch,francisbouvier,katie-lpd
|
||||
|
||||
remote-organization-name: lightpanda-io
|
||||
remote-repository-name: cla
|
||||
1
.github/workflows/wpt.yml
vendored
1
.github/workflows/wpt.yml
vendored
@@ -50,7 +50,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GH_CI_PAT }}
|
||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
||||
submodules: recursive
|
||||
|
||||
|
||||
38
.github/workflows/zig-test.yml
vendored
38
.github/workflows/zig-test.yml
vendored
@@ -48,7 +48,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GH_CI_PAT }}
|
||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
||||
submodules: recursive
|
||||
|
||||
@@ -57,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
|
||||
|
||||
@@ -69,7 +76,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GH_CI_PAT }}
|
||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
||||
submodules: recursive
|
||||
|
||||
@@ -90,7 +96,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GH_CI_PAT }}
|
||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
||||
submodules: recursive
|
||||
|
||||
@@ -134,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
.gitignore
vendored
4
.gitignore
vendored
@@ -1,7 +1,5 @@
|
||||
zig-cache
|
||||
/.zig-cache/
|
||||
zig-out
|
||||
/vendor/netsurf/build/
|
||||
/vendor/netsurf/lib/
|
||||
/vendor/netsurf/include/
|
||||
/vendor/netsurf/out
|
||||
/vendor/libiconv/
|
||||
|
||||
7
.gitmodules
vendored
7
.gitmodules
vendored
@@ -25,3 +25,10 @@
|
||||
[submodule "vendor/tls.zig"]
|
||||
path = vendor/tls.zig
|
||||
url = git@github.com:ianic/tls.zig.git
|
||||
[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
|
||||
|
||||
93
CLA.md
Normal file
93
CLA.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Lightpanda (Selecy SAS) Grant and Contributor License Agreement (“Agreement”)
|
||||
|
||||
This agreement is based on the Apache Software Foundation Contributor License
|
||||
Agreement. (v r190612)
|
||||
|
||||
Thank you for your interest in software projects stewarded by Lightpanda
|
||||
(Selecy SAS) (“Lightpanda”). In order to clarify the intellectual property
|
||||
license granted with Contributions from any person or entity, Lightpanda must
|
||||
have a Contributor License Agreement (CLA) on file that has been agreed to by
|
||||
each Contributor, indicating agreement to the license terms below. This license
|
||||
is for your protection as a Contributor as well as the protection of Lightpanda
|
||||
and its users; it does not change your rights to use your own Contributions for
|
||||
any other purpose. This Agreement allows an individual to contribute to
|
||||
Lightpanda on that individual’s own behalf, or an entity (the “Corporation”) to
|
||||
submit Contributions to Lightpanda, to authorize Contributions submitted by its
|
||||
designated employees to Lightpanda, and to grant copyright and patent licenses
|
||||
thereto.
|
||||
|
||||
You accept and agree to the following terms and conditions for Your present and
|
||||
future Contributions submitted to Lightpanda. Except for the license granted
|
||||
herein to Lightpanda and recipients of software distributed by Lightpanda, You
|
||||
reserve all right, title, and interest in and to Your Contributions.
|
||||
|
||||
1. Definitions. “You” (or “Your”) shall mean the copyright owner or legal
|
||||
entity authorized by the copyright owner that is making this Agreement with
|
||||
Lightpanda. For legal entities, the entity making a Contribution and all
|
||||
other entities that control, are controlled by, or are under common control
|
||||
with that entity are considered to be a single Contributor. For the purposes
|
||||
of this definition, “control” means (i) the power, direct or indirect, to
|
||||
cause the direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
“Contribution” shall mean any work, as well as any modifications or
|
||||
additions to an existing work, that is intentionally submitted by You to
|
||||
Lightpanda for inclusion in, or documentation of, any of the products owned
|
||||
or managed by Lightpanda (the “Work”). For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication
|
||||
sent to Lightpanda or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems (such
|
||||
as GitHub), and issue tracking systems that are managed by, or on behalf of,
|
||||
Lightpanda for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise designated
|
||||
in writing by You as “Not a Contribution.”
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of this
|
||||
Agreement, You hereby grant to Lightpanda and to recipients of software
|
||||
distributed by Lightpanda a perpetual, worldwide, non-exclusive, no-charge,
|
||||
royalty-free, irrevocable copyright license to reproduce, prepare derivative
|
||||
works of, publicly display, publicly perform, sublicense, and distribute
|
||||
Your Contributions and such derivative works.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of this
|
||||
Agreement, You hereby grant to Lightpanda and to recipients of software
|
||||
distributed by Lightpanda a perpetual, worldwide, non-exclusive, no-charge,
|
||||
royalty-free, irrevocable (except as stated in this section) patent license
|
||||
to make, have made, use, offer to sell, sell, import, and otherwise transfer
|
||||
the Work, where such license applies only to those patent claims licensable
|
||||
by You that are necessarily infringed by Your Contribution(s) alone or by
|
||||
combination of Your Contribution(s) with the Work to which such
|
||||
Contribution(s) were submitted. If any entity institutes patent litigation
|
||||
against You or any other entity (including a cross-claim or counterclaim in
|
||||
a lawsuit) alleging that your Contribution, or the Work to which you have
|
||||
contributed, constitutes direct or contributory patent infringement, then
|
||||
any patent licenses granted to that entity under this Agreement for that
|
||||
Contribution or Work shall terminate as of the date such litigation is
|
||||
filed.
|
||||
|
||||
4. You represent that You are legally entitled to grant the above license. If
|
||||
You are an individual, and if Your employer(s) has rights to intellectual
|
||||
property that you create that includes Your Contributions, you represent
|
||||
that You have received permission to make Contributions on behalf of that
|
||||
employer, or that Your employer has waived such rights for your
|
||||
Contributions to Lightpanda. If You are a Corporation, any individual who
|
||||
makes a contribution from an account associated with You will be considered
|
||||
authorized to Contribute on Your behalf.
|
||||
|
||||
5. You represent that each of Your Contributions is Your original creation (see
|
||||
section 7 for submissions on behalf of others).
|
||||
|
||||
6. You are not expected to provide support for Your Contributions,except to the
|
||||
extent You desire to provide support. You may provide support for free, for
|
||||
a fee, or not at all. Unless required by applicable law or agreed to in
|
||||
writing, You provide Your Contributions on an “AS IS” BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including,
|
||||
without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT,
|
||||
MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
7. Should You wish to submit work that is not Your original creation, You may
|
||||
submit it to Lightpanda separately from any Contribution, identifying the
|
||||
complete details of its source and of any license or other restriction
|
||||
(including, but not limited to, related patents, trademarks, and license
|
||||
agreements) of which you are personally aware, and conspicuously marking the
|
||||
work as “Submitted on behalf of a third-party: [named here]”.
|
||||
10
CONTRIBUTING.md
Normal file
10
CONTRIBUTING.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Contributing
|
||||
|
||||
Lightpanda accepts pull requests through GitHub.
|
||||
|
||||
You have to sign our [CLA](CLA.md) during your first pull request process
|
||||
otherwise we're not able to accept your contributions.
|
||||
|
||||
The process signature uses the [CLA assistant
|
||||
lite](https://github.com/marketplace/actions/cla-assistant-lite). You can see
|
||||
an example of the process in [#303](https://github.com/lightpanda-io/browser/pull/303).
|
||||
79
Dockerfile
Normal file
79
Dockerfile
Normal file
@@ -0,0 +1,79 @@
|
||||
FROM ubuntu:22.04
|
||||
|
||||
ARG ZIG=0.13.0
|
||||
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
||||
ARG OS=linux
|
||||
ARG ARCH=x86_64
|
||||
ARG V8=11.1.134
|
||||
ARG ZIG_V8=v0.1.9
|
||||
|
||||
RUN apt-get update -yq && \
|
||||
apt-get install -yq xz-utils \
|
||||
python3 ca-certificates git \
|
||||
pkg-config libglib2.0-dev \
|
||||
gperf libexpat1-dev \
|
||||
cmake clang \
|
||||
curl git
|
||||
|
||||
# install minisig
|
||||
RUN curl -L -O https://github.com/jedisct1/minisign/releases/download/0.11/minisign-0.11-linux.tar.gz && \
|
||||
tar xvzf minisign-0.11-linux.tar.gz
|
||||
|
||||
# install zig
|
||||
RUN curl -O https://ziglang.org/download/${ZIG}/zig-linux-x86_64-${ZIG}.tar.xz && \
|
||||
curl -O https://ziglang.org/download/${ZIG}/zig-linux-x86_64-${ZIG}.tar.xz.minisig
|
||||
|
||||
RUN minisign-linux/x86_64/minisign -Vm zig-linux-x86_64-${ZIG}.tar.xz -P ${ZIG_MINISIG}
|
||||
|
||||
# clean minisg
|
||||
RUN rm -fr minisign-0.11-linux.tar.gz minisign-linux
|
||||
|
||||
# install zig
|
||||
RUN tar xvf zig-linux-x86_64-${ZIG}.tar.xz && \
|
||||
mv zig-linux-x86_64-${ZIG} /usr/local/lib && \
|
||||
ln -s /usr/local/lib/zig-linux-x86_64-${ZIG}/zig /usr/local/bin/zig
|
||||
|
||||
# clean up zig install
|
||||
RUN rm -fr zig-linux-x86_64-${ZIG}.tar.xz zig-linux-x86_64-${ZIG}.tar.xz.minisig
|
||||
|
||||
# force use of http instead of ssh with github
|
||||
RUN cat <<EOF > /root/.gitconfig
|
||||
[url "https://github.com/"]
|
||||
insteadOf="git@github.com:"
|
||||
EOF
|
||||
|
||||
# clone lightpanda
|
||||
RUN git clone git@github.com:lightpanda-io/browser.git
|
||||
|
||||
WORKDIR /browser
|
||||
|
||||
# install deps
|
||||
RUN git submodule init && \
|
||||
git submodule update --recursive
|
||||
|
||||
RUN cd vendor/zig-js-runtime && \
|
||||
git submodule init && \
|
||||
git submodule update --recursive
|
||||
|
||||
RUN make install-libiconv && \
|
||||
make install-netsurf && \
|
||||
make install-mimalloc
|
||||
|
||||
# download and install v8
|
||||
RUN curl -L -o libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${ZIG_V8}/libc_v8_${V8}_${OS}_${ARCH}.a && \
|
||||
mkdir -p vendor/zig-js-runtime/vendor/v8/${ARCH}-${OS}/release && \
|
||||
mv libc_v8.a vendor/zig-js-runtime/vendor/v8/${ARCH}-${OS}/release/libc_v8.a
|
||||
|
||||
# build release
|
||||
RUN make build
|
||||
|
||||
FROM ubuntu:22.04
|
||||
|
||||
# copy ca certificates
|
||||
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
COPY --from=0 /browser/zig-out/bin/lightpanda /bin/lightpanda
|
||||
|
||||
EXPOSE 9222/tcp
|
||||
|
||||
CMD ["/bin/lightpanda", "--host", "0.0.0.0", "--port", "9222"]
|
||||
23
LICENSING.md
Normal file
23
LICENSING.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Licensing
|
||||
|
||||
License names used in this document are as per [SPDX License
|
||||
List](https://spdx.org/licenses/).
|
||||
|
||||
The default license for this project is [AGPL-3.0-only](LICENSE).
|
||||
|
||||
## MIT
|
||||
|
||||
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
|
||||
original upstream licenses:
|
||||
|
||||
```
|
||||
vendor/
|
||||
tests/wpt/
|
||||
```
|
||||
101
Makefile
101
Makefile
@@ -4,6 +4,25 @@
|
||||
ZIG := zig
|
||||
BC := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
|
||||
# OS and ARCH
|
||||
kernel = $(shell uname -ms)
|
||||
ifeq ($(kernel), Darwin arm64)
|
||||
OS := macos
|
||||
ARCH := aarch64
|
||||
else ifeq ($(kernel), Linux aarch64)
|
||||
OS := linux
|
||||
ARCH := aarch64
|
||||
else ifeq ($(kernel), Linux arm64)
|
||||
OS := linux
|
||||
ARCH := aarch64
|
||||
else ifeq ($(kernel), Linux x86_64)
|
||||
OS := linux
|
||||
ARCH := x86_64
|
||||
else
|
||||
$(error "Unhandled kernel: $(kernel)")
|
||||
endif
|
||||
|
||||
|
||||
# Infos
|
||||
# -----
|
||||
.PHONY: help
|
||||
@@ -26,30 +45,11 @@ help:
|
||||
.PHONY: build build-dev run run-release shell test bench download-zig wpt
|
||||
|
||||
zig_version = $(shell grep 'recommended_zig_version = "' "vendor/zig-js-runtime/build.zig" | cut -d'"' -f2)
|
||||
kernel = $(shell uname -ms)
|
||||
|
||||
## Download the zig recommended version
|
||||
download-zig:
|
||||
ifeq ($(kernel), Darwin x86_64)
|
||||
$(eval target="macos")
|
||||
$(eval arch="x86_64")
|
||||
else ifeq ($(kernel), Darwin arm64)
|
||||
$(eval target="macos")
|
||||
$(eval arch="aarch64")
|
||||
else ifeq ($(kernel), Linux aarch64)
|
||||
$(eval target="linux")
|
||||
$(eval arch="aarch64")
|
||||
else ifeq ($(kernel), Linux arm64)
|
||||
$(eval target="linux")
|
||||
$(eval arch="aarch64")
|
||||
else ifeq ($(kernel), Linux x86_64)
|
||||
$(eval target="linux")
|
||||
$(eval arch="x86_64")
|
||||
else
|
||||
$(error "Unhandled kernel: $(kernel)")
|
||||
endif
|
||||
$(eval url = "https://ziglang.org/builds/zig-$(target)-$(arch)-$(zig_version).tar.xz")
|
||||
$(eval dest = "/tmp/zig-$(target)-$(arch)-$(zig_version).tar.xz")
|
||||
$(eval url = "https://ziglang.org/download/$(zig_version)/zig-$(OS)-$(ARCH)-$(zig_version).tar.xz")
|
||||
$(eval dest = "/tmp/zig-$(OS)-$(ARCH)-$(zig_version).tar.xz")
|
||||
@printf "\e[36mDownload zig version $(zig_version)...\e[0m\n"
|
||||
@curl -o "$(dest)" -L "$(url)" || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
@printf "\e[33mDownloaded $(dest)\e[0m\n"
|
||||
@@ -69,7 +69,7 @@ build-dev:
|
||||
## Run the server in debug mode
|
||||
run: build
|
||||
@printf "\e[36mRunning...\e[0m\n"
|
||||
@./zig-out/bin/browsercore || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
|
||||
@./zig-out/bin/lightpanda || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
|
||||
|
||||
## Run a JS shell in debug mode
|
||||
shell:
|
||||
@@ -100,10 +100,10 @@ test:
|
||||
.PHONY: install-dev install
|
||||
|
||||
## Install and build dependencies for release
|
||||
install: install-submodule install-zig-js-runtime install-netsurf install-mimalloc
|
||||
install: install-submodule install-zig-js-runtime install-libiconv install-netsurf install-mimalloc
|
||||
|
||||
## Install and build dependencies for dev
|
||||
install-dev: install-submodule install-zig-js-runtime-dev install-netsurf-dev install-mimalloc-dev
|
||||
install-dev: install-submodule install-zig-js-runtime-dev install-libiconv install-netsurf-dev install-mimalloc-dev
|
||||
|
||||
install-netsurf-dev: _install-netsurf
|
||||
install-netsurf-dev: OPTCFLAGS := -O0 -g -DNDEBUG
|
||||
@@ -111,14 +111,16 @@ install-netsurf-dev: OPTCFLAGS := -O0 -g -DNDEBUG
|
||||
install-netsurf: _install-netsurf
|
||||
install-netsurf: OPTCFLAGS := -DNDEBUG
|
||||
|
||||
BC_NS := $(BC)vendor/netsurf
|
||||
ICONV := $(BC)vendor/libiconv
|
||||
BC_NS := $(BC)vendor/netsurf/out/$(OS)-$(ARCH)
|
||||
ICONV := $(BC)vendor/libiconv/out/$(OS)-$(ARCH)
|
||||
# TODO: add Linux iconv path (I guess it depends on the distro)
|
||||
# TODO: this way of linking libiconv is not ideal. We should have a more generic way
|
||||
# and stick to a specif version. Maybe build from source. Anyway not now.
|
||||
_install-netsurf: install-libiconv
|
||||
_install-netsurf: clean-netsurf
|
||||
@printf "\e[36mInstalling NetSurf...\e[0m\n" && \
|
||||
ls $(ICONV) 1> /dev/null || (printf "\e[33mERROR: you need to install libiconv in your system (on MacOS on with Homebrew)\e[0m\n"; exit 1;) && \
|
||||
ls $(ICONV)/lib/libiconv.a 1> /dev/null || (printf "\e[33mERROR: you need to execute 'make install-libiconv'\e[0m\n"; exit 1;) && \
|
||||
mkdir -p $(BC_NS) && \
|
||||
cp -R vendor/netsurf/share $(BC_NS) && \
|
||||
export PREFIX=$(BC_NS) && \
|
||||
export OPTLDFLAGS="-L$(ICONV)/lib" && \
|
||||
export OPTCFLAGS="$(OPTCFLAGS) -I$(ICONV)/include" && \
|
||||
@@ -156,10 +158,7 @@ _install-netsurf: install-libiconv
|
||||
|
||||
clean-netsurf:
|
||||
@printf "\e[36mCleaning NetSurf build...\e[0m\n" && \
|
||||
cd vendor/netsurf && \
|
||||
rm -R build && \
|
||||
rm -R lib && \
|
||||
rm -R include
|
||||
rm -Rf $(BC_NS)
|
||||
|
||||
test-netsurf:
|
||||
@printf "\e[36mTesting NetSurf...\e[0m\n" && \
|
||||
@@ -169,14 +168,22 @@ test-netsurf:
|
||||
cd vendor/netsurf/libdom && \
|
||||
BUILDDIR=$(BC_NS)/build/libdom make test
|
||||
|
||||
install-libiconv:
|
||||
ifeq ("$(wildcard vendor/libiconv/lib/libiconv.a)","")
|
||||
download-libiconv:
|
||||
ifeq ("$(wildcard vendor/libiconv/libiconv-1.17)","")
|
||||
@mkdir -p vendor/libiconv
|
||||
@cd vendor/libiconv && \
|
||||
curl https://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.17.tar.gz | tar -xvzf -
|
||||
endif
|
||||
|
||||
install-libiconv: download-libiconv clean-libiconv
|
||||
@cd vendor/libiconv/libiconv-1.17 && \
|
||||
./configure --prefix=$(BC)vendor/libiconv --enable-static && \
|
||||
./configure --prefix=$(ICONV) --enable-static && \
|
||||
make && make install
|
||||
|
||||
clean-libiconv:
|
||||
ifneq ("$(wildcard vendor/libiconv/libiconv-1.17/Makefile)","")
|
||||
@cd vendor/libiconv/libiconv-1.17 && \
|
||||
make clean
|
||||
endif
|
||||
|
||||
install-zig-js-runtime-dev:
|
||||
@@ -188,24 +195,28 @@ install-zig-js-runtime:
|
||||
make install
|
||||
|
||||
.PHONY: _build_mimalloc
|
||||
_build_mimalloc:
|
||||
@cd vendor/mimalloc && \
|
||||
mkdir -p out/include && \
|
||||
cp include/mimalloc.h out/include/ && \
|
||||
cd out && \
|
||||
cmake -DMI_BUILD_SHARED=OFF -DMI_BUILD_OBJECT=OFF -DMI_BUILD_TESTS=OFF -DMI_OVERRIDE=OFF $(OPTS) .. && \
|
||||
make
|
||||
|
||||
MIMALLOC := $(BC)vendor/mimalloc/out/$(OS)-$(ARCH)
|
||||
_build_mimalloc: clean-mimalloc
|
||||
@mkdir -p $(MIMALLOC)/build && \
|
||||
cd $(MIMALLOC)/build && \
|
||||
cmake -DMI_BUILD_SHARED=OFF -DMI_BUILD_OBJECT=OFF -DMI_BUILD_TESTS=OFF -DMI_OVERRIDE=OFF $(OPTS) ../../.. && \
|
||||
make && \
|
||||
mkdir -p $(MIMALLOC)/lib
|
||||
|
||||
install-mimalloc-dev: _build_mimalloc
|
||||
install-mimalloc-dev: OPTS=-DCMAKE_BUILD_TYPE=Debug
|
||||
install-mimalloc-dev:
|
||||
@cd vendor/mimalloc/out && \
|
||||
mv libmimalloc-debug.a libmimalloc.a
|
||||
@cd $(MIMALLOC) && \
|
||||
mv build/libmimalloc-debug.a lib/libmimalloc.a
|
||||
|
||||
install-mimalloc: _build_mimalloc
|
||||
install-mimalloc:
|
||||
@cd $(MIMALLOC) && \
|
||||
mv build/libmimalloc.a lib/libmimalloc.a
|
||||
|
||||
clean-mimalloc:
|
||||
@rm -fr vendor/mimalloc/lib/*
|
||||
@rm -Rf $(MIMALLOC)/build
|
||||
|
||||
## Init and update git submodule
|
||||
install-submodule:
|
||||
|
||||
119
README.md
119
README.md
@@ -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. It’s not possible anymore, because Javascript is everywhere, like it or not:
|
||||
In the good old days, scraping a webpage was as easy as making an HTTP request, cURL-like. It’s not possible anymore, because Javascript is everywhere, like it or not:
|
||||
|
||||
- Ajax, Single Page App, Infinite loading, “click to display”, instant search, etc.
|
||||
- Ajax, Single Page App, infinite loading, “click to display”, instant search, etc.
|
||||
- JS web frameworks: React, Vue, Angular & others
|
||||
|
||||
### Chrome is not the right tool
|
||||
|
||||
So if we need Javascript, why not use a real web browser. Let’s take a huge desktop application, hack it, and run it on the server, right? Hundreds of instance of Chrome if you use it at scale. Are you sure it’s such a good idea?
|
||||
If we need Javascript, why not use a real web browser? Take a huge desktop application, hack it, and run it on the server. Hundreds or thousands of instances of Chrome if you use it at scale. Are you sure it’s such a good idea?
|
||||
|
||||
- Heavy on RAM and CPU, expensive to run
|
||||
- Hard to package, deploy and maintain at scale
|
||||
@@ -42,37 +44,103 @@ So if we need Javascript, why not use a real web browser. Let’s take a huge de
|
||||
|
||||
### Lightpanda is built for performance
|
||||
|
||||
If we want both Javascript and performance, for a real headless browser, we need to start from scratch. Not yet another iteration of Chromium, really from a blank page. Crazy right? But that’s we did:
|
||||
If we want both Javascript and performance in a true headless browser, we need to start from scratch. Not another iteration of Chromium, really from a blank page. Crazy right? But that’s we did:
|
||||
|
||||
- Not based on Chromium, Blink or WebKit
|
||||
- Low-level system programming language (Zig) with optimisations in mind
|
||||
- Opinionated, 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
|
||||
|
||||
@@ -196,3 +264,10 @@ To add a new test, copy the file you want from the [WPT
|
||||
repo](https://github.com/web-platform-tests/wpt) into the `tests/wpt` directory.
|
||||
|
||||
:warning: Please keep the original directory tree structure of `tests/wpt`.
|
||||
|
||||
## Contributing
|
||||
|
||||
Lightpanda accepts pull requests through GitHub.
|
||||
|
||||
You have to sign our [CLA](CLA.md) during the pull request process otherwise
|
||||
we're not able to accept your contributions.
|
||||
|
||||
110
build.zig
110
build.zig
@@ -45,21 +45,17 @@ pub fn build(b: *std.Build) !void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const mode = b.standardOptimizeOption(.{});
|
||||
|
||||
const options = try jsruntime.buildOptions(b);
|
||||
|
||||
const x86 = b.option(bool, "x86", "Use x86 backend") orelse false;
|
||||
const options = jsruntime.buildOptions(b);
|
||||
|
||||
// browser
|
||||
// -------
|
||||
|
||||
// compile and install
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "browsercore",
|
||||
.name = "lightpanda",
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = mode,
|
||||
.use_llvm = !x86,
|
||||
.use_lld = !x86,
|
||||
});
|
||||
try common(b, exe, options);
|
||||
b.installArtifact(exe);
|
||||
@@ -79,12 +75,10 @@ pub fn build(b: *std.Build) !void {
|
||||
|
||||
// compile and install
|
||||
const shell = b.addExecutable(.{
|
||||
.name = "browsercore-shell",
|
||||
.name = "lightpanda-shell",
|
||||
.root_source_file = b.path("src/main_shell.zig"),
|
||||
.target = target,
|
||||
.optimize = mode,
|
||||
.use_llvm = !x86,
|
||||
.use_lld = !x86,
|
||||
});
|
||||
try common(b, shell, options);
|
||||
try jsruntime_pkgs.add_shell(shell);
|
||||
@@ -108,8 +102,6 @@ pub fn build(b: *std.Build) !void {
|
||||
.test_runner = b.path("src/test_runner.zig"),
|
||||
.target = target,
|
||||
.optimize = mode,
|
||||
.use_llvm = !x86,
|
||||
.use_lld = !x86,
|
||||
});
|
||||
try common(b, tests, options);
|
||||
|
||||
@@ -132,12 +124,10 @@ pub fn build(b: *std.Build) !void {
|
||||
|
||||
// compile and install
|
||||
const wpt = b.addExecutable(.{
|
||||
.name = "browsercore-wpt",
|
||||
.name = "lightpanda-wpt",
|
||||
.root_source_file = b.path("src/main_wpt.zig"),
|
||||
.target = target,
|
||||
.optimize = mode,
|
||||
.use_llvm = !x86,
|
||||
.use_lld = !x86,
|
||||
});
|
||||
try common(b, wpt, options);
|
||||
|
||||
@@ -149,30 +139,6 @@ pub fn build(b: *std.Build) !void {
|
||||
// step
|
||||
const wpt_step = b.step("wpt", "WPT tests");
|
||||
wpt_step.dependOn(&wpt_cmd.step);
|
||||
|
||||
// get
|
||||
// -----
|
||||
|
||||
// compile and install
|
||||
const get = b.addExecutable(.{
|
||||
.name = "browsercore-get",
|
||||
.root_source_file = b.path("src/main_get.zig"),
|
||||
.target = target,
|
||||
.optimize = mode,
|
||||
.use_llvm = !x86,
|
||||
.use_lld = !x86,
|
||||
});
|
||||
try common(b, get, options);
|
||||
b.installArtifact(get);
|
||||
|
||||
// run
|
||||
const get_cmd = b.addRunArtifact(get);
|
||||
if (b.args) |args| {
|
||||
get_cmd.addArgs(args);
|
||||
}
|
||||
// step
|
||||
const get_step = b.step("get", "request URL");
|
||||
get_step.dependOn(&get_cmd.step);
|
||||
}
|
||||
|
||||
fn common(
|
||||
@@ -180,38 +146,69 @@ fn common(
|
||||
step: *std.Build.Step.Compile,
|
||||
options: jsruntime.Options,
|
||||
) !void {
|
||||
const target = step.root_module.resolved_target.?;
|
||||
const jsruntimemod = try jsruntime_pkgs.module(
|
||||
b,
|
||||
options,
|
||||
step.root_module.optimize.?,
|
||||
step.root_module.resolved_target.?,
|
||||
target,
|
||||
);
|
||||
step.root_module.addImport("jsruntime", jsruntimemod);
|
||||
|
||||
const netsurf = moduleNetSurf(b);
|
||||
const netsurf = try moduleNetSurf(b, target);
|
||||
netsurf.addImport("jsruntime", jsruntimemod);
|
||||
step.root_module.addImport("netsurf", netsurf);
|
||||
|
||||
const asyncio = b.addModule("asyncio", .{
|
||||
.root_source_file = b.path("vendor/zig-async-io/src/lib.zig"),
|
||||
});
|
||||
step.root_module.addImport("asyncio", asyncio);
|
||||
|
||||
const tlsmod = b.addModule("tls", .{
|
||||
.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) *std.Build.Module {
|
||||
fn moduleNetSurf(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module {
|
||||
const mod = b.addModule("netsurf", .{
|
||||
.root_source_file = b.path("src/netsurf/netsurf.zig"),
|
||||
.target = target,
|
||||
});
|
||||
|
||||
const os = target.result.os.tag;
|
||||
const arch = target.result.cpu.arch;
|
||||
|
||||
// iconv
|
||||
mod.addObjectFile(b.path("vendor/libiconv/lib/libiconv.a"));
|
||||
mod.addIncludePath(b.path("vendor/libiconv/include"));
|
||||
const libiconv_lib_path = try std.fmt.allocPrint(
|
||||
mod.owner.allocator,
|
||||
"vendor/libiconv/out/{s}-{s}/lib/libiconv.a",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
const libiconv_include_path = try std.fmt.allocPrint(
|
||||
mod.owner.allocator,
|
||||
"vendor/libiconv/out/{s}-{s}/lib/libiconv.a",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
mod.addObjectFile(b.path(libiconv_lib_path));
|
||||
mod.addIncludePath(b.path(libiconv_include_path));
|
||||
|
||||
// mimalloc
|
||||
mod.addImport("mimalloc", moduleMimalloc(b));
|
||||
mod.addImport("mimalloc", (try moduleMimalloc(b, target)));
|
||||
|
||||
// netsurf libs
|
||||
const ns = "vendor/netsurf";
|
||||
mod.addIncludePath(b.path(ns ++ "/include"));
|
||||
const ns_include_path = try std.fmt.allocPrint(
|
||||
mod.owner.allocator,
|
||||
ns ++ "/out/{s}-{s}/include",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
mod.addIncludePath(b.path(ns_include_path));
|
||||
|
||||
const libs: [4][]const u8 = .{
|
||||
"libdom",
|
||||
@@ -220,20 +217,35 @@ fn moduleNetSurf(b: *std.Build) *std.Build.Module {
|
||||
"libwapcaplet",
|
||||
};
|
||||
inline for (libs) |lib| {
|
||||
mod.addObjectFile(b.path(ns ++ "/lib/" ++ lib ++ ".a"));
|
||||
const ns_lib_path = try std.fmt.allocPrint(
|
||||
mod.owner.allocator,
|
||||
ns ++ "/out/{s}-{s}/lib/" ++ lib ++ ".a",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
mod.addObjectFile(b.path(ns_lib_path));
|
||||
mod.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src"));
|
||||
}
|
||||
|
||||
return mod;
|
||||
}
|
||||
|
||||
fn moduleMimalloc(b: *std.Build) *std.Build.Module {
|
||||
fn moduleMimalloc(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module {
|
||||
const mod = b.addModule("mimalloc", .{
|
||||
.root_source_file = b.path("src/mimalloc/mimalloc.zig"),
|
||||
.target = target,
|
||||
});
|
||||
|
||||
mod.addObjectFile(b.path("vendor/mimalloc/out/libmimalloc.a"));
|
||||
mod.addIncludePath(b.path("vendor/mimalloc/out/include"));
|
||||
const os = target.result.os.tag;
|
||||
const arch = target.result.cpu.arch;
|
||||
|
||||
const mimalloc = "vendor/mimalloc";
|
||||
const lib_path = try std.fmt.allocPrint(
|
||||
mod.owner.allocator,
|
||||
mimalloc ++ "/out/{s}-{s}/lib/libmimalloc.a",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
mod.addObjectFile(b.path(lib_path));
|
||||
mod.addIncludePath(b.path(mimalloc ++ "/include"));
|
||||
|
||||
return mod;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ const Events = @import("events/event.zig");
|
||||
const XHR = @import("xhr/xhr.zig");
|
||||
const Storage = @import("storage/storage.zig");
|
||||
const URL = @import("url/url.zig");
|
||||
const Iterators = @import("iterator/iterator.zig");
|
||||
|
||||
pub const HTMLDocument = @import("html/document.zig").HTMLDocument;
|
||||
|
||||
@@ -38,6 +39,7 @@ pub const Interfaces = generate.Tuple(.{
|
||||
XHR.Interfaces,
|
||||
Storage.Interfaces,
|
||||
URL.Interfaces,
|
||||
Iterators.Interfaces,
|
||||
});
|
||||
|
||||
pub const UserContext = @import("user_context.zig").UserContext;
|
||||
|
||||
1766
src/async/Client.zig
1766
src/async/Client.zig
File diff suppressed because it is too large
Load Diff
@@ -1,133 +0,0 @@
|
||||
// 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 posix = std.posix;
|
||||
const io = std.io;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const tcp = @import("tcp.zig");
|
||||
|
||||
pub const Stream = struct {
|
||||
alloc: std.mem.Allocator,
|
||||
conn: *tcp.Conn,
|
||||
|
||||
handle: posix.socket_t,
|
||||
|
||||
pub fn close(self: Stream) void {
|
||||
posix.close(self.handle);
|
||||
self.alloc.destroy(self.conn);
|
||||
}
|
||||
|
||||
pub const ReadError = posix.ReadError;
|
||||
pub const WriteError = posix.WriteError;
|
||||
|
||||
pub const Reader = io.Reader(Stream, ReadError, read);
|
||||
pub const Writer = io.Writer(Stream, WriteError, write);
|
||||
|
||||
pub fn reader(self: Stream) Reader {
|
||||
return .{ .context = self };
|
||||
}
|
||||
|
||||
pub fn writer(self: Stream) Writer {
|
||||
return .{ .context = self };
|
||||
}
|
||||
|
||||
pub fn read(self: Stream, buffer: []u8) ReadError!usize {
|
||||
return self.conn.receive(self.handle, buffer) catch |err| switch (err) {
|
||||
else => return error.Unexpected,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn readv(s: Stream, iovecs: []const posix.iovec) ReadError!usize {
|
||||
return posix.readv(s.handle, iovecs);
|
||||
}
|
||||
|
||||
/// Returns the number of bytes read. If the number read is smaller than
|
||||
/// `buffer.len`, it means the stream reached the end. Reaching the end of
|
||||
/// a stream is not an error condition.
|
||||
pub fn readAll(s: Stream, buffer: []u8) ReadError!usize {
|
||||
return readAtLeast(s, buffer, buffer.len);
|
||||
}
|
||||
|
||||
/// Returns the number of bytes read, calling the underlying read function
|
||||
/// the minimal number of times until the buffer has at least `len` bytes
|
||||
/// filled. If the number read is less than `len` it means the stream
|
||||
/// reached the end. Reaching the end of the stream is not an error
|
||||
/// condition.
|
||||
pub fn readAtLeast(s: Stream, buffer: []u8, len: usize) ReadError!usize {
|
||||
assert(len <= buffer.len);
|
||||
var index: usize = 0;
|
||||
while (index < len) {
|
||||
const amt = try s.read(buffer[index..]);
|
||||
if (amt == 0) break;
|
||||
index += amt;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
/// TODO in evented I/O mode, this implementation incorrectly uses the event loop's
|
||||
/// file system thread instead of non-blocking. It needs to be reworked to properly
|
||||
/// use non-blocking I/O.
|
||||
pub fn write(self: Stream, buffer: []const u8) WriteError!usize {
|
||||
return self.conn.send(self.handle, buffer) catch |err| switch (err) {
|
||||
error.AccessDenied => error.AccessDenied,
|
||||
error.WouldBlock => error.WouldBlock,
|
||||
error.ConnectionResetByPeer => error.ConnectionResetByPeer,
|
||||
error.MessageTooBig => error.FileTooBig,
|
||||
error.BrokenPipe => error.BrokenPipe,
|
||||
else => return error.Unexpected,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn writeAll(self: Stream, bytes: []const u8) WriteError!void {
|
||||
var index: usize = 0;
|
||||
while (index < bytes.len) {
|
||||
index += try self.write(bytes[index..]);
|
||||
}
|
||||
}
|
||||
|
||||
/// See https://github.com/ziglang/zig/issues/7699
|
||||
/// See equivalent function: `std.fs.File.writev`.
|
||||
pub fn writev(self: Stream, iovecs: []const posix.iovec_const) WriteError!usize {
|
||||
if (iovecs.len == 0) return 0;
|
||||
const first_buffer = iovecs[0].base[0..iovecs[0].len];
|
||||
return try self.write(first_buffer);
|
||||
}
|
||||
|
||||
/// The `iovecs` parameter is mutable because this function needs to mutate the fields in
|
||||
/// order to handle partial writes from the underlying OS layer.
|
||||
/// See https://github.com/ziglang/zig/issues/7699
|
||||
/// See equivalent function: `std.fs.File.writevAll`.
|
||||
pub fn writevAll(self: Stream, iovecs: []posix.iovec_const) WriteError!void {
|
||||
if (iovecs.len == 0) return;
|
||||
|
||||
var i: usize = 0;
|
||||
while (true) {
|
||||
var amt = try self.writev(iovecs[i..]);
|
||||
while (amt >= iovecs[i].len) {
|
||||
amt -= iovecs[i].len;
|
||||
i += 1;
|
||||
if (i >= iovecs.len) return;
|
||||
}
|
||||
iovecs[i].base += amt;
|
||||
iovecs[i].len -= amt;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,112 +0,0 @@
|
||||
// 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 net = std.net;
|
||||
const Stream = @import("stream.zig").Stream;
|
||||
const Loop = @import("jsruntime").Loop;
|
||||
const NetworkImpl = Loop.Network(Conn.Command);
|
||||
|
||||
// Conn is a TCP connection using jsruntime Loop async I/O.
|
||||
// connect, send and receive are blocking, but use async I/O in the background.
|
||||
// Client doesn't own the socket used for the connection, the caller is
|
||||
// responsible for closing it.
|
||||
pub const Conn = struct {
|
||||
const Command = struct {
|
||||
impl: NetworkImpl,
|
||||
|
||||
done: bool = false,
|
||||
err: ?anyerror = null,
|
||||
ln: usize = 0,
|
||||
|
||||
fn ok(self: *Command, err: ?anyerror, ln: usize) void {
|
||||
self.err = err;
|
||||
self.ln = ln;
|
||||
self.done = true;
|
||||
}
|
||||
|
||||
fn wait(self: *Command) !usize {
|
||||
while (!self.done) try self.impl.tick();
|
||||
|
||||
if (self.err) |err| return err;
|
||||
return self.ln;
|
||||
}
|
||||
pub fn onConnect(self: *Command, err: ?anyerror) void {
|
||||
self.ok(err, 0);
|
||||
}
|
||||
pub fn onSend(self: *Command, ln: usize, err: ?anyerror) void {
|
||||
self.ok(err, ln);
|
||||
}
|
||||
pub fn onReceive(self: *Command, ln: usize, err: ?anyerror) void {
|
||||
self.ok(err, ln);
|
||||
}
|
||||
};
|
||||
|
||||
loop: *Loop,
|
||||
|
||||
pub fn connect(self: *Conn, socket: std.posix.socket_t, address: std.net.Address) !void {
|
||||
var cmd = Command{ .impl = NetworkImpl.init(self.loop) };
|
||||
cmd.impl.connect(&cmd, socket, address);
|
||||
_ = try cmd.wait();
|
||||
}
|
||||
|
||||
pub fn send(self: *Conn, socket: std.posix.socket_t, buffer: []const u8) !usize {
|
||||
var cmd = Command{ .impl = NetworkImpl.init(self.loop) };
|
||||
cmd.impl.send(&cmd, socket, buffer);
|
||||
return try cmd.wait();
|
||||
}
|
||||
|
||||
pub fn receive(self: *Conn, socket: std.posix.socket_t, buffer: []u8) !usize {
|
||||
var cmd = Command{ .impl = NetworkImpl.init(self.loop) };
|
||||
cmd.impl.receive(&cmd, socket, buffer);
|
||||
return try cmd.wait();
|
||||
}
|
||||
};
|
||||
|
||||
pub fn tcpConnectToHost(alloc: std.mem.Allocator, loop: *Loop, name: []const u8, port: u16) !Stream {
|
||||
// TODO async resolve
|
||||
const list = try net.getAddressList(alloc, name, port);
|
||||
defer list.deinit();
|
||||
|
||||
if (list.addrs.len == 0) return error.UnknownHostName;
|
||||
|
||||
for (list.addrs) |addr| {
|
||||
return tcpConnectToAddress(alloc, loop, addr) catch |err| switch (err) {
|
||||
error.ConnectionRefused => {
|
||||
continue;
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
}
|
||||
return std.posix.ConnectError.ConnectionRefused;
|
||||
}
|
||||
|
||||
pub fn tcpConnectToAddress(alloc: std.mem.Allocator, loop: *Loop, addr: net.Address) !Stream {
|
||||
const sockfd = try std.posix.socket(addr.any.family, std.posix.SOCK.STREAM, std.posix.IPPROTO.TCP);
|
||||
errdefer std.posix.close(sockfd);
|
||||
|
||||
var conn = try alloc.create(Conn);
|
||||
conn.* = Conn{ .loop = loop };
|
||||
try conn.connect(sockfd, addr);
|
||||
|
||||
return Stream{
|
||||
.alloc = alloc,
|
||||
.conn = conn,
|
||||
.handle = sockfd,
|
||||
};
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
// 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 http = std.http;
|
||||
const Client = @import("Client.zig");
|
||||
const Request = @import("Client.zig").Request;
|
||||
|
||||
pub const Loop = @import("jsruntime").Loop;
|
||||
|
||||
const url = "https://w3.org";
|
||||
|
||||
test "blocking mode fetch API" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
var loop = try Loop.init(alloc);
|
||||
defer loop.deinit();
|
||||
|
||||
var client: Client = .{
|
||||
.allocator = alloc,
|
||||
.loop = &loop,
|
||||
};
|
||||
defer client.deinit();
|
||||
|
||||
// force client's CA cert scan from system.
|
||||
try client.ca_bundle.rescan(client.allocator);
|
||||
|
||||
const res = try client.fetch(.{
|
||||
.location = .{ .uri = try std.Uri.parse(url) },
|
||||
});
|
||||
|
||||
try std.testing.expect(res.status == .ok);
|
||||
}
|
||||
|
||||
test "blocking mode open/send/wait API" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
var loop = try Loop.init(alloc);
|
||||
defer loop.deinit();
|
||||
|
||||
var client: Client = .{
|
||||
.allocator = alloc,
|
||||
.loop = &loop,
|
||||
};
|
||||
defer client.deinit();
|
||||
|
||||
// force client's CA cert scan from system.
|
||||
try client.ca_bundle.rescan(client.allocator);
|
||||
|
||||
var buf: [2014]u8 = undefined;
|
||||
var req = try client.open(.GET, try std.Uri.parse(url), .{
|
||||
.server_header_buffer = &buf,
|
||||
});
|
||||
defer req.deinit();
|
||||
|
||||
try req.send();
|
||||
try req.finish();
|
||||
try req.wait();
|
||||
|
||||
try std.testing.expect(req.response.status == .ok);
|
||||
}
|
||||
|
||||
// Example how to write an async http client using the modified standard client.
|
||||
const AsyncClient = struct {
|
||||
cli: Client,
|
||||
|
||||
const YieldImpl = Loop.Yield(AsyncRequest);
|
||||
const AsyncRequest = struct {
|
||||
const State = enum { new, open, send, finish, wait, done };
|
||||
|
||||
cli: *Client,
|
||||
uri: std.Uri,
|
||||
|
||||
req: ?Request = undefined,
|
||||
state: State = .new,
|
||||
|
||||
impl: YieldImpl,
|
||||
err: ?anyerror = null,
|
||||
|
||||
buf: [2014]u8 = undefined,
|
||||
|
||||
pub fn deinit(self: *AsyncRequest) void {
|
||||
if (self.req) |*r| r.deinit();
|
||||
}
|
||||
|
||||
pub fn fetch(self: *AsyncRequest) void {
|
||||
self.state = .new;
|
||||
return self.impl.yield(self);
|
||||
}
|
||||
|
||||
fn onerr(self: *AsyncRequest, err: anyerror) void {
|
||||
self.state = .done;
|
||||
self.err = err;
|
||||
}
|
||||
|
||||
pub fn onYield(self: *AsyncRequest, err: ?anyerror) void {
|
||||
if (err) |e| return self.onerr(e);
|
||||
|
||||
switch (self.state) {
|
||||
.new => {
|
||||
self.state = .open;
|
||||
self.req = self.cli.open(.GET, self.uri, .{
|
||||
.server_header_buffer = &self.buf,
|
||||
}) catch |e| return self.onerr(e);
|
||||
},
|
||||
.open => {
|
||||
self.state = .send;
|
||||
self.req.?.send() catch |e| return self.onerr(e);
|
||||
},
|
||||
.send => {
|
||||
self.state = .finish;
|
||||
self.req.?.finish() catch |e| return self.onerr(e);
|
||||
},
|
||||
.finish => {
|
||||
self.state = .wait;
|
||||
self.req.?.wait() catch |e| return self.onerr(e);
|
||||
},
|
||||
.wait => {
|
||||
self.state = .done;
|
||||
return;
|
||||
},
|
||||
.done => return,
|
||||
}
|
||||
|
||||
return self.impl.yield(self);
|
||||
}
|
||||
|
||||
pub fn wait(self: *AsyncRequest) !void {
|
||||
while (self.state != .done) try self.impl.tick();
|
||||
if (self.err) |err| return err;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator, loop: *Loop) AsyncClient {
|
||||
return .{
|
||||
.cli = .{
|
||||
.allocator = alloc,
|
||||
.loop = loop,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *AsyncClient) void {
|
||||
self.cli.deinit();
|
||||
}
|
||||
|
||||
pub fn createRequest(self: *AsyncClient, uri: std.Uri) !AsyncRequest {
|
||||
return .{
|
||||
.impl = YieldImpl.init(self.cli.loop),
|
||||
.cli = &self.cli,
|
||||
.uri = uri,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
test "non blocking client" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
var loop = try Loop.init(alloc);
|
||||
defer loop.deinit();
|
||||
|
||||
var client = AsyncClient.init(alloc, &loop);
|
||||
defer client.deinit();
|
||||
|
||||
var reqs: [3]AsyncClient.AsyncRequest = undefined;
|
||||
for (0..reqs.len) |i| {
|
||||
reqs[i] = try client.createRequest(try std.Uri.parse(url));
|
||||
reqs[i].fetch();
|
||||
}
|
||||
for (0..reqs.len) |i| {
|
||||
try reqs[i].wait();
|
||||
reqs[i].deinit();
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -40,33 +41,43 @@ const storage = @import("../storage/storage.zig");
|
||||
const FetchResult = @import("../http/Client.zig").Client.FetchResult;
|
||||
|
||||
const UserContext = @import("../user_context.zig").UserContext;
|
||||
const HttpClient = @import("../async/Client.zig");
|
||||
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,
|
||||
session: Session = undefined,
|
||||
agent: []const u8 = user_agent,
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator, vm: jsruntime.VM) !Browser {
|
||||
const uri = "about:blank";
|
||||
|
||||
pub fn init(self: *Browser, alloc: std.mem.Allocator, loop: *Loop, vm: jsruntime.VM) !void {
|
||||
// We want to ensure the caller initialised a VM, but the browser
|
||||
// doesn't use it directly...
|
||||
_ = vm;
|
||||
|
||||
return Browser{
|
||||
.session = try Session.init(alloc, "about:blank"),
|
||||
};
|
||||
try Session.init(&self.session, alloc, loop, uri);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Browser) void {
|
||||
self.session.deinit();
|
||||
}
|
||||
|
||||
pub fn currentSession(self: *Browser) *Session {
|
||||
return self.session;
|
||||
pub fn newSession(
|
||||
self: *Browser,
|
||||
alloc: std.mem.Allocator,
|
||||
loop: *jsruntime.Loop,
|
||||
) !void {
|
||||
self.session.deinit();
|
||||
try Session.init(&self.session, alloc, loop, uri);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -90,37 +101,52 @@ pub const Session = struct {
|
||||
// TODO handle proxy
|
||||
loader: Loader,
|
||||
env: Env = undefined,
|
||||
loop: Loop,
|
||||
inspector: ?jsruntime.Inspector = null,
|
||||
window: Window,
|
||||
// TODO move the shed to the browser?
|
||||
storageShed: storage.Shed,
|
||||
page: ?*Page = null,
|
||||
page: ?Page = null,
|
||||
httpClient: HttpClient,
|
||||
|
||||
jstypes: [Types.len]usize = undefined,
|
||||
|
||||
fn init(alloc: std.mem.Allocator, uri: []const u8) !*Session {
|
||||
var self = try alloc.create(Session);
|
||||
fn init(self: *Session, alloc: std.mem.Allocator, loop: *Loop, uri: []const u8) !void {
|
||||
self.* = Session{
|
||||
.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),
|
||||
.loop = try Loop.init(alloc),
|
||||
.storageShed = storage.Shed.init(alloc),
|
||||
.httpClient = undefined,
|
||||
};
|
||||
|
||||
self.env = try Env.init(self.arena.allocator(), &self.loop, null);
|
||||
self.httpClient = .{ .allocator = alloc, .loop = &self.loop };
|
||||
Env.init(&self.env, self.arena.allocator(), loop, null);
|
||||
self.httpClient = .{ .allocator = alloc };
|
||||
try self.env.load(&self.jstypes);
|
||||
}
|
||||
|
||||
return self;
|
||||
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) |page| page.end();
|
||||
if (self.page) |*p| p.end();
|
||||
|
||||
if (self.inspector) |inspector| {
|
||||
inspector.deinit(self.alloc);
|
||||
}
|
||||
|
||||
self.env.deinit();
|
||||
self.arena.deinit();
|
||||
@@ -128,12 +154,35 @@ pub const Session = struct {
|
||||
self.httpClient.deinit();
|
||||
self.loader.deinit();
|
||||
self.storageShed.deinit();
|
||||
self.loop.deinit();
|
||||
self.alloc.destroy(self);
|
||||
}
|
||||
|
||||
pub fn createPage(self: *Session) !Page {
|
||||
return Page.init(self.alloc, self);
|
||||
pub fn initInspector(
|
||||
self: *Session,
|
||||
ctx: anytype,
|
||||
onResp: jsruntime.InspectorOnResponseFn,
|
||||
onEvent: jsruntime.InspectorOnEventFn,
|
||||
) !void {
|
||||
const ctx_opaque = @as(*anyopaque, @ptrCast(ctx));
|
||||
self.inspector = try jsruntime.Inspector.init(self.alloc, self.env, ctx_opaque, onResp, onEvent);
|
||||
self.env.setInspector(self.inspector.?);
|
||||
}
|
||||
|
||||
pub fn callInspector(self: *Session, msg: []const u8) void {
|
||||
if (self.inspector) |inspector| {
|
||||
inspector.send(msg, self.env);
|
||||
} else {
|
||||
@panic("No Inspector");
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: the caller is not the owner of the returned value,
|
||||
// the pointer on Page is just returned as a convenience
|
||||
pub fn createPage(self: *Session) !*Page {
|
||||
if (self.page != null) return error.SessionPageExists;
|
||||
const p: Page = undefined;
|
||||
self.page = p;
|
||||
Page.init(&self.page.?, self.alloc, self);
|
||||
return &self.page.?;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -155,16 +204,39 @@ pub const Page = struct {
|
||||
raw_data: ?[]const u8 = null,
|
||||
|
||||
fn init(
|
||||
self: *Page,
|
||||
alloc: std.mem.Allocator,
|
||||
session: *Session,
|
||||
) !Page {
|
||||
if (session.page != null) return error.SessionPageExists;
|
||||
var page = Page{
|
||||
) void {
|
||||
self.* = .{
|
||||
.arena = std.heap.ArenaAllocator.init(alloc),
|
||||
.session = session,
|
||||
};
|
||||
session.page = &page;
|
||||
return page;
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -219,11 +291,18 @@ pub const Page = struct {
|
||||
}
|
||||
|
||||
// spec reference: https://html.spec.whatwg.org/#document-lifecycle
|
||||
pub fn navigate(self: *Page, uri: []const u8) !void {
|
||||
// - auxData: extra data forwarded to the Inspector
|
||||
// see Inspector.contextCreated
|
||||
pub fn navigate(self: *Page, uri: []const u8, auxData: ?[]const u8) !void {
|
||||
const alloc = self.arena.allocator();
|
||||
|
||||
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);
|
||||
@@ -246,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
|
||||
@@ -280,7 +356,7 @@ pub const Page = struct {
|
||||
log.debug("header content-type: {s}", .{ct.?});
|
||||
const mime = try Mime.parse(ct.?);
|
||||
if (mime.eql(Mime.HTML)) {
|
||||
try self.loadHTMLDoc(req.reader(), mime.charset orelse "utf-8");
|
||||
try self.loadHTMLDoc(req.reader(), mime.charset orelse "utf-8", auxData);
|
||||
} else {
|
||||
log.info("non-HTML document: {s}", .{ct.?});
|
||||
|
||||
@@ -290,7 +366,7 @@ pub const Page = struct {
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/#read-html
|
||||
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8) !void {
|
||||
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8, auxData: ?[]const u8) !void {
|
||||
const alloc = self.arena.allocator();
|
||||
|
||||
// start netsurf memory arena.
|
||||
@@ -322,10 +398,10 @@ 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);
|
||||
}
|
||||
|
||||
// replace the user context document with the new one.
|
||||
try self.session.env.setUserContext(.{
|
||||
@@ -333,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
|
||||
@@ -345,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);
|
||||
@@ -360,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.
|
||||
@@ -388,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;
|
||||
}
|
||||
|
||||
@@ -411,7 +472,9 @@ pub const Page = struct {
|
||||
// > immediately before the browser continues to parse the
|
||||
// > page.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#notes
|
||||
self.evalScript(e) catch |err| log.warn("evaljs: {any}", .{err});
|
||||
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(e));
|
||||
self.evalScript(script) catch |err| log.warn("evaljs: {any}", .{err});
|
||||
try parser.documentHTMLSetCurrentScript(html_doc, null);
|
||||
}
|
||||
|
||||
// TODO wait for deferred scripts
|
||||
@@ -427,8 +490,10 @@ pub const Page = struct {
|
||||
_ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, html_doc), evt);
|
||||
|
||||
// eval async scripts.
|
||||
for (sasync.items) |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);
|
||||
}
|
||||
|
||||
// TODO wait for async scripts
|
||||
@@ -449,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,
|
||||
|
||||
@@ -476,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;
|
||||
}
|
||||
|
||||
@@ -510,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..];
|
||||
@@ -526,46 +572,91 @@ pub const Page = struct {
|
||||
|
||||
const resp = fetchres.req.response;
|
||||
|
||||
log.info("fech script {any}: {d}", .{ u, resp.status });
|
||||
log.info("fetch {any}: {d}", .{ u, resp.status });
|
||||
|
||||
if (resp.status != .ok) return FetchError.BadStatusCode;
|
||||
|
||||
// TODO check content-type
|
||||
const body = try fetchres.req.reader().readAllAlloc(alloc, 16 * 1024 * 1024);
|
||||
defer alloc.free(body);
|
||||
|
||||
// check no body
|
||||
if (body.len == 0) return FetchError.NoBody;
|
||||
|
||||
var try_catch: jsruntime.TryCatch = undefined;
|
||||
try_catch.init(self.session.env);
|
||||
defer try_catch.deinit();
|
||||
return body;
|
||||
}
|
||||
|
||||
const res = self.session.env.exec(body, src) catch {
|
||||
if (try try_catch.err(alloc, self.session.env)) |msg| {
|
||||
defer alloc.free(msg);
|
||||
log.info("eval remote {s}: {s}", .{ src, msg });
|
||||
}
|
||||
return FetchError.JsErr;
|
||||
// fetchScript senf a GET request to the src and execute the script
|
||||
// received.
|
||||
fn fetchScript(self: *Page, s: Script) !void {
|
||||
const alloc = self.arena.allocator();
|
||||
const body = try self.fetchData(alloc, s.src);
|
||||
defer alloc.free(body);
|
||||
|
||||
try s.eval(alloc, self.session.env, body);
|
||||
}
|
||||
|
||||
const Script = struct {
|
||||
element: *parser.Element,
|
||||
kind: Kind,
|
||||
isasync: bool,
|
||||
|
||||
src: []const u8,
|
||||
|
||||
const Kind = enum {
|
||||
unknown,
|
||||
javascript,
|
||||
module,
|
||||
};
|
||||
|
||||
if (builtin.mode == .Debug) {
|
||||
const msg = try res.toString(alloc, self.session.env);
|
||||
defer alloc.free(msg);
|
||||
log.debug("eval remote {s}: {s}", .{ src, msg });
|
||||
fn init(e: *parser.Element) !?Script {
|
||||
// ignore non-script tags
|
||||
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e)));
|
||||
if (tag != .script) return null;
|
||||
|
||||
return .{
|
||||
.element = e,
|
||||
.kind = kind(try parser.elementGetAttribute(e, "type")),
|
||||
.isasync = try parser.elementGetAttribute(e, "async") != null,
|
||||
|
||||
.src = try parser.elementGetAttribute(e, "src") orelse "inline",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// > type
|
||||
// > Attribute is not set (default), an empty string, or a JavaScript MIME
|
||||
// > type indicates that the script is a "classic script", containing
|
||||
// > JavaScript code.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type
|
||||
fn isJS(stype: ?[]const u8) bool {
|
||||
if (stype == null or stype.?.len == 0) return true;
|
||||
if (std.mem.eql(u8, stype.?, "application/javascript")) return true;
|
||||
if (!std.mem.eql(u8, stype.?, "module")) return true;
|
||||
// > type
|
||||
// > Attribute is not set (default), an empty string, or a JavaScript MIME
|
||||
// > type indicates that the script is a "classic script", containing
|
||||
// > JavaScript code.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type
|
||||
fn kind(stype: ?[]const u8) Kind {
|
||||
if (stype == null or stype.?.len == 0) return .javascript;
|
||||
if (std.mem.eql(u8, stype.?, "application/javascript")) return .javascript;
|
||||
if (std.mem.eql(u8, stype.?, "module")) return .module;
|
||||
|
||||
return false;
|
||||
}
|
||||
return .unknown;
|
||||
}
|
||||
|
||||
fn eval(self: Script, alloc: std.mem.Allocator, env: Env, body: []const u8) !void {
|
||||
var try_catch: jsruntime.TryCatch = undefined;
|
||||
try_catch.init(env);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const res = switch (self.kind) {
|
||||
.unknown => return error.UnknownScript,
|
||||
.javascript => env.exec(body, self.src),
|
||||
.module => env.module(body, self.src),
|
||||
} catch {
|
||||
if (try try_catch.err(alloc, env)) |msg| {
|
||||
defer alloc.free(msg);
|
||||
log.info("eval script {s}: {s}", .{ self.src, msg });
|
||||
}
|
||||
return FetchError.JsErr;
|
||||
};
|
||||
|
||||
if (builtin.mode == .Debug) {
|
||||
const msg = try res.toString(alloc, env);
|
||||
defer alloc.free(msg);
|
||||
log.debug("eval script {s}: {s}", .{ self.src, msg });
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
const std = @import("std");
|
||||
const Client = @import("../http/Client.zig");
|
||||
|
||||
const user_agent = "Lightpanda.io/1.0";
|
||||
const user_agent = @import("browser.zig").user_agent;
|
||||
|
||||
pub const Loader = struct {
|
||||
client: Client,
|
||||
|
||||
148
src/cdp/browser.zig
Normal file
148
src/cdp/browser.zig
Normal file
@@ -0,0 +1,148 @@
|
||||
// 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 {
|
||||
getVersion,
|
||||
setDownloadBehavior,
|
||||
getWindowForTarget,
|
||||
setWindowBounds,
|
||||
};
|
||||
|
||||
pub fn browser(
|
||||
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) {
|
||||
.getVersion => getVersion(alloc, msg, ctx),
|
||||
.setDownloadBehavior => setDownloadBehavior(alloc, msg, ctx),
|
||||
.getWindowForTarget => getWindowForTarget(alloc, msg, ctx),
|
||||
.setWindowBounds => setWindowBounds(alloc, msg, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: hard coded data
|
||||
const ProtocolVersion = "1.3";
|
||||
const Product = "Chrome/124.0.6367.29";
|
||||
const Revision = "@9e6ded5ac1ff5e38d930ae52bd9aec09bd1a68e4";
|
||||
const UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
|
||||
const JsVersion = "12.4.254.8";
|
||||
|
||||
fn getVersion(
|
||||
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, "browser.getVersion" });
|
||||
|
||||
// ouput
|
||||
const Res = struct {
|
||||
protocolVersion: []const u8 = ProtocolVersion,
|
||||
product: []const u8 = Product,
|
||||
revision: []const u8 = Revision,
|
||||
userAgent: []const u8 = UserAgent,
|
||||
jsVersion: []const u8 = JsVersion,
|
||||
};
|
||||
return result(alloc, input.id, Res, .{}, null);
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setDownloadBehavior(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const Params = struct {
|
||||
behavior: []const u8,
|
||||
browserContextId: ?[]const u8 = null,
|
||||
downloadPath: ?[]const u8 = null,
|
||||
eventsEnabled: ?bool = null,
|
||||
};
|
||||
const input = try Input(Params).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("REQ > id {d}, method {s}", .{ input.id, "browser.setDownloadBehavior" });
|
||||
|
||||
// output
|
||||
return result(alloc, input.id, null, null, null);
|
||||
}
|
||||
|
||||
// TODO: hard coded ID
|
||||
const DevToolsWindowID = 1923710101;
|
||||
|
||||
fn getWindowForTarget(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const Params = struct {
|
||||
targetId: ?[]const u8 = null,
|
||||
};
|
||||
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" });
|
||||
|
||||
// output
|
||||
const Resp = struct {
|
||||
windowId: u64 = DevToolsWindowID,
|
||||
bounds: struct {
|
||||
left: ?u64 = null,
|
||||
top: ?u64 = null,
|
||||
width: ?u64 = null,
|
||||
height: ?u64 = null,
|
||||
windowState: []const u8 = "normal",
|
||||
} = .{},
|
||||
};
|
||||
return result(alloc, input.id, Resp, Resp{}, input.sessionId);
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setWindowBounds(
|
||||
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, "browser.setWindowBounds" });
|
||||
|
||||
// output
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
233
src/cdp/cdp.zig
Normal file
233
src/cdp/cdp.zig
Normal file
@@ -0,0 +1,233 @@
|
||||
// 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 browser = @import("browser.zig").browser;
|
||||
const target = @import("target.zig").target;
|
||||
const page = @import("page.zig").page;
|
||||
const log = @import("log.zig").log;
|
||||
const runtime = @import("runtime.zig").runtime;
|
||||
const network = @import("network.zig").network;
|
||||
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);
|
||||
|
||||
pub const Error = error{
|
||||
UnknonwDomain,
|
||||
UnknownMethod,
|
||||
NoResponse,
|
||||
RequestWithoutID,
|
||||
};
|
||||
|
||||
pub fn isCdpError(err: anyerror) ?Error {
|
||||
// see https://github.com/ziglang/zig/issues/2473
|
||||
const errors = @typeInfo(Error).ErrorSet.?;
|
||||
inline for (errors) |e| {
|
||||
if (std.mem.eql(u8, e.name, @errorName(err))) {
|
||||
return @errorCast(err);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const Domains = enum {
|
||||
Browser,
|
||||
Target,
|
||||
Page,
|
||||
Log,
|
||||
Runtime,
|
||||
Network,
|
||||
DOM,
|
||||
CSS,
|
||||
Inspector,
|
||||
Emulation,
|
||||
Fetch,
|
||||
Performance,
|
||||
Security,
|
||||
};
|
||||
|
||||
// The caller is responsible for calling `free` on the returned slice.
|
||||
pub fn do(
|
||||
alloc: std.mem.Allocator,
|
||||
s: []const u8,
|
||||
ctx: *Ctx,
|
||||
) 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
|
||||
var iter = std.mem.splitScalar(u8, method, '.');
|
||||
const domain = std.meta.stringToEnum(Domains, iter.first()) orelse
|
||||
return error.UnknonwDomain;
|
||||
|
||||
// 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),
|
||||
.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,
|
||||
secureContextType: []const u8 = "Secure", // TODO: enum
|
||||
loaderID: []const u8 = LoaderID,
|
||||
|
||||
page_life_cycle_events: bool = false, // TODO; Target based value
|
||||
};
|
||||
|
||||
// Utils
|
||||
// -----
|
||||
|
||||
pub fn dumpFile(
|
||||
alloc: std.mem.Allocator,
|
||||
id: u16,
|
||||
script: []const u8,
|
||||
) !void {
|
||||
const name = try std.fmt.allocPrint(alloc, "id_{d}.js", .{id});
|
||||
defer alloc.free(name);
|
||||
var dir = try std.fs.cwd().makeOpenPath("zig-cache/tmp", .{});
|
||||
defer dir.close();
|
||||
const f = try dir.createFile(name, .{});
|
||||
defer f.close();
|
||||
const nb = try f.write(script);
|
||||
std.debug.assert(nb == script.len);
|
||||
const p = try dir.realpathAlloc(alloc, name);
|
||||
defer alloc.free(p);
|
||||
}
|
||||
|
||||
// caller owns the slice returned
|
||||
pub fn stringify(alloc: std.mem.Allocator, res: anytype) ![]const u8 {
|
||||
var out = std.ArrayList(u8).init(alloc);
|
||||
defer out.deinit();
|
||||
|
||||
// Do not emit optional null fields
|
||||
const options: std.json.StringifyOptions = .{ .emit_null_optional_fields = false };
|
||||
|
||||
try std.json.stringify(res, options, out.writer());
|
||||
const ret = try alloc.alloc(u8, out.items.len);
|
||||
@memcpy(ret, out.items);
|
||||
return ret;
|
||||
}
|
||||
|
||||
const resultNull = "{{\"id\": {d}, \"result\": {{}}}}";
|
||||
const resultNullSession = "{{\"id\": {d}, \"result\": {{}}, \"sessionId\": \"{s}\"}}";
|
||||
|
||||
// caller owns the slice returned
|
||||
pub fn result(
|
||||
alloc: std.mem.Allocator,
|
||||
id: u16,
|
||||
comptime T: ?type,
|
||||
res: anytype,
|
||||
sessionID: ?[]const u8,
|
||||
) ![]const u8 {
|
||||
log_cdp.debug(
|
||||
"Res > id {d}, sessionID {?s}, result {any}",
|
||||
.{ id, sessionID, res },
|
||||
);
|
||||
if (T == null) {
|
||||
// No need to stringify a custom JSON msg, just use string templates
|
||||
if (sessionID) |sID| {
|
||||
return try std.fmt.allocPrint(alloc, resultNullSession, .{ id, sID });
|
||||
}
|
||||
return try std.fmt.allocPrint(alloc, resultNull, .{id});
|
||||
}
|
||||
|
||||
const Resp = struct {
|
||||
id: u16,
|
||||
result: T.?,
|
||||
sessionId: ?[]const u8,
|
||||
};
|
||||
const resp = Resp{ .id = id, .result = res, .sessionId = sessionID };
|
||||
|
||||
return stringify(alloc, resp);
|
||||
}
|
||||
|
||||
pub fn sendEvent(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: *Ctx,
|
||||
name: []const u8,
|
||||
comptime T: type,
|
||||
params: T,
|
||||
sessionID: ?[]const u8,
|
||||
) !void {
|
||||
log_cdp.debug("Event > method {s}, sessionID {?s}", .{ name, sessionID });
|
||||
const Resp = struct {
|
||||
method: []const u8,
|
||||
params: T,
|
||||
sessionId: ?[]const u8,
|
||||
};
|
||||
const resp = Resp{ .method = name, .params = params, .sessionId = sessionID };
|
||||
|
||||
const event_msg = try stringify(alloc, resp);
|
||||
try ctx.send(event_msg);
|
||||
}
|
||||
|
||||
// Common
|
||||
// ------
|
||||
|
||||
// TODO: hard coded IDs
|
||||
pub const BrowserSessionID = "BROWSERSESSIONID597D9875C664CAC0";
|
||||
pub const ContextSessionID = "CONTEXTSESSIONID0497A05C95417CF4";
|
||||
pub const URLBase = "chrome://newtab/";
|
||||
pub const LoaderID = "LOADERID24DD2FD56CF1EF33C965C79C";
|
||||
pub const FrameID = "FRAMEIDD8AED408A0467AC93100BCDBE";
|
||||
|
||||
pub const TimestampEvent = struct {
|
||||
timestamp: f64,
|
||||
};
|
||||
59
src/cdp/css.zig
Normal file
59
src/cdp/css.zig
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const cdp = @import("cdp.zig");
|
||||
const result = cdp.result;
|
||||
const IncomingMessage = @import("msg.zig").IncomingMessage;
|
||||
const Input = @import("msg.zig").Input;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
enable,
|
||||
};
|
||||
|
||||
pub fn css(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
action: []const u8,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
const method = std.meta.stringToEnum(Methods, action) orelse
|
||||
return error.UnknownMethod;
|
||||
|
||||
return switch (method) {
|
||||
.enable => enable(alloc, msg, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
fn enable(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const input = try Input(void).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "inspector.enable" });
|
||||
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
59
src/cdp/dom.zig
Normal file
59
src/cdp/dom.zig
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const cdp = @import("cdp.zig");
|
||||
const result = cdp.result;
|
||||
const IncomingMessage = @import("msg.zig").IncomingMessage;
|
||||
const Input = @import("msg.zig").Input;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
enable,
|
||||
};
|
||||
|
||||
pub fn dom(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
action: []const u8,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
const method = std.meta.stringToEnum(Methods, action) orelse
|
||||
return error.UnknownMethod;
|
||||
|
||||
return switch (method) {
|
||||
.enable => enable(alloc, msg, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
fn enable(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const input = try Input(void).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "inspector.enable" });
|
||||
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
123
src/cdp/emulation.zig
Normal file
123
src/cdp/emulation.zig
Normal file
@@ -0,0 +1,123 @@
|
||||
// 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 stringify = cdp.stringify;
|
||||
const IncomingMessage = @import("msg.zig").IncomingMessage;
|
||||
const Input = @import("msg.zig").Input;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
setEmulatedMedia,
|
||||
setFocusEmulationEnabled,
|
||||
setDeviceMetricsOverride,
|
||||
setTouchEmulationEnabled,
|
||||
};
|
||||
|
||||
pub fn emulation(
|
||||
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) {
|
||||
.setEmulatedMedia => setEmulatedMedia(alloc, msg, ctx),
|
||||
.setFocusEmulationEnabled => setFocusEmulationEnabled(alloc, msg, ctx),
|
||||
.setDeviceMetricsOverride => setDeviceMetricsOverride(alloc, msg, ctx),
|
||||
.setTouchEmulationEnabled => setTouchEmulationEnabled(alloc, msg, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
const MediaFeature = struct {
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
};
|
||||
|
||||
// TODO: noop method
|
||||
fn setEmulatedMedia(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const Params = struct {
|
||||
media: ?[]const u8 = null,
|
||||
features: ?[]MediaFeature = null,
|
||||
};
|
||||
const input = try Input(Params).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "emulation.setEmulatedMedia" });
|
||||
|
||||
// output
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setFocusEmulationEnabled(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const Params = struct {
|
||||
enabled: bool,
|
||||
};
|
||||
const input = try Input(Params).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "emulation.setFocusEmulationEnabled" });
|
||||
|
||||
// output
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setDeviceMetricsOverride(
|
||||
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, "emulation.setDeviceMetricsOverride" });
|
||||
|
||||
// output
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setTouchEmulationEnabled(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
const input = try Input(void).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "emulation.setTouchEmulationEnabled" });
|
||||
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
59
src/cdp/fetch.zig
Normal file
59
src/cdp/fetch.zig
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const cdp = @import("cdp.zig");
|
||||
const result = cdp.result;
|
||||
const IncomingMessage = @import("msg.zig").IncomingMessage;
|
||||
const Input = @import("msg.zig").Input;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
disable,
|
||||
};
|
||||
|
||||
pub fn fetch(
|
||||
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) {
|
||||
.disable => disable(alloc, msg, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn disable(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
const input = try Input(void).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "fetch.disable" });
|
||||
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
59
src/cdp/inspector.zig
Normal file
59
src/cdp/inspector.zig
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const cdp = @import("cdp.zig");
|
||||
const result = cdp.result;
|
||||
const IncomingMessage = @import("msg.zig").IncomingMessage;
|
||||
const Input = @import("msg.zig").Input;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
enable,
|
||||
};
|
||||
|
||||
pub fn inspector(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
action: []const u8,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
const method = std.meta.stringToEnum(Methods, action) orelse
|
||||
return error.UnknownMethod;
|
||||
|
||||
return switch (method) {
|
||||
.enable => enable(alloc, msg, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
fn enable(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const input = try Input(void).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "inspector.enable" });
|
||||
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
59
src/cdp/log.zig
Normal file
59
src/cdp/log.zig
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const cdp = @import("cdp.zig");
|
||||
const result = cdp.result;
|
||||
const IncomingMessage = @import("msg.zig").IncomingMessage;
|
||||
const Input = @import("msg.zig").Input;
|
||||
const stringify = cdp.stringify;
|
||||
|
||||
const log_cdp = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
enable,
|
||||
};
|
||||
|
||||
pub fn log(
|
||||
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 {
|
||||
const input = try Input(void).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log_cdp.debug("Req > id {d}, method {s}", .{ input.id, "log.enable" });
|
||||
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
293
src/cdp/msg.zig
Normal file
293
src/cdp/msg.zig
Normal file
@@ -0,0 +1,293 @@
|
||||
// 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");
|
||||
|
||||
// Parse incoming protocol message in json format.
|
||||
pub const IncomingMessage = struct {
|
||||
scanner: std.json.Scanner,
|
||||
json: []const u8,
|
||||
|
||||
obj_begin: bool = false,
|
||||
obj_end: bool = false,
|
||||
|
||||
id: ?u16 = null,
|
||||
scan_sessionId: bool = false,
|
||||
sessionId: ?[]const u8 = null,
|
||||
method: ?[]const u8 = null,
|
||||
params_skip: bool = false,
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator, json: []const u8) IncomingMessage {
|
||||
return .{
|
||||
.json = json,
|
||||
.scanner = std.json.Scanner.initCompleteInput(alloc, json),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *IncomingMessage) void {
|
||||
self.scanner.deinit();
|
||||
}
|
||||
|
||||
fn scanUntil(self: *IncomingMessage, key: []const u8) !void {
|
||||
while (true) {
|
||||
switch (try self.scanner.next()) {
|
||||
.end_of_document => return error.EndOfDocument,
|
||||
.object_begin => {
|
||||
if (self.obj_begin) return error.InvalidObjectBegin;
|
||||
self.obj_begin = true;
|
||||
},
|
||||
.object_end => {
|
||||
if (!self.obj_begin) return error.InvalidObjectEnd;
|
||||
if (self.obj_end) return error.InvalidObjectEnd;
|
||||
self.obj_end = true;
|
||||
},
|
||||
.string => |s| {
|
||||
// is the key what we expects?
|
||||
if (std.mem.eql(u8, s, key)) return;
|
||||
|
||||
// save other known keys
|
||||
if (std.mem.eql(u8, s, "id")) try self.scanId();
|
||||
if (std.mem.eql(u8, s, "sessionId")) try self.scanSessionId();
|
||||
if (std.mem.eql(u8, s, "method")) try self.scanMethod();
|
||||
if (std.mem.eql(u8, s, "params")) try self.scanParams();
|
||||
|
||||
// TODO should we skip unknown key?
|
||||
},
|
||||
else => return error.InvalidToken,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scanId(self: *IncomingMessage) !void {
|
||||
const t = try self.scanner.next();
|
||||
if (t != .number) return error.InvalidId;
|
||||
self.id = try std.fmt.parseUnsigned(u16, t.number, 10);
|
||||
}
|
||||
|
||||
fn getId(self: *IncomingMessage) !u16 {
|
||||
if (self.id != null) return self.id.?;
|
||||
|
||||
try self.scanUntil("id");
|
||||
try self.scanId();
|
||||
return self.id.?;
|
||||
}
|
||||
|
||||
fn scanSessionId(self: *IncomingMessage) !void {
|
||||
switch (try self.scanner.next()) {
|
||||
// session id can be null.
|
||||
.null => return,
|
||||
.string => |s| self.sessionId = s,
|
||||
else => return error.InvalidSessionId,
|
||||
}
|
||||
|
||||
self.scan_sessionId = true;
|
||||
}
|
||||
|
||||
fn getSessionId(self: *IncomingMessage) !?[]const u8 {
|
||||
if (self.scan_sessionId) return self.sessionId;
|
||||
|
||||
self.scanUntil("sessionId") catch |err| {
|
||||
if (err != error.EndOfDocument) return err;
|
||||
// if the document doesn't contains any session id key, we must
|
||||
// return null value.
|
||||
self.scan_sessionId = true;
|
||||
return null;
|
||||
};
|
||||
try self.scanSessionId();
|
||||
return self.sessionId;
|
||||
}
|
||||
|
||||
fn scanMethod(self: *IncomingMessage) !void {
|
||||
const t = try self.scanner.next();
|
||||
if (t != .string) return error.InvalidMethod;
|
||||
self.method = t.string;
|
||||
}
|
||||
|
||||
pub fn getMethod(self: *IncomingMessage) ![]const u8 {
|
||||
if (self.method != null) return self.method.?;
|
||||
|
||||
try self.scanUntil("method");
|
||||
try self.scanMethod();
|
||||
return self.method.?;
|
||||
}
|
||||
|
||||
// scanParams skip found parameters b/c if we encounter params *before*
|
||||
// asking for getParams, we don't know how to parse them.
|
||||
fn scanParams(self: *IncomingMessage) !void {
|
||||
const tt = try self.scanner.peekNextTokenType();
|
||||
// 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;
|
||||
}
|
||||
|
||||
// getParams restart the JSON parsing
|
||||
fn getParams(self: *IncomingMessage, alloc: ?std.mem.Allocator, T: type) !T {
|
||||
if (T == void) return void{};
|
||||
std.debug.assert(alloc != null); // if T is not void, alloc should not be null
|
||||
|
||||
if (self.params_skip) {
|
||||
// TODO if the params have been skipped, we have to retart the
|
||||
// parsing from start.
|
||||
return error.SkippedParams;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
return try std.json.innerParse(T, alloc.?, &self.scanner, options);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn Input(T: type) type {
|
||||
return struct {
|
||||
arena: ?*std.heap.ArenaAllocator = null,
|
||||
id: u16,
|
||||
params: T,
|
||||
sessionId: ?[]const u8,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn get(alloc: std.mem.Allocator, msg: *IncomingMessage) !Self {
|
||||
var arena: ?*std.heap.ArenaAllocator = null;
|
||||
var allocator: ?std.mem.Allocator = null;
|
||||
|
||||
if (T != void) {
|
||||
arena = try alloc.create(std.heap.ArenaAllocator);
|
||||
arena.?.* = std.heap.ArenaAllocator.init(alloc);
|
||||
allocator = arena.?.allocator();
|
||||
}
|
||||
|
||||
errdefer {
|
||||
if (arena) |_arena| {
|
||||
_arena.deinit();
|
||||
alloc.destroy(_arena);
|
||||
}
|
||||
}
|
||||
|
||||
return .{
|
||||
.arena = arena,
|
||||
.params = try msg.getParams(allocator, T),
|
||||
.id = try msg.getId(),
|
||||
.sessionId = try msg.getSessionId(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Self) void {
|
||||
if (self.arena) |arena| {
|
||||
const allocator = arena.child_allocator;
|
||||
arena.deinit();
|
||||
allocator.destroy(arena);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test "read incoming message" {
|
||||
const inputs = [_][]const u8{
|
||||
\\{"id":1,"method":"foo","sessionId":"bar","params":{"bar":"baz"}}
|
||||
,
|
||||
\\{"params":{"bar":"baz"},"id":1,"method":"foo","sessionId":"bar"}
|
||||
,
|
||||
\\{"sessionId":"bar","params":{"bar":"baz"},"id":1,"method":"foo"}
|
||||
,
|
||||
\\{"method":"foo","sessionId":"bar","params":{"bar":"baz"},"id":1}
|
||||
,
|
||||
};
|
||||
|
||||
for (inputs) |input| {
|
||||
var msg = IncomingMessage.init(std.testing.allocator, input);
|
||||
defer msg.deinit();
|
||||
|
||||
try std.testing.expectEqual(1, try msg.getId());
|
||||
try std.testing.expectEqualSlices(u8, "foo", try msg.getMethod());
|
||||
try std.testing.expectEqualSlices(u8, "bar", (try msg.getSessionId()).?);
|
||||
|
||||
const T = struct { bar: []const u8 };
|
||||
const in = Input(T).get(std.testing.allocator, &msg) catch |err| {
|
||||
if (err != error.SkippedParams) return err;
|
||||
// TODO remove this check when params in the beginning is handled.
|
||||
continue;
|
||||
};
|
||||
defer in.deinit();
|
||||
try std.testing.expectEqualSlices(u8, "baz", in.params.bar);
|
||||
}
|
||||
}
|
||||
|
||||
test "read incoming message with null session id" {
|
||||
const inputs = [_][]const u8{
|
||||
\\{"id":1}
|
||||
,
|
||||
\\{"params":{"bar":"baz"},"id":1,"method":"foo"}
|
||||
,
|
||||
\\{"sessionId":null,"params":{"bar":"baz"},"id":1,"method":"foo"}
|
||||
,
|
||||
};
|
||||
|
||||
for (inputs) |input| {
|
||||
var msg = IncomingMessage.init(std.testing.allocator, input);
|
||||
defer msg.deinit();
|
||||
|
||||
try std.testing.expect(try msg.getSessionId() == null);
|
||||
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);
|
||||
}
|
||||
75
src/cdp/network.zig
Normal file
75
src/cdp/network.zig
Normal file
@@ -0,0 +1,75 @@
|
||||
// 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,
|
||||
setCacheDisabled,
|
||||
};
|
||||
|
||||
pub fn network(
|
||||
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),
|
||||
.setCacheDisabled => setCacheDisabled(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, "network.enable" });
|
||||
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setCacheDisabled(
|
||||
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, "network.setCacheDisabled" });
|
||||
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
453
src/cdp/page.zig
Normal file
453
src/cdp/page.zig
Normal file
@@ -0,0 +1,453 @@
|
||||
// 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 stringify = cdp.stringify;
|
||||
const sendEvent = cdp.sendEvent;
|
||||
const IncomingMessage = @import("msg.zig").IncomingMessage;
|
||||
const Input = @import("msg.zig").Input;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Runtime = @import("runtime.zig");
|
||||
|
||||
const Methods = enum {
|
||||
enable,
|
||||
getFrameTree,
|
||||
setLifecycleEventsEnabled,
|
||||
addScriptToEvaluateOnNewDocument,
|
||||
createIsolatedWorld,
|
||||
navigate,
|
||||
};
|
||||
|
||||
pub fn page(
|
||||
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),
|
||||
.getFrameTree => getFrameTree(alloc, msg, ctx),
|
||||
.setLifecycleEventsEnabled => setLifecycleEventsEnabled(alloc, msg, ctx),
|
||||
.addScriptToEvaluateOnNewDocument => addScriptToEvaluateOnNewDocument(alloc, msg, ctx),
|
||||
.createIsolatedWorld => createIsolatedWorld(alloc, msg, ctx),
|
||||
.navigate => navigate(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, "page.enable" });
|
||||
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
|
||||
const Frame = struct {
|
||||
id: []const u8,
|
||||
loaderId: []const u8,
|
||||
url: []const u8,
|
||||
domainAndRegistry: []const u8 = "",
|
||||
securityOrigin: []const u8,
|
||||
mimeType: []const u8 = "text/html",
|
||||
adFrameStatus: struct {
|
||||
adFrameType: []const u8 = "none",
|
||||
} = .{},
|
||||
secureContextType: []const u8,
|
||||
crossOriginIsolatedContextType: []const u8 = "NotIsolated",
|
||||
gatedAPIFeatures: [][]const u8 = &[0][]const u8{},
|
||||
};
|
||||
|
||||
fn getFrameTree(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const input = try Input(void).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "page.getFrameTree" });
|
||||
|
||||
// output
|
||||
const FrameTree = struct {
|
||||
frameTree: struct {
|
||||
frame: Frame,
|
||||
},
|
||||
childFrames: ?[]@This() = null,
|
||||
|
||||
pub fn format(
|
||||
self: @This(),
|
||||
comptime _: []const u8,
|
||||
options: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
try writer.writeAll("cdp.page.getFrameTree { ");
|
||||
try writer.writeAll(".frameTree = { ");
|
||||
try writer.writeAll(".frame = { ");
|
||||
const frame = self.frameTree.frame;
|
||||
try writer.writeAll(".id = ");
|
||||
try std.fmt.formatText(frame.id, "s", options, writer);
|
||||
try writer.writeAll(", .loaderId = ");
|
||||
try std.fmt.formatText(frame.loaderId, "s", options, writer);
|
||||
try writer.writeAll(", .url = ");
|
||||
try std.fmt.formatText(frame.url, "s", options, writer);
|
||||
try writer.writeAll(" } } }");
|
||||
}
|
||||
};
|
||||
const frameTree = FrameTree{
|
||||
.frameTree = .{
|
||||
.frame = .{
|
||||
.id = ctx.state.frameID,
|
||||
.url = ctx.state.url,
|
||||
.securityOrigin = ctx.state.securityOrigin,
|
||||
.secureContextType = ctx.state.secureContextType,
|
||||
.loaderId = ctx.state.loaderID,
|
||||
},
|
||||
},
|
||||
};
|
||||
return result(alloc, input.id, FrameTree, frameTree, input.sessionId);
|
||||
}
|
||||
|
||||
fn setLifecycleEventsEnabled(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const Params = struct {
|
||||
enabled: bool,
|
||||
};
|
||||
const input = try Input(Params).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "page.setLifecycleEventsEnabled" });
|
||||
|
||||
ctx.state.page_life_cycle_events = true;
|
||||
|
||||
// output
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
|
||||
const LifecycleEvent = struct {
|
||||
frameId: []const u8,
|
||||
loaderId: ?[]const u8,
|
||||
name: []const u8 = undefined,
|
||||
timestamp: f32 = undefined,
|
||||
};
|
||||
|
||||
// TODO: hard coded method
|
||||
fn addScriptToEvaluateOnNewDocument(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const Params = struct {
|
||||
source: []const u8,
|
||||
worldName: ?[]const u8 = null,
|
||||
includeCommandLineAPI: bool = false,
|
||||
runImmediately: bool = false,
|
||||
};
|
||||
const input = try Input(Params).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "page.addScriptToEvaluateOnNewDocument" });
|
||||
|
||||
// output
|
||||
const Res = struct {
|
||||
identifier: []const u8 = "1",
|
||||
|
||||
pub fn format(
|
||||
self: @This(),
|
||||
comptime _: []const u8,
|
||||
options: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
try writer.writeAll("cdp.page.addScriptToEvaluateOnNewDocument { ");
|
||||
try writer.writeAll(".identifier = ");
|
||||
try std.fmt.formatText(self.identifier, "s", options, writer);
|
||||
try writer.writeAll(" }");
|
||||
}
|
||||
};
|
||||
return result(alloc, input.id, Res, Res{}, input.sessionId);
|
||||
}
|
||||
|
||||
// TODO: hard coded method
|
||||
fn createIsolatedWorld(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const Params = struct {
|
||||
frameId: []const u8,
|
||||
worldName: []const u8,
|
||||
grantUniveralAccess: bool,
|
||||
};
|
||||
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, "page.createIsolatedWorld" });
|
||||
|
||||
// noop executionContextCreated event
|
||||
try Runtime.executionContextCreated(
|
||||
alloc,
|
||||
ctx,
|
||||
0,
|
||||
"",
|
||||
input.params.worldName,
|
||||
// TODO: hard coded ID
|
||||
"7102379147004877974.3265385113993241162",
|
||||
.{
|
||||
.isDefault = false,
|
||||
.type = "isolated",
|
||||
.frameId = input.params.frameId,
|
||||
},
|
||||
input.sessionId,
|
||||
);
|
||||
|
||||
// output
|
||||
const Resp = struct {
|
||||
executionContextId: u8 = 0,
|
||||
};
|
||||
|
||||
return result(alloc, input.id, Resp, .{}, input.sessionId);
|
||||
}
|
||||
|
||||
fn navigate(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const Params = struct {
|
||||
url: []const u8,
|
||||
referrer: ?[]const u8 = null,
|
||||
transitionType: ?[]const u8 = null, // TODO: enum
|
||||
frameId: ?[]const u8 = null,
|
||||
referrerPolicy: ?[]const u8 = null, // TODO: enum
|
||||
};
|
||||
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, "page.navigate" });
|
||||
|
||||
// change state
|
||||
ctx.state.url = input.params.url;
|
||||
// TODO: hard coded ID
|
||||
ctx.state.loaderID = "AF8667A203C5392DBE9AC290044AA4C2";
|
||||
|
||||
var life_event = LifecycleEvent{
|
||||
.frameId = ctx.state.frameID,
|
||||
.loaderId = ctx.state.loaderID,
|
||||
};
|
||||
var ts_event: cdp.TimestampEvent = undefined;
|
||||
|
||||
// frameStartedLoading event
|
||||
// TODO: event partially hard coded
|
||||
const FrameStartedLoading = struct {
|
||||
frameId: []const u8,
|
||||
};
|
||||
const frame_started_loading = FrameStartedLoading{ .frameId = ctx.state.frameID };
|
||||
try sendEvent(
|
||||
alloc,
|
||||
ctx,
|
||||
"Page.frameStartedLoading",
|
||||
FrameStartedLoading,
|
||||
frame_started_loading,
|
||||
input.sessionId,
|
||||
);
|
||||
if (ctx.state.page_life_cycle_events) {
|
||||
life_event.name = "init";
|
||||
life_event.timestamp = 343721.796037;
|
||||
try sendEvent(
|
||||
alloc,
|
||||
ctx,
|
||||
"Page.lifecycleEvent",
|
||||
LifecycleEvent,
|
||||
life_event,
|
||||
input.sessionId,
|
||||
);
|
||||
}
|
||||
|
||||
// output
|
||||
const Resp = struct {
|
||||
frameId: []const u8,
|
||||
loaderId: ?[]const u8,
|
||||
errorText: ?[]const u8 = null,
|
||||
|
||||
pub fn format(
|
||||
self: @This(),
|
||||
comptime _: []const u8,
|
||||
options: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
try writer.writeAll("cdp.page.navigate.Resp { ");
|
||||
try writer.writeAll(".frameId = ");
|
||||
try std.fmt.formatText(self.frameId, "s", options, writer);
|
||||
if (self.loaderId) |loaderId| {
|
||||
try writer.writeAll(", .loaderId = '");
|
||||
try std.fmt.formatText(loaderId, "s", options, writer);
|
||||
}
|
||||
try writer.writeAll(" }");
|
||||
}
|
||||
};
|
||||
const resp = Resp{
|
||||
.frameId = ctx.state.frameID,
|
||||
.loaderId = ctx.state.loaderID,
|
||||
};
|
||||
const res = try result(alloc, input.id, Resp, resp, input.sessionId);
|
||||
try ctx.send(res);
|
||||
|
||||
// TODO: at this point do we need async the following actions to be async?
|
||||
|
||||
// Send Runtime.executionContextsCleared event
|
||||
// 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, 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,
|
||||
// NOTE: we assume this is the default web page
|
||||
"{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}",
|
||||
.{ctx.state.frameID},
|
||||
);
|
||||
defer alloc.free(auxData);
|
||||
try p.navigate(input.params.url, auxData);
|
||||
|
||||
// Events
|
||||
|
||||
// lifecycle init event
|
||||
// TODO: partially hard coded
|
||||
if (ctx.state.page_life_cycle_events) {
|
||||
life_event.name = "init";
|
||||
life_event.timestamp = 343721.796037;
|
||||
try sendEvent(
|
||||
alloc,
|
||||
ctx,
|
||||
"Page.lifecycleEvent",
|
||||
LifecycleEvent,
|
||||
life_event,
|
||||
input.sessionId,
|
||||
);
|
||||
}
|
||||
|
||||
// frameNavigated event
|
||||
const FrameNavigated = struct {
|
||||
frame: Frame,
|
||||
type: []const u8 = "Navigation",
|
||||
};
|
||||
const frame_navigated = FrameNavigated{
|
||||
.frame = .{
|
||||
.id = ctx.state.frameID,
|
||||
.url = ctx.state.url,
|
||||
.securityOrigin = ctx.state.securityOrigin,
|
||||
.secureContextType = ctx.state.secureContextType,
|
||||
.loaderId = ctx.state.loaderID,
|
||||
},
|
||||
};
|
||||
try sendEvent(
|
||||
alloc,
|
||||
ctx,
|
||||
"Page.frameNavigated",
|
||||
FrameNavigated,
|
||||
frame_navigated,
|
||||
input.sessionId,
|
||||
);
|
||||
|
||||
// domContentEventFired event
|
||||
// TODO: partially hard coded
|
||||
ts_event = .{ .timestamp = 343721.803338 };
|
||||
try sendEvent(
|
||||
alloc,
|
||||
ctx,
|
||||
"Page.domContentEventFired",
|
||||
cdp.TimestampEvent,
|
||||
ts_event,
|
||||
input.sessionId,
|
||||
);
|
||||
|
||||
// lifecycle DOMContentLoaded event
|
||||
// TODO: partially hard coded
|
||||
if (ctx.state.page_life_cycle_events) {
|
||||
life_event.name = "DOMContentLoaded";
|
||||
life_event.timestamp = 343721.803338;
|
||||
try sendEvent(
|
||||
alloc,
|
||||
ctx,
|
||||
"Page.lifecycleEvent",
|
||||
LifecycleEvent,
|
||||
life_event,
|
||||
input.sessionId,
|
||||
);
|
||||
}
|
||||
|
||||
// loadEventFired event
|
||||
// TODO: partially hard coded
|
||||
ts_event = .{ .timestamp = 343721.824655 };
|
||||
try sendEvent(
|
||||
alloc,
|
||||
ctx,
|
||||
"Page.loadEventFired",
|
||||
cdp.TimestampEvent,
|
||||
ts_event,
|
||||
input.sessionId,
|
||||
);
|
||||
|
||||
// lifecycle DOMContentLoaded event
|
||||
// TODO: partially hard coded
|
||||
if (ctx.state.page_life_cycle_events) {
|
||||
life_event.name = "load";
|
||||
life_event.timestamp = 343721.824655;
|
||||
try sendEvent(
|
||||
alloc,
|
||||
ctx,
|
||||
"Page.lifecycleEvent",
|
||||
LifecycleEvent,
|
||||
life_event,
|
||||
input.sessionId,
|
||||
);
|
||||
}
|
||||
|
||||
// frameStoppedLoading
|
||||
const FrameStoppedLoading = struct { frameId: []const u8 };
|
||||
try sendEvent(
|
||||
alloc,
|
||||
ctx,
|
||||
"Page.frameStoppedLoading",
|
||||
FrameStoppedLoading,
|
||||
.{ .frameId = ctx.state.frameID },
|
||||
input.sessionId,
|
||||
);
|
||||
|
||||
return "";
|
||||
}
|
||||
59
src/cdp/performance.zig
Normal file
59
src/cdp/performance.zig
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const cdp = @import("cdp.zig");
|
||||
const result = cdp.result;
|
||||
const IncomingMessage = @import("msg.zig").IncomingMessage;
|
||||
const Input = @import("msg.zig").Input;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
enable,
|
||||
};
|
||||
|
||||
pub fn performance(
|
||||
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, "performance.enable" });
|
||||
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
190
src/cdp/runtime.zig
Normal file
190
src/cdp/runtime.zig
Normal file
@@ -0,0 +1,190 @@
|
||||
// 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 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 stringify = cdp.stringify;
|
||||
const target = @import("target.zig");
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
enable,
|
||||
runIfWaitingForDebugger,
|
||||
evaluate,
|
||||
addBinding,
|
||||
callFunctionOn,
|
||||
releaseObject,
|
||||
};
|
||||
|
||||
pub fn runtime(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
action: []const u8,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
const method = std.meta.stringToEnum(Methods, action) orelse
|
||||
// NOTE: we could send it anyway to the JS runtime but it's good to check it
|
||||
return error.UnknownMethod;
|
||||
return switch (method) {
|
||||
.runIfWaitingForDebugger => runIfWaitingForDebugger(alloc, msg, ctx),
|
||||
else => sendInspector(alloc, method, msg, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
fn sendInspector(
|
||||
alloc: std.mem.Allocator,
|
||||
method: Methods,
|
||||
msg: *IncomingMessage,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// save script in file at debug mode
|
||||
if (std.log.defaultLogEnabled(.debug)) {
|
||||
|
||||
// input
|
||||
var id: u16 = undefined;
|
||||
var script: ?[]const u8 = null;
|
||||
|
||||
if (method == .evaluate) {
|
||||
const Params = struct {
|
||||
expression: []const u8,
|
||||
contextId: ?u8 = null,
|
||||
returnByValue: ?bool = null,
|
||||
awaitPromise: ?bool = null,
|
||||
userGesture: ?bool = null,
|
||||
};
|
||||
|
||||
const input = try Input(Params).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s} (script saved on cache)", .{ input.id, "runtime.evaluate" });
|
||||
const params = input.params;
|
||||
const func = try alloc.alloc(u8, params.expression.len);
|
||||
@memcpy(func, params.expression);
|
||||
script = func;
|
||||
id = input.id;
|
||||
} else if (method == .callFunctionOn) {
|
||||
const Params = struct {
|
||||
functionDeclaration: []const u8,
|
||||
objectId: ?[]const u8 = null,
|
||||
executionContextId: ?u8 = null,
|
||||
arguments: ?[]struct {
|
||||
value: ?[]const u8 = null,
|
||||
objectId: ?[]const u8 = null,
|
||||
} = null,
|
||||
returnByValue: ?bool = null,
|
||||
awaitPromise: ?bool = null,
|
||||
userGesture: ?bool = null,
|
||||
};
|
||||
|
||||
const input = try Input(Params).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s} (script saved on cache)", .{ input.id, "runtime.callFunctionOn" });
|
||||
const params = input.params;
|
||||
const func = try alloc.alloc(u8, params.functionDeclaration.len);
|
||||
@memcpy(func, params.functionDeclaration);
|
||||
script = func;
|
||||
id = input.id;
|
||||
}
|
||||
|
||||
if (script) |src| {
|
||||
try cdp.dumpFile(alloc, id, src);
|
||||
alloc.free(src);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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 "";
|
||||
}
|
||||
}
|
||||
|
||||
ctx.sendInspector(msg.json);
|
||||
|
||||
if (msg.id == null) return "";
|
||||
|
||||
return result(alloc, msg.id.?, null, null, msg.sessionId);
|
||||
}
|
||||
|
||||
pub const AuxData = struct {
|
||||
isDefault: bool = true,
|
||||
type: []const u8 = "default",
|
||||
frameId: []const u8 = cdp.FrameID,
|
||||
};
|
||||
|
||||
pub fn executionContextCreated(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: *Ctx,
|
||||
id: u16,
|
||||
origin: []const u8,
|
||||
name: []const u8,
|
||||
uniqueID: []const u8,
|
||||
auxData: ?AuxData,
|
||||
sessionID: ?[]const u8,
|
||||
) !void {
|
||||
const Params = struct {
|
||||
context: struct {
|
||||
id: u64,
|
||||
origin: []const u8,
|
||||
name: []const u8,
|
||||
uniqueId: []const u8,
|
||||
auxData: ?AuxData = null,
|
||||
},
|
||||
};
|
||||
const params = Params{
|
||||
.context = .{
|
||||
.id = id,
|
||||
.origin = origin,
|
||||
.name = name,
|
||||
.uniqueId = uniqueID,
|
||||
.auxData = auxData,
|
||||
},
|
||||
};
|
||||
try cdp.sendEvent(alloc, ctx, "Runtime.executionContextCreated", Params, params, sessionID);
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
// should we be passing this also to the JS Inspector?
|
||||
fn runIfWaitingForDebugger(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
const input = try Input(void).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "runtime.runIfWaitingForDebugger" });
|
||||
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
59
src/cdp/security.zig
Normal file
59
src/cdp/security.zig
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const cdp = @import("cdp.zig");
|
||||
const result = cdp.result;
|
||||
const IncomingMessage = @import("msg.zig").IncomingMessage;
|
||||
const Input = @import("msg.zig").Input;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
enable,
|
||||
};
|
||||
|
||||
pub fn security(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
action: []const u8,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
const method = std.meta.stringToEnum(Methods, action) orelse
|
||||
return error.UnknownMethod;
|
||||
|
||||
return switch (method) {
|
||||
.enable => enable(alloc, msg, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
fn enable(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const input = try Input(void).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "security.enable" });
|
||||
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
523
src/cdp/target.zig
Normal file
523
src/cdp/target.zig
Normal file
@@ -0,0 +1,523 @@
|
||||
// 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 stringify = cdp.stringify;
|
||||
const IncomingMessage = @import("msg.zig").IncomingMessage;
|
||||
const Input = @import("msg.zig").Input;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
setDiscoverTargets,
|
||||
setAutoAttach,
|
||||
attachToTarget,
|
||||
getTargetInfo,
|
||||
getBrowserContexts,
|
||||
createBrowserContext,
|
||||
disposeBrowserContext,
|
||||
createTarget,
|
||||
closeTarget,
|
||||
sendMessageToTarget,
|
||||
detachFromTarget,
|
||||
};
|
||||
|
||||
pub fn target(
|
||||
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) {
|
||||
.setDiscoverTargets => setDiscoverTargets(alloc, msg, ctx),
|
||||
.setAutoAttach => setAutoAttach(alloc, msg, ctx),
|
||||
.attachToTarget => attachToTarget(alloc, msg, ctx),
|
||||
.getTargetInfo => getTargetInfo(alloc, msg, ctx),
|
||||
.getBrowserContexts => getBrowserContexts(alloc, msg, ctx),
|
||||
.createBrowserContext => createBrowserContext(alloc, msg, ctx),
|
||||
.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
|
||||
pub const PageTargetID = "PAGETARGETIDB638E9DC0F52DDC";
|
||||
pub const BrowserTargetID = "browser9-targ-et6f-id0e-83f3ab73a30c";
|
||||
pub const BrowserContextID = "BROWSERCONTEXTIDA95049E9DFE95EA9";
|
||||
|
||||
// TODO: noop method
|
||||
fn setDiscoverTargets(
|
||||
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.setDiscoverTargets" });
|
||||
|
||||
// output
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
|
||||
const AttachToTarget = 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,
|
||||
},
|
||||
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,
|
||||
};
|
||||
|
||||
// TODO: noop method
|
||||
fn setAutoAttach(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const Params = struct {
|
||||
autoAttach: bool,
|
||||
waitForDebuggerOnStart: bool,
|
||||
flatten: bool = true,
|
||||
filter: ?[]TargetFilter = null,
|
||||
};
|
||||
const input = try Input(Params).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "target.setAutoAttach" });
|
||||
|
||||
// attachedToTarget event
|
||||
if (input.sessionId == null) {
|
||||
const attached = AttachToTarget{
|
||||
.sessionId = cdp.BrowserSessionID,
|
||||
.targetInfo = .{
|
||||
.targetId = PageTargetID,
|
||||
.title = "about:blank",
|
||||
.url = cdp.URLBase,
|
||||
.browserContextId = BrowserContextID,
|
||||
},
|
||||
};
|
||||
try cdp.sendEvent(alloc, ctx, "Target.attachedToTarget", AttachToTarget, attached, null);
|
||||
}
|
||||
|
||||
// output
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn attachToTarget(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const Params = struct {
|
||||
targetId: []const u8,
|
||||
flatten: bool = true,
|
||||
};
|
||||
const input = try Input(Params).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "target.attachToTarget" });
|
||||
|
||||
// attachedToTarget event
|
||||
if (input.sessionId == null) {
|
||||
const attached = AttachToTarget{
|
||||
.sessionId = cdp.BrowserSessionID,
|
||||
.targetInfo = .{
|
||||
.targetId = input.params.targetId,
|
||||
.title = "about:blank",
|
||||
.url = cdp.URLBase,
|
||||
.browserContextId = BrowserContextID,
|
||||
},
|
||||
};
|
||||
try cdp.sendEvent(alloc, ctx, "Target.attachedToTarget", AttachToTarget, attached, null);
|
||||
}
|
||||
|
||||
// output
|
||||
const SessionId = struct {
|
||||
sessionId: []const u8,
|
||||
};
|
||||
const output = SessionId{
|
||||
.sessionId = input.sessionId orelse cdp.BrowserSessionID,
|
||||
};
|
||||
return result(alloc, input.id, SessionId, output, null);
|
||||
}
|
||||
|
||||
fn getTargetInfo(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const Params = struct {
|
||||
targetId: ?[]const u8 = null,
|
||||
};
|
||||
const input = try Input(?Params).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "target.getTargetInfo" });
|
||||
|
||||
// output
|
||||
const TargetInfo = struct {
|
||||
targetId: []const u8,
|
||||
type: []const u8,
|
||||
title: []const u8 = "",
|
||||
url: []const u8 = "",
|
||||
attached: bool = true,
|
||||
openerId: ?[]const u8 = null,
|
||||
canAccessOpener: bool = false,
|
||||
openerFrameId: ?[]const u8 = null,
|
||||
browserContextId: ?[]const u8 = null,
|
||||
subtype: ?[]const u8 = null,
|
||||
};
|
||||
const targetInfo = TargetInfo{
|
||||
.targetId = BrowserTargetID,
|
||||
.type = "browser",
|
||||
};
|
||||
return result(alloc, input.id, TargetInfo, targetInfo, null);
|
||||
}
|
||||
|
||||
// Browser context are not handled and not in the roadmap for now
|
||||
// The following methods are "fake"
|
||||
|
||||
// TODO: noop method
|
||||
fn getBrowserContexts(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
ctx: *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.getBrowserContexts" });
|
||||
|
||||
// ouptut
|
||||
const Resp = struct {
|
||||
browserContextIds: [][]const u8,
|
||||
};
|
||||
var resp: Resp = undefined;
|
||||
if (ctx.state.contextID) |contextID| {
|
||||
var contextIDs = [1][]const u8{contextID};
|
||||
resp = .{ .browserContextIds = &contextIDs };
|
||||
} else {
|
||||
const contextIDs = [0][]const u8{};
|
||||
resp = .{ .browserContextIds = &contextIDs };
|
||||
}
|
||||
return result(alloc, input.id, Resp, resp, null);
|
||||
}
|
||||
|
||||
const ContextID = "CONTEXTIDDCCDD11109E2D4FEFBE4F89";
|
||||
|
||||
// TODO: noop method
|
||||
fn createBrowserContext(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const Params = struct {
|
||||
disposeOnDetach: bool = false,
|
||||
proxyServer: ?[]const u8 = null,
|
||||
proxyBypassList: ?[]const u8 = null,
|
||||
originsWithUniversalNetworkAccess: ?[][]const u8 = null,
|
||||
};
|
||||
const input = try Input(Params).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "target.createBrowserContext" });
|
||||
|
||||
ctx.state.contextID = ContextID;
|
||||
|
||||
// output
|
||||
const Resp = struct {
|
||||
browserContextId: []const u8 = ContextID,
|
||||
|
||||
pub fn format(
|
||||
self: @This(),
|
||||
comptime _: []const u8,
|
||||
options: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
try writer.writeAll("cdp.target.createBrowserContext { ");
|
||||
try writer.writeAll(".browserContextId = ");
|
||||
try std.fmt.formatText(self.browserContextId, "s", options, writer);
|
||||
try writer.writeAll(" }");
|
||||
}
|
||||
};
|
||||
return result(alloc, input.id, Resp, Resp{}, input.sessionId);
|
||||
}
|
||||
|
||||
fn disposeBrowserContext(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const Params = struct {
|
||||
browserContextId: []const u8,
|
||||
};
|
||||
const input = try Input(Params).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "target.disposeBrowserContext" });
|
||||
|
||||
// output
|
||||
const res = try result(alloc, input.id, null, .{}, null);
|
||||
try ctx.send(res);
|
||||
|
||||
return error.DisposeBrowserContext;
|
||||
}
|
||||
|
||||
// TODO: hard coded IDs
|
||||
const TargetID = "TARGETID460A8F29706A2ADF14316298";
|
||||
const LoaderID = "LOADERID42AA389647D702B4D805F49A";
|
||||
|
||||
fn createTarget(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const Params = struct {
|
||||
url: []const u8,
|
||||
width: ?u64 = null,
|
||||
height: ?u64 = null,
|
||||
browserContextId: ?[]const u8 = null,
|
||||
enableBeginFrameControl: bool = false,
|
||||
newWindow: bool = false,
|
||||
background: bool = false,
|
||||
forTab: ?bool = null,
|
||||
};
|
||||
const input = try Input(Params).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "target.createTarget" });
|
||||
|
||||
// change CDP state
|
||||
ctx.state.frameID = TargetID;
|
||||
ctx.state.url = "about:blank";
|
||||
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 = "about:blank",
|
||||
.url = ctx.state.url,
|
||||
.browserContextId = input.params.browserContextId orelse ContextID,
|
||||
.attached = true,
|
||||
},
|
||||
.waitingForDebugger = true,
|
||||
};
|
||||
try cdp.sendEvent(alloc, ctx, "Target.attachedToTarget", AttachToTarget, attached, input.sessionId);
|
||||
|
||||
// output
|
||||
const Resp = struct {
|
||||
targetId: []const u8 = TargetID,
|
||||
|
||||
pub fn format(
|
||||
self: @This(),
|
||||
comptime _: []const u8,
|
||||
options: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
try writer.writeAll("cdp.target.createTarget { ");
|
||||
try writer.writeAll(".targetId = ");
|
||||
try std.fmt.formatText(self.targetId, "s", options, writer);
|
||||
try writer.writeAll(" }");
|
||||
}
|
||||
};
|
||||
return result(alloc, input.id, Resp, Resp{}, input.sessionId);
|
||||
}
|
||||
|
||||
fn closeTarget(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const Params = struct {
|
||||
targetId: []const u8,
|
||||
};
|
||||
const input = try Input(Params).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "target.closeTarget" });
|
||||
|
||||
// output
|
||||
const Resp = struct {
|
||||
success: bool = true,
|
||||
};
|
||||
const res = try result(alloc, input.id, Resp, Resp{}, null);
|
||||
try ctx.send(res);
|
||||
|
||||
// Inspector.detached event
|
||||
const InspectorDetached = struct {
|
||||
reason: []const u8 = "Render process gone.",
|
||||
};
|
||||
try cdp.sendEvent(
|
||||
alloc,
|
||||
ctx,
|
||||
"Inspector.detached",
|
||||
InspectorDetached,
|
||||
.{},
|
||||
input.sessionId orelse cdp.ContextSessionID,
|
||||
);
|
||||
|
||||
// detachedFromTarget event
|
||||
const TargetDetached = struct {
|
||||
sessionId: []const u8,
|
||||
targetId: []const u8,
|
||||
};
|
||||
try cdp.sendEvent(
|
||||
alloc,
|
||||
ctx,
|
||||
"Target.detachedFromTarget",
|
||||
TargetDetached,
|
||||
.{
|
||||
.sessionId = input.sessionId orelse cdp.ContextSessionID,
|
||||
.targetId = input.params.targetId,
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
if (ctx.browser.session.page != null) ctx.browser.session.page.?.end();
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
fn sendMessageToTarget(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const Params = struct {
|
||||
message: []const u8,
|
||||
sessionId: []const u8,
|
||||
};
|
||||
const input = try Input(Params).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s} ({s})", .{ input.id, "target.sendMessageToTarget", input.params.message });
|
||||
|
||||
// get the wrapped message.
|
||||
var wmsg = IncomingMessage.init(alloc, input.params.message);
|
||||
defer wmsg.deinit();
|
||||
|
||||
const res = cdp.dispatch(alloc, &wmsg, ctx) catch |e| {
|
||||
log.err("send message {d} ({s}): {any}", .{ input.id, input.params.message, e });
|
||||
// TODO dispatch error correctly.
|
||||
return e;
|
||||
};
|
||||
|
||||
// receivedMessageFromTarget event
|
||||
const ReceivedMessageFromTarget = struct {
|
||||
message: []const u8,
|
||||
sessionId: []const u8,
|
||||
};
|
||||
try cdp.sendEvent(
|
||||
alloc,
|
||||
ctx,
|
||||
"Target.receivedMessageFromTarget",
|
||||
ReceivedMessageFromTarget,
|
||||
.{
|
||||
.message = res,
|
||||
.sessionId = input.params.sessionId,
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
// noop
|
||||
fn detachFromTarget(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const input = try Input(void).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "target.detachFromTarget" });
|
||||
|
||||
// output
|
||||
return result(alloc, input.id, bool, true, input.sessionId);
|
||||
}
|
||||
@@ -29,7 +29,6 @@ const Node = @import("node.zig").Node;
|
||||
const NodeList = @import("nodelist.zig").NodeList;
|
||||
const NodeUnion = @import("node.zig").Union;
|
||||
|
||||
const Walker = @import("walker.zig").WalkerDepthFirst;
|
||||
const collection = @import("html_collection.zig");
|
||||
const css = @import("css.zig");
|
||||
|
||||
@@ -435,7 +434,11 @@ pub fn testExecFn(
|
||||
.{ .src = "document.querySelector(':root').nodeName", .ex = "HTML" },
|
||||
|
||||
.{ .src = "document.querySelectorAll('p').length", .ex = "2" },
|
||||
.{ .src = "document.querySelectorAll('.ok').item(0).id", .ex = "link" },
|
||||
.{ .src =
|
||||
\\Array.from(document.querySelectorAll('#content > p#para-empty'))
|
||||
\\.map(row => row.querySelector('span').textContent)
|
||||
\\.length;
|
||||
, .ex = "1" },
|
||||
};
|
||||
try checkCases(js_env, &querySelector);
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ const EventTarget = @import("event_target.zig").EventTarget;
|
||||
const DOMImplementation = @import("implementation.zig").DOMImplementation;
|
||||
const NamedNodeMap = @import("namednodemap.zig").NamedNodeMap;
|
||||
const DOMTokenList = @import("token_list.zig").DOMTokenList;
|
||||
const NodeList = @import("nodelist.zig").NodeList;
|
||||
const NodeList = @import("nodelist.zig");
|
||||
const Nod = @import("node.zig");
|
||||
const MutationObserver = @import("mutation_observer.zig");
|
||||
|
||||
@@ -33,7 +33,7 @@ pub const Interfaces = generate.Tuple(.{
|
||||
DOMImplementation,
|
||||
NamedNodeMap,
|
||||
DOMTokenList,
|
||||
NodeList,
|
||||
NodeList.Interfaces,
|
||||
Nod.Node,
|
||||
Nod.Interfaces,
|
||||
MutationObserver.Interfaces,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -40,6 +40,7 @@ const DocumentType = @import("document_type.zig").DocumentType;
|
||||
const DocumentFragment = @import("document_fragment.zig").DocumentFragment;
|
||||
const HTMLCollection = @import("html_collection.zig").HTMLCollection;
|
||||
const HTMLCollectionIterator = @import("html_collection.zig").HTMLCollectionIterator;
|
||||
const Walker = @import("walker.zig").WalkerDepthFirst;
|
||||
|
||||
// HTML
|
||||
const HTML = @import("../html/html.zig");
|
||||
@@ -195,21 +196,68 @@ pub const Node = struct {
|
||||
return try Node.toInterface(clone);
|
||||
}
|
||||
|
||||
pub fn _compareDocumentPosition(self: *parser.Node, other: *parser.Node) void {
|
||||
// TODO
|
||||
_ = other;
|
||||
_ = self;
|
||||
std.log.err("Not implemented {s}", .{"node.compareDocumentPosition()"});
|
||||
pub fn _compareDocumentPosition(self: *parser.Node, other: *parser.Node) !u32 {
|
||||
if (self == other) return 0;
|
||||
|
||||
const docself = try parser.nodeOwnerDocument(self);
|
||||
const docother = try parser.nodeOwnerDocument(other);
|
||||
|
||||
// Both are in different document.
|
||||
if (docself == null or docother == null or docother.? != docself.?) {
|
||||
return @intFromEnum(parser.DocumentPosition.disconnected);
|
||||
}
|
||||
|
||||
// TODO Both are in a different trees in the same document.
|
||||
|
||||
const w = Walker{};
|
||||
var next: ?*parser.Node = null;
|
||||
|
||||
// Is other a descendant of self?
|
||||
while (true) {
|
||||
next = try w.get_next(self, next) orelse break;
|
||||
if (other == next) {
|
||||
return @intFromEnum(parser.DocumentPosition.following) +
|
||||
@intFromEnum(parser.DocumentPosition.contained_by);
|
||||
}
|
||||
}
|
||||
|
||||
// Is self a descendant of other?
|
||||
next = null;
|
||||
while (true) {
|
||||
next = try w.get_next(other, next) orelse break;
|
||||
if (self == next) {
|
||||
return @intFromEnum(parser.DocumentPosition.contains) +
|
||||
@intFromEnum(parser.DocumentPosition.preceding);
|
||||
}
|
||||
}
|
||||
|
||||
next = null;
|
||||
while (true) {
|
||||
next = try w.get_next(parser.documentToNode(docself.?), next) orelse break;
|
||||
if (other == next) {
|
||||
// other precedes self.
|
||||
return @intFromEnum(parser.DocumentPosition.preceding);
|
||||
}
|
||||
if (self == next) {
|
||||
// other follows self.
|
||||
return @intFromEnum(parser.DocumentPosition.following);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
pub fn _contains(self: *parser.Node, other: *parser.Node) !bool {
|
||||
return try parser.nodeContains(self, other);
|
||||
}
|
||||
|
||||
pub fn _getRootNode(self: *parser.Node) void {
|
||||
// TODO
|
||||
_ = self;
|
||||
std.log.err("Not implemented {s}", .{"node.getRootNode()"});
|
||||
pub fn _getRootNode(self: *parser.Node) !?HTMLElem.Union {
|
||||
// TODO return this’s shadow-including root if options["composed"] is true
|
||||
const res = try parser.nodeOwnerDocument(self);
|
||||
if (res == null) {
|
||||
return null;
|
||||
}
|
||||
return try HTMLElem.toInterface(HTMLElem.Union, @as(*parser.Element, @ptrCast(res.?)));
|
||||
}
|
||||
|
||||
pub fn _hasChildNodes(self: *parser.Node) !bool {
|
||||
@@ -384,6 +432,21 @@ pub fn testExecFn(
|
||||
;
|
||||
try runScript(js_env, alloc, trim_and_replace, "proto_test");
|
||||
|
||||
var node_compare_document_position = [_]Case{
|
||||
.{ .src = "document.body.compareDocumentPosition(document.firstChild); ", .ex = "10" },
|
||||
.{ .src = "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"content\"));", .ex = "10" },
|
||||
.{ .src = "document.getElementById(\"content\").compareDocumentPosition(document.getElementById(\"para-empty\"));", .ex = "20" },
|
||||
.{ .src = "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"link\"));", .ex = "0" },
|
||||
.{ .src = "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"link\"));", .ex = "2" },
|
||||
.{ .src = "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"para-empty\"));", .ex = "4" },
|
||||
};
|
||||
try checkCases(js_env, &node_compare_document_position);
|
||||
|
||||
var get_root_node = [_]Case{
|
||||
.{ .src = "document.getElementById('content').getRootNode().__proto__.constructor.name", .ex = "HTMLDocument" },
|
||||
};
|
||||
try checkCases(js_env, &get_root_node);
|
||||
|
||||
var first_child = [_]Case{
|
||||
// for next test cases
|
||||
.{ .src = "let content = document.getElementById('content')", .ex = "undefined" },
|
||||
|
||||
@@ -21,14 +21,82 @@ const std = @import("std");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Callback = jsruntime.Callback;
|
||||
const CallbackResult = jsruntime.CallbackResult;
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
const generate = @import("../generate.zig");
|
||||
|
||||
const NodeUnion = @import("node.zig").Union;
|
||||
const Node = @import("node.zig").Node;
|
||||
|
||||
const U32Iterator = @import("../iterator/iterator.zig").U32Iterator;
|
||||
|
||||
const log = std.log.scoped(.nodelist);
|
||||
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
NodeListIterator,
|
||||
NodeList,
|
||||
});
|
||||
|
||||
pub const NodeListIterator = struct {
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
coll: *NodeList,
|
||||
index: u32 = 0,
|
||||
|
||||
pub const Return = struct {
|
||||
value: ?NodeUnion,
|
||||
done: bool,
|
||||
};
|
||||
|
||||
pub fn _next(self: *NodeListIterator) !Return {
|
||||
const e = try self.coll._item(self.index);
|
||||
if (e == null) {
|
||||
return Return{
|
||||
.value = null,
|
||||
.done = true,
|
||||
};
|
||||
}
|
||||
|
||||
self.index += 1;
|
||||
return Return{
|
||||
.value = e,
|
||||
.done = false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const NodeListEntriesIterator = struct {
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
coll: *NodeList,
|
||||
index: u32 = 0,
|
||||
|
||||
pub const Return = struct {
|
||||
value: ?NodeUnion,
|
||||
done: bool,
|
||||
};
|
||||
|
||||
pub fn _next(self: *NodeListEntriesIterator) !Return {
|
||||
const e = try self.coll._item(self.index);
|
||||
if (e == null) {
|
||||
return Return{
|
||||
.value = null,
|
||||
.done = true,
|
||||
};
|
||||
}
|
||||
|
||||
self.index += 1;
|
||||
return Return{
|
||||
.value = e,
|
||||
.done = false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Nodelist is implemented in pure Zig b/c libdom's NodeList doesn't allow to
|
||||
// append nodes.
|
||||
// WEB IDL https://dom.spec.whatwg.org/#nodelist
|
||||
@@ -72,9 +140,50 @@ pub const NodeList = struct {
|
||||
return try Node.toInterface(n);
|
||||
}
|
||||
|
||||
// TODO _symbol_iterator
|
||||
pub fn _forEach(self: *NodeList, alloc: std.mem.Allocator, cbk: Callback) !void { // TODO handle thisArg
|
||||
var res = CallbackResult.init(alloc);
|
||||
defer res.deinit();
|
||||
|
||||
// TODO implement postAttach
|
||||
for (self.nodes.items, 0..) |n, i| {
|
||||
const ii: u32 = @intCast(i);
|
||||
cbk.trycall(.{ n, ii, self }, &res) catch |e| {
|
||||
log.err("callback error: {s}", .{res.result orelse "unknown"});
|
||||
log.debug("{s}", .{res.stack orelse "no stack trace"});
|
||||
|
||||
return e;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _keys(self: *NodeList) U32Iterator {
|
||||
return .{
|
||||
.length = self.get_length(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _values(self: *NodeList) NodeListIterator {
|
||||
return .{
|
||||
.coll = self,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _symbol_iterator(self: *NodeList) NodeListIterator {
|
||||
return self._values();
|
||||
}
|
||||
|
||||
// TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries
|
||||
|
||||
pub fn postAttach(self: *NodeList, alloc: std.mem.Allocator, js_obj: jsruntime.JSObject) !void {
|
||||
const ln = self.get_length();
|
||||
var i: u32 = 0;
|
||||
while (i < ln) {
|
||||
defer i += 1;
|
||||
const k = try std.fmt.allocPrint(alloc, "{d}", .{i});
|
||||
|
||||
const node = try self._item(i) orelse unreachable;
|
||||
try js_obj.set(k, node);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
@@ -87,6 +196,14 @@ pub fn testExecFn(
|
||||
var childnodes = [_]Case{
|
||||
.{ .src = "let list = document.getElementById('content').childNodes", .ex = "undefined" },
|
||||
.{ .src = "list.length", .ex = "9" },
|
||||
.{ .src = "list[0].__proto__.constructor.name", .ex = "Text" },
|
||||
.{ .src =
|
||||
\\let i = 0;
|
||||
\\list.forEach(function (n, idx) {
|
||||
\\ i += idx;
|
||||
\\});
|
||||
\\i;
|
||||
, .ex = "36" },
|
||||
};
|
||||
try checkCases(js_env, &childnodes);
|
||||
}
|
||||
|
||||
@@ -251,12 +251,12 @@ pub const EventHandler = struct {
|
||||
Event.toInterface(evt) catch unreachable,
|
||||
}, &res) catch |e| log.err("event handler error: {any}", .{e});
|
||||
} else {
|
||||
data.cbk.trycall(.{event}, &res) catch |e| log.err("event handler error: {any}", .{e});
|
||||
data.cbk.trycall(.{event}, &res) catch |e| log.err("event handler error (null event): {any}", .{e});
|
||||
}
|
||||
|
||||
// in case of function error, we log the result and the trace.
|
||||
if (!res.success) {
|
||||
log.info("event handler error: {s}", .{res.result orelse "unknown"});
|
||||
log.info("event handler error try catch: {s}", .{res.result orelse "unknown"});
|
||||
log.debug("{s}", .{res.stack orelse "no stack trace"});
|
||||
}
|
||||
}
|
||||
|
||||
95
src/handler.zig
Normal file
95
src/handler.zig
Normal 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);
|
||||
}
|
||||
};
|
||||
@@ -153,8 +153,8 @@ pub const HTMLDocument = struct {
|
||||
return try collection.HTMLCollectionAll(parser.documentHTMLToNode(self), true);
|
||||
}
|
||||
|
||||
pub fn get_currentScript(_: *parser.DocumentHTML) !?*parser.Element {
|
||||
return null;
|
||||
pub fn get_currentScript(self: *parser.DocumentHTML) !?*parser.Script {
|
||||
return try parser.documentHTMLGetCurrentScript(self);
|
||||
}
|
||||
|
||||
pub fn get_designMode(_: *parser.DocumentHTML) []const u8 {
|
||||
|
||||
@@ -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 = .{
|
||||
@@ -96,6 +97,7 @@ pub const Interfaces = .{
|
||||
HTMLTrackElement,
|
||||
HTMLUListElement,
|
||||
HTMLVideoElement,
|
||||
CSSProperties,
|
||||
};
|
||||
const Generated = generate.Union.compile(Interfaces);
|
||||
pub const Union = Generated._union;
|
||||
@@ -104,10 +106,37 @@ pub const Tags = Generated._enum;
|
||||
// Abstract class
|
||||
// --------------
|
||||
|
||||
const CSSProperties = struct {
|
||||
pub const mem_guarantied = true;
|
||||
};
|
||||
|
||||
pub const HTMLElement = struct {
|
||||
pub const Self = parser.ElementHTML;
|
||||
pub const prototype = *Element;
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
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)
|
||||
@@ -1059,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
128
src/html/history.zig
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const builtin = @import("builtin");
|
||||
const jsruntime = @import("jsruntime");
|
||||
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
|
||||
pub const History = struct {
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
const ScrollRestorationMode = enum {
|
||||
auto,
|
||||
manual,
|
||||
};
|
||||
|
||||
scrollRestoration: ScrollRestorationMode = .auto,
|
||||
state: std.json.Value = .null,
|
||||
|
||||
// count tracks the history length until we implement correctly pushstate.
|
||||
count: u32 = 0,
|
||||
|
||||
pub fn get_length(self: *History) u32 {
|
||||
// TODO return the real history length value.
|
||||
return self.count;
|
||||
}
|
||||
|
||||
pub fn get_scrollRestoration(self: *History) []const u8 {
|
||||
return switch (self.scrollRestoration) {
|
||||
.auto => "auto",
|
||||
.manual => "manual",
|
||||
};
|
||||
}
|
||||
|
||||
pub fn set_scrollRestoration(self: *History, mode: []const u8) void {
|
||||
if (std.mem.eql(u8, "manual", mode)) self.scrollRestoration = .manual;
|
||||
if (std.mem.eql(u8, "auto", mode)) self.scrollRestoration = .auto;
|
||||
}
|
||||
|
||||
pub fn get_state(self: *History) std.json.Value {
|
||||
return self.state;
|
||||
}
|
||||
|
||||
// TODO implement the function
|
||||
// data must handle any argument. We could expect a std.json.Value but
|
||||
// https://github.com/lightpanda-io/zig-js-runtime/issues/267 is missing.
|
||||
pub fn _pushState(self: *History, data: []const u8, _: ?[]const u8, url: ?[]const u8) void {
|
||||
self.count += 1;
|
||||
_ = url;
|
||||
_ = data;
|
||||
}
|
||||
|
||||
// TODO implement the function
|
||||
// data must handle any argument. We could expect a std.json.Value but
|
||||
// https://github.com/lightpanda-io/zig-js-runtime/issues/267 is missing.
|
||||
pub fn _replaceState(self: *History, data: []const u8, _: ?[]const u8, url: ?[]const u8) void {
|
||||
_ = self;
|
||||
_ = url;
|
||||
_ = data;
|
||||
}
|
||||
|
||||
// TODO implement the function
|
||||
pub fn _go(self: *History, delta: ?i32) void {
|
||||
_ = self;
|
||||
_ = delta;
|
||||
}
|
||||
|
||||
// TODO implement the function
|
||||
pub fn _back(self: *History) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
// TODO implement the function
|
||||
pub fn _forward(self: *History) void {
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var history = [_]Case{
|
||||
.{ .src = "history.scrollRestoration", .ex = "auto" },
|
||||
.{ .src = "history.scrollRestoration = 'manual'", .ex = "manual" },
|
||||
.{ .src = "history.scrollRestoration = 'foo'", .ex = "foo" },
|
||||
.{ .src = "history.scrollRestoration", .ex = "manual" },
|
||||
.{ .src = "history.scrollRestoration = 'auto'", .ex = "auto" },
|
||||
.{ .src = "history.scrollRestoration", .ex = "auto" },
|
||||
|
||||
.{ .src = "history.state", .ex = "null" },
|
||||
|
||||
.{ .src = "history.pushState({}, null, '')", .ex = "undefined" },
|
||||
|
||||
.{ .src = "history.replaceState({}, null, '')", .ex = "undefined" },
|
||||
|
||||
.{ .src = "history.go()", .ex = "undefined" },
|
||||
.{ .src = "history.go(1)", .ex = "undefined" },
|
||||
.{ .src = "history.go(-1)", .ex = "undefined" },
|
||||
|
||||
.{ .src = "history.forward()", .ex = "undefined" },
|
||||
|
||||
.{ .src = "history.back()", .ex = "undefined" },
|
||||
};
|
||||
try checkCases(js_env, &history);
|
||||
}
|
||||
@@ -21,6 +21,8 @@ const generate = @import("../generate.zig");
|
||||
const HTMLDocument = @import("document.zig").HTMLDocument;
|
||||
const HTMLElem = @import("elements.zig");
|
||||
const Window = @import("window.zig").Window;
|
||||
const Navigator = @import("navigator.zig").Navigator;
|
||||
const History = @import("history.zig").History;
|
||||
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
HTMLDocument,
|
||||
@@ -28,4 +30,6 @@ pub const Interfaces = generate.Tuple(.{
|
||||
HTMLElem.HTMLMediaElement,
|
||||
HTMLElem.Interfaces,
|
||||
Window,
|
||||
Navigator,
|
||||
History,
|
||||
});
|
||||
|
||||
102
src/html/navigator.zig
Normal file
102
src/html/navigator.zig
Normal file
@@ -0,0 +1,102 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const builtin = @import("builtin");
|
||||
const jsruntime = @import("jsruntime");
|
||||
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/system-state.html#navigator
|
||||
pub const Navigator = struct {
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
agent: []const u8 = "Lightpanda/1.0",
|
||||
version: []const u8 = "1.0",
|
||||
vendor: []const u8 = "",
|
||||
platform: []const u8 = std.fmt.comptimePrint("{any} {any}", .{ builtin.os.tag, builtin.cpu.arch }),
|
||||
|
||||
language: []const u8 = "en-US",
|
||||
|
||||
pub fn get_userAgent(self: *Navigator) []const u8 {
|
||||
return self.agent;
|
||||
}
|
||||
pub fn get_appCodeName(_: *Navigator) []const u8 {
|
||||
return "Mozilla";
|
||||
}
|
||||
pub fn get_appName(_: *Navigator) []const u8 {
|
||||
return "Netscape";
|
||||
}
|
||||
pub fn get_appVersion(self: *Navigator) []const u8 {
|
||||
return self.version;
|
||||
}
|
||||
pub fn get_platform(self: *Navigator) []const u8 {
|
||||
return self.platform;
|
||||
}
|
||||
pub fn get_product(_: *Navigator) []const u8 {
|
||||
return "Gecko";
|
||||
}
|
||||
pub fn get_productSub(_: *Navigator) []const u8 {
|
||||
return "20030107";
|
||||
}
|
||||
pub fn get_vendor(self: *Navigator) []const u8 {
|
||||
return self.vendor;
|
||||
}
|
||||
pub fn get_vendorSub(_: *Navigator) []const u8 {
|
||||
return "";
|
||||
}
|
||||
pub fn get_language(self: *Navigator) []const u8 {
|
||||
return self.language;
|
||||
}
|
||||
// TODO wait for arrays.
|
||||
//pub fn get_languages(self: *Navigator) [][]const u8 {
|
||||
// return .{self.language};
|
||||
//}
|
||||
pub fn get_online(_: *Navigator) bool {
|
||||
return true;
|
||||
}
|
||||
pub fn _registerProtocolHandler(_: *Navigator, scheme: []const u8, url: []const u8) void {
|
||||
_ = scheme;
|
||||
_ = url;
|
||||
}
|
||||
pub fn _unregisterProtocolHandler(_: *Navigator, scheme: []const u8, url: []const u8) void {
|
||||
_ = scheme;
|
||||
_ = url;
|
||||
}
|
||||
|
||||
pub fn get_cookieEnabled(_: *Navigator) bool {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var navigator = [_]Case{
|
||||
.{ .src = "navigator.userAgent", .ex = "Lightpanda/1.0" },
|
||||
.{ .src = "navigator.appVersion", .ex = "1.0" },
|
||||
.{ .src = "navigator.language", .ex = "en-US" },
|
||||
};
|
||||
try checkCases(js_env, &navigator);
|
||||
}
|
||||
@@ -25,6 +25,8 @@ const CallbackArg = jsruntime.CallbackArg;
|
||||
const Loop = jsruntime.Loop;
|
||||
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const Navigator = @import("navigator.zig").Navigator;
|
||||
const History = @import("history.zig").History;
|
||||
|
||||
const storage = @import("../storage/storage.zig");
|
||||
|
||||
@@ -40,6 +42,7 @@ pub const Window = struct {
|
||||
|
||||
document: ?*parser.DocumentHTML = null,
|
||||
target: []const u8,
|
||||
history: History = .{},
|
||||
|
||||
storageShelf: ?*storage.Shelf = null,
|
||||
|
||||
@@ -48,9 +51,13 @@ pub const Window = struct {
|
||||
timeoutid: u32 = 0,
|
||||
timeoutids: [512]u64 = undefined,
|
||||
|
||||
pub fn create(target: ?[]const u8) Window {
|
||||
navigator: Navigator,
|
||||
|
||||
pub fn create(target: ?[]const u8, navigator: ?Navigator) Window {
|
||||
return Window{
|
||||
.target = target orelse "",
|
||||
.navigator = navigator orelse .{},
|
||||
.base = .{ .et_type = @intFromEnum(parser.EventTargetType.window) },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,6 +73,10 @@ pub const Window = struct {
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn get_navigator(self: *Window) *Navigator {
|
||||
return &self.navigator;
|
||||
}
|
||||
|
||||
pub fn get_self(self: *Window) *Window {
|
||||
return self;
|
||||
}
|
||||
@@ -78,6 +89,10 @@ pub const Window = struct {
|
||||
return self.document;
|
||||
}
|
||||
|
||||
pub fn get_history(self: *Window) *History {
|
||||
return &self.history;
|
||||
}
|
||||
|
||||
pub fn get_name(self: *Window) []const u8 {
|
||||
return self.target;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,3 @@
|
||||
// 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/>.
|
||||
|
||||
//! HTTP(S) Client implementation.
|
||||
//!
|
||||
//! Connections are opened in a thread-safe manner, but individual Requests are not.
|
||||
|
||||
35
src/iterator/iterator.zig
Normal file
35
src/iterator/iterator.zig
Normal file
@@ -0,0 +1,35 @@
|
||||
const std = @import("std");
|
||||
|
||||
const generate = @import("../generate.zig");
|
||||
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
U32Iterator,
|
||||
});
|
||||
|
||||
pub const U32Iterator = struct {
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
length: u32,
|
||||
index: u32 = 0,
|
||||
|
||||
pub const Return = struct {
|
||||
value: u32,
|
||||
done: bool,
|
||||
};
|
||||
|
||||
pub fn _next(self: *U32Iterator) !Return {
|
||||
const i = self.index;
|
||||
if (i >= self.length) {
|
||||
return Return{
|
||||
.value = 0,
|
||||
.done = true,
|
||||
};
|
||||
}
|
||||
|
||||
self.index += 1;
|
||||
return Return{
|
||||
.value = i,
|
||||
.done = false,
|
||||
};
|
||||
}
|
||||
};
|
||||
387
src/main.zig
387
src/main.zig
@@ -17,97 +17,350 @@
|
||||
// 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 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");
|
||||
const Window = @import("html/window.zig").Window;
|
||||
|
||||
pub const Types = jsruntime.reflect(apiweb.Interfaces);
|
||||
pub const UserContext = apiweb.UserContext;
|
||||
pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
|
||||
|
||||
const socket_path = "/tmp/browsercore-server.sock";
|
||||
// Simple blocking websocket connection model
|
||||
// ie. 1 thread per ws connection without thread pool and epoll/kqueue
|
||||
pub const websocket_blocking = true;
|
||||
|
||||
var doc: *parser.DocumentHTML = undefined;
|
||||
var server: std.net.Server = undefined;
|
||||
const log = std.log.scoped(.cli);
|
||||
|
||||
fn execJS(
|
||||
alloc: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
// start JS env
|
||||
try js_env.start();
|
||||
defer js_env.stop();
|
||||
pub const std_options = .{
|
||||
// Set the log level to info
|
||||
.log_level = .debug,
|
||||
|
||||
// alias global as self and window
|
||||
var window = Window.create(null);
|
||||
window.replaceDocument(doc);
|
||||
try js_env.bindGlobal(window);
|
||||
// Define logFn to override the std implementation
|
||||
.logFn = logFn,
|
||||
};
|
||||
|
||||
// try catch
|
||||
var try_catch: jsruntime.TryCatch = undefined;
|
||||
try_catch.init(js_env.*);
|
||||
defer try_catch.deinit();
|
||||
const usage =
|
||||
\\usage: {s} [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.
|
||||
\\ --verbose Display all logs. By default only info, warn and err levels are displayed.
|
||||
\\ --host Host of the CDP server (default "127.0.0.1")
|
||||
\\ --port Port of the CDP server (default "9222")
|
||||
\\ --timeout Timeout for incoming connections of the CDP server (in seconds, default "3")
|
||||
\\ --dump Dump document in stdout (fetch mode only)
|
||||
\\
|
||||
;
|
||||
|
||||
while (true) {
|
||||
fn printUsageExit(execname: []const u8, res: u8) anyerror {
|
||||
std.io.getStdErr().writer().print(usage, .{execname}) catch |err| {
|
||||
std.log.err("Print usage error: {any}", .{err});
|
||||
return error.Cli;
|
||||
};
|
||||
if (res == 1) return error.Usage;
|
||||
return error.NoError;
|
||||
}
|
||||
|
||||
// read cmd
|
||||
const conn = try server.accept();
|
||||
var buf: [100]u8 = undefined;
|
||||
const read = try conn.stream.read(&buf);
|
||||
const cmd = buf[0..read];
|
||||
std.debug.print("<- {s}\n", .{cmd});
|
||||
if (std.mem.eql(u8, cmd, "exit")) {
|
||||
break;
|
||||
const CliModeTag = enum {
|
||||
server,
|
||||
fetch,
|
||||
};
|
||||
|
||||
const CliMode = union(CliModeTag) {
|
||||
server: Server,
|
||||
fetch: Fetch,
|
||||
|
||||
const Server = struct {
|
||||
execname: []const u8 = undefined,
|
||||
args: *std.process.ArgIterator = undefined,
|
||||
addr: std.net.Address = undefined,
|
||||
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 = 9222;
|
||||
const Timeout = 3; // in seconds
|
||||
};
|
||||
|
||||
const Fetch = struct {
|
||||
execname: []const u8 = undefined,
|
||||
args: *std.process.ArgIterator = undefined,
|
||||
url: []const u8 = "",
|
||||
dump: bool = false,
|
||||
};
|
||||
|
||||
fn init(alloc: std.mem.Allocator, args: *std.process.ArgIterator) !CliMode {
|
||||
args.* = try std.process.argsWithAllocator(alloc);
|
||||
errdefer args.deinit();
|
||||
|
||||
const execname = args.next().?;
|
||||
var default_mode: CliModeTag = .server;
|
||||
|
||||
var _server = Server{};
|
||||
var _fetch = Fetch{};
|
||||
|
||||
while (args.next()) |opt| {
|
||||
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;
|
||||
}
|
||||
if (std.mem.eql(u8, "--host", opt)) {
|
||||
if (args.next()) |arg| {
|
||||
_server.host = arg;
|
||||
continue;
|
||||
} else {
|
||||
std.log.err("--host not provided\n", .{});
|
||||
return printUsageExit(execname, 1);
|
||||
}
|
||||
}
|
||||
if (std.mem.eql(u8, "--port", opt)) {
|
||||
if (args.next()) |arg| {
|
||||
_server.port = std.fmt.parseInt(u16, arg, 10) catch |err| {
|
||||
log.err("--port {any}\n", .{err});
|
||||
return printUsageExit(execname, 1);
|
||||
};
|
||||
continue;
|
||||
} else {
|
||||
log.err("--port not provided\n", .{});
|
||||
return printUsageExit(execname, 1);
|
||||
}
|
||||
}
|
||||
if (std.mem.eql(u8, "--timeout", opt)) {
|
||||
if (args.next()) |arg| {
|
||||
_server.timeout = std.fmt.parseInt(u8, arg, 10) catch |err| {
|
||||
log.err("--timeout {any}\n", .{err});
|
||||
return printUsageExit(execname, 1);
|
||||
};
|
||||
continue;
|
||||
} else {
|
||||
log.err("--timeout not provided\n", .{});
|
||||
return printUsageExit(execname, 1);
|
||||
}
|
||||
}
|
||||
if (std.mem.eql(u8, "--tcp", opt)) {
|
||||
_server.tcp = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// unknown option
|
||||
if (std.mem.startsWith(u8, opt, "--")) {
|
||||
log.err("unknown option\n", .{});
|
||||
return printUsageExit(execname, 1);
|
||||
}
|
||||
|
||||
// other argument is considered to be an URL, ie. fetch mode
|
||||
default_mode = .fetch;
|
||||
|
||||
// allow only one url
|
||||
if (_fetch.url.len != 0) {
|
||||
log.err("more than 1 url provided\n", .{});
|
||||
return printUsageExit(execname, 1);
|
||||
}
|
||||
|
||||
_fetch.url = opt;
|
||||
}
|
||||
|
||||
const res = try js_env.exec(cmd, "cdp");
|
||||
const res_str = try res.toString(alloc, js_env.*);
|
||||
defer alloc.free(res_str);
|
||||
std.debug.print("-> {s}\n", .{res_str});
|
||||
if (default_mode == .server) {
|
||||
|
||||
_ = try conn.stream.write(res_str);
|
||||
// server mode
|
||||
_server.addr = std.net.Address.parseIp4(_server.host, _server.port) catch |err| {
|
||||
log.err("address (host:port) {any}\n", .{err});
|
||||
return printUsageExit(execname, 1);
|
||||
};
|
||||
_server.execname = execname;
|
||||
_server.args = args;
|
||||
return CliMode{ .server = _server };
|
||||
} else {
|
||||
|
||||
// fetch mode
|
||||
_fetch.execname = execname;
|
||||
_fetch.args = args;
|
||||
return CliMode{ .fetch = _fetch };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deinit(self: CliMode) void {
|
||||
switch (self) {
|
||||
inline .server, .fetch => |*_mode| {
|
||||
_mode.args.deinit();
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
|
||||
// create v8 vm
|
||||
const vm = jsruntime.VM.init();
|
||||
defer vm.deinit();
|
||||
|
||||
// alloc
|
||||
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
try parser.init();
|
||||
defer parser.deinit();
|
||||
|
||||
// document
|
||||
const file = try std.fs.cwd().openFile("test.html", .{});
|
||||
defer file.close();
|
||||
|
||||
doc = try parser.documentHTMLParse(file.reader(), "UTF-8");
|
||||
defer parser.documentHTMLClose(doc) catch |err| {
|
||||
std.debug.print("documentHTMLClose error: {s}\n", .{@errorName(err)});
|
||||
};
|
||||
|
||||
// remove socket file of internal server
|
||||
// reuse_address (SO_REUSEADDR flag) does not seems to work on unix socket
|
||||
// see: https://gavv.net/articles/unix-socket-reuse/
|
||||
// TODO: use a lock file instead
|
||||
std.posix.unlink(socket_path) catch |err| {
|
||||
if (err != error.FileNotFound) {
|
||||
return err;
|
||||
// allocator
|
||||
// - in Debug mode we use the General Purpose Allocator to detect memory leaks
|
||||
// - in Release mode we use the page allocator
|
||||
var alloc: std.mem.Allocator = undefined;
|
||||
var _gpa: ?std.heap.GeneralPurposeAllocator(.{}) = null;
|
||||
if (builtin.mode == .Debug) {
|
||||
_gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
alloc = _gpa.?.allocator();
|
||||
} else {
|
||||
alloc = std.heap.page_allocator;
|
||||
}
|
||||
defer {
|
||||
if (_gpa) |*gpa| {
|
||||
switch (gpa.deinit()) {
|
||||
.ok => std.debug.print("No memory leaks\n", .{}),
|
||||
.leak => @panic("Memory leak"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// args
|
||||
var args: std.process.ArgIterator = undefined;
|
||||
const cli_mode = CliMode.init(alloc, &args) catch |err| {
|
||||
if (err == error.NoError) {
|
||||
std.posix.exit(0);
|
||||
} else {
|
||||
std.posix.exit(1);
|
||||
}
|
||||
return;
|
||||
};
|
||||
defer cli_mode.deinit();
|
||||
|
||||
// server
|
||||
const addr = try std.net.Address.initUnix(socket_path);
|
||||
server = try addr.listen(.{});
|
||||
defer server.deinit();
|
||||
std.debug.print("Listening on: {s}...\n", .{socket_path});
|
||||
switch (cli_mode) {
|
||||
.server => |opts| {
|
||||
|
||||
try jsruntime.loadEnv(&arena, null, execJS);
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
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();
|
||||
|
||||
// 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 => |opts| {
|
||||
log.debug("Fetch mode: url {s}, dump {any}", .{ opts.url, opts.dump });
|
||||
|
||||
// vm
|
||||
const vm = jsruntime.VM.init();
|
||||
defer vm.deinit();
|
||||
|
||||
// loop
|
||||
var loop = try jsruntime.Loop.init(alloc);
|
||||
defer loop.deinit();
|
||||
|
||||
// browser
|
||||
var browser = Browser{};
|
||||
try Browser.init(&browser, alloc, &loop, vm);
|
||||
defer browser.deinit();
|
||||
|
||||
// page
|
||||
const page = try browser.session.createPage();
|
||||
try page.start(null);
|
||||
defer page.end();
|
||||
|
||||
_ = page.navigate(opts.url, null) catch |err| switch (err) {
|
||||
error.UnsupportedUriScheme, error.UriMissingHost => {
|
||||
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", .{ opts.url, err });
|
||||
return printUsageExit(opts.execname, 1);
|
||||
},
|
||||
};
|
||||
|
||||
try page.wait();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
// 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 Browser = @import("browser/browser.zig").Browser;
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const apiweb = @import("apiweb.zig");
|
||||
|
||||
pub const Types = jsruntime.reflect(apiweb.Interfaces);
|
||||
pub const UserContext = apiweb.UserContext;
|
||||
|
||||
pub const std_options = std.Options{
|
||||
.log_level = .debug,
|
||||
};
|
||||
|
||||
const usage =
|
||||
\\usage: {s} [options] <url>
|
||||
\\ request the url with the browser
|
||||
\\
|
||||
\\ -h, --help Print this help message and exit.
|
||||
\\ --dump Dump document in stdout
|
||||
\\
|
||||
;
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer {
|
||||
const check = gpa.deinit();
|
||||
if (check == .leak) {
|
||||
std.log.warn("leaks detected\n", .{});
|
||||
}
|
||||
}
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var args = try std.process.argsWithAllocator(allocator);
|
||||
defer args.deinit();
|
||||
|
||||
const execname = args.next().?;
|
||||
var url: []const u8 = "";
|
||||
var dump: bool = false;
|
||||
|
||||
while (args.next()) |arg| {
|
||||
if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) {
|
||||
try std.io.getStdErr().writer().print(usage, .{execname});
|
||||
std.posix.exit(0);
|
||||
}
|
||||
if (std.mem.eql(u8, "--dump", arg)) {
|
||||
dump = true;
|
||||
continue;
|
||||
}
|
||||
// allow only one url
|
||||
if (url.len != 0) {
|
||||
try std.io.getStdErr().writer().print(usage, .{execname});
|
||||
std.posix.exit(1);
|
||||
}
|
||||
url = arg;
|
||||
}
|
||||
|
||||
if (url.len == 0) {
|
||||
try std.io.getStdErr().writer().print(usage, .{execname});
|
||||
std.posix.exit(1);
|
||||
}
|
||||
|
||||
const vm = jsruntime.VM.init();
|
||||
defer vm.deinit();
|
||||
|
||||
var browser = try Browser.init(allocator, vm);
|
||||
defer browser.deinit();
|
||||
|
||||
var page = try browser.currentSession().createPage();
|
||||
defer page.deinit();
|
||||
|
||||
try page.navigate(url);
|
||||
defer page.end();
|
||||
|
||||
try page.wait();
|
||||
|
||||
if (dump) {
|
||||
try page.dump(std.io.getStdOut());
|
||||
}
|
||||
}
|
||||
@@ -24,12 +24,13 @@ const parser = @import("netsurf");
|
||||
const apiweb = @import("apiweb.zig");
|
||||
const Window = @import("html/window.zig").Window;
|
||||
const storage = @import("storage/storage.zig");
|
||||
const Client = @import("asyncio").Client;
|
||||
|
||||
const html_test = @import("html_test.zig").html;
|
||||
|
||||
pub const Types = jsruntime.reflect(apiweb.Interfaces);
|
||||
pub const UserContext = apiweb.UserContext;
|
||||
const Client = @import("async/Client.zig");
|
||||
pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
|
||||
|
||||
var doc: *parser.DocumentHTML = undefined;
|
||||
|
||||
@@ -41,7 +42,7 @@ fn execJS(
|
||||
try js_env.start();
|
||||
defer js_env.stop();
|
||||
|
||||
var cli = Client{ .allocator = alloc, .loop = js_env.nat_ctx.loop };
|
||||
var cli = Client{ .allocator = alloc };
|
||||
defer cli.deinit();
|
||||
|
||||
try js_env.setUserContext(UserContext{
|
||||
@@ -53,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);
|
||||
@@ -87,5 +88,5 @@ pub fn main() !void {
|
||||
defer vm.deinit();
|
||||
|
||||
// launch shell
|
||||
try jsruntime.shell(&arena, execJS, .{ .app_name = "browsercore" });
|
||||
try jsruntime.shell(&arena, execJS, .{ .app_name = "lightpanda-shell" });
|
||||
}
|
||||
|
||||
@@ -50,11 +50,12 @@ const Out = enum {
|
||||
pub const Types = jsruntime.reflect(apiweb.Interfaces);
|
||||
pub const GlobalType = apiweb.GlobalType;
|
||||
pub const UserContext = apiweb.UserContext;
|
||||
pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
|
||||
|
||||
// TODO For now the WPT tests run is specific to WPT.
|
||||
// It manually load js framwork libs, and run the first script w/ js content in
|
||||
// the HTML page.
|
||||
// Once browsercore will have the html loader, it would be useful to refacto
|
||||
// Once lightpanda will have the html loader, it would be useful to refacto
|
||||
// this test to use it.
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
|
||||
166
src/msg.zig
Normal file
166
src/msg.zig
Normal file
@@ -0,0 +1,166 @@
|
||||
// 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");
|
||||
|
||||
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 Buffer
|
||||
/// on several reads of the same stream.
|
||||
pub const Buffer = struct {
|
||||
buf: []u8,
|
||||
size: usize = 0,
|
||||
pos: usize = 0,
|
||||
|
||||
fn isFinished(self: *const Buffer) bool {
|
||||
return self.pos >= self.size;
|
||||
}
|
||||
|
||||
fn isEmpty(self: *const Buffer) bool {
|
||||
return self.size == 0 and self.pos == 0;
|
||||
}
|
||||
|
||||
fn reset(self: *Buffer) void {
|
||||
self.size = 0;
|
||||
self.pos = 0;
|
||||
}
|
||||
|
||||
// read input
|
||||
pub fn read(self: *Buffer, input: []const u8) !struct {
|
||||
msg: []const u8,
|
||||
left: []const u8,
|
||||
} {
|
||||
var _input = input; // make input writable
|
||||
|
||||
// msg size
|
||||
var msg_size: usize = undefined;
|
||||
if (self.isEmpty()) {
|
||||
// decode msg size header
|
||||
msg_size = Msg.getSize(_input);
|
||||
_input = _input[HeaderSize..];
|
||||
} else {
|
||||
msg_size = self.size;
|
||||
}
|
||||
|
||||
// multipart
|
||||
const is_multipart = !self.isEmpty() or _input.len < msg_size;
|
||||
if (is_multipart) {
|
||||
|
||||
// set msg size on empty Buffer
|
||||
if (self.isEmpty()) {
|
||||
self.size = msg_size;
|
||||
}
|
||||
|
||||
// get the new position of the cursor
|
||||
const new_pos = self.pos + _input.len;
|
||||
|
||||
// check max limit size
|
||||
if (new_pos > MaxSize) {
|
||||
return error.MsgTooBig;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// if multipart is not finished, go fetch the next input
|
||||
if (!self.isFinished()) return error.MsgMultipart;
|
||||
|
||||
// otherwhise multipart is finished, use its buffer as input
|
||||
_input = self.buf[0..self.pos];
|
||||
self.reset();
|
||||
}
|
||||
|
||||
// handle several JSON msg in 1 read
|
||||
return .{ .msg = _input[0..msg_size], .left = _input[msg_size..] };
|
||||
}
|
||||
};
|
||||
|
||||
test "Buffer" {
|
||||
const Case = struct {
|
||||
input: []const u8,
|
||||
nb: u8,
|
||||
};
|
||||
|
||||
const cases = [_]Case{
|
||||
// simple
|
||||
.{ .input = .{ 2, 0, 0, 0 } ++ "ok", .nb = 1 },
|
||||
// combined
|
||||
.{ .input = .{ 2, 0, 0, 0 } ++ "ok" ++ .{ 3, 0, 0, 0 } ++ "foo", .nb = 2 },
|
||||
// multipart
|
||||
.{ .input = .{ 9, 0, 0, 0 } ++ "multi", .nb = 0 },
|
||||
.{ .input = "part", .nb = 1 },
|
||||
// multipart & combined
|
||||
.{ .input = .{ 9, 0, 0, 0 } ++ "multi", .nb = 0 },
|
||||
.{ .input = "part" ++ .{ 2, 0, 0, 0 } ++ "ok", .nb = 2 },
|
||||
// multipart & combined with other multipart
|
||||
.{ .input = .{ 9, 0, 0, 0 } ++ "multi", .nb = 0 },
|
||||
.{ .input = "part" ++ .{ 8, 0, 0, 0 } ++ "co", .nb = 1 },
|
||||
.{ .input = "mbined", .nb = 1 },
|
||||
// several multipart
|
||||
.{ .input = .{ 23, 0, 0, 0 } ++ "multi", .nb = 0 },
|
||||
.{ .input = "several", .nb = 0 },
|
||||
.{ .input = "complex", .nb = 0 },
|
||||
.{ .input = "part", .nb = 1 },
|
||||
// combined & multipart
|
||||
.{ .input = .{ 2, 0, 0, 0 } ++ "ok" ++ .{ 9, 0, 0, 0 } ++ "multi", .nb = 1 },
|
||||
.{ .input = "part", .nb = 1 },
|
||||
};
|
||||
|
||||
var b: [MaxSize]u8 = undefined;
|
||||
var buf = Buffer{ .buf = &b };
|
||||
|
||||
for (cases) |case| {
|
||||
var nb: u8 = 0;
|
||||
var input = case.input;
|
||||
while (input.len > 0) {
|
||||
const parts = buf.read(input) catch |err| {
|
||||
if (err == error.MsgMultipart) break; // go to the next case input
|
||||
return err;
|
||||
};
|
||||
nb += 1;
|
||||
input = parts.left;
|
||||
}
|
||||
try std.testing.expect(nb == case.nb);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -1794,7 +1820,7 @@ pub fn documentFragmentBodyChildren(doc: *DocumentFragment) !?*NodeList {
|
||||
|
||||
// Document Position
|
||||
|
||||
pub const DocumentPosition = enum(u2) {
|
||||
pub const DocumentPosition = enum(u32) {
|
||||
disconnected = c.DOM_DOCUMENT_POSITION_DISCONNECTED,
|
||||
preceding = c.DOM_DOCUMENT_POSITION_PRECEDING,
|
||||
following = c.DOM_DOCUMENT_POSITION_FOLLOWING,
|
||||
@@ -2249,3 +2275,18 @@ pub inline fn documentHTMLSetTitle(doc: *DocumentHTML, v: []const u8) !void {
|
||||
const err = documentHTMLVtable(doc).set_title.?(doc, try strFromData(v));
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn documentHTMLSetCurrentScript(doc: *DocumentHTML, script: ?*Script) !void {
|
||||
var s: ?*ElementHTML = null;
|
||||
if (script != null) s = @ptrCast(script.?);
|
||||
const err = documentHTMLVtable(doc).set_current_script.?(doc, s);
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn documentHTMLGetCurrentScript(doc: *DocumentHTML) !?*Script {
|
||||
var elem: ?*ElementHTML = undefined;
|
||||
const err = documentHTMLVtable(doc).get_current_script.?(doc, &elem);
|
||||
try DOMErr(err);
|
||||
if (elem == null) return null;
|
||||
return @ptrCast(elem.?);
|
||||
}
|
||||
|
||||
671
src/polyfill/fetch.js
Normal file
671
src/polyfill/fetch.js
Normal 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
55
src/polyfill/fetch.zig
Normal 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
56
src/polyfill/polyfill.zig
Normal 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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ const xhr = @import("xhr/xhr.zig");
|
||||
const storage = @import("storage/storage.zig");
|
||||
const url = @import("url/url.zig");
|
||||
const urlquery = @import("url/query.zig");
|
||||
const Client = @import("async/Client.zig");
|
||||
const Client = @import("asyncio").Client;
|
||||
|
||||
const documentTestExecFn = @import("dom/document.zig").testExecFn;
|
||||
const HTMLDocumentTestExecFn = @import("html/document.zig").testExecFn;
|
||||
@@ -59,6 +59,7 @@ const MutationObserverTestExecFn = @import("dom/mutation_observer.zig").testExec
|
||||
|
||||
pub const Types = jsruntime.reflect(apiweb.Interfaces);
|
||||
pub const UserContext = @import("user_context.zig").UserContext;
|
||||
pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
|
||||
|
||||
var doc: *parser.DocumentHTML = undefined;
|
||||
|
||||
@@ -86,7 +87,7 @@ fn testExecFn(
|
||||
std.debug.print("documentHTMLClose error: {s}\n", .{@errorName(err)});
|
||||
};
|
||||
|
||||
var cli = Client{ .allocator = alloc, .loop = js_env.nat_ctx.loop };
|
||||
var cli = Client{ .allocator = alloc };
|
||||
defer cli.deinit();
|
||||
|
||||
try js_env.setUserContext(.{
|
||||
@@ -95,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);
|
||||
@@ -135,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| {
|
||||
@@ -283,7 +287,7 @@ fn run_js(out: Out) !void {
|
||||
const row_shape = .{ []const u8, pretty.Measure, u64, u64, pretty.Measure };
|
||||
const table = try pretty.GenerateTable(4, row_shape, pretty.TableConf{ .margin_left = " " });
|
||||
const header = .{ "FUNCTION", "DURATION", "ALLOCATIONS (nb)", "RE-ALLOCATIONS (nb)", "HEAP SIZE" };
|
||||
var t = table.init("Benchmark browsercore 🚀", header);
|
||||
var t = table.init("Benchmark lightpanda 🚀", header);
|
||||
try t.addRow(.{ "browser", dur, stats.alloc_nb, stats.realloc_nb, size });
|
||||
try t.addRow(.{ "libdom", dur, 0, 0, zerosize }); // TODO get libdom bench info.
|
||||
try t.addRow(.{ "v8", dur, 0, 0, zerosize }); // TODO get v8 bench info.
|
||||
@@ -295,8 +299,8 @@ const kb = 1024;
|
||||
const ms = std.time.ns_per_ms;
|
||||
|
||||
test {
|
||||
const asyncTest = @import("async/test.zig");
|
||||
std.testing.refAllDecls(asyncTest);
|
||||
const msgTest = @import("msg.zig");
|
||||
std.testing.refAllDecls(msgTest);
|
||||
|
||||
const dumpTest = @import("browser/dump.zig");
|
||||
std.testing.refAllDecls(dumpTest);
|
||||
@@ -318,6 +322,8 @@ test {
|
||||
|
||||
const queryTest = @import("url/query.zig");
|
||||
std.testing.refAllDecls(queryTest);
|
||||
|
||||
std.testing.refAllDecls(@import("cdp/msg.zig"));
|
||||
}
|
||||
|
||||
fn testJSRuntime(alloc: std.mem.Allocator) !void {
|
||||
@@ -355,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", .{});
|
||||
|
||||
530
src/server.zig
Normal file
530
src/server.zig
Normal file
@@ -0,0 +1,530 @@
|
||||
// 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 Stream = @import("handler.zig").Stream;
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Completion = jsruntime.IO.Completion;
|
||||
const AcceptError = jsruntime.IO.AcceptError;
|
||||
const RecvError = jsruntime.IO.RecvError;
|
||||
const SendError = jsruntime.IO.SendError;
|
||||
const CloseError = jsruntime.IO.CloseError;
|
||||
const CancelError = jsruntime.IO.CancelError;
|
||||
const TimeoutError = jsruntime.IO.TimeoutError;
|
||||
|
||||
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");
|
||||
|
||||
const NoError = error{NoError};
|
||||
const IOError = AcceptError || RecvError || SendError || CloseError || TimeoutError || CancelError;
|
||||
const Error = IOError || std.fmt.ParseIntError || cdp.Error || NoError;
|
||||
|
||||
const TimeoutCheck = std.time.ns_per_ms * 100;
|
||||
|
||||
const log = std.log.scoped(.server);
|
||||
const isLinux = builtin.target.os.tag == .linux;
|
||||
|
||||
// I/O Main
|
||||
// --------
|
||||
|
||||
const BufReadSize = 1024; // 1KB
|
||||
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,
|
||||
conn_socket: std.posix.socket_t = undefined,
|
||||
read_buf: []u8, // only for read operations
|
||||
msg_buf: *MsgBuffer,
|
||||
err: ?Error = null,
|
||||
|
||||
// I/O fields
|
||||
accept_completion: *Completion,
|
||||
conn_completion: *Completion,
|
||||
timeout_completion: *Completion,
|
||||
timeout: u64,
|
||||
last_active: ?std.time.Instant = null,
|
||||
|
||||
// CDP
|
||||
state: cdp.State = .{},
|
||||
|
||||
// JS fields
|
||||
browser: *Browser, // TODO: is pointer mandatory here?
|
||||
sessionNew: bool,
|
||||
// try_catch: jsruntime.TryCatch, // TODO
|
||||
|
||||
// callbacks
|
||||
// ---------
|
||||
|
||||
fn acceptCbk(
|
||||
self: *Ctx,
|
||||
completion: *Completion,
|
||||
result: AcceptError!std.posix.socket_t,
|
||||
) void {
|
||||
std.debug.assert(completion == self.acceptCompletion());
|
||||
|
||||
self.conn_socket = result catch |err| {
|
||||
log.err("accept error: {any}", .{err});
|
||||
self.err = err;
|
||||
return;
|
||||
};
|
||||
log.info("client connected", .{});
|
||||
|
||||
// set connection timestamp and timeout
|
||||
self.last_active = std.time.Instant.now() catch |err| {
|
||||
log.err("accept timestamp error: {any}", .{err});
|
||||
return;
|
||||
};
|
||||
self.loop.io.timeout(
|
||||
*Ctx,
|
||||
self,
|
||||
Ctx.timeoutCbk,
|
||||
self.timeout_completion,
|
||||
TimeoutCheck,
|
||||
);
|
||||
|
||||
// receving incomming messages asynchronously
|
||||
self.loop.io.recv(
|
||||
*Ctx,
|
||||
self,
|
||||
Ctx.readCbk,
|
||||
self.conn_completion,
|
||||
self.conn_socket,
|
||||
self.read_buf,
|
||||
);
|
||||
}
|
||||
|
||||
fn readCbk(self: *Ctx, completion: *Completion, result: RecvError!usize) void {
|
||||
std.debug.assert(completion == self.conn_completion);
|
||||
|
||||
const size = result catch |err| {
|
||||
if (self.isClosed() and err == error.FileDescriptorInvalid) {
|
||||
log.debug("read has been canceled", .{});
|
||||
return;
|
||||
}
|
||||
log.err("read error: {any}", .{err});
|
||||
self.err = err;
|
||||
return;
|
||||
};
|
||||
|
||||
if (size == 0) {
|
||||
// continue receving incomming messages asynchronously
|
||||
self.loop.io.recv(
|
||||
*Ctx,
|
||||
self,
|
||||
Ctx.readCbk,
|
||||
self.conn_completion,
|
||||
self.conn_socket,
|
||||
self.read_buf,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// set connection timestamp
|
||||
self.last_active = std.time.Instant.now() catch |err| {
|
||||
log.err("read timestamp error: {any}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
// continue receving incomming messages asynchronously
|
||||
self.loop.io.recv(
|
||||
*Ctx,
|
||||
self,
|
||||
Ctx.readCbk,
|
||||
self.conn_completion,
|
||||
self.conn_socket,
|
||||
self.read_buf,
|
||||
);
|
||||
|
||||
// read and execute input
|
||||
var input: []const u8 = self.read_buf[0..size];
|
||||
while (input.len > 0) {
|
||||
const parts = self.msg_buf.read(input) catch |err| {
|
||||
if (err == error.MsgMultipart) {
|
||||
return;
|
||||
} else {
|
||||
log.err("msg read error: {any}", .{err});
|
||||
return;
|
||||
}
|
||||
};
|
||||
input = parts.left;
|
||||
// execute
|
||||
self.do(parts.msg) catch |err| {
|
||||
if (err != error.Closed) {
|
||||
log.err("do error: {any}", .{err});
|
||||
log.debug("last msg: {s}", .{parts.msg});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn timeoutCbk(self: *Ctx, completion: *Completion, result: TimeoutError!void) void {
|
||||
std.debug.assert(completion == self.timeout_completion);
|
||||
|
||||
_ = result catch |err| {
|
||||
log.err("timeout error: {any}", .{err});
|
||||
self.err = err;
|
||||
return;
|
||||
};
|
||||
|
||||
if (self.isClosed()) {
|
||||
// conn is already closed, ignore timeout
|
||||
return;
|
||||
}
|
||||
|
||||
// check time since last read
|
||||
const now = std.time.Instant.now() catch |err| {
|
||||
log.err("timeout timestamp error: {any}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
if (now.since(self.last_active.?) > self.timeout) {
|
||||
// close current connection
|
||||
log.debug("conn timeout, closing...", .{});
|
||||
self.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// continue checking timeout
|
||||
self.loop.io.timeout(
|
||||
*Ctx,
|
||||
self,
|
||||
Ctx.timeoutCbk,
|
||||
self.timeout_completion,
|
||||
TimeoutCheck,
|
||||
);
|
||||
}
|
||||
|
||||
// shortcuts
|
||||
// ---------
|
||||
|
||||
inline fn isClosed(self: *Ctx) bool {
|
||||
// last_active is first saved on acceptCbk
|
||||
return self.last_active == null;
|
||||
}
|
||||
|
||||
// allocator of the current session
|
||||
inline fn alloc(self: *Ctx) std.mem.Allocator {
|
||||
return self.browser.session.alloc;
|
||||
}
|
||||
|
||||
// JS env of the current session
|
||||
inline fn env(self: Ctx) jsruntime.Env {
|
||||
return self.browser.session.env;
|
||||
}
|
||||
|
||||
inline fn acceptCompletion(self: *Ctx) *Completion {
|
||||
// NOTE: the logical completion to use here is the accept_completion
|
||||
// as the pipe_connection can be used simulteanously by a recv I/O operation.
|
||||
// But on MacOS (kqueue) the recv I/O operation on a closed socket leads to a panic
|
||||
// so we use the pipe_connection to avoid this problem
|
||||
if (isLinux) return self.accept_completion;
|
||||
return self.conn_completion;
|
||||
}
|
||||
|
||||
// actions
|
||||
// -------
|
||||
|
||||
fn do(self: *Ctx, cmd: []const u8) anyerror!void {
|
||||
|
||||
// close cmd
|
||||
if (std.mem.eql(u8, cmd, "close")) {
|
||||
// close connection
|
||||
log.info("close cmd, closing conn...", .{});
|
||||
self.close();
|
||||
return error.Closed;
|
||||
}
|
||||
|
||||
if (self.sessionNew) self.sessionNew = false;
|
||||
|
||||
const res = cdp.do(self.alloc(), cmd, self) catch |err| {
|
||||
|
||||
// cdp end cmd
|
||||
if (err == error.DisposeBrowserContext) {
|
||||
// restart a new browser session
|
||||
std.log.scoped(.cdp).debug("end cmd, restarting a new session...", .{});
|
||||
try self.newSession();
|
||||
return;
|
||||
}
|
||||
|
||||
return err;
|
||||
};
|
||||
|
||||
// send result
|
||||
if (!std.mem.eql(u8, res, "")) {
|
||||
return self.send(res);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
// otherwise write asynchronously on the socket connection
|
||||
return sendAsync(self, msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn close(self: *Ctx) void {
|
||||
|
||||
// conn is 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) {
|
||||
self.newSession() catch |err| {
|
||||
log.err("new session error: {any}", .{err});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
log.info("accepting new conn...", .{});
|
||||
|
||||
// continue accepting incoming requests
|
||||
self.loop.io.accept(
|
||||
*Ctx,
|
||||
self,
|
||||
Ctx.acceptCbk,
|
||||
self.acceptCompletion(),
|
||||
self.accept_socket,
|
||||
);
|
||||
}
|
||||
|
||||
fn newSession(self: *Ctx) !void {
|
||||
try self.browser.newSession(self.alloc(), self.loop);
|
||||
try self.browser.session.initInspector(
|
||||
self,
|
||||
Ctx.onInspectorResp,
|
||||
Ctx.onInspectorNotif,
|
||||
);
|
||||
self.sessionNew = true;
|
||||
}
|
||||
|
||||
// inspector
|
||||
// ---------
|
||||
|
||||
pub fn sendInspector(self: *Ctx, msg: []const u8) void {
|
||||
if (self.env().getInspector()) |inspector| {
|
||||
inspector.send(self.env(), msg);
|
||||
} else @panic("Inspector has not been set");
|
||||
}
|
||||
|
||||
inline fn inspectorCtx(ctx_opaque: *anyopaque) *Ctx {
|
||||
const aligned = @as(*align(@alignOf(Ctx)) anyopaque, @alignCast(ctx_opaque));
|
||||
return @as(*Ctx, @ptrCast(aligned));
|
||||
}
|
||||
|
||||
fn inspectorMsg(allocator: std.mem.Allocator, ctx: *Ctx, msg: []const u8) !void {
|
||||
// inject sessionID in cdp msg
|
||||
const tpl = "{s},\"sessionId\":\"{s}\"}}";
|
||||
const msg_open = msg[0 .. msg.len - 1]; // remove closing bracket
|
||||
const s = try std.fmt.allocPrint(
|
||||
allocator,
|
||||
tpl,
|
||||
.{ msg_open, ctx.state.sessionID orelse cdp.ContextSessionID },
|
||||
);
|
||||
|
||||
try ctx.send(s);
|
||||
}
|
||||
|
||||
pub fn onInspectorResp(ctx_opaque: *anyopaque, _: u32, msg: []const u8) void {
|
||||
if (std.log.defaultLogEnabled(.debug)) {
|
||||
// msg should be {"id":<id>,...
|
||||
const id_end = std.mem.indexOfScalar(u8, msg, ',') orelse unreachable;
|
||||
const id = msg[6..id_end];
|
||||
std.log.scoped(.cdp).debug("Res (inspector) > id {s}", .{id});
|
||||
}
|
||||
const ctx = inspectorCtx(ctx_opaque);
|
||||
inspectorMsg(ctx.alloc(), ctx, msg) catch unreachable;
|
||||
}
|
||||
|
||||
pub fn onInspectorNotif(ctx_opaque: *anyopaque, msg: []const u8) void {
|
||||
if (std.log.defaultLogEnabled(.debug)) {
|
||||
// msg should be {"method":<method>,...
|
||||
const method_end = std.mem.indexOfScalar(u8, msg, ',') orelse unreachable;
|
||||
const method = msg[10..method_end];
|
||||
std.log.scoped(.cdp).debug("Event (inspector) > method {s}", .{method});
|
||||
}
|
||||
const ctx = inspectorCtx(ctx_opaque);
|
||||
inspectorMsg(ctx.alloc(), ctx, msg) catch unreachable;
|
||||
}
|
||||
};
|
||||
|
||||
// I/O Send
|
||||
// --------
|
||||
|
||||
// NOTE: to allow concurrent send we create each time a dedicated context
|
||||
// (with its own completion), allocated on the heap.
|
||||
// After the send (on the sendCbk) the dedicated context will be destroy
|
||||
// and the msg slice will be free.
|
||||
const Send = struct {
|
||||
ctx: *Ctx,
|
||||
msg: []const u8,
|
||||
completion: Completion = undefined,
|
||||
|
||||
fn init(ctx: *Ctx, msg: []const u8) !*Send {
|
||||
const sd = try ctx.alloc().create(Send);
|
||||
sd.* = .{ .ctx = ctx, .msg = msg };
|
||||
return sd;
|
||||
}
|
||||
|
||||
fn deinit(self: *Send) void {
|
||||
self.ctx.alloc().free(self.msg);
|
||||
self.ctx.alloc().destroy(self);
|
||||
}
|
||||
|
||||
fn asyncCbk(self: *Send, _: *Completion, result: SendError!usize) void {
|
||||
_ = result catch |err| {
|
||||
log.err("send error: {any}", .{err});
|
||||
self.ctx.err = err;
|
||||
};
|
||||
self.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
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, sd.msg);
|
||||
}
|
||||
|
||||
// Listener and handler
|
||||
// --------------------
|
||||
|
||||
pub fn handle(
|
||||
alloc: std.mem.Allocator,
|
||||
loop: *jsruntime.Loop,
|
||||
server_socket: std.posix.socket_t,
|
||||
stream: ?*Stream,
|
||||
timeout: u64,
|
||||
) anyerror!void {
|
||||
|
||||
// create v8 vm
|
||||
const vm = jsruntime.VM.init();
|
||||
defer vm.deinit();
|
||||
|
||||
// browser
|
||||
var browser: Browser = undefined;
|
||||
try Browser.init(&browser, alloc, loop, vm);
|
||||
defer browser.deinit();
|
||||
|
||||
// create buffers
|
||||
var read_buf: [BufReadSize]u8 = undefined;
|
||||
var buf: [MaxSize]u8 = undefined;
|
||||
var msg_buf = MsgBuffer{ .buf = &buf };
|
||||
|
||||
// create I/O completions
|
||||
var accept_completion: Completion = undefined;
|
||||
var conn_completion: Completion = undefined;
|
||||
var timeout_completion: Completion = undefined;
|
||||
|
||||
// create I/O contexts and callbacks
|
||||
// for accepting connections and receving messages
|
||||
var ctx = Ctx{
|
||||
.loop = loop,
|
||||
.stream = stream,
|
||||
.browser = &browser,
|
||||
.sessionNew = true,
|
||||
.read_buf = &read_buf,
|
||||
.msg_buf = &msg_buf,
|
||||
.accept_socket = server_socket,
|
||||
.timeout = timeout,
|
||||
.accept_completion = &accept_completion,
|
||||
.conn_completion = &conn_completion,
|
||||
.timeout_completion = &timeout_completion,
|
||||
};
|
||||
try browser.session.initInspector(
|
||||
&ctx,
|
||||
Ctx.onInspectorResp,
|
||||
Ctx.onInspectorNotif,
|
||||
);
|
||||
|
||||
// accepting connection asynchronously on internal server
|
||||
log.info("accepting new conn...", .{});
|
||||
loop.io.accept(*Ctx, &ctx, Ctx.acceptCbk, ctx.acceptCompletion(), ctx.accept_socket);
|
||||
|
||||
// infinite loop on I/O events, either:
|
||||
// - cmd from incoming connection on server socket
|
||||
// - JS callbacks events from scripts
|
||||
while (true) {
|
||||
try loop.io.run_for_ns(10 * std.time.ns_per_ms);
|
||||
if (loop.cbk_error) {
|
||||
log.err("JS error", .{});
|
||||
// if (try try_catch.exception(alloc, js_env.*)) |msg| {
|
||||
// std.debug.print("\n\rUncaught {s}\n\r", .{msg});
|
||||
// alloc.free(msg);
|
||||
// }
|
||||
// loop.cbk_error = false;
|
||||
}
|
||||
if (ctx.err) |err| {
|
||||
if (err != error.NoError) log.err("Server error: {any}", .{err});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -22,6 +22,7 @@ const tests = @import("run_tests.zig");
|
||||
|
||||
pub const Types = tests.Types;
|
||||
pub const UserContext = tests.UserContext;
|
||||
pub const IO = tests.IO;
|
||||
|
||||
pub fn main() !void {
|
||||
try tests.main();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const std = @import("std");
|
||||
const parser = @import("netsurf");
|
||||
const Client = @import("async/Client.zig");
|
||||
const Client = @import("asyncio").Client;
|
||||
|
||||
pub const UserContext = struct {
|
||||
document: *parser.DocumentHTML,
|
||||
|
||||
@@ -28,10 +28,12 @@ const Loop = jsruntime.Loop;
|
||||
const Env = jsruntime.Env;
|
||||
const Window = @import("../html/window.zig").Window;
|
||||
const storage = @import("../storage/storage.zig");
|
||||
const Client = @import("asyncio").Client;
|
||||
|
||||
const Types = @import("../main_wpt.zig").Types;
|
||||
const UserContext = @import("../main_wpt.zig").UserContext;
|
||||
const Client = @import("../async/Client.zig");
|
||||
|
||||
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.
|
||||
@@ -53,10 +55,11 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const
|
||||
var loop = try Loop.init(alloc);
|
||||
defer loop.deinit();
|
||||
|
||||
var cli = Client{ .allocator = alloc, .loop = &loop };
|
||||
var cli = Client{ .allocator = alloc };
|
||||
defer cli.deinit();
|
||||
|
||||
var js_env = try Env.init(alloc, &loop, UserContext{
|
||||
var js_env: Env = undefined;
|
||||
Env.init(&js_env, alloc, &loop, UserContext{
|
||||
.document = html_doc,
|
||||
.httpClient = &cli,
|
||||
});
|
||||
@@ -73,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;
|
||||
@@ -84,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);
|
||||
|
||||
327
src/xhr/xhr.zig
327
src/xhr/xhr.zig
@@ -32,8 +32,7 @@ const XMLHttpRequestEventTarget = @import("event_target.zig").XMLHttpRequestEven
|
||||
const Mime = @import("../browser/mime.zig");
|
||||
|
||||
const Loop = jsruntime.Loop;
|
||||
const YieldImpl = Loop.Yield(XMLHttpRequest);
|
||||
const Client = @import("../async/Client.zig");
|
||||
const Client = @import("asyncio").Client;
|
||||
|
||||
const parser = @import("netsurf");
|
||||
|
||||
@@ -98,10 +97,11 @@ pub const XMLHttpRequest = struct {
|
||||
proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{},
|
||||
alloc: std.mem.Allocator,
|
||||
cli: *Client,
|
||||
impl: YieldImpl,
|
||||
io: Client.IO,
|
||||
|
||||
priv_state: PrivState = .new,
|
||||
req: ?Client.Request = null,
|
||||
ctx: ?Client.Ctx = null,
|
||||
|
||||
method: std.http.Method,
|
||||
state: u16,
|
||||
@@ -135,7 +135,13 @@ pub const XMLHttpRequest = struct {
|
||||
response_header_buffer: [1024 * 16]u8 = undefined,
|
||||
|
||||
response_status: u10 = 0,
|
||||
response_override_mime_type: ?[]const u8 = null,
|
||||
|
||||
// TODO uncomment this field causes casting issue with
|
||||
// XMLHttpRequestEventTarget. I think it's dueto an alignement issue, but
|
||||
// not sure. see
|
||||
// https://lightpanda.slack.com/archives/C05TRU6RBM1/p1707819010681019
|
||||
// response_override_mime_type: ?[]const u8 = null,
|
||||
|
||||
response_mime: Mime = undefined,
|
||||
response_obj: ?ResponseObj = null,
|
||||
send_flag: bool = false,
|
||||
@@ -288,7 +294,7 @@ pub const XMLHttpRequest = struct {
|
||||
.alloc = alloc,
|
||||
.headers = Headers.init(alloc),
|
||||
.response_headers = Headers.init(alloc),
|
||||
.impl = YieldImpl.init(loop),
|
||||
.io = Client.IO.init(loop),
|
||||
.method = undefined,
|
||||
.url = null,
|
||||
.uri = undefined,
|
||||
@@ -320,10 +326,11 @@ pub const XMLHttpRequest = struct {
|
||||
|
||||
self.priv_state = .new;
|
||||
|
||||
if (self.req) |*r| {
|
||||
r.deinit();
|
||||
self.req = null;
|
||||
}
|
||||
if (self.ctx) |*c| c.deinit();
|
||||
self.ctx = null;
|
||||
|
||||
if (self.req) |*r| r.deinit();
|
||||
self.req = null;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *XMLHttpRequest, alloc: std.mem.Allocator) void {
|
||||
@@ -382,7 +389,11 @@ pub const XMLHttpRequest = struct {
|
||||
self.reset(alloc);
|
||||
|
||||
self.url = try alloc.dupe(u8, url);
|
||||
self.uri = std.Uri.parse(self.url.?) catch return DOMError.Syntax;
|
||||
self.uri = std.Uri.parse(self.url.?) catch |err| {
|
||||
log.debug("parse url ({s}): {any}", .{ self.url.?, err });
|
||||
return DOMError.Syntax;
|
||||
};
|
||||
log.debug("open url ({s})", .{self.url.?});
|
||||
self.sync = if (asyn) |b| !b else false;
|
||||
|
||||
self.state = OPENED;
|
||||
@@ -494,138 +505,160 @@ pub const XMLHttpRequest = struct {
|
||||
log.debug("{any} {any}", .{ self.method, self.uri });
|
||||
|
||||
self.send_flag = true;
|
||||
self.impl.yield(self);
|
||||
}
|
||||
|
||||
// onYield is a callback called between each request's steps.
|
||||
// Between each step, the code is blocking.
|
||||
// Yielding allows pseudo-async and gives a chance to other async process
|
||||
// to be called.
|
||||
pub fn onYield(self: *XMLHttpRequest, err: ?anyerror) void {
|
||||
if (err) |e| return self.onErr(e);
|
||||
self.priv_state = .open;
|
||||
|
||||
switch (self.priv_state) {
|
||||
.new => {
|
||||
self.priv_state = .open;
|
||||
self.req = self.cli.open(self.method, self.uri, .{
|
||||
.server_header_buffer = &self.response_header_buffer,
|
||||
.extra_headers = self.headers.all(),
|
||||
}) catch |e| return self.onErr(e);
|
||||
},
|
||||
.open => {
|
||||
// prepare payload transfert.
|
||||
if (self.payload) |v| self.req.?.transfer_encoding = .{ .content_length = v.len };
|
||||
|
||||
self.priv_state = .send;
|
||||
self.req.?.send() catch |e| return self.onErr(e);
|
||||
},
|
||||
.send => {
|
||||
if (self.payload) |payload| {
|
||||
self.priv_state = .write;
|
||||
self.req.?.writeAll(payload) catch |e| return self.onErr(e);
|
||||
} else {
|
||||
self.priv_state = .finish;
|
||||
self.req.?.finish() catch |e| return self.onErr(e);
|
||||
}
|
||||
},
|
||||
.write => {
|
||||
self.priv_state = .finish;
|
||||
self.req.?.finish() catch |e| return self.onErr(e);
|
||||
},
|
||||
.finish => {
|
||||
self.priv_state = .wait;
|
||||
self.req.?.wait() catch |e| return self.onErr(e);
|
||||
},
|
||||
.wait => {
|
||||
log.info("{any} {any} {d}", .{ self.method, self.uri, self.req.?.response.status });
|
||||
|
||||
self.priv_state = .done;
|
||||
var it = self.req.?.response.iterateHeaders();
|
||||
self.response_headers.load(&it) catch |e| return self.onErr(e);
|
||||
|
||||
// extract a mime type from headers.
|
||||
const ct = self.response_headers.getFirstValue("Content-Type") orelse "text/xml";
|
||||
self.response_mime = Mime.parse(ct) catch |e| return self.onErr(e);
|
||||
|
||||
// TODO handle override mime type
|
||||
|
||||
self.state = HEADERS_RECEIVED;
|
||||
self.dispatchEvt("readystatechange");
|
||||
|
||||
self.response_status = @intFromEnum(self.req.?.response.status);
|
||||
|
||||
var buf: std.ArrayListUnmanaged(u8) = .{};
|
||||
|
||||
// TODO set correct length
|
||||
const total = 0;
|
||||
var loaded: u64 = 0;
|
||||
|
||||
// dispatch a progress event loadstart.
|
||||
self.dispatchProgressEvent("loadstart", .{ .loaded = loaded, .total = total });
|
||||
|
||||
const reader = self.req.?.reader();
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var ln = buffer.len;
|
||||
var prev_dispatch: ?std.time.Instant = null;
|
||||
while (ln > 0) {
|
||||
ln = reader.read(&buffer) catch |e| {
|
||||
buf.deinit(self.alloc);
|
||||
return self.onErr(e);
|
||||
};
|
||||
buf.appendSlice(self.alloc, buffer[0..ln]) catch |e| {
|
||||
buf.deinit(self.alloc);
|
||||
return self.onErr(e);
|
||||
};
|
||||
loaded = loaded + ln;
|
||||
|
||||
// Dispatch only if 50ms have passed.
|
||||
const now = std.time.Instant.now() catch |e| {
|
||||
buf.deinit(self.alloc);
|
||||
return self.onErr(e);
|
||||
};
|
||||
if (prev_dispatch != null and now.since(prev_dispatch.?) < min_delay) continue;
|
||||
defer prev_dispatch = now;
|
||||
|
||||
self.state = LOADING;
|
||||
self.dispatchEvt("readystatechange");
|
||||
|
||||
// dispatch a progress event progress.
|
||||
self.dispatchProgressEvent("progress", .{
|
||||
.loaded = loaded,
|
||||
.total = total,
|
||||
});
|
||||
}
|
||||
self.response_bytes = buf.items;
|
||||
self.send_flag = false;
|
||||
|
||||
self.state = DONE;
|
||||
self.dispatchEvt("readystatechange");
|
||||
|
||||
// dispatch a progress event load.
|
||||
self.dispatchProgressEvent("load", .{ .loaded = loaded, .total = total });
|
||||
// dispatch a progress event loadend.
|
||||
self.dispatchProgressEvent("loadend", .{ .loaded = loaded, .total = total });
|
||||
},
|
||||
.done => {
|
||||
if (self.req) |*r| {
|
||||
r.deinit();
|
||||
self.req = null;
|
||||
}
|
||||
|
||||
// finalize fetch process.
|
||||
return;
|
||||
},
|
||||
self.req = try self.cli.create(self.method, self.uri, .{
|
||||
.server_header_buffer = &self.response_header_buffer,
|
||||
.extra_headers = self.headers.all(),
|
||||
});
|
||||
errdefer {
|
||||
self.req.?.deinit();
|
||||
self.req = null;
|
||||
}
|
||||
|
||||
self.impl.yield(self);
|
||||
self.ctx = try Client.Ctx.init(&self.io, &self.req.?);
|
||||
errdefer {
|
||||
self.ctx.?.deinit();
|
||||
self.ctx = null;
|
||||
}
|
||||
self.ctx.?.userData = self;
|
||||
|
||||
try self.cli.async_open(
|
||||
self.method,
|
||||
self.uri,
|
||||
.{ .server_header_buffer = &self.response_header_buffer },
|
||||
&self.ctx.?,
|
||||
onRequestConnect,
|
||||
);
|
||||
}
|
||||
|
||||
fn onRequestWait(ctx: *Client.Ctx, res: anyerror!void) !void {
|
||||
var self = selfCtx(ctx);
|
||||
res catch |err| return self.onErr(err);
|
||||
|
||||
log.info("{any} {any} {d}", .{ self.method, self.uri, self.req.?.response.status });
|
||||
|
||||
self.priv_state = .done;
|
||||
var it = self.req.?.response.iterateHeaders();
|
||||
self.response_headers.load(&it) catch |e| return self.onErr(e);
|
||||
|
||||
// extract a mime type from headers.
|
||||
const ct = self.response_headers.getFirstValue("Content-Type") orelse "text/xml";
|
||||
self.response_mime = Mime.parse(ct) catch |e| return self.onErr(e);
|
||||
|
||||
// TODO handle override mime type
|
||||
|
||||
self.state = HEADERS_RECEIVED;
|
||||
self.dispatchEvt("readystatechange");
|
||||
|
||||
self.response_status = @intFromEnum(self.req.?.response.status);
|
||||
|
||||
var buf: std.ArrayListUnmanaged(u8) = .{};
|
||||
|
||||
// TODO set correct length
|
||||
const total = 0;
|
||||
var loaded: u64 = 0;
|
||||
|
||||
// dispatch a progress event loadstart.
|
||||
self.dispatchProgressEvent("loadstart", .{ .loaded = loaded, .total = total });
|
||||
|
||||
// TODO read async
|
||||
const reader = self.req.?.reader();
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var ln = buffer.len;
|
||||
var prev_dispatch: ?std.time.Instant = null;
|
||||
while (ln > 0) {
|
||||
ln = reader.read(&buffer) catch |e| {
|
||||
buf.deinit(self.alloc);
|
||||
return self.onErr(e);
|
||||
};
|
||||
buf.appendSlice(self.alloc, buffer[0..ln]) catch |e| {
|
||||
buf.deinit(self.alloc);
|
||||
return self.onErr(e);
|
||||
};
|
||||
loaded = loaded + ln;
|
||||
|
||||
// Dispatch only if 50ms have passed.
|
||||
const now = std.time.Instant.now() catch |e| {
|
||||
buf.deinit(self.alloc);
|
||||
return self.onErr(e);
|
||||
};
|
||||
if (prev_dispatch != null and now.since(prev_dispatch.?) < min_delay) continue;
|
||||
defer prev_dispatch = now;
|
||||
|
||||
self.state = LOADING;
|
||||
self.dispatchEvt("readystatechange");
|
||||
|
||||
// dispatch a progress event progress.
|
||||
self.dispatchProgressEvent("progress", .{
|
||||
.loaded = loaded,
|
||||
.total = total,
|
||||
});
|
||||
}
|
||||
self.response_bytes = buf.items;
|
||||
self.send_flag = false;
|
||||
|
||||
self.state = DONE;
|
||||
self.dispatchEvt("readystatechange");
|
||||
|
||||
// dispatch a progress event load.
|
||||
self.dispatchProgressEvent("load", .{ .loaded = loaded, .total = total });
|
||||
// dispatch a progress event loadend.
|
||||
self.dispatchProgressEvent("loadend", .{ .loaded = loaded, .total = total });
|
||||
|
||||
if (self.ctx) |*c| c.deinit();
|
||||
self.ctx = null;
|
||||
|
||||
if (self.req) |*r| r.deinit();
|
||||
self.req = null;
|
||||
}
|
||||
|
||||
fn onRequestFinish(ctx: *Client.Ctx, res: anyerror!void) !void {
|
||||
var self = selfCtx(ctx);
|
||||
res catch |err| return self.onErr(err);
|
||||
|
||||
self.priv_state = .wait;
|
||||
return ctx.req.async_wait(ctx, onRequestWait) catch |e| return self.onErr(e);
|
||||
}
|
||||
|
||||
fn onRequestSend(ctx: *Client.Ctx, res: anyerror!void) !void {
|
||||
var self = selfCtx(ctx);
|
||||
res catch |err| return self.onErr(err);
|
||||
|
||||
if (self.payload) |payload| {
|
||||
self.priv_state = .write;
|
||||
return ctx.req.async_writeAll(payload, ctx, onRequestWrite) catch |e| return self.onErr(e);
|
||||
}
|
||||
|
||||
self.priv_state = .finish;
|
||||
return ctx.req.async_finish(ctx, onRequestFinish) catch |e| return self.onErr(e);
|
||||
}
|
||||
|
||||
fn onRequestWrite(ctx: *Client.Ctx, res: anyerror!void) !void {
|
||||
var self = selfCtx(ctx);
|
||||
res catch |err| return self.onErr(err);
|
||||
self.priv_state = .finish;
|
||||
return ctx.req.async_finish(ctx, onRequestFinish) catch |e| return self.onErr(e);
|
||||
}
|
||||
|
||||
fn onRequestConnect(ctx: *Client.Ctx, res: anyerror!void) anyerror!void {
|
||||
var self = selfCtx(ctx);
|
||||
res catch |err| return self.onErr(err);
|
||||
|
||||
// prepare payload transfert.
|
||||
if (self.payload) |v| self.req.?.transfer_encoding = .{ .content_length = v.len };
|
||||
|
||||
self.priv_state = .send;
|
||||
return ctx.req.async_send(ctx, onRequestSend) catch |err| return self.onErr(err);
|
||||
}
|
||||
|
||||
fn selfCtx(ctx: *Client.Ctx) *XMLHttpRequest {
|
||||
return @ptrCast(@alignCast(ctx.userData));
|
||||
}
|
||||
|
||||
fn onErr(self: *XMLHttpRequest, err: anyerror) void {
|
||||
self.priv_state = .done;
|
||||
if (self.req) |*r| {
|
||||
r.deinit();
|
||||
self.req = null;
|
||||
}
|
||||
|
||||
self.err = err;
|
||||
self.state = DONE;
|
||||
@@ -635,6 +668,12 @@ pub const XMLHttpRequest = struct {
|
||||
self.dispatchProgressEvent("loadend", .{});
|
||||
|
||||
log.debug("{any} {any} {any}", .{ self.method, self.uri, self.err });
|
||||
|
||||
if (self.ctx) |*c| c.deinit();
|
||||
self.ctx = null;
|
||||
|
||||
if (self.req) |*r| r.deinit();
|
||||
self.req = null;
|
||||
}
|
||||
|
||||
pub fn _abort(self: *XMLHttpRequest) void {
|
||||
@@ -717,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.
|
||||
@@ -735,6 +776,7 @@ pub const XMLHttpRequest = struct {
|
||||
// response object to a new ArrayBuffer object representing this’s
|
||||
// received bytes. If this throws an exception, then set this’s
|
||||
// response object to failure and return null.
|
||||
log.err("response type ArrayBuffer not implemented", .{});
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -743,6 +785,7 @@ pub const XMLHttpRequest = struct {
|
||||
// response object to a new Blob object representing this’s
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -882,7 +925,7 @@ pub fn testExecFn(
|
||||
// .{ .src = "req.onload", .ex = "function cbk(event) { nb ++; evt = event; }" },
|
||||
//.{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; evt = event; }" },
|
||||
|
||||
.{ .src = "req.open('GET', 'http://httpbin.io/html')", .ex = "undefined" },
|
||||
.{ .src = "req.open('GET', 'https://httpbin.io/html')", .ex = "undefined" },
|
||||
.{ .src = "req.setRequestHeader('User-Agent', 'lightpanda/1.0')", .ex = "undefined" },
|
||||
|
||||
// ensure open resets values
|
||||
@@ -905,14 +948,14 @@ 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);
|
||||
|
||||
var document = [_]Case{
|
||||
.{ .src = "const req2 = new XMLHttpRequest()", .ex = "undefined" },
|
||||
.{ .src = "req2.open('GET', 'http://httpbin.io/html')", .ex = "undefined" },
|
||||
.{ .src = "req2.open('GET', 'https://httpbin.io/html')", .ex = "undefined" },
|
||||
.{ .src = "req2.responseType = 'document'", .ex = "document" },
|
||||
|
||||
.{ .src = "req2.send()", .ex = "undefined" },
|
||||
@@ -928,7 +971,7 @@ pub fn testExecFn(
|
||||
|
||||
var json = [_]Case{
|
||||
.{ .src = "const req3 = new XMLHttpRequest()", .ex = "undefined" },
|
||||
.{ .src = "req3.open('GET', 'http://httpbin.io/json')", .ex = "undefined" },
|
||||
.{ .src = "req3.open('GET', 'https://httpbin.io/json')", .ex = "undefined" },
|
||||
.{ .src = "req3.responseType = 'json'", .ex = "json" },
|
||||
|
||||
.{ .src = "req3.send()", .ex = "undefined" },
|
||||
@@ -943,7 +986,7 @@ pub fn testExecFn(
|
||||
|
||||
var post = [_]Case{
|
||||
.{ .src = "const req4 = new XMLHttpRequest()", .ex = "undefined" },
|
||||
.{ .src = "req4.open('POST', 'http://httpbin.io/post')", .ex = "undefined" },
|
||||
.{ .src = "req4.open('POST', 'https://httpbin.io/post')", .ex = "undefined" },
|
||||
.{ .src = "req4.send('foo')", .ex = "undefined" },
|
||||
|
||||
// Each case executed waits for all loop callaback calls.
|
||||
@@ -956,7 +999,7 @@ pub fn testExecFn(
|
||||
|
||||
var cbk = [_]Case{
|
||||
.{ .src = "const req5 = new XMLHttpRequest()", .ex = "undefined" },
|
||||
.{ .src = "req5.open('GET', 'http://httpbin.io/json')", .ex = "undefined" },
|
||||
.{ .src = "req5.open('GET', 'https://httpbin.io/json')", .ex = "undefined" },
|
||||
.{ .src = "var status = 0; req5.onload = function () { status = this.status };", .ex = "function () { status = this.status }" },
|
||||
.{ .src = "req5.send()", .ex = "undefined" },
|
||||
|
||||
|
||||
Submodule tests/wpt updated: e0a721a3b8...ea16a1e361
2
vendor/netsurf/libdom
vendored
2
vendor/netsurf/libdom
vendored
Submodule vendor/netsurf/libdom updated: 3677430b15...fe6c9b8d40
1
vendor/websocket.zig
vendored
Submodule
1
vendor/websocket.zig
vendored
Submodule
Submodule vendor/websocket.zig added at 1b49626c78
1
vendor/zig-async-io
vendored
Submodule
1
vendor/zig-async-io
vendored
Submodule
Submodule vendor/zig-async-io added at 570f436c72
2
vendor/zig-js-runtime
vendored
2
vendor/zig-js-runtime
vendored
Submodule vendor/zig-js-runtime updated: f2a6e94a18...1b1b3431ff
Reference in New Issue
Block a user