Compare commits

...

39 Commits

Author SHA1 Message Date
Adrià Arrufat
e354b7315f fix(cdp): auto-close existing target on createTarget
When creating a new target via `Target.createTarget`, any existing target in the browser context is now automatically closed and detached. This resolves an issue where automation frameworks (like Stagehand) would fail with a TargetAlreadyLoaded error when attempting to open multiple tabs sequentially.

Closes #1962
2026-03-30 11:07:31 +02:00
Pierre Tachoire
8723ecdd2d Merge pull request #2028 from lightpanda-io/http_client_safe_kill
Protect transfer.kill() the way transfer.abort() is protected
2026-03-30 09:22:40 +02:00
Pierre Tachoire
451178558a Merge pull request #2026 from lightpanda-io/invalid_access_dom_exception
Add missing InvalidAccessError DOMException mapping
2026-03-30 09:21:51 +02:00
Karl Seguin
70dc0f6b95 Merge pull request #2027 from lightpanda-io/mcp-protocol-version
mcp: allow configuring protocol version
2026-03-30 13:38:06 +08:00
Adrià Arrufat
d99599fa21 zig fmt 2026-03-30 07:24:08 +02:00
Adrià Arrufat
20e62a5551 mcp: inline mcpVersion helper from Config 2026-03-30 07:13:45 +02:00
Adrià Arrufat
e083d4a3d1 Config: remove LIGHTPANDA_MCP_VERSION env var 2026-03-30 07:07:23 +02:00
Karl Seguin
7a23686cbd Merge pull request #2033 from lightpanda-io/canvas_context_cache
Canvas context cache
2026-03-30 12:27:04 +08:00
Karl Seguin
25889ff918 Improve canvas context caching
Improve https://github.com/lightpanda-io/browser/pull/2022 to also cache webgl
context and add tests.
2026-03-30 12:14:32 +08:00
Karl Seguin
b4e3f246ca Merge remote-tracking branch 'evan108108/fix/canvas-getcontext-caching' into canvas_context_cache 2026-03-30 11:58:45 +08:00
Karl Seguin
8eeeeda8c1 Merge pull request #2021 from evan108108/fix/navigator-spec-compliance
fix: navigator.languages should include base language per spec
2026-03-30 11:40:49 +08:00
Karl Seguin
75dc4d5b0e Merge pull request #2031 from lightpanda-io/cdp-add-script-to-evaluate-on-new-document
Cdp add script to evaluate on new document
2026-03-30 11:16:39 +08:00
Karl Seguin
0d40aed1b7 zig fmt 2026-03-30 09:32:22 +08:00
Karl Seguin
78cb766298 Log for unimplemented parameter
Wrap script_on_new_document execution in try/catch for better error reporting.

Improve test for script_on_new_document
2026-03-30 09:31:13 +08:00
Karl Seguin
f60e5cce6d Protect transfer.kill() the way transfer.abort() is protected
Transfer.abort() is protected from aborting the transfer while inside of a
libcurl callback (since libcurl doesn't support mutating the easy while inside
of a callback AND it causes issues in the zig code).

This applies similar logic to Transfer.kill() which is less likely to be called
but worse if it is called in a callback, as transfer.kill() deinit's the
transfer - something the callback caller is not expecting. Since killing isn't
safe to do, we flag the transfer as aborted AND null/noop all the callbacks.

Fixes WPT crash /content-security-policy/frame-src/frame-src-blocked-path-matching.sub.html
2026-03-29 19:48:47 +08:00
Adrià Arrufat
81d4bdb157 mcp: change default protocol version to 2024-11-05 2026-03-29 08:34:24 +02:00
Adrià Arrufat
cf5e4d7d1e mcp: allow configuring protocol version
Closes #2023
2026-03-29 08:29:04 +02:00
Karl Seguin
9f81d7d3ff Add missing InvalidAccessError DOMException mapping
Fixes WPT crash /WebCryptoAPI/sign_verify/eddsa_curve25519.https.any.html
2026-03-29 11:46:44 +08:00
evan108108
1f22462f13 fix: cache canvas 2D context and lock context type per spec
Per the HTML spec, HTMLCanvasElement.getContext() should:
1. Return the same object on repeated calls with the same type
2. Return null if a different context type was already requested

Previously, every getContext("2d") call created a new
CanvasRenderingContext2D object. This caused issues with code
that relies on identity checks (ctx === canvas.getContext("2d"))
and wasted memory by allocating duplicate contexts.

The fix caches the 2D context and tracks which context type was
first requested, returning null for incompatible subsequent calls.
2026-03-27 21:06:09 -04:00
evan108108
273ea91378 fix: navigator.languages should include base language per spec
Per the HTML spec, navigator.languages should return the user's
preferred languages. Most browsers return at least ["en-US", "en"]
to include the base language tag alongside the regional variant.

This matches Chrome, Firefox, and Safari behavior and improves
compatibility with sites that check for language negotiation.
2026-03-27 21:04:55 -04:00
Pierre Tachoire
03ed45637a Merge pull request #1889 from lightpanda-io/wp/mrdimidium/refactor-redirects
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig fmt (push) Has been cancelled
zig-test / zig test using v8 in debug mode (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / wba-demo-scripts (push) Has been cancelled
e2e-test / wba-test (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
e2e-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / zig build release (push) Has been cancelled
wpt / build wpt runner (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
e2e-integration-test / zig build release (push) Has been cancelled
e2e-integration-test / demo-integration-scripts (push) Has been cancelled
Rework header/data callbacks in HttpClient
2026-03-27 14:22:58 +01:00
Nikolay Govorov
9068fe718e Fix SameSite cookies 2026-03-27 11:16:46 +00:00
Nikolay Govorov
5369d25213 fix recv e2e test 2026-03-27 09:49:16 +00:00
Nikolay Govorov
649d8d1024 Remove duplication in cookies instalation 2026-03-27 09:49:13 +00:00
Nikolay Govorov
15d60d845a Fixup error handling in HttpClient process messages 2026-03-27 09:49:11 +00:00
Nikolay Govorov
c4b837b598 Revert log reimport 2026-03-27 09:49:09 +00:00
Nikolay Govorov
54391238c9 Move cdp callbacks from dataCallback to processMessages 2026-03-27 09:49:07 +00:00
Nikolay Govorov
d33edc5697 Fixup cookies management 2026-03-27 09:49:05 +00:00
Nikolay Govorov
16ca8d4b14 Fix cleanup connections in HttpClient 2026-03-27 09:49:03 +00:00
Nikolay Govorov
707ffb4893 Move redirects handling from curl callbacks 2026-03-27 09:48:59 +00:00
Pierre Tachoire
4782b37216 Merge pull request #2016 from lightpanda-io/readme-mention-cors
mention CORS is missing in the README's status
2026-03-27 08:34:09 +01:00
Pierre Tachoire
ce197256dd Merge pull request #2010 from lightpanda-io/build-pre-nightly
build: simplify nightly versioning
2026-03-27 08:33:45 +01:00
Pierre Tachoire
e6d644998a mention CORS is missing in the README's status 2026-03-27 08:26:56 +01:00
Karl Seguin
67bd555e75 Merge pull request #2013 from lightpanda-io/cleanup_dead_code_removal
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / wba-demo-scripts (push) Has been cancelled
e2e-test / wba-test (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig fmt (push) Has been cancelled
zig-test / zig test using v8 in debug mode (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Remove unused imports
2026-03-27 13:52:49 +08:00
Adrià Arrufat
a10e533701 Remove more unused imports 2026-03-27 14:24:17 +09:00
Karl Seguin
226d9bfc6f zig fmt 2026-03-27 12:47:24 +08:00
Karl Seguin
ea422075c7 Remove unused imports
And some smaller cleanups.
2026-03-27 12:45:26 +08:00
Adrià Arrufat
7f2139f612 build: simplify nightly versioning 2026-03-27 10:47:43 +09:00
Navid EMAD
886aa3abba CDP: implement Page.addScriptToEvaluateOnNewDocument
Replace the hardcoded stub with a working implementation that stores
registered scripts and evaluates them in each new document.

Changes:
- Add ScriptOnNewDocument struct and storage list on BrowserContext
- Store scripts with unique identifiers when addScript is called
- Evaluate all registered scripts in pageNavigated, after the execution
  context is created but before frameNavigated/loadEventFired events
  are sent to the CDP client
- Add removeScriptToEvaluateOnNewDocument for cleanup
- Return unique identifiers per the CDP spec (was hardcoded to "1")

Scripts are evaluated with error suppression (warns on failure) to
avoid breaking navigation if a script has issues.

This unblocks CDP clients that rely on auto-injected scripts (polyfills,
monitoring, test helpers) persisting across navigations. Previously
clients had to manually re-inject after every Page.navigate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 19:48:07 +01:00
68 changed files with 993 additions and 888 deletions

View File

@@ -7,7 +7,7 @@ env:
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }} AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
RELEASE: ${{ github.ref_type == 'tag' && github.ref_name || 'nightly' }} RELEASE: ${{ github.ref_type == 'tag' && github.ref_name || 'nightly' }}
VERSION_FLAG: ${{ github.ref_type == 'tag' && format('-Dversion_string={0}', github.ref_name) || format('-Dpre_version={0}', 'nightly') }} VERSION_FLAG: ${{ github.ref_type == 'tag' && format('-Dversion={0}', github.ref_name) || '-Dversion=nightly' }}
on: on:
push: push:

View File

@@ -170,6 +170,7 @@ You may still encounter errors or crashes. Please open an issue with specifics i
Here are the key features we have implemented: Here are the key features we have implemented:
- [ ] CORS [#2015](https://github.com/lightpanda-io/browser/issues/2015)
- [x] HTTP loader ([Libcurl](https://curl.se/libcurl/)) - [x] HTTP loader ([Libcurl](https://curl.se/libcurl/))
- [x] HTML parser ([html5ever](https://github.com/servo/html5ever)) - [x] HTML parser ([html5ever](https://github.com/servo/html5ever))
- [x] DOM tree - [x] DOM tree

View File

@@ -719,39 +719,45 @@ fn buildCurl(
return lib; return lib;
} }
/// Returns `MAJOR.MINOR.PATCH-dev` when `git describe` fails. /// Resolves the semantic version of the build.
///
/// The base version is read from `build.zig.zon`. This can be overridden
/// using the `-Dversion` command-line flag:
/// - If the flag contains a full semantic version (e.g., `1.2.3`), it replaces
/// the base version entirely.
/// - If the flag contains a simple string (e.g., `nightly`), it replaces only
/// the pre-release tag of the base version (e.g., `1.0.0-dev` -> `1.0.0-nightly`).
///
/// For versions that have a pre-release tag and no explicit build metadata,
/// this function automatically enriches the version with the git commit count
/// and short hash (e.g., `1.0.0-dev.5243+dbe45229`).
fn resolveVersion(b: *std.Build) std.SemanticVersion { fn resolveVersion(b: *std.Build) std.SemanticVersion {
const version_string = b.option([]const u8, "version_string", "Override the version of this build"); const opt_version = b.option([]const u8, "version", "Override the version of this build");
if (version_string) |semver_string| {
return std.SemanticVersion.parse(semver_string) catch |err| {
std.debug.panic("Expected -Dversion-string={s} to be a semantic version: {}", .{ semver_string, err });
};
}
const pre_version = b.option([]const u8, "pre_version", "Override the pre version of this build"); const version = if (opt_version) |v|
const pre = blk: { std.SemanticVersion.parse(v) catch blk: {
if (pre_version) |pre| { var fallback = lightpanda_version;
break :blk pre; fallback.pre = v;
break :blk fallback;
} }
else
lightpanda_version;
break :blk lightpanda_version.pre; // Only enrich versions that have a pre-release field and no explicit build metadata.
}; if (version.pre == null or version.build != null) return version;
// If it's a stable release (no pre or build metadata in build.zig.zon), use it as is
if (pre == null and lightpanda_version.build == null) return lightpanda_version;
// For dev/nightly versions, calculate the commit count and hash // For dev/nightly versions, calculate the commit count and hash
const git_hash_raw = runGit(b, &.{ "rev-parse", "--short", "HEAD" }) catch return lightpanda_version; const git_hash_raw = runGit(b, &.{ "rev-parse", "--short", "HEAD" }) catch return version;
const commit_hash = std.mem.trim(u8, git_hash_raw, " \n\r"); const commit_hash = std.mem.trim(u8, git_hash_raw, " \n\r");
const git_count_raw = runGit(b, &.{ "rev-list", "--count", "HEAD" }) catch return lightpanda_version; const git_count_raw = runGit(b, &.{ "rev-list", "--count", "HEAD" }) catch return version;
const commit_count = std.mem.trim(u8, git_count_raw, " \n\r"); const commit_count = std.mem.trim(u8, git_count_raw, " \n\r");
return .{ return .{
.major = lightpanda_version.major, .major = version.major,
.minor = lightpanda_version.minor, .minor = version.minor,
.patch = lightpanda_version.patch, .patch = version.patch,
.pre = b.fmt("{s}.{s}", .{ pre.?, commit_count }), .pre = b.fmt("{s}.{s}", .{ version.pre.?, commit_count }),
.build = commit_hash, .build = commit_hash,
}; };
} }

View File

@@ -18,6 +18,7 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const log = @import("log.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator; const ArenaAllocator = std.heap.ArenaAllocator;
@@ -62,7 +63,7 @@ pub fn deinit(self: *ArenaPool) void {
var it = self._leak_track.iterator(); var it = self._leak_track.iterator();
while (it.next()) |kv| { while (it.next()) |kv| {
if (kv.value_ptr.* != 0) { if (kv.value_ptr.* != 0) {
std.debug.print("ArenaPool leak detected: '{s}' count={d}\n", .{ kv.key_ptr.*, kv.value_ptr.* }); log.err(.bug, "ArenaPool leak", .{ .name = kv.key_ptr.*, .count = kv.value_ptr.* });
has_leaks = true; has_leaks = true;
} }
} }
@@ -129,11 +130,11 @@ pub fn release(self: *ArenaPool, allocator: Allocator) void {
if (self._leak_track.getPtr(entry.debug)) |count| { if (self._leak_track.getPtr(entry.debug)) |count| {
count.* -= 1; count.* -= 1;
if (count.* < 0) { if (count.* < 0) {
std.debug.print("ArenaPool double-free detected: '{s}'\n", .{entry.debug}); log.err(.bug, "ArenaPool double-free", .{ .name = entry.debug });
@panic("ArenaPool: double-free detected"); @panic("ArenaPool: double-free detected");
} }
} else { } else {
std.debug.print("ArenaPool release of untracked arena: '{s}'\n", .{entry.debug}); log.err(.bug, "ArenaPool release unknown", .{ .name = entry.debug });
@panic("ArenaPool: release of untracked arena"); @panic("ArenaPool: release of untracked arena");
} }
} }

View File

@@ -24,6 +24,7 @@ const log = @import("log.zig");
const dump = @import("browser/dump.zig"); const dump = @import("browser/dump.zig");
const WebBotAuthConfig = @import("network/WebBotAuth.zig").Config; const WebBotAuthConfig = @import("network/WebBotAuth.zig").Config;
const mcp = @import("mcp.zig");
pub const RunMode = enum { pub const RunMode = enum {
help, help,
@@ -222,6 +223,7 @@ pub const Serve = struct {
pub const Mcp = struct { pub const Mcp = struct {
common: Common = .{}, common: Common = .{},
version: mcp.Version = .default,
}; };
pub const DumpFormat = enum { pub const DumpFormat = enum {
@@ -453,6 +455,12 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
\\Starts an MCP (Model Context Protocol) server over stdio \\Starts an MCP (Model Context Protocol) server over stdio
\\Example: {s} mcp \\Example: {s} mcp
\\ \\
\\Options:
\\--version
\\ Override the reported MCP version.
\\ Valid: 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25.
\\ Defaults to "2024-11-05".
\\
++ common_options ++ ++ common_options ++
\\ \\
\\version command \\version command
@@ -640,10 +648,22 @@ fn parseMcpArgs(
allocator: Allocator, allocator: Allocator,
args: *std.process.ArgIterator, args: *std.process.ArgIterator,
) !Mcp { ) !Mcp {
var mcp: Mcp = .{}; var result: Mcp = .{};
while (args.next()) |opt| { while (args.next()) |opt| {
if (try parseCommonArg(allocator, opt, args, &mcp.common)) { if (std.mem.eql(u8, "--version", opt)) {
const str = args.next() orelse {
log.fatal(.mcp, "missing argument value", .{ .arg = opt });
return error.InvalidArgument;
};
result.version = std.meta.stringToEnum(mcp.Version, str) orelse {
log.fatal(.mcp, "invalid protocol version", .{ .value = str });
return error.InvalidArgument;
};
continue;
}
if (try parseCommonArg(allocator, opt, args, &result.common)) {
continue; continue;
} }
@@ -651,7 +671,7 @@ fn parseMcpArgs(
return error.UnkownOption; return error.UnkownOption;
} }
return mcp; return result;
} }
fn parseFetchArgs( fn parseFetchArgs(

View File

@@ -22,7 +22,6 @@ const net = std.net;
const posix = std.posix; const posix = std.posix;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const log = @import("log.zig"); const log = @import("log.zig");
const App = @import("App.zig"); const App = @import("App.zig");

View File

@@ -19,17 +19,13 @@
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const js = @import("js/js.zig"); const js = @import("js/js.zig");
const log = @import("../log.zig");
const App = @import("../App.zig"); const App = @import("../App.zig");
const HttpClient = @import("HttpClient.zig"); const HttpClient = @import("HttpClient.zig");
const ArenaPool = App.ArenaPool; const ArenaPool = App.ArenaPool;
const IS_DEBUG = @import("builtin").mode == .Debug;
const Session = @import("Session.zig"); const Session = @import("Session.zig");
const Notification = @import("../Notification.zig"); const Notification = @import("../Notification.zig");

View File

@@ -425,7 +425,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
ls.deinit(); ls.deinit();
} }
const activation_state = ActivationState.create(event, target, page); const activation_state = try ActivationState.create(event, target, page);
// Defer runs even on early return - ensures event phase is reset // Defer runs even on early return - ensures event phase is reset
// and default actions execute (unless prevented) // and default actions execute (unless prevented)
@@ -820,7 +820,7 @@ const ActivationState = struct {
const Input = Element.Html.Input; const Input = Element.Html.Input;
fn create(event: *const Event, target: *Node, page: *Page) ?ActivationState { fn create(event: *const Event, target: *Node, page: *Page) !?ActivationState {
if (event._type_string.eql(comptime .wrap("click")) == false) { if (event._type_string.eql(comptime .wrap("click")) == false) {
return null; return null;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,6 @@ const IS_DEBUG = builtin.mode == .Debug;
const log = @import("../log.zig"); const log = @import("../log.zig");
const App = @import("../App.zig");
const String = @import("../string.zig").String; const String = @import("../string.zig").String;
const Mime = @import("Mime.zig"); const Mime = @import("Mime.zig");
@@ -43,7 +42,6 @@ const URL = @import("URL.zig");
const Blob = @import("webapi/Blob.zig"); const Blob = @import("webapi/Blob.zig");
const Node = @import("webapi/Node.zig"); const Node = @import("webapi/Node.zig");
const Event = @import("webapi/Event.zig"); const Event = @import("webapi/Event.zig");
const EventTarget = @import("webapi/EventTarget.zig");
const CData = @import("webapi/CData.zig"); const CData = @import("webapi/CData.zig");
const Element = @import("webapi/Element.zig"); const Element = @import("webapi/Element.zig");
const HtmlElement = @import("webapi/element/Html.zig"); const HtmlElement = @import("webapi/element/Html.zig");
@@ -59,7 +57,6 @@ const AbstractRange = @import("webapi/AbstractRange.zig");
const MutationObserver = @import("webapi/MutationObserver.zig"); const MutationObserver = @import("webapi/MutationObserver.zig");
const IntersectionObserver = @import("webapi/IntersectionObserver.zig"); const IntersectionObserver = @import("webapi/IntersectionObserver.zig");
const CustomElementDefinition = @import("webapi/CustomElementDefinition.zig"); const CustomElementDefinition = @import("webapi/CustomElementDefinition.zig");
const storage = @import("webapi/storage/storage.zig");
const PageTransitionEvent = @import("webapi/event/PageTransitionEvent.zig"); const PageTransitionEvent = @import("webapi/event/PageTransitionEvent.zig");
const SubmitEvent = @import("webapi/event/SubmitEvent.zig"); const SubmitEvent = @import("webapi/event/SubmitEvent.zig");
const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind; const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind;
@@ -67,7 +64,6 @@ const KeyboardEvent = @import("webapi/event/KeyboardEvent.zig");
const MouseEvent = @import("webapi/event/MouseEvent.zig"); const MouseEvent = @import("webapi/event/MouseEvent.zig");
const HttpClient = @import("HttpClient.zig"); const HttpClient = @import("HttpClient.zig");
const ArenaPool = App.ArenaPool;
const timestamp = @import("../datetime.zig").timestamp; const timestamp = @import("../datetime.zig").timestamp;
const milliTimestamp = @import("../datetime.zig").milliTimestamp; const milliTimestamp = @import("../datetime.zig").milliTimestamp;
@@ -385,12 +381,9 @@ pub fn getTitle(self: *Page) !?[]const u8 {
return null; return null;
} }
// Add comon headers for a request: // Add common headers for a request:
// * cookies
// * referer // * referer
pub fn headersForRequest(self: *Page, temp: Allocator, url: [:0]const u8, headers: *HttpClient.Headers) !void { pub fn headersForRequest(self: *Page, headers: *HttpClient.Headers) !void {
try self.requestCookie(.{}).headersForRequest(temp, url, headers);
// Build the referer // Build the referer
const referer = blk: { const referer = blk: {
if (self.referer_header == null) { if (self.referer_header == null) {
@@ -545,8 +538,6 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
if (opts.header) |hdr| { if (opts.header) |hdr| {
try headers.add(hdr); try headers.add(hdr);
} }
try self.requestCookie(.{ .is_navigation = true }).headersForRequest(self.arena, self.url, &headers);
// We dispatch page_navigate event before sending the request. // We dispatch page_navigate event before sending the request.
// It ensures the event page_navigated is not dispatched before this one. // It ensures the event page_navigated is not dispatched before this one.
session.notification.dispatch(.page_navigate, &.{ session.notification.dispatch(.page_navigate, &.{
@@ -573,6 +564,7 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
.headers = headers, .headers = headers,
.body = opts.body, .body = opts.body,
.cookie_jar = &session.cookie_jar, .cookie_jar = &session.cookie_jar,
.cookie_origin = self.url,
.resource_type = .document, .resource_type = .document,
.notification = self._session.notification, .notification = self._session.notification,
.header_callback = pageHeaderDoneCallback, .header_callback = pageHeaderDoneCallback,
@@ -1036,6 +1028,7 @@ fn pageDoneCallback(ctx: *anyopaque) !void {
}); });
parser.parse(html); parser.parse(html);
self._parse_state = .complete;
self.documentIsComplete(); self.documentIsComplete();
}, },
else => unreachable, else => unreachable,
@@ -3554,19 +3547,6 @@ pub fn insertText(self: *Page, v: []const u8) !void {
} }
} }
const RequestCookieOpts = struct {
is_http: bool = true,
is_navigation: bool = false,
};
pub fn requestCookie(self: *const Page, opts: RequestCookieOpts) HttpClient.RequestCookie {
return .{
.jar = &self._session.cookie_jar,
.origin = self.url,
.is_http = opts.is_http,
.is_navigation = opts.is_navigation,
};
}
fn asUint(comptime string: anytype) std.meta.Int( fn asUint(comptime string: anytype) std.meta.Int(
.unsigned, .unsigned,
@bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0 @bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0

View File

@@ -21,12 +21,9 @@ const lp = @import("lightpanda");
const builtin = @import("builtin"); const builtin = @import("builtin");
const log = @import("../log.zig"); const log = @import("../log.zig");
const App = @import("../App.zig");
const Page = @import("Page.zig"); const Page = @import("Page.zig");
const Session = @import("Session.zig"); const Session = @import("Session.zig");
const Browser = @import("Browser.zig");
const Factory = @import("Factory.zig");
const HttpClient = @import("HttpClient.zig"); const HttpClient = @import("HttpClient.zig");
const IS_DEBUG = builtin.mode == .Debug; const IS_DEBUG = builtin.mode == .Debug;

View File

@@ -28,12 +28,10 @@ const String = @import("../string.zig").String;
const js = @import("js/js.zig"); const js = @import("js/js.zig");
const URL = @import("URL.zig"); const URL = @import("URL.zig");
const Page = @import("Page.zig"); const Page = @import("Page.zig");
const Browser = @import("Browser.zig");
const Element = @import("webapi/Element.zig"); const Element = @import("webapi/Element.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArrayList = std.ArrayList;
const IS_DEBUG = builtin.mode == .Debug; const IS_DEBUG = builtin.mode == .Debug;
@@ -138,9 +136,9 @@ fn clearList(list: *std.DoublyLinkedList) void {
} }
} }
fn getHeaders(self: *ScriptManager, arena: Allocator, url: [:0]const u8) !net_http.Headers { fn getHeaders(self: *ScriptManager) !net_http.Headers {
var headers = try self.client.newHeaders(); var headers = try self.client.newHeaders();
try self.page.headersForRequest(arena, url, &headers); try self.page.headersForRequest(&headers);
return headers; return headers;
} }
@@ -280,9 +278,10 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
.ctx = script, .ctx = script,
.method = .GET, .method = .GET,
.frame_id = page._frame_id, .frame_id = page._frame_id,
.headers = try self.getHeaders(arena, url), .headers = try self.getHeaders(),
.blocking = is_blocking, .blocking = is_blocking,
.cookie_jar = &page._session.cookie_jar, .cookie_jar = &page._session.cookie_jar,
.cookie_origin = page.url,
.resource_type = .script, .resource_type = .script,
.notification = page._session.notification, .notification = page._session.notification,
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null, .start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
@@ -405,8 +404,9 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
.ctx = script, .ctx = script,
.method = .GET, .method = .GET,
.frame_id = page._frame_id, .frame_id = page._frame_id,
.headers = try self.getHeaders(arena, url), .headers = try self.getHeaders(),
.cookie_jar = &page._session.cookie_jar, .cookie_jar = &page._session.cookie_jar,
.cookie_origin = page.url,
.resource_type = .script, .resource_type = .script,
.notification = page._session.notification, .notification = page._session.notification,
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null, .start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
@@ -508,10 +508,11 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
.url = url, .url = url,
.method = .GET, .method = .GET,
.frame_id = page._frame_id, .frame_id = page._frame_id,
.headers = try self.getHeaders(arena, url), .headers = try self.getHeaders(),
.ctx = script, .ctx = script,
.resource_type = .script, .resource_type = .script,
.cookie_jar = &page._session.cookie_jar, .cookie_jar = &page._session.cookie_jar,
.cookie_origin = page.url,
.notification = page._session.notification, .notification = page._session.notification,
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null, .start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
.header_callback = Script.headerCallback, .header_callback = Script.headerCallback,
@@ -654,7 +655,6 @@ pub const Script = struct {
debug_transfer_aborted: bool = false, debug_transfer_aborted: bool = false,
debug_transfer_bytes_received: usize = 0, debug_transfer_bytes_received: usize = 0,
debug_transfer_notified_fail: bool = false, debug_transfer_notified_fail: bool = false,
debug_transfer_redirecting: bool = false,
debug_transfer_intercept_state: u8 = 0, debug_transfer_intercept_state: u8 = 0,
debug_transfer_auth_challenge: bool = false, debug_transfer_auth_challenge: bool = false,
debug_transfer_easy_id: usize = 0, debug_transfer_easy_id: usize = 0,
@@ -730,7 +730,6 @@ pub const Script = struct {
.a3 = self.debug_transfer_aborted, .a3 = self.debug_transfer_aborted,
.a4 = self.debug_transfer_bytes_received, .a4 = self.debug_transfer_bytes_received,
.a5 = self.debug_transfer_notified_fail, .a5 = self.debug_transfer_notified_fail,
.a6 = self.debug_transfer_redirecting,
.a7 = self.debug_transfer_intercept_state, .a7 = self.debug_transfer_intercept_state,
.a8 = self.debug_transfer_auth_challenge, .a8 = self.debug_transfer_auth_challenge,
.a9 = self.debug_transfer_easy_id, .a9 = self.debug_transfer_easy_id,
@@ -739,10 +738,9 @@ pub const Script = struct {
.b3 = transfer.aborted, .b3 = transfer.aborted,
.b4 = transfer.bytes_received, .b4 = transfer.bytes_received,
.b5 = transfer._notified_fail, .b5 = transfer._notified_fail,
.b6 = transfer._redirecting,
.b7 = @intFromEnum(transfer._intercept_state), .b7 = @intFromEnum(transfer._intercept_state),
.b8 = transfer._auth_challenge != null, .b8 = transfer._auth_challenge != null,
.b9 = if (transfer._conn) |c| @intFromPtr(c.easy) else 0, .b9 = if (transfer._conn) |c| @intFromPtr(c._easy) else 0,
}); });
self.header_callback_called = true; self.header_callback_called = true;
self.debug_transfer_id = transfer.id; self.debug_transfer_id = transfer.id;
@@ -750,10 +748,9 @@ pub const Script = struct {
self.debug_transfer_aborted = transfer.aborted; self.debug_transfer_aborted = transfer.aborted;
self.debug_transfer_bytes_received = transfer.bytes_received; self.debug_transfer_bytes_received = transfer.bytes_received;
self.debug_transfer_notified_fail = transfer._notified_fail; self.debug_transfer_notified_fail = transfer._notified_fail;
self.debug_transfer_redirecting = transfer._redirecting;
self.debug_transfer_intercept_state = @intFromEnum(transfer._intercept_state); self.debug_transfer_intercept_state = @intFromEnum(transfer._intercept_state);
self.debug_transfer_auth_challenge = transfer._auth_challenge != null; self.debug_transfer_auth_challenge = transfer._auth_challenge != null;
self.debug_transfer_easy_id = if (transfer._conn) |c| @intFromPtr(c.easy) else 0; self.debug_transfer_easy_id = if (transfer._conn) |c| @intFromPtr(c._easy) else 0;
} }
lp.assert(self.source.remote.capacity == 0, "ScriptManager.Header buffer", .{ .capacity = self.source.remote.capacity }); lp.assert(self.source.remote.capacity == 0, "ScriptManager.Header buffer", .{ .capacity = self.source.remote.capacity });

View File

@@ -128,7 +128,7 @@ fn _constructor(self: *Caller, func: anytype, info: FunctionCallbackInfo) !void
const new_this_handle = info.getThis(); const new_this_handle = info.getThis();
var this = js.Object{ .local = local, .handle = new_this_handle }; var this = js.Object{ .local = local, .handle = new_this_handle };
if (@typeInfo(ReturnType) == .error_union) { if (@typeInfo(ReturnType) == .error_union) {
const non_error_res = res catch |err| return err; const non_error_res = try res;
this = try local.mapZigInstanceToJs(new_this_handle, non_error_res); this = try local.mapZigInstanceToJs(new_this_handle, non_error_res);
} else { } else {
this = try local.mapZigInstanceToJs(new_this_handle, res); this = try local.mapZigInstanceToJs(new_this_handle, res);

View File

@@ -22,7 +22,6 @@ const log = @import("../../log.zig");
const js = @import("js.zig"); const js = @import("js.zig");
const Env = @import("Env.zig"); const Env = @import("Env.zig");
const bridge = @import("bridge.zig");
const Origin = @import("Origin.zig"); const Origin = @import("Origin.zig");
const Scheduler = @import("Scheduler.zig"); const Scheduler = @import("Scheduler.zig");

View File

@@ -26,7 +26,6 @@ const App = @import("../../App.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const bridge = @import("bridge.zig"); const bridge = @import("bridge.zig");
const Origin = @import("Origin.zig");
const Context = @import("Context.zig"); const Context = @import("Context.zig");
const Isolate = @import("Isolate.zig"); const Isolate = @import("Isolate.zig");
const Platform = @import("Platform.zig"); const Platform = @import("Platform.zig");
@@ -34,7 +33,6 @@ const Snapshot = @import("Snapshot.zig");
const Inspector = @import("Inspector.zig"); const Inspector = @import("Inspector.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const Session = @import("../Session.zig");
const Window = @import("../webapi/Window.zig"); const Window = @import("../webapi/Window.zig");
const JsApis = bridge.JsApis; const JsApis = bridge.JsApis;

View File

@@ -21,7 +21,6 @@ const js = @import("js.zig");
const v8 = js.v8; const v8 = js.v8;
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const Session = @import("../Session.zig");
const Function = @This(); const Function = @This();

View File

@@ -32,7 +32,6 @@ const js = @import("js.zig");
const Session = @import("../Session.zig"); const Session = @import("../Session.zig");
const v8 = js.v8; const v8 = js.v8;
const Allocator = std.mem.Allocator;
const Identity = @This(); const Identity = @This();

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const Page = @import("../Page.zig");
const Session = @import("../Session.zig"); const Session = @import("../Session.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const string = @import("../../string.zig"); const string = @import("../../string.zig");
@@ -33,7 +32,6 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
const v8 = js.v8; const v8 = js.v8;
const CallOpts = Caller.CallOpts; const CallOpts = Caller.CallOpts;
const Allocator = std.mem.Allocator;
// Where js.Context has a lifetime tied to the page, and holds the // Where js.Context has a lifetime tied to the page, and holds the
// v8::Global<v8::Context>, this has a much shorter lifetime and holds a // v8::Global<v8::Context>, this has a much shorter lifetime and holds a

View File

@@ -20,8 +20,6 @@ const std = @import("std");
const js = @import("js.zig"); const js = @import("js.zig");
const v8 = js.v8; const v8 = js.v8;
const Session = @import("../Session.zig");
const Promise = @This(); const Promise = @This();
local: *const js.Local, local: *const js.Local,

View File

@@ -25,7 +25,6 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
const v8 = js.v8; const v8 = js.v8;
const JsApis = bridge.JsApis; const JsApis = bridge.JsApis;
const Allocator = std.mem.Allocator;
const Snapshot = @This(); const Snapshot = @This();

View File

@@ -25,7 +25,6 @@ const v8 = js.v8;
const IS_DEBUG = @import("builtin").mode == .Debug; const IS_DEBUG = @import("builtin").mode == .Debug;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Session = @import("../Session.zig");
const Value = @This(); const Value = @This();

View File

@@ -18,15 +18,12 @@
const std = @import("std"); const std = @import("std");
const js = @import("js.zig"); const js = @import("js.zig");
const lp = @import("lightpanda");
const log = @import("../../log.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const Session = @import("../Session.zig"); const Session = @import("../Session.zig");
const v8 = js.v8; const v8 = js.v8;
const Caller = @import("Caller.zig"); const Caller = @import("Caller.zig");
const Context = @import("Context.zig");
const IS_DEBUG = @import("builtin").mode == .Debug; const IS_DEBUG = @import("builtin").mode == .Debug;

View File

@@ -21,7 +21,6 @@ const std = @import("std");
const Page = @import("Page.zig"); const Page = @import("Page.zig");
const URL = @import("URL.zig"); const URL = @import("URL.zig");
const TreeWalker = @import("webapi/TreeWalker.zig"); const TreeWalker = @import("webapi/TreeWalker.zig");
const CData = @import("webapi/CData.zig");
const Element = @import("webapi/Element.zig"); const Element = @import("webapi/Element.zig");
const Node = @import("webapi/Node.zig"); const Node = @import("webapi/Node.zig");
const isAllWhitespace = @import("../string.zig").isAllWhitespace; const isAllWhitespace = @import("../string.zig").isAllWhitespace;

View File

@@ -148,3 +148,13 @@
} }
</script> </script>
<script id=identity>
{
const element = document.createElement('canvas');
const ctx = element.getContext('2d');
testing.expectTrue(ctx === element.getContext('2d'));
testing.expectEqual(null, element.getContext('webgl'));
}
</script>

View File

@@ -85,3 +85,13 @@
loseContext.restoreContext(); loseContext.restoreContext();
} }
</script> </script>
<script id=identity>
{
const element = document.createElement('canvas');
const ctx = element.getContext('webgl');
testing.expectTrue(ctx === element.getContext('webgl'));
testing.expectEqual(null, element.getContext('2d'));
}
</script>

View File

@@ -15,8 +15,9 @@
testing.expectEqual(true, validPlatforms.includes(navigator.platform)); testing.expectEqual(true, validPlatforms.includes(navigator.platform));
testing.expectEqual('en-US', navigator.language); testing.expectEqual('en-US', navigator.language);
testing.expectEqual(true, Array.isArray(navigator.languages)); testing.expectEqual(true, Array.isArray(navigator.languages));
testing.expectEqual(1, navigator.languages.length); testing.expectEqual(2, navigator.languages.length);
testing.expectEqual('en-US', navigator.languages[0]); testing.expectEqual('en-US', navigator.languages[0]);
testing.expectEqual('en', navigator.languages[1]);
testing.expectEqual(true, navigator.onLine); testing.expectEqual(true, navigator.onLine);
testing.expectEqual(true, navigator.cookieEnabled); testing.expectEqual(true, navigator.cookieEnabled);
testing.expectEqual(true, navigator.hardwareConcurrency > 0); testing.expectEqual(true, navigator.hardwareConcurrency > 0);

View File

@@ -24,7 +24,6 @@ const Page = @import("../Page.zig");
const Node = @import("Node.zig"); const Node = @import("Node.zig");
const Element = @import("Element.zig"); const Element = @import("Element.zig");
const DOMException = @import("DOMException.zig");
const Custom = @import("element/html/Custom.zig"); const Custom = @import("element/html/Custom.zig");
const CustomElementDefinition = @import("CustomElementDefinition.zig"); const CustomElementDefinition = @import("CustomElementDefinition.zig");

View File

@@ -59,6 +59,7 @@ pub fn fromError(err: anyerror) ?DOMException {
error.TimeoutError => .{ ._code = .timeout_error }, error.TimeoutError => .{ ._code = .timeout_error },
error.InvalidNodeType => .{ ._code = .invalid_node_type_error }, error.InvalidNodeType => .{ ._code = .invalid_node_type_error },
error.DataClone => .{ ._code = .data_clone_error }, error.DataClone => .{ ._code = .data_clone_error },
error.InvalidAccessError => .{ ._code = .invalid_access_error },
else => null, else => null,
}; };
} }

View File

@@ -18,7 +18,6 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const String = @import("../../string.zig").String;
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const Node = @import("Node.zig"); const Node = @import("Node.zig");

View File

@@ -19,10 +19,8 @@
const std = @import("std"); const std = @import("std");
const String = @import("../../string.zig").String; const String = @import("../../string.zig").String;
const log = @import("../../log.zig");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const color = @import("../color.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
/// https://developer.mozilla.org/en-US/docs/Web/API/ImageData/ImageData /// https://developer.mozilla.org/en-US/docs/Web/API/ImageData/ImageData

View File

@@ -40,8 +40,8 @@ pub fn getUserAgent(_: *const Navigator, page: *Page) []const u8 {
return page._session.browser.app.config.http_headers.user_agent; return page._session.browser.app.config.http_headers.user_agent;
} }
pub fn getLanguages(_: *const Navigator) [1][]const u8 { pub fn getLanguages(_: *const Navigator) [2][]const u8 {
return .{"en-US"}; return .{ "en-US", "en" };
} }
pub fn getPlatform(_: *const Navigator) []const u8 { pub fn getPlatform(_: *const Navigator) []const u8 {

View File

@@ -28,8 +28,6 @@ const DocumentFragment = @import("DocumentFragment.zig");
const AbstractRange = @import("AbstractRange.zig"); const AbstractRange = @import("AbstractRange.zig");
const DOMRect = @import("DOMRect.zig"); const DOMRect = @import("DOMRect.zig");
const Allocator = std.mem.Allocator;
const Range = @This(); const Range = @This();
_proto: *AbstractRange, _proto: *AbstractRange,

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const log = @import("../../log.zig");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
@@ -27,7 +26,6 @@ const Range = @import("Range.zig");
const AbstractRange = @import("AbstractRange.zig"); const AbstractRange = @import("AbstractRange.zig");
const Node = @import("Node.zig"); const Node = @import("Node.zig");
const Event = @import("Event.zig"); const Event = @import("Event.zig");
const Document = @import("Document.zig");
/// https://w3c.github.io/selection-api/ /// https://w3c.github.io/selection-api/
const Selection = @This(); const Selection = @This();

View File

@@ -19,7 +19,6 @@
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const EventTarget = @import("EventTarget.zig"); const EventTarget = @import("EventTarget.zig");
const Window = @import("Window.zig");
const VisualViewport = @This(); const VisualViewport = @This();

View File

@@ -16,7 +16,6 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const String = @import("../../../string.zig").String;
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const CData = @import("../CData.zig"); const CData = @import("../CData.zig");

View File

@@ -20,7 +20,6 @@
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda"); const lp = @import("lightpanda");
const log = @import("../../../log.zig");
const crypto = @import("../../../sys/libcrypto.zig"); const crypto = @import("../../../sys/libcrypto.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");

View File

@@ -20,12 +20,10 @@
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda"); const lp = @import("lightpanda");
const log = @import("../../../log.zig");
const crypto = @import("../../../sys/libcrypto.zig"); const crypto = @import("../../../sys/libcrypto.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Algorithm = @import("algorithm.zig").Algorithm;
const CryptoKey = @import("../CryptoKey.zig"); const CryptoKey = @import("../CryptoKey.zig");

View File

@@ -22,7 +22,6 @@ const reflect = @import("../../reflect.zig");
const log = @import("../../../log.zig"); const log = @import("../../../log.zig");
const global_event_handlers = @import("../global_event_handlers.zig"); const global_event_handlers = @import("../global_event_handlers.zig");
const GlobalEventHandlersLookup = global_event_handlers.Lookup;
const GlobalEventHandler = global_event_handlers.Handler; const GlobalEventHandler = global_event_handlers.Handler;
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");

View File

@@ -29,6 +29,9 @@ const OffscreenCanvas = @import("../../canvas/OffscreenCanvas.zig");
const Canvas = @This(); const Canvas = @This();
_proto: *HtmlElement, _proto: *HtmlElement,
_cached: ?DrawingContext = null,
const ContextType = enum { none, @"2d", webgl };
pub fn asElement(self: *Canvas) *Element { pub fn asElement(self: *Canvas) *Element {
return self._proto._proto; return self._proto._proto;
@@ -68,17 +71,28 @@ const DrawingContext = union(enum) {
}; };
pub fn getContext(self: *Canvas, context_type: []const u8, page: *Page) !?DrawingContext { pub fn getContext(self: *Canvas, context_type: []const u8, page: *Page) !?DrawingContext {
if (std.mem.eql(u8, context_type, "2d")) { if (self._cached) |cached| {
const ctx = try page._factory.create(CanvasRenderingContext2D{ ._canvas = self }); const matches = switch (cached) {
return .{ .@"2d" = ctx }; .@"2d" => std.mem.eql(u8, context_type, "2d"),
.webgl => std.mem.eql(u8, context_type, "webgl") or std.mem.eql(u8, context_type, "experimental-webgl"),
};
return if (matches) cached else null;
} }
if (std.mem.eql(u8, context_type, "webgl") or std.mem.eql(u8, context_type, "experimental-webgl")) { const drawing_context: DrawingContext = blk: {
const ctx = try page._factory.create(WebGLRenderingContext{}); if (std.mem.eql(u8, context_type, "2d")) {
return .{ .webgl = ctx }; const ctx = try page._factory.create(CanvasRenderingContext2D{ ._canvas = self });
} break :blk .{ .@"2d" = ctx };
}
return null; if (std.mem.eql(u8, context_type, "webgl") or std.mem.eql(u8, context_type, "experimental-webgl")) {
const ctx = try page._factory.create(WebGLRenderingContext{});
break :blk .{ .webgl = ctx };
}
return null;
};
self._cached = drawing_context;
return drawing_context;
} }
/// Transfers control of the canvas to an OffscreenCanvas. /// Transfers control of the canvas to an OffscreenCanvas.

View File

@@ -16,7 +16,6 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const log = @import("../../../../log.zig");
const js = @import("../../../js/js.zig"); const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig"); const Page = @import("../../../Page.zig");
const Window = @import("../../Window.zig"); const Window = @import("../../Window.zig");

View File

@@ -5,10 +5,6 @@ const URL = @import("../../../URL.zig");
const Node = @import("../../Node.zig"); const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig"); const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig"); const HtmlElement = @import("../Html.zig");
const Event = @import("../../Event.zig");
const log = @import("../../../../log.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
const Image = @This(); const Image = @This();
_proto: *HtmlElement, _proto: *HtmlElement,

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const log = @import("../../../../log.zig");
const js = @import("../../../js/js.zig"); const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig"); const Page = @import("../../../Page.zig");

View File

@@ -22,7 +22,6 @@ const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig"); const Session = @import("../../Session.zig");
const Event = @import("../Event.zig"); const Event = @import("../Event.zig");
const Allocator = std.mem.Allocator;
const CompositionEvent = @This(); const CompositionEvent = @This();

View File

@@ -24,7 +24,6 @@ const Session = @import("../../Session.zig");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Event = @import("../Event.zig"); const Event = @import("../Event.zig");
const UIEvent = @import("UIEvent.zig");
const FormData = @import("../net/FormData.zig"); const FormData = @import("../net/FormData.zig");

View File

@@ -22,7 +22,6 @@ const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig"); const Session = @import("../../Session.zig");
const Event = @import("../Event.zig"); const Event = @import("../Event.zig");
const Allocator = std.mem.Allocator;
const PromiseRejectionEvent = @This(); const PromiseRejectionEvent = @This();

View File

@@ -27,8 +27,6 @@ const Page = @import("../../Page.zig");
const Event = @import("../Event.zig"); const Event = @import("../Event.zig");
const EventTarget = @import("../EventTarget.zig"); const EventTarget = @import("../EventTarget.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
// https://developer.mozilla.org/en-US/docs/Web/API/Navigation // https://developer.mozilla.org/en-US/docs/Web/API/Navigation
const Navigation = @This(); const Navigation = @This();

View File

@@ -18,7 +18,6 @@
const std = @import("std"); const std = @import("std");
const URL = @import("../URL.zig"); const URL = @import("../URL.zig");
const EventTarget = @import("../EventTarget.zig");
const NavigationState = @import("root.zig").NavigationState; const NavigationState = @import("root.zig").NavigationState;
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");

View File

@@ -80,7 +80,7 @@ pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise {
if (request._headers) |h| { if (request._headers) |h| {
try h.populateHttpHeader(page.call_arena, &headers); try h.populateHttpHeader(page.call_arena, &headers);
} }
try page.headersForRequest(page.arena, request._url, &headers); try page.headersForRequest(&headers);
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
log.debug(.http, "fetch", .{ .url = request._url }); log.debug(.http, "fetch", .{ .url = request._url });
@@ -95,6 +95,7 @@ pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise {
.headers = headers, .headers = headers,
.resource_type = .fetch, .resource_type = .fetch,
.cookie_jar = &page._session.cookie_jar, .cookie_jar = &page._session.cookie_jar,
.cookie_origin = page.url,
.notification = page._session.notification, .notification = page._session.notification,
.start_callback = httpStartCallback, .start_callback = httpStartCallback,
.header_callback = httpHeaderDoneCallback, .header_callback = httpHeaderDoneCallback,

View File

@@ -22,7 +22,6 @@ const log = @import("../../../log.zig");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const Node = @import("../Node.zig");
const Form = @import("../element/html/Form.zig"); const Form = @import("../element/html/Form.zig");
const Element = @import("../Element.zig"); const Element = @import("../Element.zig");
const KeyValueList = @import("../KeyValueList.zig"); const KeyValueList = @import("../KeyValueList.zig");

View File

@@ -29,7 +29,6 @@ const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig"); const Session = @import("../../Session.zig");
const Node = @import("../Node.zig"); const Node = @import("../Node.zig");
const Blob = @import("../Blob.zig");
const Event = @import("../Event.zig"); const Event = @import("../Event.zig");
const Headers = @import("Headers.zig"); const Headers = @import("Headers.zig");
const EventTarget = @import("../EventTarget.zig"); const EventTarget = @import("../EventTarget.zig");
@@ -225,7 +224,7 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void {
try self._request_headers.populateHttpHeader(page.call_arena, &headers); try self._request_headers.populateHttpHeader(page.call_arena, &headers);
if (cookie_support) { if (cookie_support) {
try page.headersForRequest(self._arena, self._url, &headers); try page.headersForRequest(&headers);
} }
try http_client.request(.{ try http_client.request(.{
@@ -236,6 +235,7 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void {
.frame_id = page._frame_id, .frame_id = page._frame_id,
.body = self._request_body, .body = self._request_body,
.cookie_jar = if (cookie_support) &page._session.cookie_jar else null, .cookie_jar = if (cookie_support) &page._session.cookie_jar else null,
.cookie_origin = page.url,
.resource_type = .xhr, .resource_type = .xhr,
.notification = page._session.notification, .notification = page._session.notification,
.start_callback = httpStartCallback, .start_callback = httpStartCallback,

View File

@@ -25,7 +25,6 @@ const json = std.json;
const Incrementing = @import("id.zig").Incrementing; const Incrementing = @import("id.zig").Incrementing;
const log = @import("../log.zig"); const log = @import("../log.zig");
const App = @import("../App.zig");
const Notification = @import("../Notification.zig"); const Notification = @import("../Notification.zig");
const Client = @import("../Server.zig").Client; const Client = @import("../Server.zig").Client;
@@ -35,7 +34,6 @@ const Browser = @import("../browser/Browser.zig");
const Session = @import("../browser/Session.zig"); const Session = @import("../browser/Session.zig");
const Page = @import("../browser/Page.zig"); const Page = @import("../browser/Page.zig");
const Mime = @import("../browser/Mime.zig"); const Mime = @import("../browser/Mime.zig");
const HttpClient = @import("../browser/HttpClient.zig");
const InterceptState = @import("domains/fetch.zig").InterceptState; const InterceptState = @import("domains/fetch.zig").InterceptState;
@@ -365,6 +363,11 @@ pub fn BrowserContext(comptime CDP_T: type) type {
inspector_session: *js.Inspector.Session, inspector_session: *js.Inspector.Session,
isolated_worlds: std.ArrayList(*IsolatedWorld), isolated_worlds: std.ArrayList(*IsolatedWorld),
// Scripts registered via Page.addScriptToEvaluateOnNewDocument.
// Evaluated in each new document after navigation completes.
scripts_on_new_document: std.ArrayList(ScriptOnNewDocument) = .empty,
next_script_id: u32 = 1,
http_proxy_changed: bool = false, http_proxy_changed: bool = false,
// Extra headers to add to all requests. // Extra headers to add to all requests.
@@ -470,8 +473,8 @@ pub fn BrowserContext(comptime CDP_T: type) type {
if (self.http_proxy_changed) { if (self.http_proxy_changed) {
// has to be called after browser.closeSession, since it won't // has to be called after browser.closeSession, since it won't
// work if there are active connections. // work if there are active connections.
browser.http_client.restoreOriginalProxy() catch |err| { browser.http_client.changeProxy(null) catch |err| {
log.warn(.http, "restoreOriginalProxy", .{ .err = err }); log.warn(.http, "changeProxy", .{ .err = err });
}; };
} }
self.intercept_state.deinit(); self.intercept_state.deinit();
@@ -764,6 +767,11 @@ pub fn BrowserContext(comptime CDP_T: type) type {
/// Clients create this to be able to create variables and run code without interfering with the /// Clients create this to be able to create variables and run code without interfering with the
/// normal namespace and values of the webpage. Similar to the main context we need to pretend to recreate it after /// normal namespace and values of the webpage. Similar to the main context we need to pretend to recreate it after
/// a executionContextsCleared event which happens when navigating to a new page. A client can have a command be executed /// a executionContextsCleared event which happens when navigating to a new page. A client can have a command be executed
const ScriptOnNewDocument = struct {
identifier: u32,
source: []const u8,
};
/// in the isolated world by using its Context ID or the worldName. /// in the isolated world by using its Context ID or the worldName.
/// grantUniveralAccess Indecated whether the isolated world can reference objects like the DOM or other JS Objects. /// grantUniveralAccess Indecated whether the isolated world can reference objects like the DOM or other JS Objects.
/// An isolated world has it's own instance of globals like Window. /// An isolated world has it's own instance of globals like Window.

View File

@@ -18,7 +18,6 @@
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda"); const lp = @import("lightpanda");
const log = @import("../../log.zig");
const markdown = lp.markdown; const markdown = lp.markdown;
const SemanticTree = lp.SemanticTree; const SemanticTree = lp.SemanticTree;
const interactive = lp.interactive; const interactive = lp.interactive;

View File

@@ -351,6 +351,10 @@ pub const TransferAsRequestWriter = struct {
try jws.objectField(hdr.name); try jws.objectField(hdr.name);
try jws.write(hdr.value); try jws.write(hdr.value);
} }
if (try transfer.getCookieString()) |cookies| {
try jws.objectField("Cookie");
try jws.write(cookies[0 .. cookies.len - 1]);
}
try jws.endObject(); try jws.endObject();
} }
try jws.endObject(); try jws.endObject();

View File

@@ -37,6 +37,7 @@ pub fn processMessage(cmd: anytype) !void {
getFrameTree, getFrameTree,
setLifecycleEventsEnabled, setLifecycleEventsEnabled,
addScriptToEvaluateOnNewDocument, addScriptToEvaluateOnNewDocument,
removeScriptToEvaluateOnNewDocument,
createIsolatedWorld, createIsolatedWorld,
navigate, navigate,
reload, reload,
@@ -51,6 +52,7 @@ pub fn processMessage(cmd: anytype) !void {
.getFrameTree => return getFrameTree(cmd), .getFrameTree => return getFrameTree(cmd),
.setLifecycleEventsEnabled => return setLifecycleEventsEnabled(cmd), .setLifecycleEventsEnabled => return setLifecycleEventsEnabled(cmd),
.addScriptToEvaluateOnNewDocument => return addScriptToEvaluateOnNewDocument(cmd), .addScriptToEvaluateOnNewDocument => return addScriptToEvaluateOnNewDocument(cmd),
.removeScriptToEvaluateOnNewDocument => return removeScriptToEvaluateOnNewDocument(cmd),
.createIsolatedWorld => return createIsolatedWorld(cmd), .createIsolatedWorld => return createIsolatedWorld(cmd),
.navigate => return navigate(cmd), .navigate => return navigate(cmd),
.reload => return doReload(cmd), .reload => return doReload(cmd),
@@ -147,22 +149,55 @@ fn setLifecycleEventsEnabled(cmd: anytype) !void {
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
// TODO: hard coded method
// With the command we receive a script we need to store and run for each new document.
// Note that the worldName refers to the name given to the isolated world.
fn addScriptToEvaluateOnNewDocument(cmd: anytype) !void { fn addScriptToEvaluateOnNewDocument(cmd: anytype) !void {
// const params = (try cmd.params(struct { const params = (try cmd.params(struct {
// source: []const u8, source: []const u8,
// worldName: ?[]const u8 = null, worldName: ?[]const u8 = null,
// includeCommandLineAPI: bool = false, includeCommandLineAPI: bool = false,
// runImmediately: bool = false, runImmediately: bool = false,
// })) orelse return error.InvalidParams; })) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
if (params.runImmediately) {
log.warn(.not_implemented, "addScriptOnNewDocument", .{ .param = "runImmediately" });
}
const script_id = bc.next_script_id;
bc.next_script_id += 1;
const source_dupe = try bc.arena.dupe(u8, params.source);
try bc.scripts_on_new_document.append(bc.arena, .{
.identifier = script_id,
.source = source_dupe,
});
var id_buf: [16]u8 = undefined;
const id_str = std.fmt.bufPrint(&id_buf, "{d}", .{script_id}) catch "1";
return cmd.sendResult(.{ return cmd.sendResult(.{
.identifier = "1", .identifier = id_str,
}, .{}); }, .{});
} }
fn removeScriptToEvaluateOnNewDocument(cmd: anytype) !void {
const params = (try cmd.params(struct {
identifier: []const u8,
})) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const target_id = std.fmt.parseInt(u32, params.identifier, 10) catch
return cmd.sendResult(null, .{});
for (bc.scripts_on_new_document.items, 0..) |script, i| {
if (script.identifier == target_id) {
_ = bc.scripts_on_new_document.orderedRemove(i);
break;
}
}
return cmd.sendResult(null, .{});
}
fn close(cmd: anytype) !void { fn close(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
@@ -482,6 +517,27 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
); );
} }
// Evaluate scripts registered via Page.addScriptToEvaluateOnNewDocument.
// Must run after the execution context is created but before the client
// receives frameNavigated/loadEventFired so polyfills are available for
// subsequent CDP commands.
if (bc.scripts_on_new_document.items.len > 0) {
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
for (bc.scripts_on_new_document.items) |script| {
var try_catch: lp.js.TryCatch = undefined;
try_catch.init(&ls.local);
defer try_catch.deinit();
ls.local.eval(script.source, null) catch |err| {
const caught = try_catch.caughtOrError(arena, err);
log.warn(.cdp, "script on new doc", .{ .caught = caught });
};
}
}
// frameNavigated event // frameNavigated event
try cdp.sendEvent("Page.frameNavigated", .{ try cdp.sendEvent("Page.frameNavigated", .{
.type = "Navigation", .type = "Navigation",
@@ -840,3 +896,55 @@ test "cdp.page: reload" {
try ctx.processMessage(.{ .id = 32, .method = "Page.reload", .params = .{ .ignoreCache = true } }); try ctx.processMessage(.{ .id = 32, .method = "Page.reload", .params = .{ .ignoreCache = true } });
} }
} }
test "cdp.page: addScriptToEvaluateOnNewDocument" {
var ctx = try testing.context();
defer ctx.deinit();
var bc = try ctx.loadBrowserContext(.{ .id = "BID-9", .url = "hi.html", .target_id = "FID-000000000X".* });
{
// Register a script — should return unique identifier "1"
try ctx.processMessage(.{ .id = 20, .method = "Page.addScriptToEvaluateOnNewDocument", .params = .{ .source = "window.__test = 1" } });
try ctx.expectSentResult(.{
.identifier = "1",
}, .{ .id = 20 });
}
{
// Register another script — should return identifier "2"
try ctx.processMessage(.{ .id = 21, .method = "Page.addScriptToEvaluateOnNewDocument", .params = .{ .source = "window.__test2 = 2" } });
try ctx.expectSentResult(.{
.identifier = "2",
}, .{ .id = 21 });
}
{
// Remove the first script — should succeed
try ctx.processMessage(.{ .id = 22, .method = "Page.removeScriptToEvaluateOnNewDocument", .params = .{ .identifier = "1" } });
try ctx.expectSentResult(null, .{ .id = 22 });
}
{
// Remove a non-existent identifier — should succeed silently
try ctx.processMessage(.{ .id = 23, .method = "Page.removeScriptToEvaluateOnNewDocument", .params = .{ .identifier = "999" } });
try ctx.expectSentResult(null, .{ .id = 23 });
}
{
try ctx.processMessage(.{ .id = 34, .method = "Page.reload" });
// wait for this event, which is sent after we've run the registered scripts
try ctx.expectSentEvent("Page.frameNavigated", .{
.frame = .{ .loaderId = "LID-0000000002" },
}, .{});
const page = bc.session.currentPage() orelse unreachable;
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
const test_val = try ls.local.exec("window.__test2", null);
try testing.expectEqual(2, try test_val.toI32());
}
}

View File

@@ -24,9 +24,6 @@ const log = @import("../../log.zig");
const URL = @import("../../browser/URL.zig"); const URL = @import("../../browser/URL.zig");
const js = @import("../../browser/js/js.zig"); const js = @import("../../browser/js/js.zig");
// TODO: hard coded IDs
const LOADER_ID = "LOADERID42AA389647D702B4D805F49A";
pub fn processMessage(cmd: anytype) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
getTargets, getTargets,
@@ -161,15 +158,20 @@ fn createTarget(cmd: anytype) !void {
else => return err, else => return err,
}; };
if (bc.target_id != null) {
return error.TargetAlreadyLoaded;
}
if (params.browserContextId) |param_browser_context_id| { if (params.browserContextId) |param_browser_context_id| {
if (std.mem.eql(u8, param_browser_context_id, bc.id) == false) { if (std.mem.eql(u8, param_browser_context_id, bc.id) == false) {
return error.UnknownBrowserContextId; return error.UnknownBrowserContextId;
} }
} }
// If a target already exists, close it first. Lightpanda only supports
// one page at a time, so we replace the existing target rather than
// rejecting the request. This unblocks automation frameworks (e.g.
// Stagehand) that call createTarget multiple times.
if (bc.target_id != null) {
try doCloseTarget(cmd, bc);
}
// if target_id is null, we should never have a page // if target_id is null, we should never have a page
lp.assert(bc.session.page == null, "CDP.target.createTarget not null page", .{}); lp.assert(bc.session.page == null, "CDP.target.createTarget not null page", .{});
@@ -283,34 +285,9 @@ fn closeTarget(cmd: anytype) !void {
return error.UnknownTargetId; return error.UnknownTargetId;
} }
// can't be null if we have a target_id
lp.assert(bc.session.page != null, "CDP.target.closeTarget null page", .{});
try cmd.sendResult(.{ .success = true }, .{ .include_session_id = false }); try cmd.sendResult(.{ .success = true }, .{ .include_session_id = false });
// could be null, created but never attached try doCloseTarget(cmd, bc);
if (bc.session_id) |session_id| {
// Inspector.detached event
try cmd.sendEvent("Inspector.detached", .{
.reason = "Render process gone.",
}, .{ .session_id = session_id });
// detachedFromTarget event
try cmd.sendEvent("Target.detachedFromTarget", .{
.targetId = target_id,
.sessionId = session_id,
.reason = "Render process gone.",
}, .{});
bc.session_id = null;
}
bc.session.removePage();
for (bc.isolated_worlds.items) |world| {
world.deinit();
}
bc.isolated_worlds.clearRetainingCapacity();
bc.target_id = null;
} }
fn getTargetInfo(cmd: anytype) !void { fn getTargetInfo(cmd: anytype) !void {
@@ -471,6 +448,37 @@ fn setAutoAttach(cmd: anytype) !void {
try cmd.sendResult(null, .{}); try cmd.sendResult(null, .{});
} }
/// Close the current target in a browser context: send detach events,
/// remove the page, clean up isolated worlds, and clear the target_id.
/// Shared by closeTarget and createTarget (which auto-closes before
/// creating a replacement).
fn doCloseTarget(cmd: anytype, bc: anytype) !void {
// can't be null if we have a target_id
lp.assert(bc.session.page != null, "CDP.target.doCloseTarget null page", .{});
// could be null, created but never attached
if (bc.session_id) |session_id| {
try cmd.sendEvent("Inspector.detached", .{
.reason = "Render process gone.",
}, .{ .session_id = session_id });
try cmd.sendEvent("Target.detachedFromTarget", .{
.targetId = &bc.target_id.?,
.sessionId = session_id,
.reason = "Render process gone.",
}, .{});
bc.session_id = null;
}
bc.session.removePage();
for (bc.isolated_worlds.items) |world| {
world.deinit();
}
bc.isolated_worlds.clearRetainingCapacity();
bc.target_id = null;
}
fn doAttachtoTarget(cmd: anytype, target_id: []const u8) !void { fn doAttachtoTarget(cmd: anytype, target_id: []const u8) !void {
const bc = cmd.browser_context.?; const bc = cmd.browser_context.?;
const session_id = bc.session_id orelse cmd.cdp.session_id_gen.next(); const session_id = bc.session_id orelse cmd.cdp.session_id_gen.next();
@@ -774,6 +782,51 @@ test "cdp.target: detachFromTarget" {
} }
} }
test "cdp.target: createTarget closes existing target (issue #1962)" {
var ctx = try testing.context();
defer ctx.deinit();
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
{
// Create first target
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } });
try testing.expectEqual(true, bc.target_id != null);
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 10 });
// Create second target — should succeed by auto-closing the first
try ctx.processMessage(.{ .id = 11, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } });
try testing.expectEqual(true, bc.target_id != null);
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 11 });
// Page should exist (new target is active)
try testing.expectEqual(true, bc.session.page != null);
}
}
test "cdp.target: createTarget closes existing attached target (issue #1962)" {
var ctx = try testing.context();
defer ctx.deinit();
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
{
// Create and attach first target
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } });
try testing.expectEqual(true, bc.target_id != null);
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 10 });
try ctx.processMessage(.{ .id = 11, .method = "Target.attachToTarget", .params = .{ .targetId = bc.target_id.? } });
const session_id = bc.session_id.?;
try ctx.expectSentResult(.{ .sessionId = session_id }, .{ .id = 11 });
// Create second target — should close and detach the first
try ctx.processMessage(.{ .id = 12, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } });
// Should have sent detach events for the old session
try ctx.expectSentEvent("Inspector.detached", .{ .reason = "Render process gone." }, .{ .session_id = session_id });
try ctx.expectSentEvent("Target.detachedFromTarget", .{ .sessionId = session_id, .reason = "Render process gone." }, .{});
// New target should be created
try testing.expectEqual(true, bc.target_id != null);
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 12 });
}
}
test "cdp.target: detachFromTarget without session" { test "cdp.target: detachFromTarget without session" {
var ctx = try testing.context(); var ctx = try testing.context();
defer ctx.deinit(); defer ctx.deinit();

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const IS_DEBUG = @import("builtin").mode == .Debug;
pub fn toPageId(comptime id_type: enum { frame_id, loader_id }, input: []const u8) !u32 { pub fn toPageId(comptime id_type: enum { frame_id, loader_id }, input: []const u8) !u32 {
const err = switch (comptime id_type) { const err = switch (comptime id_type) {

View File

@@ -19,10 +19,6 @@
const std = @import("std"); const std = @import("std");
const json = std.json; const json = std.json;
const posix = std.posix; const posix = std.posix;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const Testing = @This();
const CDP = @import("CDP.zig"); const CDP = @import("CDP.zig");
const Server = @import("../Server.zig"); const Server = @import("../Server.zig");
@@ -172,13 +168,26 @@ const TestContext = struct {
index: ?usize = null, index: ?usize = null,
}; };
pub fn expectSent(self: *TestContext, expected: anytype, opts: SentOpts) !void { pub fn expectSent(self: *TestContext, expected: anytype, opts: SentOpts) !void {
const serialized = try json.Stringify.valueAlloc(base.arena_allocator, expected, .{ const expected_json = blk: {
.whitespace = .indent_2, // Zig makes this hard. When sendJSON is called, we're sending an anytype.
.emit_null_optional_fields = false, // We can't record that in an ArrayList(???), so we serialize it to JSON.
}); // Now, ideally, we could just take our expected structure, serialize it to
// json and check if the two are equal.
// Except serializing to JSON isn't deterministic.
// So we serialize the JSON then we deserialize to json.Value. And then we can
// compare our anytype expectation with the json.Value that we captured
const serialized = try json.Stringify.valueAlloc(base.arena_allocator, expected, .{
.whitespace = .indent_2,
.emit_null_optional_fields = false,
});
break :blk try std.json.parseFromSliceLeaky(json.Value, base.arena_allocator, serialized, .{});
};
for (0..5) |_| { for (0..5) |_| {
for (self.received.items, 0..) |received, i| { for (self.received.items, 0..) |received, i| {
if (try compareExpectedToSent(serialized, received) == false) { if (try base.isEqualJson(expected_json, received) == false) {
continue; continue;
} }
@@ -191,6 +200,15 @@ const TestContext = struct {
} }
return; return;
} }
if (self.cdp_) |*cdp__| {
if (cdp__.browser_context) |*bc| {
if (bc.session.page != null) {
var runner = try bc.session.runner(.{});
_ = try runner.tick(.{ .ms = 1000 });
}
}
}
std.Thread.sleep(5 * std.time.ns_per_ms); std.Thread.sleep(5 * std.time.ns_per_ms);
try self.read(); try self.read();
} }
@@ -303,17 +321,3 @@ pub fn context() !TestContext {
.socket = pair[0], .socket = pair[0],
}; };
} }
// Zig makes this hard. When sendJSON is called, we're sending an anytype.
// We can't record that in an ArrayList(???), so we serialize it to JSON.
// Now, ideally, we could just take our expected structure, serialize it to
// json and check if the two are equal.
// Except serializing to JSON isn't deterministic.
// So we serialize the JSON then we deserialize to json.Value. And then we can
// compare our anytype expectation with the json.Value that we captured
fn compareExpectedToSent(expected: []const u8, actual: json.Value) !bool {
const expected_value = try std.json.parseFromSlice(json.Value, std.testing.allocator, expected, .{});
defer expected_value.deinit();
return base.isEqualJson(expected_value.value, actual);
}

View File

@@ -49,9 +49,6 @@ const Opts = struct {
pub var opts = Opts{}; pub var opts = Opts{};
// synchronizes writes to the output
var out_lock: Thread.Mutex = .{};
// synchronizes access to last_log // synchronizes access to last_log
var last_log_lock: Thread.Mutex = .{}; var last_log_lock: Thread.Mutex = .{};

View File

@@ -1,6 +1,7 @@
const std = @import("std"); const std = @import("std");
pub const protocol = @import("mcp/protocol.zig"); pub const protocol = @import("mcp/protocol.zig");
pub const Version = protocol.Version;
pub const router = @import("mcp/router.zig"); pub const router = @import("mcp/router.zig");
pub const Server = @import("mcp/Server.zig"); pub const Server = @import("mcp/Server.zig");

View File

@@ -114,7 +114,7 @@ test "MCP.Server - Integration: synchronous smoke test" {
try router.processRequests(server, &in_reader); try router.processRequests(server, &in_reader);
try testing.expectJson(.{ .jsonrpc = "2.0", .id = 1 }, out_alloc.writer.buffered()); try testing.expectJson(.{ .jsonrpc = "2.0", .id = 1, .result = .{ .protocolVersion = "2024-11-05" } }, out_alloc.writer.buffered());
} }
test "MCP.Server - Integration: ping request returns an empty result" { test "MCP.Server - Integration: ping request returns an empty result" {

View File

@@ -1,5 +1,14 @@
const std = @import("std"); const std = @import("std");
pub const Version = enum {
@"2024-11-05",
@"2025-03-26",
@"2025-06-18",
@"2025-11-25",
pub const default: Version = .@"2024-11-05";
};
pub const Request = struct { pub const Request = struct {
jsonrpc: []const u8 = "2.0", jsonrpc: []const u8 = "2.0",
id: ?std.json.Value = null, id: ?std.json.Value = null,

View File

@@ -110,5 +110,3 @@ pub fn handleRead(server: *Server, arena: std.mem.Allocator, req: protocol.Reque
return server.sendError(req_id, .InternalError, "Failed to serialize resource content"); return server.sendError(req_id, .InternalError, "Failed to serialize resource content");
}; };
} }
const testing = @import("../testing.zig");

View File

@@ -1,5 +1,4 @@
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const protocol = @import("protocol.zig"); const protocol = @import("protocol.zig");
const resources = @import("resources.zig"); const resources = @import("resources.zig");
const Server = @import("Server.zig"); const Server = @import("Server.zig");
@@ -82,8 +81,12 @@ pub fn handleMessage(server: *Server, arena: std.mem.Allocator, msg: []const u8)
fn handleInitialize(server: *Server, req: protocol.Request) !void { fn handleInitialize(server: *Server, req: protocol.Request) !void {
const id = req.id orelse return; const id = req.id orelse return;
const result = protocol.InitializeResult{ const version: protocol.Version = switch (server.app.config.mode) {
.protocolVersion = "2025-11-25", .mcp => |opts| opts.version,
else => .default,
};
const result: protocol.InitializeResult = .{
.protocolVersion = @tagName(version),
.capabilities = .{ .capabilities = .{
.resources = .{}, .resources = .{},
.tools = .{}, .tools = .{},
@@ -122,7 +125,7 @@ test "MCP.router - handleMessage - synchronous unit tests" {
\\{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test-client","version":"1.0.0"}}} \\{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test-client","version":"1.0.0"}}}
); );
try testing.expectJson( try testing.expectJson(
\\{ "jsonrpc": "2.0", "id": 1, "result": { "capabilities": { "tools": {} } } } \\{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2024-11-05", "capabilities": { "tools": {} } } }
, out_alloc.writer.buffered()); , out_alloc.writer.buffered());
out_alloc.writer.end = 0; out_alloc.writer.end = 0;

View File

@@ -4,9 +4,7 @@ const lp = @import("lightpanda");
const log = lp.log; const log = lp.log;
const js = lp.js; const js = lp.js;
const Element = @import("../browser/webapi/Element.zig");
const DOMNode = @import("../browser/webapi/Node.zig"); const DOMNode = @import("../browser/webapi/Node.zig");
const Selector = @import("../browser/webapi/selector/Selector.zig");
const protocol = @import("protocol.zig"); const protocol = @import("protocol.zig");
const Server = @import("Server.zig"); const Server = @import("Server.zig");
const CDPNode = @import("../cdp/Node.zig"); const CDPNode = @import("../cdp/Node.zig");

View File

@@ -461,7 +461,7 @@ fn drainQueue(self: *Runtime) void {
self.releaseConnection(conn); self.releaseConnection(conn);
continue; continue;
}; };
libcurl.curl_multi_add_handle(multi, conn.easy) catch |err| { libcurl.curl_multi_add_handle(multi, conn._easy) catch |err| {
lp.log.err(.app, "curl multi add", .{ .err = err }); lp.log.err(.app, "curl multi add", .{ .err = err });
self.releaseConnection(conn); self.releaseConnection(conn);
}; };
@@ -565,7 +565,7 @@ pub fn getConnection(self: *Runtime) ?*net_http.Connection {
} }
pub fn releaseConnection(self: *Runtime, conn: *net_http.Connection) void { pub fn releaseConnection(self: *Runtime, conn: *net_http.Connection) void {
conn.reset() catch |err| { conn.reset(self.config, self.ca_blob) catch |err| {
lp.assert(false, "couldn't reset curl easy", .{ .err = err }); lp.assert(false, "couldn't reset curl easy", .{ .err = err });
}; };

View File

@@ -17,10 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin");
const posix = std.posix;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const Config = @import("../Config.zig"); const Config = @import("../Config.zig");
const libcurl = @import("../sys/libcurl.zig"); const libcurl = @import("../sys/libcurl.zig");
@@ -29,18 +25,12 @@ const log = @import("lightpanda").log;
const assert = @import("lightpanda").assert; const assert = @import("lightpanda").assert;
pub const ENABLE_DEBUG = false; pub const ENABLE_DEBUG = false;
const IS_DEBUG = builtin.mode == .Debug;
pub const Blob = libcurl.CurlBlob; pub const Blob = libcurl.CurlBlob;
pub const WaitFd = libcurl.CurlWaitFd; pub const WaitFd = libcurl.CurlWaitFd;
pub const writefunc_error = libcurl.curl_writefunc_error; pub const writefunc_error = libcurl.curl_writefunc_error;
const Error = libcurl.Error; const Error = libcurl.Error;
const ErrorMulti = libcurl.ErrorMulti;
const errorFromCode = libcurl.errorFromCode;
const errorMFromCode = libcurl.errorMFromCode;
const errorCheck = libcurl.errorCheck;
const errorMCheck = libcurl.errorMCheck;
pub fn curl_version() [*c]const u8 { pub fn curl_version() [*c]const u8 {
return libcurl.curl_version(); return libcurl.curl_version();
@@ -64,14 +54,13 @@ pub const Header = struct {
pub const Headers = struct { pub const Headers = struct {
headers: ?*libcurl.CurlSList, headers: ?*libcurl.CurlSList,
cookies: ?[*c]const u8,
pub fn init(user_agent: [:0]const u8) !Headers { pub fn init(user_agent: [:0]const u8) !Headers {
const header_list = libcurl.curl_slist_append(null, user_agent); const header_list = libcurl.curl_slist_append(null, user_agent);
if (header_list == null) { if (header_list == null) {
return error.OutOfMemory; return error.OutOfMemory;
} }
return .{ .headers = header_list, .cookies = null }; return .{ .headers = header_list };
} }
pub fn deinit(self: *const Headers) void { pub fn deinit(self: *const Headers) void {
@@ -102,20 +91,14 @@ pub const Headers = struct {
pub fn iterator(self: *Headers) Iterator { pub fn iterator(self: *Headers) Iterator {
return .{ return .{
.header = self.headers, .header = self.headers,
.cookies = self.cookies,
}; };
} }
const Iterator = struct { const Iterator = struct {
header: [*c]libcurl.CurlSList, header: [*c]libcurl.CurlSList,
cookies: ?[*c]const u8,
pub fn next(self: *Iterator) ?Header { pub fn next(self: *Iterator) ?Header {
const h = self.header orelse { const h = self.header orelse return null;
const cookies = self.cookies orelse return null;
self.cookies = null;
return .{ .name = "Cookie", .value = std.mem.span(@as([*:0]const u8, cookies)) };
};
self.header = h.*.next; self.header = h.*.next;
return parseHeader(std.mem.span(@as([*:0]const u8, @ptrCast(h.*.data)))); return parseHeader(std.mem.span(@as([*:0]const u8, @ptrCast(h.*.data))));
@@ -142,7 +125,7 @@ pub const HeaderIterator = union(enum) {
prev: ?*libcurl.CurlHeader = null, prev: ?*libcurl.CurlHeader = null,
pub fn next(self: *CurlHeaderIterator) ?Header { pub fn next(self: *CurlHeaderIterator) ?Header {
const h = libcurl.curl_easy_nextheader(self.conn.easy, .header, -1, self.prev) orelse return null; const h = libcurl.curl_easy_nextheader(self.conn._easy, .header, -1, self.prev) orelse return null;
self.prev = h; self.prev = h;
const header = h.*; const header = h.*;
@@ -174,33 +157,24 @@ const HeaderValue = struct {
}; };
pub const AuthChallenge = struct { pub const AuthChallenge = struct {
const Source = enum { server, proxy };
const Scheme = enum { basic, digest };
status: u16, status: u16,
source: ?enum { server, proxy }, source: ?Source,
scheme: ?enum { basic, digest }, scheme: ?Scheme,
realm: ?[]const u8, realm: ?[]const u8,
pub fn parse(status: u16, header: []const u8) !AuthChallenge { pub fn parse(status: u16, source: Source, value: []const u8) !AuthChallenge {
var ac: AuthChallenge = .{ var ac: AuthChallenge = .{
.status = status, .status = status,
.source = null, .source = source,
.realm = null, .realm = null,
.scheme = null, .scheme = null,
}; };
const sep = std.mem.indexOfPos(u8, header, 0, ": ") orelse return error.InvalidHeader; const pos = std.mem.indexOfPos(u8, std.mem.trim(u8, value, std.ascii.whitespace[0..]), 0, " ") orelse value.len;
const hname = header[0..sep]; const _scheme = value[0..pos];
const hvalue = header[sep + 2 ..];
if (std.ascii.eqlIgnoreCase("WWW-Authenticate", hname)) {
ac.source = .server;
} else if (std.ascii.eqlIgnoreCase("Proxy-Authenticate", hname)) {
ac.source = .proxy;
} else {
return error.InvalidAuthChallenge;
}
const pos = std.mem.indexOfPos(u8, std.mem.trim(u8, hvalue, std.ascii.whitespace[0..]), 0, " ") orelse hvalue.len;
const _scheme = hvalue[0..pos];
if (std.ascii.eqlIgnoreCase(_scheme, "basic")) { if (std.ascii.eqlIgnoreCase(_scheme, "basic")) {
ac.scheme = .basic; ac.scheme = .basic;
} else if (std.ascii.eqlIgnoreCase(_scheme, "digest")) { } else if (std.ascii.eqlIgnoreCase(_scheme, "digest")) {
@@ -236,77 +210,28 @@ pub const ResponseHead = struct {
}; };
pub const Connection = struct { pub const Connection = struct {
easy: *libcurl.Curl, _easy: *libcurl.Curl,
node: std.DoublyLinkedList.Node = .{}, node: std.DoublyLinkedList.Node = .{},
pub fn init( pub fn init(
ca_blob_: ?libcurl.CurlBlob, ca_blob: ?libcurl.CurlBlob,
config: *const Config, config: *const Config,
) !Connection { ) !Connection {
const easy = libcurl.curl_easy_init() orelse return error.FailedToInitializeEasy; const easy = libcurl.curl_easy_init() orelse return error.FailedToInitializeEasy;
errdefer libcurl.curl_easy_cleanup(easy);
// timeouts const self = Connection{ ._easy = easy };
try libcurl.curl_easy_setopt(easy, .timeout_ms, config.httpTimeout()); errdefer self.deinit();
try libcurl.curl_easy_setopt(easy, .connect_timeout_ms, config.httpConnectTimeout());
// redirect behavior try self.reset(config, ca_blob);
try libcurl.curl_easy_setopt(easy, .max_redirs, config.httpMaxRedirects()); return self;
try libcurl.curl_easy_setopt(easy, .follow_location, 2);
try libcurl.curl_easy_setopt(easy, .redir_protocols_str, "HTTP,HTTPS"); // remove FTP and FTPS from the default
// proxy
const http_proxy = config.httpProxy();
if (http_proxy) |proxy| {
try libcurl.curl_easy_setopt(easy, .proxy, proxy.ptr);
}
// tls
if (ca_blob_) |ca_blob| {
try libcurl.curl_easy_setopt(easy, .ca_info_blob, ca_blob);
if (http_proxy != null) {
try libcurl.curl_easy_setopt(easy, .proxy_ca_info_blob, ca_blob);
}
} else {
assert(config.tlsVerifyHost() == false, "Http.init tls_verify_host", .{});
try libcurl.curl_easy_setopt(easy, .ssl_verify_host, false);
try libcurl.curl_easy_setopt(easy, .ssl_verify_peer, false);
if (http_proxy != null) {
try libcurl.curl_easy_setopt(easy, .proxy_ssl_verify_host, false);
try libcurl.curl_easy_setopt(easy, .proxy_ssl_verify_peer, false);
}
}
// compression, don't remove this. CloudFront will send gzip content
// even if we don't support it, and then it won't be decompressed.
// empty string means: use whatever's available
try libcurl.curl_easy_setopt(easy, .accept_encoding, "");
// debug
if (comptime ENABLE_DEBUG) {
try libcurl.curl_easy_setopt(easy, .verbose, true);
// Sometimes the default debug output hides some useful data. You can
// uncomment the following line (BUT KEEP THE LIVE ABOVE AS-IS), to
// get more control over the data (specifically, the `CURLINFO_TEXT`
// can include useful data).
// try libcurl.curl_easy_setopt(easy, .debug_function, debugCallback);
}
return .{
.easy = easy,
};
} }
pub fn deinit(self: *const Connection) void { pub fn deinit(self: *const Connection) void {
libcurl.curl_easy_cleanup(self.easy); libcurl.curl_easy_cleanup(self._easy);
} }
pub fn setURL(self: *const Connection, url: [:0]const u8) !void { pub fn setURL(self: *const Connection, url: [:0]const u8) !void {
try libcurl.curl_easy_setopt(self.easy, .url, url.ptr); try libcurl.curl_easy_setopt(self._easy, .url, url.ptr);
} }
// a libcurl request has 2 methods. The first is the method that // a libcurl request has 2 methods. The first is the method that
@@ -329,7 +254,7 @@ pub const Connection = struct {
// can infer that based on the presence of the body, but we also reset it // can infer that based on the presence of the body, but we also reset it
// to be safe); // to be safe);
pub fn setMethod(self: *const Connection, method: Method) !void { pub fn setMethod(self: *const Connection, method: Method) !void {
const easy = self.easy; const easy = self._easy;
const m: [:0]const u8 = switch (method) { const m: [:0]const u8 = switch (method) {
.GET => "GET", .GET => "GET",
.POST => "POST", .POST => "POST",
@@ -344,56 +269,97 @@ pub const Connection = struct {
} }
pub fn setBody(self: *const Connection, body: []const u8) !void { pub fn setBody(self: *const Connection, body: []const u8) !void {
const easy = self.easy; const easy = self._easy;
try libcurl.curl_easy_setopt(easy, .post, true); try libcurl.curl_easy_setopt(easy, .post, true);
try libcurl.curl_easy_setopt(easy, .post_field_size, body.len); try libcurl.curl_easy_setopt(easy, .post_field_size, body.len);
try libcurl.curl_easy_setopt(easy, .copy_post_fields, body.ptr); try libcurl.curl_easy_setopt(easy, .copy_post_fields, body.ptr);
} }
pub fn setGetMode(self: *const Connection) !void { pub fn setGetMode(self: *const Connection) !void {
try libcurl.curl_easy_setopt(self.easy, .http_get, true); try libcurl.curl_easy_setopt(self._easy, .http_get, true);
} }
pub fn setHeaders(self: *const Connection, headers: *Headers) !void { pub fn setHeaders(self: *const Connection, headers: *Headers) !void {
try libcurl.curl_easy_setopt(self.easy, .http_header, headers.headers); try libcurl.curl_easy_setopt(self._easy, .http_header, headers.headers);
} }
pub fn setCookies(self: *const Connection, cookies: [*c]const u8) !void { pub fn setCookies(self: *const Connection, cookies: [*c]const u8) !void {
try libcurl.curl_easy_setopt(self.easy, .cookie, cookies); try libcurl.curl_easy_setopt(self._easy, .cookie, cookies);
} }
pub fn setPrivate(self: *const Connection, ptr: *anyopaque) !void { pub fn setPrivate(self: *const Connection, ptr: *anyopaque) !void {
try libcurl.curl_easy_setopt(self.easy, .private, ptr); try libcurl.curl_easy_setopt(self._easy, .private, ptr);
} }
pub fn setProxyCredentials(self: *const Connection, creds: [:0]const u8) !void { pub fn setProxyCredentials(self: *const Connection, creds: [:0]const u8) !void {
try libcurl.curl_easy_setopt(self.easy, .proxy_user_pwd, creds.ptr); try libcurl.curl_easy_setopt(self._easy, .proxy_user_pwd, creds.ptr);
} }
pub fn setCredentials(self: *const Connection, creds: [:0]const u8) !void { pub fn setCredentials(self: *const Connection, creds: [:0]const u8) !void {
try libcurl.curl_easy_setopt(self.easy, .user_pwd, creds.ptr); try libcurl.curl_easy_setopt(self._easy, .user_pwd, creds.ptr);
} }
pub fn setCallbacks( pub fn setCallbacks(
self: *const Connection, self: *Connection,
comptime header_cb: libcurl.CurlHeaderFunction,
comptime data_cb: libcurl.CurlWriteFunction, comptime data_cb: libcurl.CurlWriteFunction,
) !void { ) !void {
try libcurl.curl_easy_setopt(self.easy, .header_data, self.easy); try libcurl.curl_easy_setopt(self._easy, .write_data, self);
try libcurl.curl_easy_setopt(self.easy, .header_function, header_cb); try libcurl.curl_easy_setopt(self._easy, .write_function, data_cb);
try libcurl.curl_easy_setopt(self.easy, .write_data, self.easy);
try libcurl.curl_easy_setopt(self.easy, .write_function, data_cb);
} }
pub fn reset(self: *const Connection) !void { pub fn reset(
try libcurl.curl_easy_setopt(self.easy, .proxy, null); self: *const Connection,
try libcurl.curl_easy_setopt(self.easy, .http_header, null); config: *const Config,
ca_blob: ?libcurl.CurlBlob,
) !void {
libcurl.curl_easy_reset(self._easy);
try libcurl.curl_easy_setopt(self.easy, .header_data, null); // timeouts
try libcurl.curl_easy_setopt(self.easy, .header_function, null); try libcurl.curl_easy_setopt(self._easy, .timeout_ms, config.httpTimeout());
try libcurl.curl_easy_setopt(self._easy, .connect_timeout_ms, config.httpConnectTimeout());
try libcurl.curl_easy_setopt(self.easy, .write_data, null); // compression, don't remove this. CloudFront will send gzip content
try libcurl.curl_easy_setopt(self.easy, .write_function, discardBody); // even if we don't support it, and then it won't be decompressed.
// empty string means: use whatever's available
try libcurl.curl_easy_setopt(self._easy, .accept_encoding, "");
// proxy
const http_proxy = config.httpProxy();
if (http_proxy) |proxy| {
try libcurl.curl_easy_setopt(self._easy, .proxy, proxy.ptr);
} else {
try libcurl.curl_easy_setopt(self._easy, .proxy, null);
}
// tls
if (ca_blob) |ca| {
try libcurl.curl_easy_setopt(self._easy, .ca_info_blob, ca);
if (http_proxy != null) {
try libcurl.curl_easy_setopt(self._easy, .proxy_ca_info_blob, ca);
}
} else {
assert(config.tlsVerifyHost() == false, "Http.init tls_verify_host", .{});
try libcurl.curl_easy_setopt(self._easy, .ssl_verify_host, false);
try libcurl.curl_easy_setopt(self._easy, .ssl_verify_peer, false);
if (http_proxy != null) {
try libcurl.curl_easy_setopt(self._easy, .proxy_ssl_verify_host, false);
try libcurl.curl_easy_setopt(self._easy, .proxy_ssl_verify_peer, false);
}
}
// debug
if (comptime ENABLE_DEBUG) {
try libcurl.curl_easy_setopt(self._easy, .verbose, true);
// Sometimes the default debug output hides some useful data. You can
// uncomment the following line (BUT KEEP THE LIVE ABOVE AS-IS), to
// get more control over the data (specifically, the `CURLINFO_TEXT`
// can include useful data).
// try libcurl.curl_easy_setopt(easy, .debug_function, debugCallback);
}
} }
fn discardBody(_: [*]const u8, count: usize, len: usize, _: ?*anyopaque) usize { fn discardBody(_: [*]const u8, count: usize, len: usize, _: ?*anyopaque) usize {
@@ -401,27 +367,31 @@ pub const Connection = struct {
} }
pub fn setProxy(self: *const Connection, proxy: ?[:0]const u8) !void { pub fn setProxy(self: *const Connection, proxy: ?[:0]const u8) !void {
try libcurl.curl_easy_setopt(self.easy, .proxy, if (proxy) |p| p.ptr else null); try libcurl.curl_easy_setopt(self._easy, .proxy, if (proxy) |p| p.ptr else null);
}
pub fn setFollowLocation(self: *const Connection, follow: bool) !void {
try libcurl.curl_easy_setopt(self._easy, .follow_location, @as(c_long, if (follow) 2 else 0));
} }
pub fn setTlsVerify(self: *const Connection, verify: bool, use_proxy: bool) !void { pub fn setTlsVerify(self: *const Connection, verify: bool, use_proxy: bool) !void {
try libcurl.curl_easy_setopt(self.easy, .ssl_verify_host, verify); try libcurl.curl_easy_setopt(self._easy, .ssl_verify_host, verify);
try libcurl.curl_easy_setopt(self.easy, .ssl_verify_peer, verify); try libcurl.curl_easy_setopt(self._easy, .ssl_verify_peer, verify);
if (use_proxy) { if (use_proxy) {
try libcurl.curl_easy_setopt(self.easy, .proxy_ssl_verify_host, verify); try libcurl.curl_easy_setopt(self._easy, .proxy_ssl_verify_host, verify);
try libcurl.curl_easy_setopt(self.easy, .proxy_ssl_verify_peer, verify); try libcurl.curl_easy_setopt(self._easy, .proxy_ssl_verify_peer, verify);
} }
} }
pub fn getEffectiveUrl(self: *const Connection) ![*c]const u8 { pub fn getEffectiveUrl(self: *const Connection) ![*c]const u8 {
var url: [*c]u8 = undefined; var url: [*c]u8 = undefined;
try libcurl.curl_easy_getinfo(self.easy, .effective_url, &url); try libcurl.curl_easy_getinfo(self._easy, .effective_url, &url);
return url; return url;
} }
pub fn getResponseCode(self: *const Connection) !u16 { pub fn getResponseCode(self: *const Connection) !u16 {
var status: c_long = undefined; var status: c_long = undefined;
try libcurl.curl_easy_getinfo(self.easy, .response_code, &status); try libcurl.curl_easy_getinfo(self._easy, .response_code, &status);
if (status < 0 or status > std.math.maxInt(u16)) { if (status < 0 or status > std.math.maxInt(u16)) {
return 0; return 0;
} }
@@ -430,13 +400,13 @@ pub const Connection = struct {
pub fn getRedirectCount(self: *const Connection) !u32 { pub fn getRedirectCount(self: *const Connection) !u32 {
var count: c_long = undefined; var count: c_long = undefined;
try libcurl.curl_easy_getinfo(self.easy, .redirect_count, &count); try libcurl.curl_easy_getinfo(self._easy, .redirect_count, &count);
return @intCast(count); return @intCast(count);
} }
pub fn getResponseHeader(self: *const Connection, name: [:0]const u8, index: usize) ?HeaderValue { pub fn getResponseHeader(self: *const Connection, name: [:0]const u8, index: usize) ?HeaderValue {
var hdr: ?*libcurl.CurlHeader = null; var hdr: ?*libcurl.CurlHeader = null;
libcurl.curl_easy_header(self.easy, name, index, .header, -1, &hdr) catch |err| { libcurl.curl_easy_header(self._easy, name, index, .header, -1, &hdr) catch |err| {
// ErrorHeader includes OutOfMemory — rare but real errors from curl internals. // ErrorHeader includes OutOfMemory — rare but real errors from curl internals.
// Logged and returned as null since callers don't expect errors. // Logged and returned as null since callers don't expect errors.
log.err(.http, "get response header", .{ log.err(.http, "get response header", .{
@@ -454,7 +424,7 @@ pub const Connection = struct {
pub fn getPrivate(self: *const Connection) !*anyopaque { pub fn getPrivate(self: *const Connection) !*anyopaque {
var private: *anyopaque = undefined; var private: *anyopaque = undefined;
try libcurl.curl_easy_getinfo(self.easy, .private, &private); try libcurl.curl_easy_getinfo(self._easy, .private, &private);
return private; return private;
} }
@@ -471,12 +441,7 @@ pub const Connection = struct {
try self.secretHeaders(&header_list, http_headers); try self.secretHeaders(&header_list, http_headers);
try self.setHeaders(&header_list); try self.setHeaders(&header_list);
// Add cookies. try libcurl.curl_easy_perform(self._easy);
if (header_list.cookies) |cookies| {
try self.setCookies(cookies);
}
try libcurl.curl_easy_perform(self.easy);
return self.getResponseCode(); return self.getResponseCode();
} }
}; };
@@ -498,11 +463,11 @@ pub const Handles = struct {
} }
pub fn add(self: *Handles, conn: *const Connection) !void { pub fn add(self: *Handles, conn: *const Connection) !void {
try libcurl.curl_multi_add_handle(self.multi, conn.easy); try libcurl.curl_multi_add_handle(self.multi, conn._easy);
} }
pub fn remove(self: *Handles, conn: *const Connection) !void { pub fn remove(self: *Handles, conn: *const Connection) !void {
try libcurl.curl_multi_remove_handle(self.multi, conn.easy); try libcurl.curl_multi_remove_handle(self.multi, conn._easy);
} }
pub fn perform(self: *Handles) !c_int { pub fn perform(self: *Handles) !c_int {
@@ -525,7 +490,7 @@ pub const Handles = struct {
const msg = libcurl.curl_multi_info_read(self.multi, &messages_count) orelse return null; const msg = libcurl.curl_multi_info_read(self.multi, &messages_count) orelse return null;
return switch (msg.data) { return switch (msg.data) {
.done => |err| .{ .done => |err| .{
.conn = .{ .easy = msg.easy_handle }, .conn = .{ ._easy = msg.easy_handle },
.err = err, .err = err,
}, },
else => unreachable, else => unreachable,

View File

@@ -516,6 +516,10 @@ pub fn curl_easy_cleanup(easy: *Curl) void {
c.curl_easy_cleanup(easy); c.curl_easy_cleanup(easy);
} }
pub fn curl_easy_reset(easy: *Curl) void {
c.curl_easy_reset(easy);
}
pub fn curl_easy_perform(easy: *Curl) Error!void { pub fn curl_easy_perform(easy: *Curl) Error!void {
try errorCheck(c.curl_easy_perform(easy)); try errorCheck(c.curl_easy_perform(easy));
} }

View File

@@ -9,7 +9,6 @@ const App = @import("../App.zig");
const Config = @import("../Config.zig"); const Config = @import("../Config.zig");
const telemetry = @import("telemetry.zig"); const telemetry = @import("telemetry.zig");
const Runtime = @import("../network/Runtime.zig"); const Runtime = @import("../network/Runtime.zig");
const Connection = @import("../network/http.zig").Connection;
const URL = "https://telemetry.lightpanda.io"; const URL = "https://telemetry.lightpanda.io";
const BUFFER_SIZE = 1024; const BUFFER_SIZE = 1024;