Skip to content

Commit a201ec3

Browse files
committed
Fix host parser to handle IPv6
1 parent a3b23de commit a201ec3

8 files changed

Lines changed: 108 additions & 43 deletions

File tree

src/core/events.zig

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,17 @@ fn domainForEvent(event: types.LifecycleEvent) ?[]const u8 {
124124
}
125125

126126
fn hostFromUrl(url: []const u8) ?[]const u8 {
127-
const scheme = std.mem.indexOf(u8, url, "://") orelse return null;
128-
const rest = url[scheme + 3 ..];
129-
const slash = std.mem.indexOfScalar(u8, rest, '/') orelse rest.len;
130-
const host_port = rest[0..slash];
131-
const colon = std.mem.indexOfScalar(u8, host_port, ':') orelse host_port.len;
132-
const host = host_port[0..colon];
133-
if (host.len == 0) return null;
134-
return host;
127+
const parsed = std.Uri.parse(url) catch return null;
128+
const host_component = parsed.host orelse return null;
129+
const raw_host = switch (host_component) {
130+
.raw => |value| value,
131+
.percent_encoded => |value| value,
132+
};
133+
if (raw_host.len == 0) return null;
134+
if (raw_host.len >= 2 and raw_host[0] == '[' and raw_host[raw_host.len - 1] == ']') {
135+
return raw_host[1 .. raw_host.len - 1];
136+
}
137+
return raw_host;
135138
}
136139

137140
fn domainMatches(value: []const u8, filter: []const u8) bool {
@@ -149,6 +152,8 @@ fn freeSubscription(allocator: std.mem.Allocator, sub: EventSubscription) void {
149152
test "host parser extracts host" {
150153
try std.testing.expectEqualStrings("example.com", hostFromUrl("https://example.com/path").?);
151154
try std.testing.expectEqualStrings("example.com", hostFromUrl("https://example.com:443/path").?);
155+
try std.testing.expectEqualStrings("2001:db8::1", hostFromUrl("https://[2001:db8::1]:443/path").?);
156+
try std.testing.expectEqualStrings("example.com", hostFromUrl("https://user:pass@example.com/path?x=1").?);
152157
try std.testing.expect(hostFromUrl("data:text/html,hello") == null);
153158
}
154159

src/core/network.zig

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -233,18 +233,22 @@ pub fn listNetworkRecords(
233233
allocator: std.mem.Allocator,
234234
include_bodies: bool,
235235
) ![]types.NetworkRecord {
236-
session.network_lock.lock();
237-
var out = try allocator.alloc(types.NetworkRecord, session.network_records.items.len);
238-
var copied: usize = 0;
239-
errdefer {
240-
for (out[0..copied]) |*record| freeNetworkRecord(allocator, record);
241-
allocator.free(out);
242-
}
243-
for (session.network_records.items, 0..) |record, idx| {
244-
out[idx] = try cloneNetworkRecord(allocator, record, include_bodies);
245-
copied = idx + 1;
246-
}
247-
session.network_lock.unlock();
236+
const out = blk: {
237+
session.network_lock.lock();
238+
defer session.network_lock.unlock();
239+
240+
var copied_out = try allocator.alloc(types.NetworkRecord, session.network_records.items.len);
241+
var copied_count: usize = 0;
242+
errdefer {
243+
for (copied_out[0..copied_count]) |*record| freeNetworkRecord(allocator, record);
244+
allocator.free(copied_out);
245+
}
246+
for (session.network_records.items, 0..) |record, idx| {
247+
copied_out[idx] = try cloneNetworkRecord(allocator, record, include_bodies);
248+
copied_count = idx + 1;
249+
}
250+
break :blk copied_out;
251+
};
248252

249253
if (!include_bodies or session.transport != .cdp_ws) return out;
250254

src/core/session.zig

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ pub const Session = struct {
4646
event_subscriptions: std.ArrayList(events.EventSubscription) = .empty,
4747
next_event_subscription_id: u64 = 1,
4848
challenge_active: bool = false,
49+
challenge_lock: std.Thread.Mutex = .{},
4950
network_lock: std.Thread.Mutex = .{},
5051
network_records: std.ArrayList(types.NetworkRecord) = .empty,
5152
frames_lock: std.Thread.Mutex = .{},
@@ -148,7 +149,8 @@ pub const Session = struct {
148149
}
149150

150151
pub fn reload(self: *Session) !void {
151-
const url = currentUrlForLifecycle(self);
152+
const url = try currentUrlForLifecycle(self);
153+
defer self.allocator.free(url);
152154
events.emit(self, .{ .navigation_started = .{ .url = url, .cause = .reload } });
153155
events.emit(self, .{ .reload_started = .{ .url = url, .cause = .reload } });
154156
capturePhaseSnapshotBestEffort(self, .navigation_started, url);
@@ -731,10 +733,11 @@ fn elapsedSince(start_ms: i64) u32 {
731733
return @intCast(delta);
732734
}
733735

734-
fn currentUrlForLifecycle(self: *Session) []const u8 {
736+
fn currentUrlForLifecycle(self: *Session) ![]u8 {
735737
self.state_lock.lock();
736738
defer self.state_lock.unlock();
737-
return self.current_url orelse "";
739+
if (self.current_url) |url| return self.allocator.dupe(u8, url);
740+
return self.allocator.dupe(u8, "");
738741
}
739742

740743
fn emitNavigationMilestones(self: *Session, url: []const u8, cause: types.NavigationCause) !void {
@@ -867,6 +870,7 @@ const SessionEventCapture = struct {
867870
last_navigation_completed_cause: ?types.NavigationCause = null,
868871
last_navigation_failed_error: ?[]const u8 = null,
869872
last_navigation_failed_cause: ?types.NavigationCause = null,
873+
reload_url_buf: [512]u8 = [_]u8{0} ** 512,
870874
last_reload_url: ?[]const u8 = null,
871875
last_reload_failed_error: ?[]const u8 = null,
872876
last_wait_target: ?types.WaitTargetTag = null,
@@ -900,12 +904,16 @@ fn captureSessionEvent(event: types.LifecycleEvent) void {
900904
},
901905
.reload_started => |e| {
902906
session_event_capture.reload_started += 1;
903-
session_event_capture.last_reload_url = e.url;
907+
const copy_len = @min(e.url.len, session_event_capture.reload_url_buf.len);
908+
@memcpy(session_event_capture.reload_url_buf[0..copy_len], e.url[0..copy_len]);
909+
session_event_capture.last_reload_url = session_event_capture.reload_url_buf[0..copy_len];
904910
},
905911
.reload_completed => session_event_capture.reload_completed += 1,
906912
.reload_failed => |e| {
907913
session_event_capture.reload_failed += 1;
908-
session_event_capture.last_reload_url = e.url;
914+
const copy_len = @min(e.url.len, session_event_capture.reload_url_buf.len);
915+
@memcpy(session_event_capture.reload_url_buf[0..copy_len], e.url[0..copy_len]);
916+
session_event_capture.last_reload_url = session_event_capture.reload_url_buf[0..copy_len];
909917
session_event_capture.last_reload_failed_error = e.error_code;
910918
},
911919
.wait_started => |e| {

src/core/wait.zig

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,9 @@ fn waitNetworkIdleStep(session: *Session) !bool {
168168
fn waitUrlContainsStep(session: *Session, needle: []const u8) !bool {
169169
const payload = try evaluateForWait(session, "location.href");
170170
defer session.allocator.free(payload);
171-
return strings.containsIgnoreCase(payload, needle);
171+
const url = try extractEvaluationString(session.allocator, payload);
172+
defer session.allocator.free(url);
173+
return strings.containsIgnoreCase(url, needle);
172174
}
173175

174176
fn waitCookieStep(session: *Session, query: types.CookieQuery) !bool {
@@ -274,16 +276,21 @@ fn maybeEmitChallengeSignals(session: *Session) !void {
274276
if (!session.supports(.js_eval)) return;
275277
const title_payload = try evaluateForWait(session, "document.title");
276278
defer session.allocator.free(title_payload);
279+
const title = try extractEvaluationString(session.allocator, title_payload);
280+
defer session.allocator.free(title);
277281

278-
const looks_like_challenge = strings.containsIgnoreCase(title_payload, "challenge") or
279-
strings.containsIgnoreCase(title_payload, "just a moment") or
280-
strings.containsIgnoreCase(title_payload, "attention required") or
281-
strings.containsIgnoreCase(title_payload, "cf-chl") or
282-
strings.containsIgnoreCase(title_payload, "cloudflare");
282+
const looks_like_challenge = strings.containsIgnoreCase(title, "challenge") or
283+
strings.containsIgnoreCase(title, "just a moment") or
284+
strings.containsIgnoreCase(title, "attention required") or
285+
strings.containsIgnoreCase(title, "cf-chl") or
286+
strings.containsIgnoreCase(title, "cloudflare");
283287

284288
const current_url = try currentUrl(session);
285289
defer session.allocator.free(current_url);
286290

291+
session.challenge_lock.lock();
292+
defer session.challenge_lock.unlock();
293+
287294
if (looks_like_challenge and !session.challenge_active) {
288295
session.challenge_active = true;
289296
events.emit(session, .{
@@ -306,14 +313,27 @@ fn maybeEmitChallengeSignals(session: *Session) !void {
306313
fn currentUrl(session: *Session) ![]u8 {
307314
if (session.supports(.js_eval)) {
308315
const payload = try evaluateForWait(session, "location.href");
309-
return payload;
316+
defer session.allocator.free(payload);
317+
return extractEvaluationString(session.allocator, payload);
310318
}
311319
session.state_lock.lock();
312320
defer session.state_lock.unlock();
313321
if (session.current_url) |url| return session.allocator.dupe(u8, url);
314322
return session.allocator.dupe(u8, "");
315323
}
316324

325+
fn extractEvaluationString(allocator: std.mem.Allocator, payload: []const u8) ![]u8 {
326+
var parsed = std.json.parseFromSlice(std.json.Value, allocator, payload, .{}) catch {
327+
return allocator.dupe(u8, payload);
328+
};
329+
defer parsed.deinit();
330+
const value = extractEvaluationValue(parsed.value) orelse return allocator.dupe(u8, payload);
331+
return switch (value) {
332+
.string => allocator.dupe(u8, value.string),
333+
else => std.json.Stringify.valueAlloc(allocator, value, .{}),
334+
};
335+
}
336+
317337
fn clampTimeout(poll_interval_ms: u32) u32 {
318338
if (poll_interval_ms < 25) return 25;
319339
if (poll_interval_ms > 500) return 500;

src/protocol/executor.zig

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ fn subscribeBidiCoreEvents(session: *Session) !void {
117117
const raw = callBidi(
118118
session,
119119
"session.subscribe",
120-
"{\"events\":[\"network.beforeRequestSent\",\"network.responseStarted\",\"network.responseCompleted\",\"browsingContext.domContentLoaded\",\"browsingContext.load\"]}",
120+
"{\"events\":[\"network.beforeRequestSent\",\"network.responseCompleted\",\"browsingContext.domContentLoaded\",\"browsingContext.load\"]}",
121121
) catch |err| switch (err) {
122122
error.ProtocolCommandFailed => return,
123123
else => return err,
@@ -870,8 +870,7 @@ fn processBidiNotification(session: *Session, payload: []const u8) void {
870870
handleBidiBeforeRequest(session, params);
871871
return;
872872
}
873-
if (std.mem.eql(u8, method, "network.responseStarted") or
874-
std.mem.eql(u8, method, "network.responseCompleted"))
873+
if (std.mem.eql(u8, method, "network.responseCompleted"))
875874
{
876875
handleBidiResponse(session, params);
877876
return;
@@ -933,7 +932,6 @@ fn handleCdpResponseReceived(session: *Session, params: std.json.ObjectMap) void
933932
const headers = stringifyObjectField(session.allocator, response, "headers") catch session.allocator.dupe(u8, "{}") catch return;
934933
defer session.allocator.free(headers);
935934

936-
session.recordNetworkStatus(request_id, status, timestampFromEvent(params, "timestamp"));
937935
session.emitNetworkResponseObserved(.{
938936
.request_id = request_id,
939937
.status = status,
@@ -1034,7 +1032,6 @@ fn handleBidiResponse(session: *Session, params: std.json.ObjectMap) void {
10341032
defer session.allocator.free(headers);
10351033
const body = jsonObjectString(response, "body");
10361034

1037-
session.recordNetworkStatus(request_id, status, nowMs());
10381035
session.emitNetworkResponseObserved(.{
10391036
.request_id = request_id,
10401037
.status = status,

src/runtime.zig

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,10 @@ pub fn launch(allocator: std.mem.Allocator, opts: types.LaunchOptions) !Session
3838
const transport = common.transportForAdapter(adapter_kind);
3939
const capability_set = capabilitiesFor(opts.install.engine, adapter_kind);
4040
const effective_profile_dir = try resolveEffectiveProfileDir(allocator, opts.profile_mode, opts.profile_dir);
41+
const should_cleanup_ephemeral_profile = opts.profile_mode == .ephemeral and opts.profile_dir == null;
4142
var profile_dir_owned = true;
4243
defer if (profile_dir_owned) allocator.free(effective_profile_dir);
43-
errdefer if (opts.profile_mode == .ephemeral) {
44+
errdefer if (should_cleanup_ephemeral_profile) {
4445
std.fs.cwd().deleteTree(effective_profile_dir) catch {};
4546
};
4647

@@ -127,7 +128,7 @@ pub fn launch(allocator: std.mem.Allocator, opts: types.LaunchOptions) !Session
127128

128129
const id = session_mod.nextSessionId();
129130
const endpoint = try buildEndpoint(allocator, adapter_kind, id, debug_port);
130-
const ephemeral_profile_dir = if (opts.profile_mode == .ephemeral) blk: {
131+
const ephemeral_profile_dir = if (should_cleanup_ephemeral_profile) blk: {
131132
profile_dir_owned = false;
132133
break :blk effective_profile_dir;
133134
} else null;

src/session_cache/store.zig

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ pub const SessionCacheStore = struct {
3939

4040
var entry = try parseEntry(allocator, payload);
4141
errdefer deinitEntry(allocator, &entry);
42+
if (!std.mem.eql(u8, entry.domain, domain) or !std.mem.eql(u8, entry.profile_key, profile_key)) {
43+
_ = self.invalidate(domain, profile_key) catch {};
44+
deinitEntry(allocator, &entry);
45+
return error.InvalidState;
46+
}
4247

4348
if (entry.expires_at_ms) |expires_at| {
4449
if (expires_at <= nowMs()) {
@@ -283,12 +288,14 @@ fn cachePathFor(
283288
domain: []const u8,
284289
profile_key: []const u8,
285290
) ![]u8 {
286-
var hasher = std.hash.Wyhash.init(0);
291+
var digest: [32]u8 = undefined;
292+
var hasher = std.crypto.hash.sha2.Sha256.init(.{});
287293
hasher.update(domain);
288-
hasher.update("|");
294+
hasher.update(&[_]u8{0});
289295
hasher.update(profile_key);
290-
const key = hasher.final();
291-
const file_name = try std.fmt.allocPrint(allocator, "{x}.json", .{key});
296+
hasher.final(&digest);
297+
const digest_hex = std.fmt.bytesToHex(digest, .lower);
298+
const file_name = try std.fmt.allocPrint(allocator, "{s}.json", .{digest_hex[0..]});
292299
defer allocator.free(file_name);
293300
return std.fs.path.join(allocator, &.{ root_dir, file_name });
294301
}
@@ -613,6 +620,28 @@ test "session cache payload mask supports custom combos" {
613620
try std.testing.expect(!mask.extra_headers);
614621
}
615622

623+
test "session cache load rejects mismatched identity payload" {
624+
const allocator = std.testing.allocator;
625+
var tmp = std.testing.tmpDir(.{});
626+
defer tmp.cleanup();
627+
628+
const root = try std.fs.path.join(allocator, &.{ ".zig-cache", "tmp", &tmp.sub_path, "cache-identity" });
629+
defer allocator.free(root);
630+
631+
var store = try SessionCacheStore.open(allocator, root);
632+
defer store.deinit();
633+
634+
const path = try cachePathFor(allocator, store.root_dir, "example.com", "default");
635+
defer allocator.free(path);
636+
637+
const payload =
638+
\\{"schema_version":1,"domain":"wrong.example","profile_key":"default","captured_at_ms":1,"expires_at_ms":null,"user_agent":"ua","cookies":[],"local_storage":[],"session_storage":[],"current_url":null,"extra_headers":[]}
639+
;
640+
try atomicWriteFile(path, payload);
641+
642+
try std.testing.expectError(error.InvalidState, store.load(allocator, "example.com", "default"));
643+
}
644+
616645
test "session cache ttl expiry invalidates on load" {
617646
const allocator = std.testing.allocator;
618647
var tmp = std.testing.tmpDir(.{});

src/types.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,7 @@ pub const LaunchError = error{
594594
UnsupportedEngine,
595595
SpawnFailed,
596596
PersistentProfileDirRequired,
597+
Timeout,
597598
};
598599

599600
fn browserTierForKind(kind: BrowserKind) ApiTier {

0 commit comments

Comments
 (0)