Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 40 additions & 16 deletions src/cdp/cdp.zig
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,15 @@ pub fn BrowserContext(comptime CDP_T: type) type {

intercept_state: InterceptState,

// When network is enabled, we'll capture the transfer.id -> body
// This is awfully memory intensive, but our underlying http client and
// its users (script manager and page) correctly do not hold the body
// memory longer than they have to. In fact, the main request is only
// ever streamed. So if CDP is the only thing that needs bodies in
// memory for an arbitrary amount of time, then that's where we're going
// to store the,
captured_responses: std.AutoHashMapUnmanaged(usize, std.ArrayListUnmanaged(u8)),

const Self = @This();

fn init(self: *Self, id: []const u8, cdp: *CDP_T) !void {
Expand Down Expand Up @@ -374,6 +383,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
.inspector = inspector,
.notification_arena = cdp.notification_arena.allocator(),
.intercept_state = try InterceptState.init(allocator),
.captured_responses = .empty,
};
self.node_search_list = Node.Search.List.init(allocator, &self.node_registry);
errdefer self.deinit();
Expand Down Expand Up @@ -454,15 +464,17 @@ pub fn BrowserContext(comptime CDP_T: type) type {
pub fn networkEnable(self: *Self) !void {
try self.cdp.browser.notification.register(.http_request_fail, self, onHttpRequestFail);
try self.cdp.browser.notification.register(.http_request_start, self, onHttpRequestStart);
try self.cdp.browser.notification.register(.http_headers_done, self, onHttpHeadersDone);
try self.cdp.browser.notification.register(.http_request_done, self, onHttpRequestDone);
try self.cdp.browser.notification.register(.http_response_data, self, onHttpResponseData);
try self.cdp.browser.notification.register(.http_response_header_done, self, onHttpResponseHeadersDone);
}

pub fn networkDisable(self: *Self) void {
self.cdp.browser.notification.unregister(.http_request_fail, self);
self.cdp.browser.notification.unregister(.http_request_start, self);
self.cdp.browser.notification.unregister(.http_headers_done, self);
self.cdp.browser.notification.unregister(.http_request_done, self);
self.cdp.browser.notification.unregister(.http_response_data, self);
self.cdp.browser.notification.unregister(.http_response_header_done, self);
}

pub fn fetchEnable(self: *Self) !void {
Expand All @@ -483,45 +495,57 @@ pub fn BrowserContext(comptime CDP_T: type) type {
return @import("domains/page.zig").pageCreated(self, page);
}

pub fn onPageNavigate(ctx: *anyopaque, data: *const Notification.PageNavigate) !void {
pub fn onPageNavigate(ctx: *anyopaque, msg: *const Notification.PageNavigate) !void {
const self: *Self = @alignCast(@ptrCast(ctx));
defer self.resetNotificationArena();
return @import("domains/page.zig").pageNavigate(self.notification_arena, self, data);
return @import("domains/page.zig").pageNavigate(self.notification_arena, self, msg);
}

pub fn onPageNavigated(ctx: *anyopaque, data: *const Notification.PageNavigated) !void {
pub fn onPageNavigated(ctx: *anyopaque, msg: *const Notification.PageNavigated) !void {
const self: *Self = @alignCast(@ptrCast(ctx));
return @import("domains/page.zig").pageNavigated(self, data);
return @import("domains/page.zig").pageNavigated(self, msg);
}

pub fn onHttpRequestStart(ctx: *anyopaque, data: *const Notification.RequestStart) !void {
pub fn onHttpRequestStart(ctx: *anyopaque, msg: *const Notification.RequestStart) !void {
const self: *Self = @alignCast(@ptrCast(ctx));
defer self.resetNotificationArena();
try @import("domains/network.zig").httpRequestStart(self.notification_arena, self, data);
try @import("domains/network.zig").httpRequestStart(self.notification_arena, self, msg);
}

pub fn onHttpRequestIntercept(ctx: *anyopaque, data: *const Notification.RequestIntercept) !void {
pub fn onHttpRequestIntercept(ctx: *anyopaque, msg: *const Notification.RequestIntercept) !void {
const self: *Self = @alignCast(@ptrCast(ctx));
defer self.resetNotificationArena();
try @import("domains/fetch.zig").requestIntercept(self.notification_arena, self, data);
try @import("domains/fetch.zig").requestIntercept(self.notification_arena, self, msg);
}

pub fn onHttpRequestFail(ctx: *anyopaque, data: *const Notification.RequestFail) !void {
pub fn onHttpRequestFail(ctx: *anyopaque, msg: *const Notification.RequestFail) !void {
const self: *Self = @alignCast(@ptrCast(ctx));
defer self.resetNotificationArena();
return @import("domains/network.zig").httpRequestFail(self.notification_arena, self, data);
return @import("domains/network.zig").httpRequestFail(self.notification_arena, self, msg);
}

pub fn onHttpHeadersDone(ctx: *anyopaque, data: *const Notification.ResponseHeadersDone) !void {
pub fn onHttpResponseHeadersDone(ctx: *anyopaque, msg: *const Notification.ResponseHeaderDone) !void {
const self: *Self = @alignCast(@ptrCast(ctx));
defer self.resetNotificationArena();
return @import("domains/network.zig").httpHeadersDone(self.notification_arena, self, data);
return @import("domains/network.zig").httpResponseHeaderDone(self.notification_arena, self, msg);
}

pub fn onHttpRequestDone(ctx: *anyopaque, data: *const Notification.RequestDone) !void {
pub fn onHttpRequestDone(ctx: *anyopaque, msg: *const Notification.RequestDone) !void {
const self: *Self = @alignCast(@ptrCast(ctx));
defer self.resetNotificationArena();
return @import("domains/network.zig").httpRequestDone(self.notification_arena, self, data);
return @import("domains/network.zig").httpRequestDone(self.notification_arena, self, msg);
}

pub fn onHttpResponseData(ctx: *anyopaque, msg: *const Notification.ResponseData) !void {
const self: *Self = @alignCast(@ptrCast(ctx));
const arena = self.arena;

const id = msg.transfer.id;
const gop = try self.captured_responses.getOrPut(arena, id);
if (!gop.found_existing) {
gop.value_ptr.* = .{};
}
try gop.value_ptr.appendSlice(arena, try arena.dupe(u8, msg.data));
}

fn resetNotificationArena(self: *Self) void {
Expand Down
47 changes: 23 additions & 24 deletions src/cdp/domains/network.zig
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,6 @@ pub fn processMessage(cmd: anytype) !void {
}
}

const Response = struct {
status: u16,
headers: std.StringArrayHashMapUnmanaged([]const u8) = .empty,
// These may not be complete yet, but we only tell the client
// Network.responseReceived when all the headers are in.
// Later should store body as well to support getResponseBody which should
// only work once Network.loadingFinished is sent but the body itself would
// be loaded with each chunks as Network.dataReceiveds are coming in.
};

fn enable(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
try bc.networkEnable();
Expand Down Expand Up @@ -209,15 +199,17 @@ fn getResponseBody(cmd: anytype) !void {
requestId: []const u8, // "REQ-{d}"
})) orelse return error.InvalidParams;

_ = params;
const request_id = try idFromRequestId(params.requestId);
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const buf = bc.captured_responses.getPtr(request_id) orelse return error.RequestNotFound;

try cmd.sendResult(.{
.body = "TODO",
.body = buf.items,
.base64Encoded = false,
}, .{});
}

pub fn httpRequestFail(arena: Allocator, bc: anytype, data: *const Notification.RequestFail) !void {
pub fn httpRequestFail(arena: Allocator, bc: anytype, msg: *const Notification.RequestFail) !void {
// It's possible that the request failed because we aborted when the client
// sent Target.closeTarget. In that case, bc.session_id will be cleared
// already, and we can skip sending these messages to the client.
Expand All @@ -229,15 +221,15 @@ pub fn httpRequestFail(arena: Allocator, bc: anytype, data: *const Notification.

// We're missing a bunch of fields, but, for now, this seems like enough
try bc.cdp.sendEvent("Network.loadingFailed", .{
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{data.transfer.id}),
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{msg.transfer.id}),
// Seems to be what chrome answers with. I assume it depends on the type of error?
.type = "Ping",
.errorText = data.err,
.errorText = msg.err,
.canceled = false,
}, .{ .session_id = session_id });
}

pub fn httpRequestStart(arena: Allocator, bc: anytype, data: *const Notification.RequestStart) !void {
pub fn httpRequestStart(arena: Allocator, bc: anytype, msg: *const Notification.RequestStart) !void {
// Isn't possible to do a network request within a Browser (which our
// notification is tied to), without a page.
std.debug.assert(bc.session.page != null);
Expand All @@ -251,15 +243,15 @@ pub fn httpRequestStart(arena: Allocator, bc: anytype, data: *const Notification

// Modify request with extra CDP headers
for (bc.extra_headers.items) |extra| {
try data.transfer.req.headers.add(extra);
try msg.transfer.req.headers.add(extra);
}

const transfer = data.transfer;
const transfer = msg.transfer;
// We're missing a bunch of fields, but, for now, this seems like enough
try cdp.sendEvent("Network.requestWillBeSent", .{ .requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}), .frameId = target_id, .loaderId = bc.loader_id, .documentUrl = DocumentUrlWriter.init(&page.url.uri), .request = TransferAsRequestWriter.init(transfer) }, .{ .session_id = session_id });
}

pub fn httpHeadersDone(arena: Allocator, bc: anytype, data: *const Notification.ResponseHeadersDone) !void {
pub fn httpResponseHeaderDone(arena: Allocator, bc: anytype, msg: *const Notification.ResponseHeaderDone) !void {
// Isn't possible to do a network request within a Browser (which our
// notification is tied to), without a page.
std.debug.assert(bc.session.page != null);
Expand All @@ -272,14 +264,14 @@ pub fn httpHeadersDone(arena: Allocator, bc: anytype, data: *const Notification.

// We're missing a bunch of fields, but, for now, this seems like enough
try cdp.sendEvent("Network.responseReceived", .{
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{data.transfer.id}),
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{msg.transfer.id}),
.loaderId = bc.loader_id,
.frameId = target_id,
.response = TransferAsResponseWriter.init(data.transfer),
.response = TransferAsResponseWriter.init(msg.transfer),
}, .{ .session_id = session_id });
}

pub fn httpRequestDone(arena: Allocator, bc: anytype, data: *const Notification.RequestDone) !void {
pub fn httpRequestDone(arena: Allocator, bc: anytype, msg: *const Notification.RequestDone) !void {
// Isn't possible to do a network request within a Browser (which our
// notification is tied to), without a page.
std.debug.assert(bc.session.page != null);
Expand All @@ -290,8 +282,8 @@ pub fn httpRequestDone(arena: Allocator, bc: anytype, data: *const Notification.
const session_id = bc.session_id orelse unreachable;

try cdp.sendEvent("Network.loadingFinished", .{
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{data.transfer.id}),
.encodedDataLength = data.transfer.bytes_received,
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{msg.transfer.id}),
.encodedDataLength = msg.transfer.bytes_received,
}, .{ .session_id = session_id });
}

Expand Down Expand Up @@ -439,6 +431,13 @@ const DocumentUrlWriter = struct {
}
};

fn idFromRequestId(request_id: []const u8) !u64 {
if (!std.mem.startsWith(u8, request_id, "REQ-")) {
return error.InvalidParams;
}
return std.fmt.parseInt(u64, request_id[4..], 10) catch return error.InvalidParams;
}

const testing = @import("../testing.zig");
test "cdp.network setExtraHTTPHeaders" {
var ctx = testing.context();
Expand Down
13 changes: 11 additions & 2 deletions src/http/Client.zig
Original file line number Diff line number Diff line change
Expand Up @@ -757,7 +757,7 @@ pub const Transfer = struct {
};

if (transfer.client.notification) |notification| {
notification.dispatch(.http_headers_done, &.{
notification.dispatch(.http_response_header_done, &.{
.transfer = transfer,
});
}
Expand All @@ -780,10 +780,19 @@ pub const Transfer = struct {
}

transfer.bytes_received += chunk_len;
transfer.req.data_callback(transfer, buffer[0..chunk_len]) catch |err| {
const chunk = buffer[0..chunk_len];
transfer.req.data_callback(transfer, chunk) catch |err| {
log.err(.http, "data_callback", .{ .err = err, .req = transfer });
return c.CURL_WRITEFUNC_ERROR;
};

if (transfer.client.notification) |notification| {
notification.dispatch(.http_response_data, &.{
.data = chunk,
.transfer = transfer,
});
}

return chunk_len;
}

Expand Down
13 changes: 10 additions & 3 deletions src/notification.zig
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ pub const Notification = struct {
http_request_fail: List = .{},
http_request_start: List = .{},
http_request_intercept: List = .{},
http_headers_done: List = .{},
http_request_done: List = .{},
http_response_data: List = .{},
http_response_header_done: List = .{},
notification_created: List = .{},
};

Expand All @@ -76,8 +77,9 @@ pub const Notification = struct {
http_request_fail: *const RequestFail,
http_request_start: *const RequestStart,
http_request_intercept: *const RequestIntercept,
http_headers_done: *const ResponseHeadersDone,
http_request_done: *const RequestDone,
http_response_data: *const ResponseData,
http_response_header_done: *const ResponseHeaderDone,
notification_created: *Notification,
};
const EventType = std.meta.FieldEnum(Events);
Expand All @@ -104,7 +106,12 @@ pub const Notification = struct {
wait_for_interception: *bool,
};

pub const ResponseHeadersDone = struct {
pub const ResponseData = struct {
data: []const u8,
transfer: *Transfer,
};

pub const ResponseHeaderDone = struct {
transfer: *Transfer,
};

Expand Down