/src/serenity/Userland/Libraries/LibWeb/HTML/MessagePort.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2021, Andreas Kling <kling@serenityos.org> |
3 | | * Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org> |
4 | | * |
5 | | * SPDX-License-Identifier: BSD-2-Clause |
6 | | */ |
7 | | |
8 | | #include <AK/ByteReader.h> |
9 | | #include <AK/MemoryStream.h> |
10 | | #include <LibCore/Socket.h> |
11 | | #include <LibCore/System.h> |
12 | | #include <LibIPC/Decoder.h> |
13 | | #include <LibIPC/Encoder.h> |
14 | | #include <LibIPC/File.h> |
15 | | #include <LibWeb/Bindings/ExceptionOrUtils.h> |
16 | | #include <LibWeb/Bindings/Intrinsics.h> |
17 | | #include <LibWeb/Bindings/MessagePortPrototype.h> |
18 | | #include <LibWeb/DOM/EventDispatcher.h> |
19 | | #include <LibWeb/HTML/EventNames.h> |
20 | | #include <LibWeb/HTML/MessageEvent.h> |
21 | | #include <LibWeb/HTML/MessagePort.h> |
22 | | #include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h> |
23 | | #include <LibWeb/HTML/StructuredSerializeOptions.h> |
24 | | #include <LibWeb/HTML/WorkerGlobalScope.h> |
25 | | |
26 | | namespace Web::HTML { |
27 | | |
28 | | constexpr u8 IPC_FILE_TAG = 0xA5; |
29 | | |
30 | | JS_DEFINE_ALLOCATOR(MessagePort); |
31 | | |
32 | | static HashTable<JS::RawGCPtr<MessagePort>>& all_message_ports() |
33 | 0 | { |
34 | 0 | static HashTable<JS::RawGCPtr<MessagePort>> ports; |
35 | 0 | return ports; |
36 | 0 | } |
37 | | |
38 | | JS::NonnullGCPtr<MessagePort> MessagePort::create(JS::Realm& realm) |
39 | 0 | { |
40 | 0 | return realm.heap().allocate<MessagePort>(realm, realm); |
41 | 0 | } |
42 | | |
43 | | MessagePort::MessagePort(JS::Realm& realm) |
44 | 0 | : DOM::EventTarget(realm) |
45 | 0 | { |
46 | 0 | all_message_ports().set(this); |
47 | 0 | } |
48 | | |
49 | | MessagePort::~MessagePort() |
50 | 0 | { |
51 | 0 | all_message_ports().remove(this); |
52 | 0 | disentangle(); |
53 | 0 | } |
54 | | |
55 | | void MessagePort::for_each_message_port(Function<void(MessagePort&)> callback) |
56 | 0 | { |
57 | 0 | for (auto port : all_message_ports()) |
58 | 0 | callback(*port); |
59 | 0 | } |
60 | | |
61 | | void MessagePort::initialize(JS::Realm& realm) |
62 | 0 | { |
63 | 0 | Base::initialize(realm); |
64 | 0 | WEB_SET_PROTOTYPE_FOR_INTERFACE(MessagePort); |
65 | 0 | } |
66 | | |
67 | | void MessagePort::visit_edges(Cell::Visitor& visitor) |
68 | 0 | { |
69 | 0 | Base::visit_edges(visitor); |
70 | 0 | visitor.visit(m_remote_port); |
71 | 0 | visitor.visit(m_worker_event_target); |
72 | 0 | } |
73 | | |
74 | | void MessagePort::set_worker_event_target(JS::NonnullGCPtr<DOM::EventTarget> target) |
75 | 0 | { |
76 | 0 | m_worker_event_target = target; |
77 | 0 | } |
78 | | |
79 | | // https://html.spec.whatwg.org/multipage/web-messaging.html#message-ports:transfer-steps |
80 | | WebIDL::ExceptionOr<void> MessagePort::transfer_steps(HTML::TransferDataHolder& data_holder) |
81 | 0 | { |
82 | | // 1. Set value's has been shipped flag to true. |
83 | 0 | m_has_been_shipped = true; |
84 | | |
85 | | // FIXME: 2. Set dataHolder.[[PortMessageQueue]] to value's port message queue. |
86 | | // FIXME: Support delivery of messages that haven't been delivered yet on the other side |
87 | | |
88 | | // 3. If value is entangled with another port remotePort, then: |
89 | 0 | if (is_entangled()) { |
90 | | // 1. Set remotePort's has been shipped flag to true. |
91 | 0 | m_remote_port->m_has_been_shipped = true; |
92 | | |
93 | | // 2. Set dataHolder.[[RemotePort]] to remotePort. |
94 | 0 | auto fd = MUST(m_socket->release_fd()); |
95 | 0 | m_socket = nullptr; |
96 | 0 | data_holder.fds.append(IPC::File::adopt_fd(fd)); |
97 | 0 | data_holder.data.append(IPC_FILE_TAG); |
98 | 0 | } |
99 | | |
100 | | // 4. Otherwise, set dataHolder.[[RemotePort]] to null. |
101 | 0 | else { |
102 | 0 | data_holder.data.append(0); |
103 | 0 | } |
104 | |
|
105 | 0 | return {}; |
106 | 0 | } |
107 | | |
108 | | WebIDL::ExceptionOr<void> MessagePort::transfer_receiving_steps(HTML::TransferDataHolder& data_holder) |
109 | 0 | { |
110 | | // 1. Set value's has been shipped flag to true. |
111 | 0 | m_has_been_shipped = true; |
112 | | |
113 | | // FIXME 2. Move all the tasks that are to fire message events in dataHolder.[[PortMessageQueue]] to the port message queue of value, |
114 | | // if any, leaving value's port message queue in its initial disabled state, and, if value's relevant global object is a Window, |
115 | | // associating the moved tasks with value's relevant global object's associated Document. |
116 | | |
117 | | // 3. If dataHolder.[[RemotePort]] is not null, then entangle dataHolder.[[RemotePort]] and value. |
118 | | // (This will disentangle dataHolder.[[RemotePort]] from the original port that was transferred.) |
119 | 0 | auto fd_tag = data_holder.data.take_first(); |
120 | 0 | if (fd_tag == IPC_FILE_TAG) { |
121 | 0 | auto fd = data_holder.fds.take_first(); |
122 | 0 | m_socket = MUST(Core::LocalSocket::adopt_fd(fd.take_fd())); |
123 | |
|
124 | 0 | m_socket->on_ready_to_read = [strong_this = JS::make_handle(this)]() { |
125 | 0 | strong_this->read_from_socket(); |
126 | 0 | }; |
127 | 0 | } else if (fd_tag != 0) { |
128 | 0 | dbgln("Unexpected byte {:x} in MessagePort transfer data", fd_tag); |
129 | 0 | VERIFY_NOT_REACHED(); |
130 | 0 | } |
131 | | |
132 | 0 | return {}; |
133 | 0 | } |
134 | | |
135 | | void MessagePort::disentangle() |
136 | 0 | { |
137 | 0 | if (m_remote_port) |
138 | 0 | m_remote_port->m_remote_port = nullptr; |
139 | 0 | m_remote_port = nullptr; |
140 | |
|
141 | 0 | m_socket = nullptr; |
142 | |
|
143 | 0 | m_worker_event_target = nullptr; |
144 | 0 | } |
145 | | |
146 | | // https://html.spec.whatwg.org/multipage/web-messaging.html#entangle |
147 | | void MessagePort::entangle_with(MessagePort& remote_port) |
148 | 0 | { |
149 | 0 | if (m_remote_port.ptr() == &remote_port) |
150 | 0 | return; |
151 | | |
152 | | // 1. If one of the ports is already entangled, then disentangle it and the port that it was entangled with. |
153 | 0 | if (is_entangled()) |
154 | 0 | disentangle(); |
155 | 0 | if (remote_port.is_entangled()) |
156 | 0 | remote_port.disentangle(); |
157 | | |
158 | | // 2. Associate the two ports to be entangled, so that they form the two parts of a new channel. |
159 | | // (There is no MessageChannel object that represents this channel.) |
160 | 0 | remote_port.m_remote_port = this; |
161 | 0 | m_remote_port = &remote_port; |
162 | |
|
163 | 0 | auto create_paired_sockets = []() -> Array<NonnullOwnPtr<Core::LocalSocket>, 2> { |
164 | 0 | int fds[2] = {}; |
165 | 0 | MUST(Core::System::socketpair(AF_LOCAL, SOCK_STREAM, 0, fds)); |
166 | 0 | auto socket0 = MUST(Core::LocalSocket::adopt_fd(fds[0])); |
167 | 0 | MUST(socket0->set_blocking(false)); |
168 | 0 | MUST(socket0->set_close_on_exec(true)); |
169 | 0 | auto socket1 = MUST(Core::LocalSocket::adopt_fd(fds[1])); |
170 | 0 | MUST(socket1->set_blocking(false)); |
171 | 0 | MUST(socket1->set_close_on_exec(true)); |
172 | |
|
173 | 0 | return Array { move(socket0), move(socket1) }; |
174 | 0 | }; |
175 | |
|
176 | 0 | auto sockets = create_paired_sockets(); |
177 | 0 | m_socket = move(sockets[0]); |
178 | 0 | m_remote_port->m_socket = move(sockets[1]); |
179 | |
|
180 | 0 | m_socket->on_ready_to_read = [strong_this = JS::make_handle(this)]() { |
181 | 0 | strong_this->read_from_socket(); |
182 | 0 | }; |
183 | |
|
184 | 0 | m_remote_port->m_socket->on_ready_to_read = [remote_port = JS::make_handle(m_remote_port)]() { |
185 | 0 | remote_port->read_from_socket(); |
186 | 0 | }; |
187 | 0 | } |
188 | | |
189 | | // https://html.spec.whatwg.org/multipage/web-messaging.html#dom-messageport-postmessage-options |
190 | | WebIDL::ExceptionOr<void> MessagePort::post_message(JS::Value message, Vector<JS::Handle<JS::Object>> const& transfer) |
191 | 0 | { |
192 | | // 1. Let targetPort be the port with which this MessagePort is entangled, if any; otherwise let it be null. |
193 | 0 | JS::GCPtr<MessagePort> target_port = m_remote_port; |
194 | | |
195 | | // 2. Let options be «[ "transfer" → transfer ]». |
196 | 0 | auto options = StructuredSerializeOptions { transfer }; |
197 | | |
198 | | // 3. Run the message port post message steps providing this, targetPort, message and options. |
199 | 0 | return message_port_post_message_steps(target_port, message, options); |
200 | 0 | } |
201 | | |
202 | | // https://html.spec.whatwg.org/multipage/web-messaging.html#dom-messageport-postmessage |
203 | | WebIDL::ExceptionOr<void> MessagePort::post_message(JS::Value message, StructuredSerializeOptions const& options) |
204 | 0 | { |
205 | | // 1. Let targetPort be the port with which this MessagePort is entangled, if any; otherwise let it be null. |
206 | 0 | JS::GCPtr<MessagePort> target_port = m_remote_port; |
207 | | |
208 | | // 2. Run the message port post message steps providing targetPort, message and options. |
209 | 0 | return message_port_post_message_steps(target_port, message, options); |
210 | 0 | } |
211 | | |
212 | | // https://html.spec.whatwg.org/multipage/web-messaging.html#message-port-post-message-steps |
213 | | WebIDL::ExceptionOr<void> MessagePort::message_port_post_message_steps(JS::GCPtr<MessagePort> target_port, JS::Value message, StructuredSerializeOptions const& options) |
214 | 0 | { |
215 | 0 | auto& realm = this->realm(); |
216 | 0 | auto& vm = this->vm(); |
217 | | |
218 | | // 1. Let transfer be options["transfer"]. |
219 | 0 | auto const& transfer = options.transfer; |
220 | | |
221 | | // 2. If transfer contains this MessagePort, then throw a "DataCloneError" DOMException. |
222 | 0 | for (auto const& handle : transfer) { |
223 | 0 | if (handle == this) |
224 | 0 | return WebIDL::DataCloneError::create(realm, "Cannot transfer a MessagePort to itself"_string); |
225 | 0 | } |
226 | | |
227 | | // 3. Let doomed be false. |
228 | 0 | bool doomed = false; |
229 | | |
230 | | // 4. If targetPort is not null and transfer contains targetPort, then set doomed to true and optionally report to a developer console that the target port was posted to itself, causing the communication channel to be lost. |
231 | 0 | if (target_port) { |
232 | 0 | for (auto const& handle : transfer) { |
233 | 0 | if (handle == target_port.ptr()) { |
234 | 0 | doomed = true; |
235 | 0 | dbgln("FIXME: Report to a developer console that the target port was posted to itself, causing the communication channel to be lost"); |
236 | 0 | } |
237 | 0 | } |
238 | 0 | } |
239 | | |
240 | | // 5. Let serializeWithTransferResult be StructuredSerializeWithTransfer(message, transfer). Rethrow any exceptions. |
241 | 0 | auto serialize_with_transfer_result = TRY(structured_serialize_with_transfer(vm, message, transfer)); |
242 | | |
243 | | // 6. If targetPort is null, or if doomed is true, then return. |
244 | | // IMPLEMENTATION DEFINED: Actually check the socket here, not the target port. |
245 | | // If there's no target message port in the same realm, we still want to send the message over IPC |
246 | 0 | if (!m_socket || doomed) { |
247 | 0 | return {}; |
248 | 0 | } |
249 | | |
250 | | // 7. Add a task that runs the following steps to the port message queue of targetPort: |
251 | 0 | post_port_message(move(serialize_with_transfer_result)); |
252 | |
|
253 | 0 | return {}; |
254 | 0 | } |
255 | | |
256 | | ErrorOr<void> MessagePort::send_message_on_socket(SerializedTransferRecord const& serialize_with_transfer_result) |
257 | 0 | { |
258 | 0 | IPC::MessageBuffer buffer; |
259 | 0 | IPC::Encoder encoder(buffer); |
260 | 0 | MUST(encoder.encode(serialize_with_transfer_result)); |
261 | |
|
262 | 0 | TRY(buffer.transfer_message(*m_socket)); |
263 | 0 | return {}; |
264 | 0 | } |
265 | | |
266 | | void MessagePort::post_port_message(SerializedTransferRecord serialize_with_transfer_result) |
267 | 0 | { |
268 | | // FIXME: Use the correct task source? |
269 | 0 | queue_global_task(Task::Source::PostedMessage, relevant_global_object(*this), JS::create_heap_function(heap(), [this, serialize_with_transfer_result = move(serialize_with_transfer_result)]() mutable { |
270 | 0 | if (!m_socket || !m_socket->is_open()) |
271 | 0 | return; |
272 | 0 | if (auto result = send_message_on_socket(serialize_with_transfer_result); result.is_error()) { |
273 | 0 | dbgln("Failed to post message: {}", result.error()); |
274 | 0 | disentangle(); |
275 | 0 | } |
276 | 0 | })); |
277 | 0 | } |
278 | | |
279 | | ErrorOr<MessagePort::ParseDecision> MessagePort::parse_message() |
280 | 0 | { |
281 | 0 | static constexpr size_t HEADER_SIZE = sizeof(u32); |
282 | |
|
283 | 0 | auto num_bytes_ready = m_buffered_data.size(); |
284 | 0 | switch (m_socket_state) { |
285 | 0 | case SocketState::Header: { |
286 | 0 | if (num_bytes_ready < HEADER_SIZE) |
287 | 0 | return ParseDecision::NotEnoughData; |
288 | | |
289 | 0 | m_socket_incoming_message_size = ByteReader::load32(m_buffered_data.data()); |
290 | | // NOTE: We don't decrement the number of ready bytes because we want to remove the entire |
291 | | // message + header from the buffer in one go on success |
292 | 0 | m_socket_state = SocketState::Data; |
293 | 0 | [[fallthrough]]; |
294 | 0 | } |
295 | 0 | case SocketState::Data: { |
296 | 0 | if (num_bytes_ready < HEADER_SIZE + m_socket_incoming_message_size) |
297 | 0 | return ParseDecision::NotEnoughData; |
298 | | |
299 | 0 | auto payload = m_buffered_data.span().slice(HEADER_SIZE, m_socket_incoming_message_size); |
300 | |
|
301 | 0 | FixedMemoryStream stream { payload, FixedMemoryStream::Mode::ReadOnly }; |
302 | 0 | IPC::Decoder decoder { stream, m_unprocessed_fds }; |
303 | |
|
304 | 0 | auto serialized_transfer_record = TRY(decoder.decode<SerializedTransferRecord>()); |
305 | | |
306 | | // Make sure to advance our state machine before dispatching the MessageEvent, |
307 | | // as dispatching events can run arbitrary JS (and cause us to receive another message!) |
308 | 0 | m_socket_state = SocketState::Header; |
309 | |
|
310 | 0 | m_buffered_data.remove(0, HEADER_SIZE + m_socket_incoming_message_size); |
311 | |
|
312 | 0 | post_message_task_steps(serialized_transfer_record); |
313 | |
|
314 | 0 | break; |
315 | 0 | } |
316 | 0 | case SocketState::Error: |
317 | 0 | return Error::from_errno(ENOMSG); |
318 | 0 | } |
319 | | |
320 | 0 | return ParseDecision::ParseNextMessage; |
321 | 0 | } |
322 | | |
323 | | void MessagePort::read_from_socket() |
324 | 0 | { |
325 | 0 | u8 buffer[4096] {}; |
326 | |
|
327 | 0 | Vector<int> fds; |
328 | | // FIXME: What if pending bytes is > 4096? Should we loop here? |
329 | 0 | auto maybe_bytes = m_socket->receive_message(buffer, MSG_NOSIGNAL, fds); |
330 | 0 | if (maybe_bytes.is_error()) { |
331 | 0 | dbgln("MessagePort::read_from_socket(): Failed to receive message: {}", maybe_bytes.error()); |
332 | 0 | return; |
333 | 0 | } |
334 | 0 | auto bytes = maybe_bytes.release_value(); |
335 | |
|
336 | 0 | m_buffered_data.append(bytes.data(), bytes.size()); |
337 | |
|
338 | 0 | for (auto fd : fds) |
339 | 0 | m_unprocessed_fds.enqueue(IPC::File::adopt_fd(fd)); |
340 | |
|
341 | 0 | while (true) { |
342 | 0 | auto parse_decision_or_error = parse_message(); |
343 | 0 | if (parse_decision_or_error.is_error()) { |
344 | 0 | dbgln("MessagePort::read_from_socket(): Failed to parse message: {}", parse_decision_or_error.error()); |
345 | 0 | return; |
346 | 0 | } |
347 | 0 | if (parse_decision_or_error.value() == ParseDecision::NotEnoughData) |
348 | 0 | break; |
349 | 0 | } |
350 | 0 | } |
351 | | |
352 | | void MessagePort::post_message_task_steps(SerializedTransferRecord& serialize_with_transfer_result) |
353 | 0 | { |
354 | | // 1. Let finalTargetPort be the MessagePort in whose port message queue the task now finds itself. |
355 | | // NOTE: This can be different from targetPort, if targetPort itself was transferred and thus all its tasks moved along with it. |
356 | 0 | auto* final_target_port = this; |
357 | | |
358 | | // IMPLEMENTATION DEFINED: |
359 | | // https://html.spec.whatwg.org/multipage/workers.html#dedicated-workers-and-the-worker-interface |
360 | | // Worker objects act as if they had an implicit MessagePort associated with them. |
361 | | // All messages received by that port must immediately be retargeted at the Worker object. |
362 | | // We therefore set a special event target for those implicit ports on the Worker and the WorkerGlobalScope objects |
363 | 0 | EventTarget* message_event_target = final_target_port; |
364 | 0 | if (m_worker_event_target != nullptr) { |
365 | 0 | message_event_target = m_worker_event_target; |
366 | 0 | } |
367 | | |
368 | | // 2. Let targetRealm be finalTargetPort's relevant realm. |
369 | 0 | auto& target_realm = relevant_realm(*final_target_port); |
370 | 0 | auto& target_vm = target_realm.vm(); |
371 | | |
372 | | // 3. Let deserializeRecord be StructuredDeserializeWithTransfer(serializeWithTransferResult, targetRealm). |
373 | 0 | TemporaryExecutionContext context { relevant_settings_object(*final_target_port) }; |
374 | 0 | auto deserialize_record_or_error = structured_deserialize_with_transfer(target_vm, serialize_with_transfer_result); |
375 | 0 | if (deserialize_record_or_error.is_error()) { |
376 | | // If this throws an exception, catch it, fire an event named messageerror at finalTargetPort, using MessageEvent, and then return. |
377 | 0 | auto exception = deserialize_record_or_error.release_error(); |
378 | 0 | MessageEventInit event_init {}; |
379 | 0 | message_event_target->dispatch_event(MessageEvent::create(target_realm, HTML::EventNames::messageerror, event_init)); |
380 | 0 | return; |
381 | 0 | } |
382 | 0 | auto deserialize_record = deserialize_record_or_error.release_value(); |
383 | | |
384 | | // 4. Let messageClone be deserializeRecord.[[Deserialized]]. |
385 | 0 | auto message_clone = deserialize_record.deserialized; |
386 | | |
387 | | // 5. Let newPorts be a new frozen array consisting of all MessagePort objects in deserializeRecord.[[TransferredValues]], if any, maintaining their relative order. |
388 | | // FIXME: Use a FrozenArray |
389 | 0 | Vector<JS::Handle<MessagePort>> new_ports; |
390 | 0 | for (auto const& object : deserialize_record.transferred_values) { |
391 | 0 | if (is<HTML::MessagePort>(*object)) { |
392 | 0 | new_ports.append(verify_cast<MessagePort>(*object)); |
393 | 0 | } |
394 | 0 | } |
395 | | |
396 | | // 6. Fire an event named message at finalTargetPort, using MessageEvent, with the data attribute initialized to messageClone and the ports attribute initialized to newPorts. |
397 | 0 | MessageEventInit event_init {}; |
398 | 0 | event_init.data = message_clone; |
399 | 0 | event_init.ports = move(new_ports); |
400 | 0 | auto event = MessageEvent::create(target_realm, HTML::EventNames::message, event_init); |
401 | 0 | event->set_is_trusted(true); |
402 | 0 | message_event_target->dispatch_event(event); |
403 | 0 | } |
404 | | |
405 | | // https://html.spec.whatwg.org/multipage/web-messaging.html#dom-messageport-start |
406 | | void MessagePort::start() |
407 | 0 | { |
408 | 0 | if (!is_entangled()) |
409 | 0 | return; |
410 | | |
411 | 0 | VERIFY(m_socket); |
412 | | |
413 | | // TODO: The start() method steps are to enable this's port message queue, if it is not already enabled. |
414 | 0 | } |
415 | | |
416 | | // https://html.spec.whatwg.org/multipage/web-messaging.html#dom-messageport-close |
417 | | void MessagePort::close() |
418 | 0 | { |
419 | | // 1. Set this MessagePort object's [[Detached]] internal slot value to true. |
420 | 0 | set_detached(true); |
421 | | |
422 | | // 2. If this MessagePort object is entangled, disentangle it. |
423 | 0 | if (is_entangled()) |
424 | 0 | disentangle(); |
425 | 0 | } |
426 | | |
427 | | #undef __ENUMERATE |
428 | | #define __ENUMERATE(attribute_name, event_name) \ |
429 | | void MessagePort::set_##attribute_name(WebIDL::CallbackType* value) \ |
430 | 0 | { \ |
431 | 0 | set_event_handler_attribute(event_name, value); \ |
432 | 0 | } \ Unexecuted instantiation: Web::HTML::MessagePort::set_onmessage(Web::WebIDL::CallbackType*) Unexecuted instantiation: Web::HTML::MessagePort::set_onmessageerror(Web::WebIDL::CallbackType*) |
433 | | WebIDL::CallbackType* MessagePort::attribute_name() \ |
434 | 0 | { \ |
435 | 0 | return event_handler_attribute(event_name); \ |
436 | 0 | } Unexecuted instantiation: Web::HTML::MessagePort::onmessage() Unexecuted instantiation: Web::HTML::MessagePort::onmessageerror() |
437 | | ENUMERATE_MESSAGE_PORT_EVENT_HANDLERS(__ENUMERATE) |
438 | | #undef __ENUMERATE |
439 | | |
440 | | } |