postMessage / MessageEvent now allow / track MessagePost

Navigator properties moved from data properties to real accessors. This impact
how they're seen (and potentially treated) by JS code.

These change result in https://www.browserscan.net/bot-detection correctly
rendering. When paired with https://github.com/lightpanda-io/browser/pull/2561
we now render that page correctly (still detected as a bot, but we no longer
fail to render)
This commit is contained in:
Karl Seguin
2026-05-28 08:07:48 +08:00
parent 7743e68c58
commit 3430bbddd8
5 changed files with 141 additions and 20 deletions

View File

@@ -161,6 +161,32 @@
}
</script>
<script id=postMessageTransfersPorts>
{
// window.postMessage(data, targetOrigin, [port]) must deliver the transferred
// MessagePort as event.ports[0] (regression: ports used to always be empty,
// so receivers doing `e.ports[0].onmessage = ...` threw on undefined).
const channel = new MessageChannel();
let received = null;
let echoed = null;
channel.port1.onmessage = (e) => { echoed = e.data; };
const handler = (e) => {
received = e.ports[0];
received.postMessage('pong');
};
window.addEventListener('message', handler, { once: true });
window.postMessage('ping', '*', [channel.port2]);
testing.onload(() => {
testing.expectEqual(true, received instanceof MessagePort);
testing.expectEqual('pong', echoed);
});
}
</script>
<script id=messageEventOriginFromLocation>
{
let receivedOrigin = null;

View File

@@ -27,6 +27,36 @@
testing.expectEqual('Gecko', navigator.product);
testing.expectEqual(false, navigator.javaEnabled());
testing.expectEqual(false, navigator.webdriver);
testing.expectEqual(null, navigator.doNotTrack);
// Every Navigator attribute must be a native accessor on the prototype
for (const name of [
'userAgent', 'appName', 'appCodeName', 'appVersion', 'platform',
'language', 'languages', 'onLine', 'cookieEnabled', 'hardwareConcurrency',
'deviceMemory', 'maxTouchPoints', 'vendor', 'product', 'webdriver',
'plugins', 'doNotTrack', 'globalPrivacyControl',
]) {
const desc = Object.getOwnPropertyDescriptor(Navigator.prototype, name);
testing.expectEqual('function', typeof desc.get); // accessor on prototype
testing.expectEqual(undefined, desc.value); // not a data property
testing.expectEqual(undefined, Object.getOwnPropertyDescriptor(navigator, name)); // not own
testing.expectEqual(true, desc.get.toString().includes('[native code]'));
}
</script>
<script id=navigator_native_descriptor_walk>
// Mirror the bot-detection fingerprint: read every navigator property's
// descriptor and call toString() on its value/getter. This must not throw.
let obj = navigator;
do {
for (const name of Object.getOwnPropertyNames(obj)) {
const d = Object.getOwnPropertyDescriptor(obj, name);
if (d.value !== undefined) { d.value.toString(); }
else if (d.get !== undefined) { d.get.toString(); }
}
} while (obj = Object.getPrototypeOf(obj));
testing.expectEqual(null, navigator.doNotTrack);
</script>
<script id=permission_query type=module>

View File

@@ -48,6 +48,62 @@ pub fn getLanguages(_: *const Navigator) [2][]const u8 {
return .{ "en-US", "en" };
}
pub fn getDoNotTrack(_: *const Navigator) ?[]const u8 {
return null;
}
pub fn getAppName(_: *const Navigator) []const u8 {
return "Netscape";
}
pub fn getAppCodeName(_: *const Navigator) []const u8 {
return "Mozilla";
}
pub fn getAppVersion(_: *const Navigator) []const u8 {
return "1.0";
}
pub fn getLanguage(_: *const Navigator) []const u8 {
return "en-US";
}
pub fn getOnLine(_: *const Navigator) bool {
return true;
}
pub fn getCookieEnabled(_: *const Navigator) bool {
return true;
}
pub fn getHardwareConcurrency(_: *const Navigator) u32 {
return 4;
}
pub fn getDeviceMemory(_: *const Navigator) f64 {
return 8.0;
}
pub fn getMaxTouchPoints(_: *const Navigator) u32 {
return 0;
}
pub fn getVendor(_: *const Navigator) []const u8 {
return "";
}
pub fn getProduct(_: *const Navigator) []const u8 {
return "Gecko";
}
pub fn getWebdriver(_: *const Navigator) bool {
return false;
}
pub fn getGlobalPrivacyControl(_: *const Navigator) bool {
return true;
}
pub fn getPlatform(_: *const Navigator) []const u8 {
return switch (builtin.os.tag) {
.macos => "MacIntel",
@@ -166,25 +222,27 @@ pub const JsApi = struct {
pub const empty_with_no_proto = true;
};
// Read-only properties
// Read-only properties. All are accessors (not data properties) so they
// present as native getters on Navigator.prototype, matching real browsers
// — see the getter definitions above for why.
pub const userAgent = bridge.accessor(Navigator.getUserAgent, null, .{});
pub const appName = bridge.property("Netscape", .{ .template = false });
pub const appCodeName = bridge.property("Mozilla", .{ .template = false });
pub const appVersion = bridge.property("1.0", .{ .template = false });
pub const appName = bridge.accessor(Navigator.getAppName, null, .{});
pub const appCodeName = bridge.accessor(Navigator.getAppCodeName, null, .{});
pub const appVersion = bridge.accessor(Navigator.getAppVersion, null, .{});
pub const platform = bridge.accessor(Navigator.getPlatform, null, .{});
pub const language = bridge.property("en-US", .{ .template = false });
pub const language = bridge.accessor(Navigator.getLanguage, null, .{});
pub const languages = bridge.accessor(Navigator.getLanguages, null, .{});
pub const onLine = bridge.property(true, .{ .template = false });
pub const cookieEnabled = bridge.property(true, .{ .template = false });
pub const hardwareConcurrency = bridge.property(4, .{ .template = false });
pub const deviceMemory = bridge.property(@as(f64, 8.0), .{ .template = false });
pub const maxTouchPoints = bridge.property(0, .{ .template = false });
pub const vendor = bridge.property("", .{ .template = false });
pub const product = bridge.property("Gecko", .{ .template = false });
pub const webdriver = bridge.property(false, .{ .template = false });
pub const onLine = bridge.accessor(Navigator.getOnLine, null, .{});
pub const cookieEnabled = bridge.accessor(Navigator.getCookieEnabled, null, .{});
pub const hardwareConcurrency = bridge.accessor(Navigator.getHardwareConcurrency, null, .{});
pub const deviceMemory = bridge.accessor(Navigator.getDeviceMemory, null, .{});
pub const maxTouchPoints = bridge.accessor(Navigator.getMaxTouchPoints, null, .{});
pub const vendor = bridge.accessor(Navigator.getVendor, null, .{});
pub const product = bridge.accessor(Navigator.getProduct, null, .{});
pub const webdriver = bridge.accessor(Navigator.getWebdriver, null, .{});
pub const plugins = bridge.accessor(Navigator.getPlugins, null, .{});
pub const doNotTrack = bridge.property(null, .{ .template = false });
pub const globalPrivacyControl = bridge.property(true, .{ .template = false });
pub const doNotTrack = bridge.accessor(Navigator.getDoNotTrack, null, .{});
pub const globalPrivacyControl = bridge.accessor(Navigator.getGlobalPrivacyControl, null, .{});
pub const registerProtocolHandler = bridge.function(Navigator.registerProtocolHandler, .{ .dom_exception = true });
pub const unregisterProtocolHandler = bridge.function(Navigator.unregisterProtocolHandler, .{ .dom_exception = true });

View File

@@ -39,6 +39,7 @@ const Event = @import("Event.zig");
const EventTarget = @import("EventTarget.zig");
const ErrorEvent = @import("event/ErrorEvent.zig");
const MessageEvent = @import("event/MessageEvent.zig");
const MessagePort = @import("MessagePort.zig");
const MediaQueryList = @import("css/MediaQueryList.zig");
const storage = @import("storage/storage.zig");
const Element = @import("Element.zig");
@@ -561,7 +562,7 @@ pub fn close(self: *Window) void {
};
}
pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]const u8, frame: *Frame) !void {
pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]const u8, transfer: ?[]const *MessagePort, frame: *Frame) !void {
// For now, we ignore targetOrigin checking and just dispatch the message
// In a full implementation, we would validate the origin
_ = target_origin;
@@ -581,6 +582,7 @@ pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]cons
.frame = target_frame,
.source = source_window,
.origin = try arena.dupe(u8, origin),
.ports = if (transfer) |t| try arena.dupe(*MessagePort, t) else &.{},
};
try target_frame.js.scheduler.add(callback, PostMessageCallback.run, 0, .{
@@ -788,6 +790,7 @@ const PostMessageCallback = struct {
arena: Allocator,
origin: []const u8,
message: js.Value.Temp,
ports: []const *MessagePort,
fn deinit(self: *PostMessageCallback) void {
self.frame.releaseArena(self.arena);
@@ -811,6 +814,7 @@ const PostMessageCallback = struct {
.data = .{ .value = self.message },
.origin = self.origin,
.source = self.source,
.ports = self.ports,
.bubbles = false,
.cancelable = false,
}, frame._page)).asEvent();
@@ -987,8 +991,8 @@ pub const JsApi = struct {
const CrossOriginWindow = struct {
window: *Window,
pub fn postMessage(self: *CrossOriginWindow, message: js.Value.Temp, target_origin: ?[]const u8, frame: *Frame) !void {
return self.window.postMessage(message, target_origin, frame);
pub fn postMessage(self: *CrossOriginWindow, message: js.Value.Temp, target_origin: ?[]const u8, transfer: ?[]const *MessagePort, frame: *Frame) !void {
return self.window.postMessage(message, target_origin, transfer, frame);
}
pub fn getTop(self: *CrossOriginWindow, frame: *Frame) Access {

View File

@@ -35,11 +35,13 @@ _proto: *Event,
_data: ?Data = null,
_origin: []const u8 = "",
_source: ?*Window = null,
_ports: []const *MessagePort = &.{},
const MessageEventOptions = struct {
data: ?Data = null,
origin: ?[]const u8 = null,
source: ?*Window = null,
ports: []const *MessagePort = &.{},
};
pub const Data = union(enum) {
@@ -75,6 +77,7 @@ fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool
._data = opts.data,
._origin = if (opts.origin) |str| try arena.dupe(u8, str) else "",
._source = opts.source,
._ports = if (opts.ports.len == 0) &.{} else try arena.dupe(*MessagePort, opts.ports),
},
);
@@ -117,8 +120,8 @@ pub fn getSource(self: *const MessageEvent) ?*Window {
return self._source;
}
pub fn getPorts(_: *const MessageEvent) []*MessagePort {
return &.{};
pub fn getPorts(self: *const MessageEvent) []const *MessagePort {
return self._ports;
}
pub const JsApi = struct {