LibWeb: Make Window.postMessage closer to the spec

The main issues are using Structured{Serialize,Deserailize} instead of
Structured{Serialize,Deserialize}WithTransfer and the temporary
execution context usage for StructuredDeserialize.

Allows Discord to load once again, as it uses a postMessage scheduler
to render components, including the main App component. The callback
checked the (previously) non-existent source attribute of the
MessageEvent and returned if it was not the main window.

Fixes the Twitch cookie consent banner saying "failed integrity check"
for unknown reasons, but presumably related to the source and origin
attributes.
This commit is contained in:
Luke Wilde 2023-11-06 20:22:56 +00:00 committed by Andreas Kling
parent 464cc55b16
commit fc42c75a0c
5 changed files with 361 additions and 13 deletions

View file

@ -0,0 +1,100 @@
originError instanceof DOMException: true
originError.name: SyntaxError
originError.message: Invalid URL for targetOrigin: 'aaaa'
originError.constructor === window.DOMException: true
originParsedBeforeSerializeError instanceof DOMException: true
originParsedBeforeSerializeError.name: SyntaxError
originParsedBeforeSerializeError.message: Invalid URL for targetOrigin: 'aaaa'
originParsedBeforeSerializeError.constructor === window.DOMException: true
serializeError instanceof DOMException: true
serializeError.name: DataCloneError
serializeError.message: Unsupported type
serializeError.constructor === window.DOMException: true
originIframeError instanceof DOMException: false
originIframeError instanceof iframe.contentWindow.DOMException: true
originIframeError.name: SyntaxError
originIframeError.message: Invalid URL for targetOrigin: 'aaaa'
originIframeError.constructor === DOMException: false
originIframeError.constructor === iframe.contentWindow.DOMException: true
originParsedBeforeSerializeIframeError instanceof DOMException: false
originParsedBeforeSerializeIframeError instanceof iframe.contentWindow.DOMException: true
originParsedBeforeSerializeIframeError.name: SyntaxError
originParsedBeforeSerializeIframeError.message: Invalid URL for targetOrigin: 'aaaa'
originParsedBeforeSerializeIframeError.constructor === DOMException: false
originParsedBeforeSerializeIframeError.constructor === iframe.contentWindow.DOMException: true
serializeIframeError instanceof DOMException: false
serializeIframeError instanceof iframe.contentWindow.DOMException: true
serializeIframeError.name: DataCloneError
serializeIframeError.message: Unsupported type
serializeIframeError.constructor === DOMException: false
serializeIframeError.constructor === iframe.contentWindow.DOMException: true
Message 1 data: undefined
Message 1 origin: file://
Message 1 lastEventId:
Message 1 source: [object Window]
Message 1 source === window: true
Message 1 source === iframe.contentWindow: false
Message 1 source === blobIframe.contentWindow: false
Message 2 data: null
Message 2 origin: file://
Message 2 lastEventId:
Message 2 source: [object Window]
Message 2 source === window: true
Message 2 source === iframe.contentWindow: false
Message 2 source === blobIframe.contentWindow: false
Message 3 data: true
Message 3 origin: file://
Message 3 lastEventId:
Message 3 source: [object Window]
Message 3 source === window: true
Message 3 source === iframe.contentWindow: false
Message 3 source === blobIframe.contentWindow: false
Message 4 data: false
Message 4 origin: file://
Message 4 lastEventId:
Message 4 source: [object Window]
Message 4 source === window: true
Message 4 source === iframe.contentWindow: false
Message 4 source === blobIframe.contentWindow: false
Message 5 data: 123
Message 5 origin: file://
Message 5 lastEventId:
Message 5 source: [object Window]
Message 5 source === window: true
Message 5 source === iframe.contentWindow: false
Message 5 source === blobIframe.contentWindow: false
Message 6 data: 123.456
Message 6 origin: file://
Message 6 lastEventId:
Message 6 source: [object Window]
Message 6 source === window: true
Message 6 source === iframe.contentWindow: false
Message 6 source === blobIframe.contentWindow: false
Message 7 data: 9007199254740991
Message 7 origin: file://
Message 7 lastEventId:
Message 7 source: [object Window]
Message 7 source === window: true
Message 7 source === iframe.contentWindow: false
Message 7 source === blobIframe.contentWindow: false
Message 8 data: This is a string
Message 8 origin: file://
Message 8 lastEventId:
Message 8 source: [object Window]
Message 8 source === window: true
Message 8 source === iframe.contentWindow: false
Message 8 source === blobIframe.contentWindow: false
Message 9 data: I am from another ~planet~ iframe
Message 9 origin: file://
Message 9 lastEventId:
Message 9 source: [object Window]
Message 9 source === window: false
Message 9 source === iframe.contentWindow: true
Message 9 source === blobIframe.contentWindow: false
Message 10 data: All done :^)
Message 10 origin: file://
Message 10 lastEventId:
Message 10 source: [object Window]
Message 10 source === window: false
Message 10 source === iframe.contentWindow: false
Message 10 source === blobIframe.contentWindow: true

View file

@ -0,0 +1,137 @@
<body>
<iframe style="display: none" id="message-iframe" srcdoc="
<body>
<script>
window.addEventListener('message', (event) => {
window.parent.postMessage(event.data, '*');
});
</script>
<body>
"></iframe>
<script src="../include.js"></script>
<script>
const iframe = document.getElementById("message-iframe");
const blobSrcdoc = new Blob([iframe.srcdoc], {
type: "text/html",
});
const iframeSrcdocBlobUrl = URL.createObjectURL(blobSrcdoc);
const blobIframe = document.createElement("iframe");
blobIframe.src = iframeSrcdocBlobUrl;
blobIframe.setAttribute("style", "display: none");
document.body.append(blobIframe);
let messageCount = 1;
window.onmessage = (messageEvent) => {
try {
println(`Message ${messageCount} data: ${messageEvent.data}`);
println(`Message ${messageCount} origin: ${messageEvent.origin}`);
println(`Message ${messageCount} lastEventId: ${messageEvent.lastEventId}`);
println(`Message ${messageCount} source: ${messageEvent.source}`);
println(`Message ${messageCount} source === window: ${messageEvent.source === window}`);
println(`Message ${messageCount} source === iframe.contentWindow: ${messageEvent.source === iframe.contentWindow}`);
println(`Message ${messageCount} source === blobIframe.contentWindow: ${messageEvent.source === blobIframe.contentWindow}`);
} catch (ex) {
println(`Accessing attributes of message ${messageCount} threw exception: ${ex}`);
}
messageCount++;
if (messageEvent.source === blobIframe.contentWindow && messageEvent.data === "All done :^)")
{
globalThis.doneCallback();
}
};
function performTest() {
window.postMessage(undefined, "*");
window.postMessage(null, "*");
window.postMessage(true, "*");
window.postMessage(false, "*");
window.postMessage(123, "*");
window.postMessage(123.456, "*");
window.postMessage(BigInt("0x1fffffffffffff"), "*");
window.postMessage("This is a string", "/");
window.postMessage("I shouldn't appear, I'm not same origin!", "https://serenityos.org");
iframe.contentWindow.postMessage("I am from another ~planet~ iframe", "*");
blobIframe.contentWindow.postMessage("All done :^)", iframeSrcdocBlobUrl);
try {
window.postMessage("This is a bad origin string", "aaaa");
} catch (originError) {
println(`originError instanceof DOMException: ${originError instanceof DOMException}`);
println(`originError.name: ${originError.name}`);
println(`originError.message: ${originError.message}`);
println(`originError.constructor === window.DOMException: ${originError.constructor === window.DOMException}`);
}
try {
window.postMessage(document, "aaaa");
} catch (originParsedBeforeSerializeError) {
println(`originParsedBeforeSerializeError instanceof DOMException: ${originParsedBeforeSerializeError instanceof DOMException}`);
println(`originParsedBeforeSerializeError.name: ${originParsedBeforeSerializeError.name}`);
println(`originParsedBeforeSerializeError.message: ${originParsedBeforeSerializeError.message}`);
println(`originParsedBeforeSerializeError.constructor === window.DOMException: ${originParsedBeforeSerializeError.constructor === window.DOMException}`);
}
try {
window.postMessage(document, "*");
} catch (serializeError) {
println(`serializeError instanceof DOMException: ${serializeError instanceof DOMException}`);
println(`serializeError.name: ${serializeError.name}`);
println(`serializeError.message: ${serializeError.message}`);
println(`serializeError.constructor === window.DOMException: ${serializeError.constructor === window.DOMException}`);
}
try {
iframe.contentWindow.postMessage("This is a bad origin string", "aaaa");
} catch (originIframeError) {
println(`originIframeError instanceof DOMException: ${originIframeError instanceof DOMException}`);
println(`originIframeError instanceof iframe.contentWindow.DOMException: ${originIframeError instanceof iframe.contentWindow.DOMException}`);
println(`originIframeError.name: ${originIframeError.name}`);
println(`originIframeError.message: ${originIframeError.message}`);
println(`originIframeError.constructor === DOMException: ${originIframeError.constructor === DOMException}`);
println(`originIframeError.constructor === iframe.contentWindow.DOMException: ${originIframeError.constructor === iframe.contentWindow.DOMException}`);
}
try {
iframe.contentWindow.postMessage(document, "aaaa");
} catch (originParsedBeforeSerializeIframeError) {
println(`originParsedBeforeSerializeIframeError instanceof DOMException: ${originParsedBeforeSerializeIframeError instanceof DOMException}`);
println(`originParsedBeforeSerializeIframeError instanceof iframe.contentWindow.DOMException: ${originParsedBeforeSerializeIframeError instanceof iframe.contentWindow.DOMException}`);
println(`originParsedBeforeSerializeIframeError.name: ${originParsedBeforeSerializeIframeError.name}`);
println(`originParsedBeforeSerializeIframeError.message: ${originParsedBeforeSerializeIframeError.message}`);
println(`originParsedBeforeSerializeIframeError.constructor === DOMException: ${originParsedBeforeSerializeIframeError.constructor === DOMException}`);
println(`originParsedBeforeSerializeIframeError.constructor === iframe.contentWindow.DOMException: ${originParsedBeforeSerializeIframeError.constructor === iframe.contentWindow.DOMException}`);
}
try {
iframe.contentWindow.postMessage(document, "*");
} catch (serializeIframeError) {
println(`serializeIframeError instanceof DOMException: ${serializeIframeError instanceof DOMException}`);
println(`serializeIframeError instanceof iframe.contentWindow.DOMException: ${serializeIframeError instanceof iframe.contentWindow.DOMException}`);
println(`serializeIframeError.name: ${serializeIframeError.name}`);
println(`serializeIframeError.message: ${serializeIframeError.message}`);
println(`serializeIframeError.constructor === DOMException: ${serializeIframeError.constructor === DOMException}`);
println(`serializeIframeError.constructor === iframe.contentWindow.DOMException: ${serializeIframeError.constructor === iframe.contentWindow.DOMException}`);
}
}
asyncTest((done) => {
globalThis.doneCallback = done;
const blobIframeLoadPromise = new Promise(resolve => {
blobIframe.onload = () => resolve();
});
const srcdocIframeLoadPromise = new Promise(resolve => {
iframe.onload = () => resolve();
});
Promise.all([blobIframeLoadPromise, srcdocIframeLoadPromise]).then(() => {
performTest();
});
});
</script>
</body>

View file

@ -50,6 +50,7 @@
#include <LibWeb/HTML/PageTransitionEvent.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/Scripting/ExceptionReporter.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/Storage.h>
#include <LibWeb/HTML/TokenizedFeatures.h>
#include <LibWeb/HTML/TraversableNavigable.h>
@ -64,6 +65,7 @@
#include <LibWeb/Page/Page.h>
#include <LibWeb/RequestIdleCallback/IdleDeadline.h>
#include <LibWeb/Selection/Selection.h>
#include <LibWeb/URL/URL.h>
#include <LibWeb/WebIDL/AbstractOperations.h>
namespace Web::HTML {
@ -997,17 +999,115 @@ Optional<String> Window::prompt(Optional<String> const& message, Optional<String
return {};
}
// https://html.spec.whatwg.org/multipage/web-messaging.html#dom-window-postmessage
void Window::post_message(JS::Value message, String const&)
// https://html.spec.whatwg.org/multipage/web-messaging.html#window-post-message-steps
WebIDL::ExceptionOr<void> Window::window_post_message_steps(JS::Value message, WindowPostMessageOptions const& options)
{
// FIXME: This is an ad-hoc hack implementation instead, since we don't currently
// have serialization and deserialization of messages.
queue_global_task(Task::Source::PostedMessage, *this, [this, message] {
MessageEventInit event_init {};
event_init.data = message;
event_init.origin = "<origin>"_string;
dispatch_event(MessageEvent::create(realm(), EventNames::message, event_init));
// 1. Let targetRealm be targetWindow's realm.
auto& target_realm = this->realm();
// 2. Let incumbentSettings be the incumbent settings object.
auto& incumbent_settings = incumbent_settings_object();
// 3. Let targetOrigin be options["targetOrigin"].
Variant<String, Origin> target_origin = options.target_origin;
// 4. If targetOrigin is a single U+002F SOLIDUS character (/), then set targetOrigin to incumbentSettings's origin.
if (options.target_origin == "/"sv) {
target_origin = incumbent_settings.origin();
}
// 5. Otherwise, if targetOrigin is not a single U+002A ASTERISK character (*), then:
else if (options.target_origin != "*"sv) {
// 1. Let parsedURL be the result of running the URL parser on targetOrigin.
auto parsed_url = URL::parse(options.target_origin);
// 2. If parsedURL is failure, then throw a "SyntaxError" DOMException.
if (!parsed_url.is_valid())
return WebIDL::SyntaxError::create(target_realm, MUST(String::formatted("Invalid URL for targetOrigin: '{}'", options.target_origin)));
// 3. Set targetOrigin to parsedURL's origin.
target_origin = URL::url_origin(parsed_url);
}
// 6. Let transfer be options["transfer"].
// FIXME: This is currently unused.
// 7. Let serializeWithTransferResult be StructuredSerializeWithTransfer(message, transfer). Rethrow any exceptions.
// FIXME: Use StructuredSerializeWithTransfer instead of StructuredSerialize
auto serialize_with_transfer_result = TRY(structured_serialize(target_realm.vm(), message));
// 8. Queue a global task on the posted message task source given targetWindow to run the following steps:
queue_global_task(Task::Source::PostedMessage, *this, [this, serialize_with_transfer_result = move(serialize_with_transfer_result), target_origin = move(target_origin), &incumbent_settings, &target_realm]() {
// 1. If the targetOrigin argument is not a single literal U+002A ASTERISK character (*) and targetWindow's
// associated Document's origin is not same origin with targetOrigin, then return.
// NOTE: Due to step 4 and 5 above, the only time it's not '*' is if target_origin contains an Origin.
if (!target_origin.has<String>()) {
auto const& actual_target_origin = target_origin.get<Origin>();
if (!document()->origin().is_same_origin(actual_target_origin))
return;
}
// 2. Let origin be the serialization of incumbentSettings's origin.
auto origin = incumbent_settings.origin().serialize();
// 3. Let source be the WindowProxy object corresponding to incumbentSettings's global object (a Window object).
auto& source = verify_cast<WindowProxy>(incumbent_settings.realm().global_environment().global_this_value());
// 4. Let deserializeRecord be StructuredDeserializeWithTransfer(serializeWithTransferResult, targetRealm).
// FIXME: Use StructuredDeserializeWithTransfer instead of StructuredDeserialize
// FIXME: Don't use a temporary execution context here.
auto& settings_object = Bindings::host_defined_environment_settings_object(target_realm);
auto temporary_execution_context = TemporaryExecutionContext { settings_object };
auto deserialize_record_or_error = structured_deserialize(vm(), serialize_with_transfer_result, target_realm, Optional<HTML::SerializationMemory> {});
// If this throws an exception, catch it, fire an event named messageerror at targetWindow, using MessageEvent,
// with the origin attribute initialized to origin and the source attribute initialized to source, and then return.
if (deserialize_record_or_error.is_exception()) {
MessageEventInit message_event_init {};
message_event_init.origin = MUST(String::from_deprecated_string(origin));
message_event_init.source = JS::make_handle(source);
auto message_error_event = MessageEvent::create(target_realm, EventNames::messageerror, message_event_init);
dispatch_event(message_error_event);
return;
}
// 5. Let messageClone be deserializeRecord.[[Deserialized]].
// FIXME: Get this from deserializeRecord.[[Deserialized]] once it uses StructuredDeserializeWithTransfer instead of StructuredDeserialize.
auto message_clone = deserialize_record_or_error.release_value();
// FIXME: 6. Let newPorts be a new frozen array consisting of all MessagePort objects in deserializeRecord.[[TransferredValues]],
// if any, maintaining their relative order.
// 7. Fire an event named message at targetWindow, using MessageEvent, with the origin attribute initialized to origin,
// the source attribute initialized to source, the data attribute initialized to messageClone, and the ports attribute
// initialized to newPorts.
// FIXME: Set the ports attribute to newPorts.
MessageEventInit message_event_init {};
message_event_init.origin = MUST(String::from_deprecated_string(origin));
message_event_init.source = JS::make_handle(source);
message_event_init.data = message_clone;
auto message_event = MessageEvent::create(target_realm, EventNames::message, message_event_init);
dispatch_event(message_event);
});
return {};
}
// https://html.spec.whatwg.org/multipage/web-messaging.html#dom-window-postmessage-options
WebIDL::ExceptionOr<void> Window::post_message(JS::Value message, WindowPostMessageOptions const& options)
{
// The Window interface's postMessage(message, options) method steps are to run the window post message steps given
// this, message, and options.
return window_post_message_steps(message, options);
}
// https://html.spec.whatwg.org/multipage/web-messaging.html#dom-window-postmessage
WebIDL::ExceptionOr<void> Window::post_message(JS::Value message, String const& target_origin, Vector<JS::Handle<JS::Object>> const& transfer)
{
// The Window interface's postMessage(message, targetOrigin, transfer) method steps are to run the window post message
// steps given this, message, and «[ "targetOrigin" → targetOrigin, "transfer" → transfer ]».
return window_post_message_steps(message, WindowPostMessageOptions { { .transfer = transfer }, target_origin });
}
// https://dom.spec.whatwg.org/#dom-window-event

View file

@ -38,6 +38,11 @@ struct ScrollToOptions : public ScrollOptions {
Optional<double> top;
};
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#windowpostmessageoptions
struct WindowPostMessageOptions : public StructuredSerializeOptions {
String target_origin { "/"_string };
};
class Window final
: public DOM::EventTarget
, public GlobalEventHandlers
@ -146,7 +151,8 @@ public:
bool confirm(Optional<String> const& message);
Optional<String> prompt(Optional<String> const& message, Optional<String> const& default_);
void post_message(JS::Value message, String const&);
WebIDL::ExceptionOr<void> post_message(JS::Value message, String const&, Vector<JS::Handle<JS::Object>> const&);
WebIDL::ExceptionOr<void> post_message(JS::Value message, WindowPostMessageOptions const&);
Variant<JS::Handle<DOM::Event>, JS::Value> event() const;
@ -215,6 +221,8 @@ private:
};
NamedObjects named_objects(StringView name);
WebIDL::ExceptionOr<void> window_post_message_steps(JS::Value, WindowPostMessageOptions const&);
// https://html.spec.whatwg.org/multipage/window-object.html#concept-document-window
JS::GCPtr<DOM::Document> m_associated_document;

View file

@ -52,9 +52,8 @@ interface Window : EventTarget {
boolean confirm(optional DOMString message = "");
DOMString? prompt(optional DOMString message = "", optional DOMString default = "");
undefined postMessage(any message, USVString targetOrigin);
// FIXME: undefined postMessage(any message, USVString targetOrigin, optional sequence<object> transfer = []);
// FIXME: undefined postMessage(any message, optional WindowPostMessageOptions options = {});
undefined postMessage(any message, USVString targetOrigin, optional sequence<object> transfer = []);
undefined postMessage(any message, optional WindowPostMessageOptions options = {});
// https://dom.spec.whatwg.org/#interface-window-extensions
[Replaceable] readonly attribute (Event or undefined) event; // legacy
@ -122,3 +121,7 @@ dictionary ScrollToOptions : ScrollOptions {
unrestricted double left;
unrestricted double top;
};
dictionary WindowPostMessageOptions : StructuredSerializeOptions {
USVString targetOrigin = "/";
};