mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-08-02 10:47:15 -04:00
Merge pull request #2801 from lightpanda-io/document-setDomain
webapi: Expose document.domain setter
This commit is contained in:
@@ -320,7 +320,7 @@ fn findPopupBy(self: *Page, comptime field: []const u8, id: u32) ?*Frame {
|
||||
//
|
||||
// The returned set is fixed, so a caller may run user JS (which can create or
|
||||
// tear down frames/workers) while walking it without invalidating the slice.
|
||||
pub fn executionsForOrigin(self: *Page, arena: Allocator, origin: *js.Origin) ![]*js.Execution {
|
||||
pub fn executionsForOrigin(self: *Page, arena: Allocator, origin: []const u8) ![]*js.Execution {
|
||||
var list: std.ArrayList(*js.Execution) = .empty;
|
||||
try appendFrameExecutions(&self.frame, origin, arena, &list);
|
||||
for (self.popups.items) |popup| {
|
||||
@@ -329,14 +329,18 @@ pub fn executionsForOrigin(self: *Page, arena: Allocator, origin: *js.Origin) ![
|
||||
return list.items;
|
||||
}
|
||||
|
||||
fn appendFrameExecutions(frame: *Frame, origin: *js.Origin, arena: Allocator, list: *std.ArrayList(*js.Execution)) !void {
|
||||
if (frame.js.origin == origin) {
|
||||
try list.append(arena, &frame.js.execution);
|
||||
fn appendFrameExecutions(frame: *Frame, origin: []const u8, arena: Allocator, list: *std.ArrayList(*js.Execution)) !void {
|
||||
if (frame.origin) |fo| {
|
||||
if (std.mem.eql(u8, fo, origin)) {
|
||||
try list.append(arena, &frame.js.execution);
|
||||
}
|
||||
}
|
||||
for (frame.workers.items) |worker| {
|
||||
const wgs = worker._worker_scope;
|
||||
if (wgs.js.origin == origin) {
|
||||
try list.append(arena, &wgs.js.execution);
|
||||
if (wgs.origin) |wo| {
|
||||
if (std.mem.eql(u8, wo, origin)) {
|
||||
try list.append(arena, &wgs.js.execution);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (frame.child_frames.items) |child| {
|
||||
|
||||
@@ -537,6 +537,17 @@ pub fn getHostname(raw: [:0]const u8) []const u8 {
|
||||
return host[0..port_sep];
|
||||
}
|
||||
|
||||
// Like getHostname, but for an origin serialization ("scheme://host[:port]"),
|
||||
// which is not necessarily sentinel-terminated. Used where the document's host
|
||||
// must come from its (possibly inherited) origin rather than its URL — e.g. an
|
||||
// about:blank iframe whose url stays "about:blank" but whose origin is the
|
||||
// parent's.
|
||||
pub fn getOriginHostname(origin: []const u8) []const u8 {
|
||||
const host = getHost(origin);
|
||||
const port_sep = findPortSeparator(host) orelse return host;
|
||||
return host[0..port_sep];
|
||||
}
|
||||
|
||||
pub fn getPort(raw: [:0]const u8) []const u8 {
|
||||
const host = getHost(raw);
|
||||
const port_sep = findPortSeparator(host) orelse return "";
|
||||
|
||||
@@ -229,18 +229,11 @@ pub fn deinit(self: *Context) void {
|
||||
v8.v8__MicrotaskQueue__DELETE(self.microtask_queue);
|
||||
}
|
||||
|
||||
// setOrigin is called at navigation (opaque -> real origin) and again when a
|
||||
// script sets document.domain (real origin -> '!'-marked effective domain).
|
||||
pub fn setOrigin(self: *Context, key: ?[]const u8) !void {
|
||||
const env = self.env;
|
||||
const isolate = env.isolate;
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
// A frame starts off with an opaque origin. After navigation, setOrigin
|
||||
// is called. This is the only time setOrigin should be called for that
|
||||
// frame. Therefore, when setOrigin is called, the previous origin should
|
||||
// have been opaque and its rc should have been 1.
|
||||
lp.assert(self.origin.rc == 1, "Ref opaque origin", .{ .rc = self.origin.rc });
|
||||
}
|
||||
|
||||
const origin = try self.page.getOrCreateOrigin(key);
|
||||
|
||||
self.page.releaseOrigin(self.origin);
|
||||
|
||||
@@ -62,6 +62,14 @@
|
||||
testing.expectEqual(document.URL, document.documentURI);
|
||||
testing.expectEqual('', document.referrer);
|
||||
testing.expectEqual(testing.HOST, document.domain);
|
||||
|
||||
document.domain = testing.HOST;
|
||||
testing.expectEqual(testing.HOST, document.domain);
|
||||
testing.expectError('SecurityError', () => { document.domain = 'example.org'; });
|
||||
testing.expectError('SecurityError', () => { document.domain = 'com'; });
|
||||
testing.expectError('SecurityError', () => { document.domain = ''; });
|
||||
// Rejected assignments must not have changed the domain.
|
||||
testing.expectEqual(testing.HOST, document.domain);
|
||||
</script>
|
||||
|
||||
<script id=programmatic_document_metadata>
|
||||
|
||||
@@ -160,11 +160,11 @@ const PostMessageCallback = struct {
|
||||
|
||||
const sender = self.sender;
|
||||
const page = self.exec.page;
|
||||
const origin = self.exec.js.origin;
|
||||
const origin = self.exec.origin();
|
||||
|
||||
// MessageEvent.origin is the serialization of the sender's origin (same
|
||||
// for every receiver); an opaque origin serializes to "null".
|
||||
const sender_origin = self.exec.origin() orelse "null";
|
||||
const sender_origin = origin orelse "null";
|
||||
|
||||
// Snapshot every same-origin global up front. Dispatch (below) runs user
|
||||
// JS that can create or tear down frames/workers, so we must not hold a
|
||||
@@ -174,7 +174,14 @@ const PostMessageCallback = struct {
|
||||
const arena = try page.getArena(.tiny, "BroadcastChannel.postMessage");
|
||||
defer page.releaseArena(arena);
|
||||
|
||||
for (try page.executionsForOrigin(arena, origin)) |exec| {
|
||||
// Opaque origins have no string form and are unique per execution, so
|
||||
// the sender is the only same-origin context
|
||||
const executions = if (origin) |o|
|
||||
try page.executionsForOrigin(arena, o)
|
||||
else
|
||||
(&self.exec)[0..1];
|
||||
|
||||
for (executions) |exec| {
|
||||
// The MessageEvent and its cloned `data` must live in the receiver's
|
||||
// realm, and its listeners are in the receiver's event manager.
|
||||
// localScope enters the receiver's v8 context, so both happen there.
|
||||
|
||||
@@ -22,6 +22,8 @@ const lp = @import("lightpanda");
|
||||
const js = @import("../js/js.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
const URL = @import("../URL.zig");
|
||||
const idna = @import("../../sys/idna.zig");
|
||||
const public_suffix_list = @import("../../data/public_suffix_list.zig");
|
||||
|
||||
const Node = @import("Node.zig");
|
||||
const Element = @import("Element.zig");
|
||||
@@ -147,7 +149,80 @@ pub fn getContentType(self: *const Document) []const u8 {
|
||||
}
|
||||
|
||||
pub fn getDomain(self: *const Document, frame: *const Frame) []const u8 {
|
||||
return URL.getHostname((self._frame orelse frame).url);
|
||||
const doc_frame = self._frame orelse frame;
|
||||
|
||||
// When document.domain has been set, the effective domain is encoded in
|
||||
// the origin key with a leading '!' marker. The key is a "!scheme://host"
|
||||
// serialization with the port already dropped, so the host *is* the
|
||||
// effective domain.
|
||||
const key = doc_frame.js.origin.key;
|
||||
if (key.len != 0 and key[0] == '!') {
|
||||
return URL.getHost(key[1..]);
|
||||
}
|
||||
|
||||
// Derive from the origin, not the URL: an about:blank child inherits the
|
||||
// parent origin while keeping url == "about:blank". Opaque origin => "".
|
||||
const origin = doc_frame.origin orelse return "";
|
||||
return URL.getOriginHostname(origin);
|
||||
}
|
||||
|
||||
pub fn setDomain(self: *Document, value: []const u8) !void {
|
||||
// e.g. (new Document().domain = '')
|
||||
const doc_frame = self._frame orelse return error.SecurityError;
|
||||
const origin = doc_frame.origin orelse return error.SecurityError;
|
||||
|
||||
const arena = doc_frame.call_arena;
|
||||
const requested = if (idna.needsAscii(value)) try idna.toAscii(arena, value) else value;
|
||||
|
||||
// Validate against the current effective domain. Once relaxed,
|
||||
// document.domain can only broaden further.
|
||||
const base = self.getDomain(doc_frame);
|
||||
if (isRelaxableTo(base, requested) == false) {
|
||||
return error.SecurityError;
|
||||
}
|
||||
|
||||
// When the domain is explicitly set, it only matches other explicitly set
|
||||
// domains. We do this by prepending a '!' to the origin, so that it can
|
||||
// only ever match another explicitly set domain.
|
||||
// The scheme is preserved (http and https must never collide) and the
|
||||
// port is dropped, per spec.
|
||||
const scheme_end = (std.mem.indexOf(u8, origin, "://") orelse return error.SecurityError) + 3;
|
||||
const key = try std.mem.concat(arena, u8, &.{ "!", origin[0..scheme_end], requested });
|
||||
try doc_frame.js.setOrigin(key);
|
||||
}
|
||||
|
||||
// Returns true if the requested domain is valid for the given host
|
||||
fn isRelaxableTo(host: []const u8, requested: []const u8) bool {
|
||||
if (requested.len == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Pure opt-in: relaxing to your own host is always allowed (and it's the
|
||||
// only valid value for IPs)
|
||||
if (std.mem.eql(u8, host, requested)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// request must be a subset of host, so it must be smaller
|
||||
if (host.len <= requested.len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (host[host.len - requested.len - 1] != '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (std.mem.endsWith(u8, host, requested) == false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// it can't be a bare TLD, "com"
|
||||
if (std.mem.indexOfScalar(u8, requested, '.') == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// and it can't be a public suffix (e.g. "gov.uk")
|
||||
return public_suffix_list.lookup(requested) == false;
|
||||
}
|
||||
|
||||
const CreateElementOptions = struct {
|
||||
@@ -1178,7 +1253,7 @@ pub const JsApi = struct {
|
||||
pub const styleSheets = bridge.accessor(Document.getStyleSheets, null, .{});
|
||||
pub const fonts = bridge.accessor(Document.getFonts, null, .{});
|
||||
pub const contentType = bridge.accessor(Document.getContentType, null, .{});
|
||||
pub const domain = bridge.accessor(Document.getDomain, null, .{});
|
||||
pub const domain = bridge.accessor(Document.getDomain, Document.setDomain, .{ .dom_exception = true });
|
||||
pub const createElement = bridge.function(Document.createElement, .{ .dom_exception = true });
|
||||
pub const createElementNS = bridge.function(Document.createElementNS, .{ .dom_exception = true });
|
||||
pub const createDocumentFragment = bridge.function(Document.createDocumentFragment, .{});
|
||||
@@ -1261,3 +1336,31 @@ test "WebApi: Document" {
|
||||
test "WebApi: Document.evaluate" {
|
||||
try testing.htmlRunner("xpath/document_evaluate.html", .{});
|
||||
}
|
||||
|
||||
test "Document: isRelaxableTo" {
|
||||
// Pure opt-in (relax to self) is always allowed, including IP hosts.
|
||||
try testing.expectEqual(true, isRelaxableTo("a.example.com", "a.example.com"));
|
||||
try testing.expectEqual(true, isRelaxableTo("127.0.0.1", "127.0.0.1"));
|
||||
|
||||
// Relaxing to a registrable superdomain.
|
||||
try testing.expectEqual(true, isRelaxableTo("a.example.com", "example.com"));
|
||||
try testing.expectEqual(true, isRelaxableTo("a.b.example.com", "example.com"));
|
||||
try testing.expectEqual(true, isRelaxableTo("a.b.example.com", "b.example.com"));
|
||||
|
||||
// Bare TLDs (single label) are never registrable. Multi-label public
|
||||
// suffixes are rejected via the PSL — note the test build stubs the PSL
|
||||
// to {gov.uk, api.gov.uk}, so those are the entries exercised here.
|
||||
try testing.expectEqual(false, isRelaxableTo("a.example.com", "com"));
|
||||
try testing.expectEqual(false, isRelaxableTo("foo.gov.uk", "gov.uk"));
|
||||
try testing.expectEqual(false, isRelaxableTo("a.api.gov.uk", "api.gov.uk"));
|
||||
// ...but a registrable domain sitting under that suffix is fine.
|
||||
try testing.expectEqual(true, isRelaxableTo("a.dept.gov.uk", "dept.gov.uk"));
|
||||
|
||||
// Must be a label-boundary suffix, not a substring suffix.
|
||||
try testing.expectEqual(false, isRelaxableTo("a.example.com", "ample.com"));
|
||||
try testing.expectEqual(false, isRelaxableTo("notexample.com", "example.com"));
|
||||
|
||||
// Unrelated domain, and the empty string.
|
||||
try testing.expectEqual(false, isRelaxableTo("a.example.com", "example.org"));
|
||||
try testing.expectEqual(false, isRelaxableTo("a.example.com", ""));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user