Compare commits

..

20 Commits

Author SHA1 Message Date
Karl Seguin
078e13d8a4 Remove DOMContentLoaded and Loaded events from page_navigated
These were moved to their own distinct events, and should have been removed from
here.
2026-03-30 11:39:38 +08:00
Karl Seguin
cabb029bed Improve/Fix CDP navigation event order
These changes all better align with chrome's event ordering/timing.

There are two big changes. The first is that our internal page_navigated event,
which is kind of our heavy hitter, is sent once the header is received as
opposed to (much later) on document load. The main goal of this internal event
is to trigger the "Page.frameNavigated" CDP event which is meant to happen
once the URL is committed, which _is_ on header response.

To accommodate this earlier trigger, new explicit events for DOMContentLoaded
and load have be added.

This drastically changes the flow of events as things go from:
Start Page Navigation
Response Received
  Start Frame Navigation
  Response Received
  End Frame Navigation
End Page Navigation
context clear + reset
DOMContentLoaded
Loaded

TO:
Start Page Navigation
Response Received
End Page Navigation
context clear + reset
Start Frame Navigation
Response Received
End Frame Navigation
DOMContentLoaded
Loaded

So not only does it remove the nesting, but it ensures that the context are
cleared and reset once the main page's navigation is locked in, and before any
frame is created.
2026-03-30 11:02:53 +08: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
59 changed files with 729 additions and 867 deletions

View File

@@ -7,7 +7,7 @@ env:
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
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:
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:
- [ ] CORS [#2015](https://github.com/lightpanda-io/browser/issues/2015)
- [x] HTTP loader ([Libcurl](https://curl.se/libcurl/))
- [x] HTML parser ([html5ever](https://github.com/servo/html5ever))
- [x] DOM tree

View File

@@ -85,15 +85,6 @@ pub fn build(b: *Build) !void {
break :blk mod;
};
// Check compilation
const check = b.step("check", "Check if lightpanda compiles");
const check_lib = b.addLibrary(.{
.name = "lightpanda_check",
.root_module = lightpanda_module,
});
check.dependOn(&check_lib.step);
{
// browser
const exe = b.addExecutable(.{
@@ -112,12 +103,6 @@ pub fn build(b: *Build) !void {
});
b.installArtifact(exe);
const exe_check = b.addLibrary(.{
.name = "lightpanda_exe_check",
.root_module = exe.root_module,
});
check.dependOn(&exe_check.step);
const run_cmd = b.addRunArtifact(exe);
if (b.args) |args| {
run_cmd.addArgs(args);
@@ -147,12 +132,6 @@ pub fn build(b: *Build) !void {
});
b.installArtifact(exe);
const exe_check = b.addLibrary(.{
.name = "snapshot_creator_check",
.root_module = exe.root_module,
});
check.dependOn(&exe_check.step);
const run_cmd = b.addRunArtifact(exe);
if (b.args) |args| {
run_cmd.addArgs(args);
@@ -191,12 +170,6 @@ pub fn build(b: *Build) !void {
});
b.installArtifact(exe);
const exe_check = b.addLibrary(.{
.name = "legacy_test_check",
.root_module = exe.root_module,
});
check.dependOn(&exe_check.step);
const run_cmd = b.addRunArtifact(exe);
if (b.args) |args| {
run_cmd.addArgs(args);
@@ -746,39 +719,45 @@ fn buildCurl(
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 {
const version_string = b.option([]const u8, "version_string", "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 opt_version = b.option([]const u8, "version", "Override the version of this build");
const pre_version = b.option([]const u8, "pre_version", "Override the pre version of this build");
const pre = blk: {
if (pre_version) |pre| {
break :blk pre;
const version = if (opt_version) |v|
std.SemanticVersion.parse(v) catch blk: {
var fallback = lightpanda_version;
fallback.pre = v;
break :blk fallback;
}
else
lightpanda_version;
break :blk lightpanda_version.pre;
};
// 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;
// Only enrich versions that have a pre-release field and no explicit build metadata.
if (version.pre == null or version.build != null) return version;
// 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 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");
return .{
.major = lightpanda_version.major,
.minor = lightpanda_version.minor,
.patch = lightpanda_version.patch,
.pre = b.fmt("{s}.{s}", .{ pre.?, commit_count }),
.major = version.major,
.minor = version.minor,
.patch = version.patch,
.pre = b.fmt("{s}.{s}", .{ version.pre.?, commit_count }),
.build = commit_hash,
};
}

View File

@@ -18,6 +18,7 @@
const std = @import("std");
const builtin = @import("builtin");
const log = @import("log.zig");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
@@ -62,7 +63,7 @@ pub fn deinit(self: *ArenaPool) void {
var it = self._leak_track.iterator();
while (it.next()) |kv| {
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;
}
}
@@ -129,11 +130,11 @@ pub fn release(self: *ArenaPool, allocator: Allocator) void {
if (self._leak_track.getPtr(entry.debug)) |count| {
count.* -= 1;
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");
}
} 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");
}
}

View File

@@ -74,6 +74,8 @@ const EventListeners = struct {
page_network_idle: List = .{},
page_network_almost_idle: List = .{},
page_frame_created: List = .{},
page_dom_content_loaded: List = .{},
page_loaded: List = .{},
http_request_fail: List = .{},
http_request_start: List = .{},
http_request_intercept: List = .{},
@@ -91,6 +93,8 @@ const Events = union(enum) {
page_network_idle: *const PageNetworkIdle,
page_network_almost_idle: *const PageNetworkAlmostIdle,
page_frame_created: *const PageFrameCreated,
page_dom_content_loaded: *const PageDOMContentLoaded,
page_loaded: *const PageLoaded,
http_request_fail: *const RequestFail,
http_request_start: *const RequestStart,
http_request_intercept: *const RequestIntercept,
@@ -137,6 +141,18 @@ pub const PageFrameCreated = struct {
timestamp: u64,
};
pub const PageDOMContentLoaded = struct {
req_id: u32,
frame_id: u32,
timestamp: u64,
};
pub const PageLoaded = struct {
req_id: u32,
frame_id: u32,
timestamp: u64,
};
pub const RequestStart = struct {
transfer: *Transfer,
};

View File

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

View File

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

View File

@@ -425,7 +425,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
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
// and default actions execute (unless prevented)
@@ -820,7 +820,7 @@ const ActivationState = struct {
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) {
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 App = @import("../App.zig");
const String = @import("../string.zig").String;
const Mime = @import("Mime.zig");
@@ -43,7 +42,6 @@ const URL = @import("URL.zig");
const Blob = @import("webapi/Blob.zig");
const Node = @import("webapi/Node.zig");
const Event = @import("webapi/Event.zig");
const EventTarget = @import("webapi/EventTarget.zig");
const CData = @import("webapi/CData.zig");
const Element = @import("webapi/Element.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 IntersectionObserver = @import("webapi/IntersectionObserver.zig");
const CustomElementDefinition = @import("webapi/CustomElementDefinition.zig");
const storage = @import("webapi/storage/storage.zig");
const PageTransitionEvent = @import("webapi/event/PageTransitionEvent.zig");
const SubmitEvent = @import("webapi/event/SubmitEvent.zig");
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 HttpClient = @import("HttpClient.zig");
const ArenaPool = App.ArenaPool;
const timestamp = @import("../datetime.zig").timestamp;
const milliTimestamp = @import("../datetime.zig").milliTimestamp;
@@ -385,12 +381,9 @@ pub fn getTitle(self: *Page) !?[]const u8 {
return null;
}
// Add comon headers for a request:
// * cookies
// Add common headers for a request:
// * referer
pub fn headersForRequest(self: *Page, temp: Allocator, url: [:0]const u8, headers: *HttpClient.Headers) !void {
try self.requestCookie(.{}).headersForRequest(temp, url, headers);
pub fn headersForRequest(self: *Page, headers: *HttpClient.Headers) !void {
// Build the referer
const referer = blk: {
if (self.referer_header == null) {
@@ -494,7 +487,6 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
return error.InjectBlankFailed;
};
}
self.documentIsComplete();
session.notification.dispatch(.page_navigate, &.{
.frame_id = self._frame_id,
@@ -526,6 +518,8 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
// force next request id manually b/c we won't create a real req.
_ = session.browser.http_client.incrReqId();
self.documentIsComplete();
return;
}
@@ -545,8 +539,6 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
if (opts.header) |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.
// It ensures the event page_navigated is not dispatched before this one.
session.notification.dispatch(.page_navigate, &.{
@@ -573,6 +565,7 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
.headers = headers,
.body = opts.body,
.cookie_jar = &session.cookie_jar,
.cookie_origin = self.url,
.resource_type = .document,
.notification = self._session.notification,
.header_callback = pageHeaderDoneCallback,
@@ -746,6 +739,12 @@ pub fn _documentIsLoaded(self: *Page) !void {
self.document.asEventTarget(),
event,
);
self._session.notification.dispatch(.page_dom_content_loaded, &.{
.frame_id = self._frame_id,
.req_id = self._req_id,
.timestamp = timestamp(.monotonic),
});
}
pub fn scriptsCompletedLoading(self: *Page) void {
@@ -804,19 +803,6 @@ pub fn documentIsComplete(self: *Page) void {
self._documentIsComplete() catch |err| {
log.err(.page, "document is complete", .{ .err = err, .type = self._type, .url = self.url });
};
if (self._navigated_options) |no| {
// _navigated_options will be null in special short-circuit cases, like
// "navigating" to about:blank, in which case this notification has
// already been sent
self._session.notification.dispatch(.page_navigated, &.{
.frame_id = self._frame_id,
.req_id = self._req_id,
.opts = no,
.url = self.url,
.timestamp = timestamp(.monotonic),
});
}
}
fn _documentIsComplete(self: *Page) !void {
@@ -835,6 +821,12 @@ fn _documentIsComplete(self: *Page) !void {
try self._event_manager.dispatchDirect(window_target, event, self.window._on_load, .{ .inject_target = false, .context = "page load" });
}
self._session.notification.dispatch(.page_loaded, &.{
.frame_id = self._frame_id,
.req_id = self._req_id,
.timestamp = timestamp(.monotonic),
});
if (self._event_manager.hasDirectListeners(window_target, "pageshow", self.window._on_pageshow)) {
const pageshow_event = (try PageTransitionEvent.initTrusted(comptime .wrap("pageshow"), .{}, self)).asEvent();
try self._event_manager.dispatchDirect(window_target, pageshow_event, self.window._on_pageshow, .{ .context = "page show" });
@@ -887,6 +879,19 @@ fn pageHeaderDoneCallback(transfer: *HttpClient.Transfer) !bool {
});
}
if (self._navigated_options) |no| {
// _navigated_options will be null in special short-circuit cases, like
// "navigating" to about:blank, in which case this notification has
// already been sent
self._session.notification.dispatch(.page_navigated, &.{
.frame_id = self._frame_id,
.req_id = self._req_id,
.opts = no,
.url = self.url,
.timestamp = timestamp(.monotonic),
});
}
return true;
}
@@ -1036,6 +1041,7 @@ fn pageDoneCallback(ctx: *anyopaque) !void {
});
parser.parse(html);
self._parse_state = .complete;
self.documentIsComplete();
},
else => unreachable,
@@ -3554,19 +3560,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(
.unsigned,
@bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0

View File

@@ -21,12 +21,9 @@ const lp = @import("lightpanda");
const builtin = @import("builtin");
const log = @import("../log.zig");
const App = @import("../App.zig");
const Page = @import("Page.zig");
const Session = @import("Session.zig");
const Browser = @import("Browser.zig");
const Factory = @import("Factory.zig");
const HttpClient = @import("HttpClient.zig");
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 URL = @import("URL.zig");
const Page = @import("Page.zig");
const Browser = @import("Browser.zig");
const Element = @import("webapi/Element.zig");
const Allocator = std.mem.Allocator;
const ArrayList = std.ArrayList;
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();
try self.page.headersForRequest(arena, url, &headers);
try self.page.headersForRequest(&headers);
return headers;
}
@@ -280,9 +278,10 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
.ctx = script,
.method = .GET,
.frame_id = page._frame_id,
.headers = try self.getHeaders(arena, url),
.headers = try self.getHeaders(),
.blocking = is_blocking,
.cookie_jar = &page._session.cookie_jar,
.cookie_origin = page.url,
.resource_type = .script,
.notification = page._session.notification,
.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,
.method = .GET,
.frame_id = page._frame_id,
.headers = try self.getHeaders(arena, url),
.headers = try self.getHeaders(),
.cookie_jar = &page._session.cookie_jar,
.cookie_origin = page.url,
.resource_type = .script,
.notification = page._session.notification,
.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,
.method = .GET,
.frame_id = page._frame_id,
.headers = try self.getHeaders(arena, url),
.headers = try self.getHeaders(),
.ctx = script,
.resource_type = .script,
.cookie_jar = &page._session.cookie_jar,
.cookie_origin = page.url,
.notification = page._session.notification,
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
.header_callback = Script.headerCallback,
@@ -654,7 +655,6 @@ pub const Script = struct {
debug_transfer_aborted: bool = false,
debug_transfer_bytes_received: usize = 0,
debug_transfer_notified_fail: bool = false,
debug_transfer_redirecting: bool = false,
debug_transfer_intercept_state: u8 = 0,
debug_transfer_auth_challenge: bool = false,
debug_transfer_easy_id: usize = 0,
@@ -730,7 +730,6 @@ pub const Script = struct {
.a3 = self.debug_transfer_aborted,
.a4 = self.debug_transfer_bytes_received,
.a5 = self.debug_transfer_notified_fail,
.a6 = self.debug_transfer_redirecting,
.a7 = self.debug_transfer_intercept_state,
.a8 = self.debug_transfer_auth_challenge,
.a9 = self.debug_transfer_easy_id,
@@ -739,10 +738,9 @@ pub const Script = struct {
.b3 = transfer.aborted,
.b4 = transfer.bytes_received,
.b5 = transfer._notified_fail,
.b6 = transfer._redirecting,
.b7 = @intFromEnum(transfer._intercept_state),
.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.debug_transfer_id = transfer.id;
@@ -750,10 +748,9 @@ pub const Script = struct {
self.debug_transfer_aborted = transfer.aborted;
self.debug_transfer_bytes_received = transfer.bytes_received;
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_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 });

View File

@@ -128,7 +128,7 @@ fn _constructor(self: *Caller, func: anytype, info: FunctionCallbackInfo) !void
const new_this_handle = info.getThis();
var this = js.Object{ .local = local, .handle = new_this_handle };
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);
} else {
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 Env = @import("Env.zig");
const bridge = @import("bridge.zig");
const Origin = @import("Origin.zig");
const Scheduler = @import("Scheduler.zig");

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Page = @import("../Page.zig");
const Session = @import("../Session.zig");
const log = @import("../../log.zig");
const string = @import("../../string.zig");
@@ -33,7 +32,6 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
const v8 = js.v8;
const CallOpts = Caller.CallOpts;
const Allocator = std.mem.Allocator;
// 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,10 +5,6 @@ const URL = @import("../../../URL.zig");
const Node = @import("../../Node.zig");
const Element = @import("../../Element.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();
_proto: *HtmlElement,

View File

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

View File

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

View File

@@ -24,7 +24,6 @@ const Session = @import("../../Session.zig");
const js = @import("../../js/js.zig");
const Event = @import("../Event.zig");
const UIEvent = @import("UIEvent.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 Session = @import("../../Session.zig");
const Event = @import("../Event.zig");
const Allocator = std.mem.Allocator;
const PromiseRejectionEvent = @This();

View File

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

View File

@@ -18,7 +18,6 @@
const std = @import("std");
const URL = @import("../URL.zig");
const EventTarget = @import("../EventTarget.zig");
const NavigationState = @import("root.zig").NavigationState;
const Page = @import("../../Page.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| {
try h.populateHttpHeader(page.call_arena, &headers);
}
try page.headersForRequest(page.arena, request._url, &headers);
try page.headersForRequest(&headers);
if (comptime IS_DEBUG) {
log.debug(.http, "fetch", .{ .url = request._url });
@@ -95,6 +95,7 @@ pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise {
.headers = headers,
.resource_type = .fetch,
.cookie_jar = &page._session.cookie_jar,
.cookie_origin = page.url,
.notification = page._session.notification,
.start_callback = httpStartCallback,
.header_callback = httpHeaderDoneCallback,

View File

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

View File

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

View File

@@ -25,7 +25,6 @@ const json = std.json;
const Incrementing = @import("id.zig").Incrementing;
const log = @import("../log.zig");
const App = @import("../App.zig");
const Notification = @import("../Notification.zig");
const Client = @import("../Server.zig").Client;
@@ -35,7 +34,6 @@ const Browser = @import("../browser/Browser.zig");
const Session = @import("../browser/Session.zig");
const Page = @import("../browser/Page.zig");
const Mime = @import("../browser/Mime.zig");
const HttpClient = @import("../browser/HttpClient.zig");
const InterceptState = @import("domains/fetch.zig").InterceptState;
@@ -429,6 +427,8 @@ pub fn BrowserContext(comptime CDP_T: type) type {
try notification.register(.page_navigate, self, onPageNavigate);
try notification.register(.page_navigated, self, onPageNavigated);
try notification.register(.page_frame_created, self, onPageFrameCreated);
try notification.register(.page_dom_content_loaded, self, onPageDOMContentLoaded);
try notification.register(.page_loaded, self, onPageLoaded);
}
pub fn deinit(self: *Self) void {
@@ -470,8 +470,8 @@ pub fn BrowserContext(comptime CDP_T: type) type {
if (self.http_proxy_changed) {
// has to be called after browser.closeSession, since it won't
// work if there are active connections.
browser.http_client.restoreOriginalProxy() catch |err| {
log.warn(.http, "restoreOriginalProxy", .{ .err = err });
browser.http_client.changeProxy(null) catch |err| {
log.warn(.http, "changeProxy", .{ .err = err });
};
}
self.intercept_state.deinit();
@@ -604,6 +604,16 @@ pub fn BrowserContext(comptime CDP_T: type) type {
return @import("domains/page.zig").pageFrameCreated(self, msg);
}
pub fn onPageDOMContentLoaded(ctx: *anyopaque, msg: *const Notification.PageDOMContentLoaded) !void {
const self: *Self = @ptrCast(@alignCast(ctx));
return @import("domains/page.zig").pageDOMContentLoaded(self, msg);
}
pub fn onPageLoaded(ctx: *anyopaque, msg: *const Notification.PageLoaded) !void {
const self: *Self = @ptrCast(@alignCast(ctx));
return @import("domains/page.zig").pageLoaded(self, msg);
}
pub fn onPageNetworkIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkIdle) !void {
const self: *Self = @ptrCast(@alignCast(ctx));
return @import("domains/page.zig").pageNetworkIdle(self, msg);

View File

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

View File

@@ -351,6 +351,10 @@ pub const TransferAsRequestWriter = struct {
try jws.objectField(hdr.name);
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();

View File

@@ -385,7 +385,6 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
// things, but no session.
const session_id = bc.session_id orelse return;
const timestamp = event.timestamp;
const frame_id = &id.toFrameId(event.frame_id);
const loader_id = &id.toLoaderId(event.req_id);
@@ -437,9 +436,9 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
// When we actually recreated the context we should have the inspector send
// this event, see: resetContextGroup Sending this event will tell the
// client that the context ids they had are invalid and the context shouls
// be dropped The client will expect us to send new contextCreated events,
// this event, see: resetContextGroup. Sending this event will tell the
// client that the context ids they had are invalid and the context should
// be dropped. The client will expect us to send new contextCreated events,
// such that the client has new id's for the active contexts.
// Only send executionContextsCleared for main frame navigations. For child
// frames (iframes), clearing all contexts would destroy the main frame's
@@ -449,6 +448,18 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
try cdp.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id });
}
// frameNavigated event
try cdp.sendEvent("Page.frameNavigated", .{
.type = "Navigation",
.frame = Frame{
.id = frame_id,
.url = event.url,
.loaderId = loader_id,
.securityOrigin = bc.security_origin,
.secureContextType = bc.secure_context_type,
},
}, .{ .session_id = session_id });
{
const aux_data = try std.fmt.allocPrint(arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\",\"loaderId\":\"{s}\"}}", .{ frame_id, loader_id });
@@ -498,18 +509,22 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
// chromedp client expects to receive the events is this order.
// see https://github.com/chromedp/chromedp/issues/1558
try cdp.sendEvent("DOM.documentUpdated", null, .{ .session_id = session_id });
}
pub fn pageDOMContentLoaded(bc: anytype, event: *const Notification.PageDOMContentLoaded) !void {
const session_id = bc.session_id orelse return;
const timestamp = event.timestamp;
var cdp = bc.cdp;
// domContentEventFired event
// TODO: partially hard coded
try cdp.sendEvent(
"Page.domContentEventFired",
.{ .timestamp = timestamp },
.{ .session_id = session_id },
);
// lifecycle DOMContentLoaded event
// TODO: partially hard coded
if (bc.page_life_cycle_events) {
const frame_id = &id.toFrameId(event.frame_id);
const loader_id = &id.toLoaderId(event.req_id);
try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{
.timestamp = timestamp,
.name = "DOMContentLoaded",
@@ -517,16 +532,23 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
.loaderId = loader_id,
}, .{ .session_id = session_id });
}
}
pub fn pageLoaded(bc: anytype, event: *const Notification.PageLoaded) !void {
const session_id = bc.session_id orelse return;
const timestamp = event.timestamp;
var cdp = bc.cdp;
const frame_id = &id.toFrameId(event.frame_id);
// loadEventFired event
try cdp.sendEvent(
"Page.loadEventFired",
.{ .timestamp = timestamp },
.{ .session_id = session_id },
);
// lifecycle DOMContentLoaded event
if (bc.page_life_cycle_events) {
const loader_id = &id.toLoaderId(event.req_id);
try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{
.timestamp = timestamp,
.name = "load",
@@ -535,7 +557,6 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
}, .{ .session_id = session_id });
}
// frameStoppedLoading
return cdp.sendEvent("Page.frameStoppedLoading", .{
.frameId = frame_id,
}, .{ .session_id = session_id });

View File

@@ -24,9 +24,6 @@ const log = @import("../../log.zig");
const URL = @import("../../browser/URL.zig");
const js = @import("../../browser/js/js.zig");
// TODO: hard coded IDs
const LOADER_ID = "LOADERID42AA389647D702B4D805F49A";
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
getTargets,

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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 {
const err = switch (comptime id_type) {

View File

@@ -19,10 +19,6 @@
const std = @import("std");
const json = std.json;
const posix = std.posix;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const Testing = @This();
const CDP = @import("CDP.zig");
const Server = @import("../Server.zig");

View File

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

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");
};
}
const testing = @import("../testing.zig");

View File

@@ -1,5 +1,4 @@
const std = @import("std");
const lp = @import("lightpanda");
const protocol = @import("protocol.zig");
const resources = @import("resources.zig");
const Server = @import("Server.zig");

View File

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

View File

@@ -461,7 +461,7 @@ fn drainQueue(self: *Runtime) void {
self.releaseConnection(conn);
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 });
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 {
conn.reset() catch |err| {
conn.reset(self.config, self.ca_blob) catch |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/>.
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 libcurl = @import("../sys/libcurl.zig");
@@ -29,18 +25,12 @@ const log = @import("lightpanda").log;
const assert = @import("lightpanda").assert;
pub const ENABLE_DEBUG = false;
const IS_DEBUG = builtin.mode == .Debug;
pub const Blob = libcurl.CurlBlob;
pub const WaitFd = libcurl.CurlWaitFd;
pub const writefunc_error = libcurl.curl_writefunc_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 {
return libcurl.curl_version();
@@ -64,14 +54,13 @@ pub const Header = struct {
pub const Headers = struct {
headers: ?*libcurl.CurlSList,
cookies: ?[*c]const u8,
pub fn init(user_agent: [:0]const u8) !Headers {
const header_list = libcurl.curl_slist_append(null, user_agent);
if (header_list == null) {
return error.OutOfMemory;
}
return .{ .headers = header_list, .cookies = null };
return .{ .headers = header_list };
}
pub fn deinit(self: *const Headers) void {
@@ -102,20 +91,14 @@ pub const Headers = struct {
pub fn iterator(self: *Headers) Iterator {
return .{
.header = self.headers,
.cookies = self.cookies,
};
}
const Iterator = struct {
header: [*c]libcurl.CurlSList,
cookies: ?[*c]const u8,
pub fn next(self: *Iterator) ?Header {
const h = self.header orelse {
const cookies = self.cookies orelse return null;
self.cookies = null;
return .{ .name = "Cookie", .value = std.mem.span(@as([*:0]const u8, cookies)) };
};
const h = self.header orelse return null;
self.header = h.*.next;
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,
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;
const header = h.*;
@@ -174,33 +157,24 @@ const HeaderValue = struct {
};
pub const AuthChallenge = struct {
const Source = enum { server, proxy };
const Scheme = enum { basic, digest };
status: u16,
source: ?enum { server, proxy },
scheme: ?enum { basic, digest },
source: ?Source,
scheme: ?Scheme,
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 = .{
.status = status,
.source = null,
.source = source,
.realm = null,
.scheme = null,
};
const sep = std.mem.indexOfPos(u8, header, 0, ": ") orelse return error.InvalidHeader;
const hname = header[0..sep];
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];
const pos = std.mem.indexOfPos(u8, std.mem.trim(u8, value, std.ascii.whitespace[0..]), 0, " ") orelse value.len;
const _scheme = value[0..pos];
if (std.ascii.eqlIgnoreCase(_scheme, "basic")) {
ac.scheme = .basic;
} else if (std.ascii.eqlIgnoreCase(_scheme, "digest")) {
@@ -236,77 +210,28 @@ pub const ResponseHead = struct {
};
pub const Connection = struct {
easy: *libcurl.Curl,
_easy: *libcurl.Curl,
node: std.DoublyLinkedList.Node = .{},
pub fn init(
ca_blob_: ?libcurl.CurlBlob,
ca_blob: ?libcurl.CurlBlob,
config: *const Config,
) !Connection {
const easy = libcurl.curl_easy_init() orelse return error.FailedToInitializeEasy;
errdefer libcurl.curl_easy_cleanup(easy);
// timeouts
try libcurl.curl_easy_setopt(easy, .timeout_ms, config.httpTimeout());
try libcurl.curl_easy_setopt(easy, .connect_timeout_ms, config.httpConnectTimeout());
const self = Connection{ ._easy = easy };
errdefer self.deinit();
// redirect behavior
try libcurl.curl_easy_setopt(easy, .max_redirs, config.httpMaxRedirects());
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,
};
try self.reset(config, ca_blob);
return self;
}
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 {
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
@@ -329,7 +254,7 @@ pub const Connection = struct {
// can infer that based on the presence of the body, but we also reset it
// to be safe);
pub fn setMethod(self: *const Connection, method: Method) !void {
const easy = self.easy;
const easy = self._easy;
const m: [:0]const u8 = switch (method) {
.GET => "GET",
.POST => "POST",
@@ -344,56 +269,97 @@ pub const Connection = struct {
}
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_field_size, body.len);
try libcurl.curl_easy_setopt(easy, .copy_post_fields, body.ptr);
}
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 {
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 {
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 {
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 {
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 {
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(
self: *const Connection,
comptime header_cb: libcurl.CurlHeaderFunction,
self: *Connection,
comptime data_cb: libcurl.CurlWriteFunction,
) !void {
try libcurl.curl_easy_setopt(self.easy, .header_data, self.easy);
try libcurl.curl_easy_setopt(self.easy, .header_function, header_cb);
try libcurl.curl_easy_setopt(self.easy, .write_data, self.easy);
try libcurl.curl_easy_setopt(self.easy, .write_function, data_cb);
try libcurl.curl_easy_setopt(self._easy, .write_data, self);
try libcurl.curl_easy_setopt(self._easy, .write_function, data_cb);
}
pub fn reset(self: *const Connection) !void {
try libcurl.curl_easy_setopt(self.easy, .proxy, null);
try libcurl.curl_easy_setopt(self.easy, .http_header, null);
pub fn reset(
self: *const Connection,
config: *const Config,
ca_blob: ?libcurl.CurlBlob,
) !void {
libcurl.curl_easy_reset(self._easy);
try libcurl.curl_easy_setopt(self.easy, .header_data, null);
try libcurl.curl_easy_setopt(self.easy, .header_function, null);
// timeouts
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);
try libcurl.curl_easy_setopt(self.easy, .write_function, discardBody);
// 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(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 {
@@ -401,27 +367,31 @@ pub const Connection = struct {
}
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 {
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_host, verify);
try libcurl.curl_easy_setopt(self._easy, .ssl_verify_peer, verify);
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_peer, 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);
}
}
pub fn getEffectiveUrl(self: *const Connection) ![*c]const u8 {
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;
}
pub fn getResponseCode(self: *const Connection) !u16 {
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)) {
return 0;
}
@@ -430,13 +400,13 @@ pub const Connection = struct {
pub fn getRedirectCount(self: *const Connection) !u32 {
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);
}
pub fn getResponseHeader(self: *const Connection, name: [:0]const u8, index: usize) ?HeaderValue {
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.
// Logged and returned as null since callers don't expect errors.
log.err(.http, "get response header", .{
@@ -454,7 +424,7 @@ pub const Connection = struct {
pub fn getPrivate(self: *const Connection) !*anyopaque {
var private: *anyopaque = undefined;
try libcurl.curl_easy_getinfo(self.easy, .private, &private);
try libcurl.curl_easy_getinfo(self._easy, .private, &private);
return private;
}
@@ -471,12 +441,7 @@ pub const Connection = struct {
try self.secretHeaders(&header_list, http_headers);
try self.setHeaders(&header_list);
// Add cookies.
if (header_list.cookies) |cookies| {
try self.setCookies(cookies);
}
try libcurl.curl_easy_perform(self.easy);
try libcurl.curl_easy_perform(self._easy);
return self.getResponseCode();
}
};
@@ -498,11 +463,11 @@ pub const Handles = struct {
}
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 {
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 {
@@ -525,7 +490,7 @@ pub const Handles = struct {
const msg = libcurl.curl_multi_info_read(self.multi, &messages_count) orelse return null;
return switch (msg.data) {
.done => |err| .{
.conn = .{ .easy = msg.easy_handle },
.conn = .{ ._easy = msg.easy_handle },
.err = err,
},
else => unreachable,

View File

@@ -516,6 +516,10 @@ pub fn curl_easy_cleanup(easy: *Curl) void {
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 {
try errorCheck(c.curl_easy_perform(easy));
}

View File

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