diff options
Diffstat (limited to 'components')
-rw-r--r-- | components/constellation/constellation.rs | 213 | ||||
-rw-r--r-- | components/msg/constellation_msg.rs | 43 | ||||
-rw-r--r-- | components/script/dom/bindings/trace.rs | 6 | ||||
-rw-r--r-- | components/script/dom/broadcastchannel.rs | 106 | ||||
-rw-r--r-- | components/script/dom/globalscope.rs | 281 | ||||
-rw-r--r-- | components/script/dom/mod.rs | 1 | ||||
-rw-r--r-- | components/script/dom/webidls/BroadcastChannel.webidl | 18 | ||||
-rw-r--r-- | components/script/dom/window.rs | 4 | ||||
-rw-r--r-- | components/script_traits/lib.rs | 65 | ||||
-rw-r--r-- | components/script_traits/script_msg.rs | 24 |
10 files changed, 747 insertions, 14 deletions
diff --git a/components/constellation/constellation.rs b/components/constellation/constellation.rs index 629921035f0..47e59c37342 100644 --- a/components/constellation/constellation.rs +++ b/components/constellation/constellation.rs @@ -126,12 +126,12 @@ use log::{Level, LevelFilter, Log, Metadata, Record}; use media::{GLPlayerThreads, WindowGLContext}; use msg::constellation_msg::{BackgroundHangMonitorRegister, HangMonitorAlert, SamplerControlMsg}; use msg::constellation_msg::{ - BrowsingContextGroupId, BrowsingContextId, HistoryStateId, PipelineId, - TopLevelBrowsingContextId, + BroadcastChannelRouterId, MessagePortId, MessagePortRouterId, PipelineNamespace, + PipelineNamespaceId, PipelineNamespaceRequest, TraversalDirection, }; use msg::constellation_msg::{ - MessagePortId, MessagePortRouterId, PipelineNamespace, PipelineNamespaceId, - PipelineNamespaceRequest, TraversalDirection, + BrowsingContextGroupId, BrowsingContextId, HistoryStateId, PipelineId, + TopLevelBrowsingContextId, }; use net_traits::pub_domains::reg_host; use net_traits::request::RequestBuilder; @@ -142,7 +142,8 @@ use profile_traits::time; use script_traits::CompositorEvent::{MouseButtonEvent, MouseMoveEvent}; use script_traits::{webdriver_msg, LogEntry, ScriptToConstellationChan, ServiceWorkerMsg}; use script_traits::{ - AnimationState, AnimationTickType, AuxiliaryBrowsingContextLoadInfo, CompositorEvent, + AnimationState, AnimationTickType, AuxiliaryBrowsingContextLoadInfo, BroadcastMsg, + CompositorEvent, }; use script_traits::{ConstellationControlMsg, DiscardBrowsingContext}; use script_traits::{DocumentActivity, DocumentState, LayoutControlMsg, LoadData, LoadOrigin}; @@ -399,6 +400,12 @@ pub struct Constellation<Message, LTF, STF> { /// A map of router-id to ipc-sender, to route messages to ports. message_port_routers: HashMap<MessagePortRouterId, IpcSender<MessagePortMsg>>, + /// A map of broadcast routers to their IPC sender. + broadcast_routers: HashMap<BroadcastChannelRouterId, IpcSender<BroadcastMsg>>, + + /// A map of origin to a map of channel-name to a list of relevant routers. + broadcast_channels: HashMap<ImmutableOrigin, HashMap<String, Vec<BroadcastChannelRouterId>>>, + /// The set of all the pipelines in the browser. (See the `pipeline` module /// for more details.) pipelines: HashMap<PipelineId, Pipeline>, @@ -961,6 +968,8 @@ where browsing_context_group_next_id: Default::default(), message_ports: HashMap::new(), message_port_routers: HashMap::new(), + broadcast_routers: HashMap::new(), + broadcast_channels: HashMap::new(), pipelines: HashMap::new(), browsing_contexts: HashMap::new(), pending_changes: vec![], @@ -1760,6 +1769,36 @@ where FromScriptMsg::EntanglePorts(port1, port2) => { self.handle_entangle_messageports(port1, port2); }, + FromScriptMsg::NewBroadcastChannelRouter(router_id, ipc_sender, origin) => { + self.handle_new_broadcast_channel_router( + source_pipeline_id, + router_id, + ipc_sender, + origin, + ); + }, + FromScriptMsg::NewBroadcastChannelNameInRouter(router_id, channel_name, origin) => { + self.handle_new_broadcast_channel_name_in_router( + source_pipeline_id, + router_id, + channel_name, + origin, + ); + }, + FromScriptMsg::RemoveBroadcastChannelNameInRouter(router_id, channel_name, origin) => { + self.handle_remove_broadcast_channel_name_in_router( + source_pipeline_id, + router_id, + channel_name, + origin, + ); + }, + FromScriptMsg::RemoveBroadcastChannelRouter(router_id, origin) => { + self.handle_remove_broadcast_channel_router(source_pipeline_id, router_id, origin); + }, + FromScriptMsg::ScheduleBroadcast(router_id, message) => { + self.handle_schedule_broadcast(source_pipeline_id, router_id, message); + }, FromScriptMsg::ForwardToEmbedder(embedder_msg) => { self.embedder_proxy .send((Some(source_top_ctx_id), embedder_msg)); @@ -1976,6 +2015,170 @@ where } } + /// Check the origin of a message against that of the pipeline it came from. + /// Note: this is still limited as a security check, + /// see https://github.com/servo/servo/issues/11722 + fn check_origin_against_pipeline( + &self, + pipeline_id: &PipelineId, + origin: &ImmutableOrigin, + ) -> Result<(), ()> { + let pipeline_origin = match self.pipelines.get(&pipeline_id) { + Some(pipeline) => pipeline.load_data.url.origin(), + None => { + warn!("Received message from closed or unknown pipeline."); + return Err(()); + }, + }; + if &pipeline_origin == origin { + return Ok(()); + } + Err(()) + } + + /// Broadcast a message via routers in various event-loops. + fn handle_schedule_broadcast( + &self, + pipeline_id: PipelineId, + router_id: BroadcastChannelRouterId, + message: BroadcastMsg, + ) { + if self + .check_origin_against_pipeline(&pipeline_id, &message.origin) + .is_err() + { + return warn!( + "Attempt to schedule broadcast from an origin not matching the origin of the msg." + ); + } + if let Some(channels) = self.broadcast_channels.get(&message.origin) { + let routers = match channels.get(&message.channel_name) { + Some(routers) => routers, + None => return warn!("Broadcast to channel name without active routers."), + }; + for router in routers { + // Exclude the sender of the broadcast. + // Broadcasting locally is done at the point of sending. + if router == &router_id { + continue; + } + + if let Some(sender) = self.broadcast_routers.get(&router) { + if sender.send(message.clone()).is_err() { + warn!("Failed to broadcast message to router: {:?}", router); + } + } else { + warn!("No sender for broadcast router: {:?}", router); + } + } + } else { + warn!( + "Attempt to schedule a broadcast for an origin without routers {:?}", + message.origin + ); + } + } + + /// Remove a channel-name for a given broadcast router. + fn handle_remove_broadcast_channel_name_in_router( + &mut self, + pipeline_id: PipelineId, + router_id: BroadcastChannelRouterId, + channel_name: String, + origin: ImmutableOrigin, + ) { + if self + .check_origin_against_pipeline(&pipeline_id, &origin) + .is_err() + { + return warn!("Attempt to remove channel name from an unexpected origin."); + } + if let Some(channels) = self.broadcast_channels.get_mut(&origin) { + let is_empty = if let Some(routers) = channels.get_mut(&channel_name) { + routers.retain(|router| router != &router_id); + routers.is_empty() + } else { + return warn!( + "Multiple attemps to remove name for broadcast-channel {:?} at {:?}", + channel_name, origin + ); + }; + if is_empty { + channels.remove(&channel_name); + } + } else { + warn!( + "Attempt to remove a channel-name for an origin without channels {:?}", + origin + ); + } + } + + /// Note a new channel-name relevant to a given broadcast router. + fn handle_new_broadcast_channel_name_in_router( + &mut self, + pipeline_id: PipelineId, + router_id: BroadcastChannelRouterId, + channel_name: String, + origin: ImmutableOrigin, + ) { + if self + .check_origin_against_pipeline(&pipeline_id, &origin) + .is_err() + { + return warn!("Attempt to add channel name from an unexpected origin."); + } + let channels = self + .broadcast_channels + .entry(origin) + .or_insert_with(HashMap::new); + + let routers = channels.entry(channel_name).or_insert_with(Vec::new); + + routers.push(router_id); + } + + /// Remove a broadcast router. + fn handle_remove_broadcast_channel_router( + &mut self, + pipeline_id: PipelineId, + router_id: BroadcastChannelRouterId, + origin: ImmutableOrigin, + ) { + if self + .check_origin_against_pipeline(&pipeline_id, &origin) + .is_err() + { + return warn!("Attempt to remove broadcast router from an unexpected origin."); + } + if self.broadcast_routers.remove(&router_id).is_none() { + warn!("Attempt to remove unknown broadcast-channel router."); + } + } + + /// Add a new broadcast router. + fn handle_new_broadcast_channel_router( + &mut self, + pipeline_id: PipelineId, + router_id: BroadcastChannelRouterId, + ipc_sender: IpcSender<BroadcastMsg>, + origin: ImmutableOrigin, + ) { + if self + .check_origin_against_pipeline(&pipeline_id, &origin) + .is_err() + { + return warn!("Attempt to add broadcast router from an unexpected origin."); + } + if self + .broadcast_routers + .insert(router_id, ipc_sender) + .is_some() + { + warn!("Multple attempt to add broadcast-channel router."); + } + } + fn handle_request_wgpu_adapter( &mut self, source_pipeline_id: PipelineId, diff --git a/components/msg/constellation_msg.rs b/components/msg/constellation_msg.rs index f4e7a517673..a878bc43d3d 100644 --- a/components/msg/constellation_msg.rs +++ b/components/msg/constellation_msg.rs @@ -171,6 +171,13 @@ impl PipelineNamespace { } } + fn next_broadcast_channel_router_id(&mut self) -> BroadcastChannelRouterId { + BroadcastChannelRouterId { + namespace_id: self.id, + index: BroadcastChannelRouterIndex(self.next_index()), + } + } + fn next_blob_id(&mut self) -> BlobId { BlobId { namespace_id: self.id, @@ -381,6 +388,42 @@ impl fmt::Display for MessagePortRouterId { } #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +pub struct BroadcastChannelRouterIndex(pub NonZeroU32); +malloc_size_of_is_0!(BroadcastChannelRouterIndex); + +#[derive( + Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, Ord, PartialEq, PartialOrd, Serialize, +)] +pub struct BroadcastChannelRouterId { + pub namespace_id: PipelineNamespaceId, + pub index: BroadcastChannelRouterIndex, +} + +impl BroadcastChannelRouterId { + pub fn new() -> BroadcastChannelRouterId { + PIPELINE_NAMESPACE.with(|tls| { + let mut namespace = tls.get().expect("No namespace set for this thread!"); + let next_broadcast_channel_router_id = namespace.next_broadcast_channel_router_id(); + tls.set(Some(namespace)); + next_broadcast_channel_router_id + }) + } +} + +impl fmt::Display for BroadcastChannelRouterId { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + let PipelineNamespaceId(namespace_id) = self.namespace_id; + let BroadcastChannelRouterIndex(index) = self.index; + write!( + fmt, + "(BroadcastChannelRouterId{},{})", + namespace_id, + index.get() + ) + } +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] pub struct BlobIndex(pub NonZeroU32); malloc_size_of_is_0!(BlobIndex); diff --git a/components/script/dom/bindings/trace.rs b/components/script/dom/bindings/trace.rs index 3fb93b98dfa..5e99e78d03d 100644 --- a/components/script/dom/bindings/trace.rs +++ b/components/script/dom/bindings/trace.rs @@ -83,8 +83,8 @@ use media::WindowGLContext; use metrics::{InteractiveMetrics, InteractiveWindow}; use mime::Mime; use msg::constellation_msg::{ - BlobId, BrowsingContextId, HistoryStateId, MessagePortId, MessagePortRouterId, PipelineId, - TopLevelBrowsingContextId, + BlobId, BroadcastChannelRouterId, BrowsingContextId, HistoryStateId, MessagePortId, + MessagePortRouterId, PipelineId, TopLevelBrowsingContextId, }; use net_traits::filemanager_thread::RelativePos; use net_traits::image::base::{Image, ImageMetadata}; @@ -175,6 +175,8 @@ unsafe_no_jsmanaged_fields!(MessagePortId); unsafe_no_jsmanaged_fields!(RefCell<Option<MessagePortId>>); unsafe_no_jsmanaged_fields!(MessagePortRouterId); +unsafe_no_jsmanaged_fields!(BroadcastChannelRouterId); + unsafe_no_jsmanaged_fields!(BlobId); unsafe_no_jsmanaged_fields!(BlobImpl); diff --git a/components/script/dom/broadcastchannel.rs b/components/script/dom/broadcastchannel.rs new file mode 100644 index 00000000000..4a1e35d2ad8 --- /dev/null +++ b/components/script/dom/broadcastchannel.rs @@ -0,0 +1,106 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::dom::bindings::codegen::Bindings::BroadcastChannelBinding::{ + BroadcastChannelMethods, Wrap, +}; +use crate::dom::bindings::error::{Error, ErrorResult}; +use crate::dom::bindings::reflector::{reflect_dom_object, DomObject}; +use crate::dom::bindings::root::DomRoot; +use crate::dom::bindings::str::DOMString; +use crate::dom::bindings::structuredclone; +use crate::dom::eventtarget::EventTarget; +use crate::dom::globalscope::GlobalScope; +use crate::script_runtime::JSContext as SafeJSContext; +use dom_struct::dom_struct; +use js::rust::HandleValue; +use script_traits::BroadcastMsg; +use std::cell::Cell; +use uuid::Uuid; + +#[dom_struct] +pub struct BroadcastChannel { + eventtarget: EventTarget, + name: DOMString, + closed: Cell<bool>, + id: Uuid, +} + +impl BroadcastChannel { + /// <https://html.spec.whatwg.org/multipage/#broadcastchannel> + #[allow(non_snake_case)] + pub fn Constructor(global: &GlobalScope, name: DOMString) -> DomRoot<BroadcastChannel> { + BroadcastChannel::new(global, name) + } + + pub fn new(global: &GlobalScope, name: DOMString) -> DomRoot<BroadcastChannel> { + let channel = reflect_dom_object( + Box::new(BroadcastChannel::new_inherited(name)), + global, + Wrap, + ); + global.track_broadcast_channel(&*channel); + channel + } + + pub fn new_inherited(name: DOMString) -> BroadcastChannel { + BroadcastChannel { + eventtarget: EventTarget::new_inherited(), + name, + closed: Default::default(), + id: Uuid::new_v4(), + } + } + + /// The unique Id of this channel. + /// Used for filtering out the sender from the local broadcast. + pub fn id(&self) -> &Uuid { + &self.id + } + + /// Is this channel closed? + pub fn closed(&self) -> bool { + self.closed.get() + } +} + +impl BroadcastChannelMethods for BroadcastChannel { + /// <https://html.spec.whatwg.org/multipage/#dom-messageport-postmessage> + fn PostMessage(&self, cx: SafeJSContext, message: HandleValue) -> ErrorResult { + // Step 3, if closed. + if self.closed.get() { + return Err(Error::InvalidState); + } + + // Step 6, StructuredSerialize(message). + let data = structuredclone::write(cx, message, None)?; + + let global = self.global(); + + let msg = BroadcastMsg { + origin: global.origin().immutable().clone(), + channel_name: self.Name().to_string(), + data, + }; + + global.schedule_broadcast(msg, &self.id); + Ok(()) + } + + /// <https://html.spec.whatwg.org/multipage/#dom-broadcastchannel-name> + fn Name(&self) -> DOMString { + self.name.clone() + } + + /// <https://html.spec.whatwg.org/multipage/#dom-broadcastchannel-close> + fn Close(&self) { + self.closed.set(true); + } + + /// <https://html.spec.whatwg.org/multipage/#handler-broadcastchannel-onmessageerror> + event_handler!(messageerror, GetOnmessageerror, SetOnmessageerror); + + /// <https://html.spec.whatwg.org/multipage/#handler-broadcastchannel-onmessage> + event_handler!(message, GetOnmessage, SetOnmessage); +} diff --git a/components/script/dom/globalscope.rs b/components/script/dom/globalscope.rs index de3e83101e1..2ee7190cfed 100644 --- a/components/script/dom/globalscope.rs +++ b/components/script/dom/globalscope.rs @@ -3,6 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use crate::dom::bindings::cell::DomRefCell; +use crate::dom::bindings::codegen::Bindings::BroadcastChannelBinding::BroadcastChannelMethods; use crate::dom::bindings::codegen::Bindings::EventSourceBinding::EventSourceBinding::EventSourceMethods; use crate::dom::bindings::codegen::Bindings::VoidFunctionBinding::VoidFunction; use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; @@ -19,6 +20,7 @@ use crate::dom::bindings::structuredclone; use crate::dom::bindings::utils::to_frozen_array; use crate::dom::bindings::weakref::{DOMTracker, WeakRef}; use crate::dom::blob::Blob; +use crate::dom::broadcastchannel::BroadcastChannel; use crate::dom::crypto::Crypto; use crate::dom::dedicatedworkerglobalscope::DedicatedWorkerGlobalScope; use crate::dom::errorevent::ErrorEvent; @@ -71,7 +73,9 @@ use js::rust::wrappers::EvaluateUtf8; use js::rust::{get_object_class, CompileOptionsWrapper, ParentRuntime, Runtime}; use js::rust::{HandleValue, MutableHandleValue}; use js::{JSCLASS_IS_DOMJSCLASS, JSCLASS_IS_GLOBAL}; -use msg::constellation_msg::{BlobId, MessagePortId, MessagePortRouterId, PipelineId}; +use msg::constellation_msg::{ + BlobId, BroadcastChannelRouterId, MessagePortId, MessagePortRouterId, PipelineId, +}; use net_traits::blob_url_store::{get_blob_origin, BlobBuf}; use net_traits::filemanager_thread::{ FileManagerResult, FileManagerThreadMsg, ReadFileProgress, RelativePos, @@ -82,7 +86,8 @@ use profile_traits::{ipc as profile_ipc, mem as profile_mem, time as profile_tim use script_traits::serializable::{BlobData, BlobImpl, FileBlob}; use script_traits::transferable::MessagePortImpl; use script_traits::{ - MessagePortMsg, MsDuration, PortMessageTask, ScriptMsg, ScriptToConstellationChan, TimerEvent, + BroadcastMsg, MessagePortMsg, MsDuration, PortMessageTask, ScriptMsg, + ScriptToConstellationChan, TimerEvent, }; use script_traits::{TimerEventId, TimerSchedulerMsg, TimerSource}; use servo_url::{MutableOrigin, ServoUrl}; @@ -124,6 +129,9 @@ pub struct GlobalScope { /// The message-port router id for this global, if it is managing ports. message_port_state: DomRefCell<MessagePortState>, + /// The broadcast channels state this global, if it is managing any. + broadcast_channel_state: DomRefCell<BroadcastChannelState>, + /// The blobs managed by this global, if any. blob_state: DomRefCell<BlobState>, @@ -237,6 +245,13 @@ struct MessageListener { context: Trusted<GlobalScope>, } +/// A wrapper for broadcasts coming in over IPC, and the event-loop. +struct BroadcastListener { + canceller: TaskCanceller, + task_source: DOMManipulationTaskSource, + context: Trusted<GlobalScope>, +} + /// A wrapper between timer events coming in over IPC, and the event-loop. struct TimerListener { canceller: TaskCanceller, @@ -310,6 +325,23 @@ pub struct ManagedMessagePort { closed: bool, } +/// State representing whether this global is currently managing broadcast channels. +#[derive(JSTraceable, MallocSizeOf)] +#[unrooted_must_root_lint::must_root] +pub enum BroadcastChannelState { + /// The broadcast-channel router id for this global, and a queue of managed channels. + /// Step 9, "sort destinations" + /// of https://html.spec.whatwg.org/multipage/#dom-broadcastchannel-postmessage + /// requires keeping track of creation order, hence the queue. + Managed( + BroadcastChannelRouterId, + /// The map of channel-name to queue of channels, in order of creation. + HashMap<DOMString, VecDeque<Dom<BroadcastChannel>>>, + ), + /// This global is not managing any broadcast channels at this time. + UnManaged, +} + /// State representing whether this global is currently managing messageports. #[derive(JSTraceable, MallocSizeOf)] #[unrooted_must_root_lint::must_root] @@ -323,6 +355,29 @@ pub enum MessagePortState { UnManaged, } +impl BroadcastListener { + /// Handle a broadcast coming in over IPC, + /// by queueing the appropriate task on the relevant event-loop. + fn handle(&self, event: BroadcastMsg) { + let context = self.context.clone(); + + // Note: strictly speaking we should just queue the message event tasks, + // not queue a task that then queues more tasks. + // This however seems to be hard to avoid in the light of the IPC. + // One can imagine queueing tasks directly, + // for channels that would be in the same script-thread. + let _ = self.task_source.queue_with_canceller( + task!(broadcast_message_event: move || { + let global = context.root(); + // Step 10 of https://html.spec.whatwg.org/multipage/#dom-broadcastchannel-postmessage, + // For each BroadcastChannel object destination in destinations, queue a task. + global.broadcast_message_event(event, None); + }), + &self.canceller, + ); + } +} + impl TimerListener { /// Handle a timer-event coming-in over IPC, /// by queuing the appropriate task on the relevant event-loop. @@ -501,6 +556,7 @@ impl GlobalScope { ) -> Self { Self { message_port_state: DomRefCell::new(MessagePortState::UnManaged), + broadcast_channel_state: DomRefCell::new(BroadcastChannelState::UnManaged), blob_state: DomRefCell::new(BlobState::UnManaged), eventtarget: EventTarget::new_inherited(), crypto: Default::default(), @@ -613,11 +669,18 @@ impl GlobalScope { pub fn perform_a_dom_garbage_collection_checkpoint(&self) { self.perform_a_message_port_garbage_collection_checkpoint(); self.perform_a_blob_garbage_collection_checkpoint(); + self.perform_a_broadcast_channel_garbage_collection_checkpoint(); + } + + /// Remove the routers for ports and broadcast-channels. + pub fn remove_web_messaging_infra(&self) { + self.remove_message_ports_router(); + self.remove_broadcast_channel_router(); } /// Update our state to un-managed, /// and tell the constellation to drop the sender to our message-port router. - pub fn remove_message_ports_router(&self) { + fn remove_message_ports_router(&self) { if let MessagePortState::Managed(router_id, _message_ports) = &*self.message_port_state.borrow() { @@ -628,6 +691,22 @@ impl GlobalScope { *self.message_port_state.borrow_mut() = MessagePortState::UnManaged; } + /// Update our state to un-managed, + /// and tell the constellation to drop the sender to our broadcast router. + fn remove_broadcast_channel_router(&self) { + if let BroadcastChannelState::Managed(router_id, _channels) = + &*self.broadcast_channel_state.borrow() + { + let _ = + self.script_to_constellation_chan() + .send(ScriptMsg::RemoveBroadcastChannelRouter( + router_id.clone(), + self.origin().immutable().clone(), + )); + } + *self.broadcast_channel_state.borrow_mut() = BroadcastChannelState::UnManaged; + } + /// <https://html.spec.whatwg.org/multipage/#entangle> pub fn entangle_ports(&self, port1: MessagePortId, port2: MessagePortId) { if let MessagePortState::Managed(_id, message_ports) = @@ -789,6 +868,115 @@ impl GlobalScope { .send(ScriptMsg::RerouteMessagePort(port_id, task)); } + /// <https://html.spec.whatwg.org/multipage/#dom-broadcastchannel-postmessage> + /// Step 7 and following steps. + pub fn schedule_broadcast(&self, msg: BroadcastMsg, channel_id: &Uuid) { + // First, broadcast locally. + self.broadcast_message_event(msg.clone(), Some(channel_id)); + + if let BroadcastChannelState::Managed(router_id, _) = + &*self.broadcast_channel_state.borrow() + { + // Second, broadcast to other globals via the constellation. + // + // Note: for globals in the same script-thread, + // we could skip the hop to the constellation. + let _ = self + .script_to_constellation_chan() + .send(ScriptMsg::ScheduleBroadcast(router_id.clone(), msg)); + } else { + panic!("Attemps to broadcast a message via global not managing any channels."); + } + } + + /// <https://html.spec.whatwg.org/multipage/#dom-broadcastchannel-postmessage> + /// Step 7 and following steps. + pub fn broadcast_message_event(&self, event: BroadcastMsg, channel_id: Option<&Uuid>) { + if let BroadcastChannelState::Managed(_, channels) = &*self.broadcast_channel_state.borrow() + { + let BroadcastMsg { + data, + origin, + channel_name, + } = event; + + // Step 7, a few preliminary steps. + + // - Check the worker is not closing. + if let Some(worker) = self.downcast::<WorkerGlobalScope>() { + if worker.is_closing() { + return; + } + } + + // - Check the associated document is fully-active. + if let Some(window) = self.downcast::<Window>() { + if !window.Document().is_fully_active() { + return; + } + } + + // - Check for a case-sensitive match for the name of the channel. + let channel_name = DOMString::from_string(channel_name); + + if let Some(channels) = channels.get(&channel_name) { + channels + .iter() + .filter(|ref channel| { + // Step 8. + // Filter out the sender. + if let Some(id) = channel_id { + channel.id() != id + } else { + true + } + }) + .map(|channel| DomRoot::from_ref(&**channel)) + // Step 9, sort by creation order, + // done by using a queue to store channels in creation order. + .for_each(|channel| { + let data = data.clone_for_broadcast(); + let origin = origin.clone(); + + // Step 10: Queue a task on the DOM manipulation task-source, + // to fire the message event + let channel = Trusted::new(&*channel); + let global = Trusted::new(&*self); + let _ = self.dom_manipulation_task_source().queue( + task!(process_pending_port_messages: move || { + let destination = channel.root(); + let global = global.root(); + + // 10.1 Check for closed flag. + if destination.closed() { + return; + } + + rooted!(in(*global.get_cx()) let mut message = UndefinedValue()); + + // Step 10.3 StructuredDeserialize(serialized, targetRealm). + if let Ok(ports) = structuredclone::read(&global, data, message.handle_mut()) { + // Step 10.4, Fire an event named message at destination. + MessageEvent::dispatch_jsval( + &*destination.upcast(), + &global, + message.handle(), + Some(&origin.ascii_serialization()), + None, + ports, + ); + } else { + // Step 10.3, fire an event named messageerror at destination. + MessageEvent::dispatch_error(&*destination.upcast(), &global); + } + }), + &self, + ); + }); + } + } + } + /// Route the task to be handled by the relevant port. pub fn route_task_to_port(&self, port_id: MessagePortId, task: PortMessageTask) { let should_dispatch = if let MessagePortState::Managed(_id, message_ports) = @@ -905,6 +1093,93 @@ impl GlobalScope { } } + /// Remove broadcast-channels that are closed. + /// TODO: Also remove them if they do not have an event-listener. + /// see https://github.com/servo/servo/issues/25772 + pub fn perform_a_broadcast_channel_garbage_collection_checkpoint(&self) { + let is_empty = if let BroadcastChannelState::Managed(router_id, ref mut channels) = + &mut *self.broadcast_channel_state.borrow_mut() + { + channels.retain(|name, ref mut channels| { + channels.retain(|ref chan| !chan.closed()); + if channels.is_empty() { + let _ = self.script_to_constellation_chan().send( + ScriptMsg::RemoveBroadcastChannelNameInRouter( + router_id.clone(), + name.to_string(), + self.origin().immutable().clone(), + ), + ); + false + } else { + true + } + }); + channels.is_empty() + } else { + false + }; + if is_empty { + self.remove_broadcast_channel_router(); + } + } + + /// Start tracking a broadcast-channel. + pub fn track_broadcast_channel(&self, dom_channel: &BroadcastChannel) { + let mut current_state = self.broadcast_channel_state.borrow_mut(); + + if let BroadcastChannelState::UnManaged = &*current_state { + // Setup a route for IPC, for broadcasts from the constellation to our channels. + let (broadcast_control_sender, broadcast_control_receiver) = + ipc::channel().expect("ipc channel failure"); + let context = Trusted::new(self); + let (task_source, canceller) = ( + self.dom_manipulation_task_source(), + self.task_canceller(TaskSourceName::DOMManipulation), + ); + let listener = BroadcastListener { + canceller, + task_source, + context, + }; + ROUTER.add_route( + broadcast_control_receiver.to_opaque(), + Box::new(move |message| { + let msg = message.to(); + match msg { + Ok(msg) => listener.handle(msg), + Err(err) => warn!("Error receiving a BroadcastMsg: {:?}", err), + } + }), + ); + let router_id = BroadcastChannelRouterId::new(); + *current_state = BroadcastChannelState::Managed(router_id.clone(), HashMap::new()); + let _ = self + .script_to_constellation_chan() + .send(ScriptMsg::NewBroadcastChannelRouter( + router_id, + broadcast_control_sender, + self.origin().immutable().clone(), + )); + } + + if let BroadcastChannelState::Managed(router_id, channels) = &mut *current_state { + let entry = channels.entry(dom_channel.Name()).or_insert_with(|| { + let _ = self.script_to_constellation_chan().send( + ScriptMsg::NewBroadcastChannelNameInRouter( + router_id.clone(), + dom_channel.Name().to_string(), + self.origin().immutable().clone(), + ), + ); + VecDeque::new() + }); + entry.push_back(Dom::from_ref(dom_channel)); + } else { + panic!("track_broadcast_channel should have first switched the state to managed."); + } + } + /// Start tracking a message-port pub fn track_message_port(&self, dom_port: &MessagePort, port_impl: Option<MessagePortImpl>) { let mut current_state = self.message_port_state.borrow_mut(); diff --git a/components/script/dom/mod.rs b/components/script/dom/mod.rs index e25a88b9da2..fba7200c892 100644 --- a/components/script/dom/mod.rs +++ b/components/script/dom/mod.rs @@ -239,6 +239,7 @@ pub mod bluetoothremotegattdescriptor; pub mod bluetoothremotegattserver; pub mod bluetoothremotegattservice; pub mod bluetoothuuid; +pub mod broadcastchannel; pub mod canvasgradient; pub mod canvaspattern; pub mod canvasrenderingcontext2d; diff --git a/components/script/dom/webidls/BroadcastChannel.webidl b/components/script/dom/webidls/BroadcastChannel.webidl new file mode 100644 index 00000000000..6d72f3997cf --- /dev/null +++ b/components/script/dom/webidls/BroadcastChannel.webidl @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* + * The origin of this IDL file is: + * https://html.spec.whatwg.org/multipage/#broadcastchannel + */ + +[Exposed=(Window,Worker)] +interface BroadcastChannel : EventTarget { + constructor(DOMString name); + + readonly attribute DOMString name; + [Throws] void postMessage(any message); + void close(); + attribute EventHandler onmessage; + attribute EventHandler onmessageerror; +}; diff --git a/components/script/dom/window.rs b/components/script/dom/window.rs index 77021c3df48..c2679c2899a 100644 --- a/components/script/dom/window.rs +++ b/components/script/dom/window.rs @@ -1407,8 +1407,8 @@ impl Window { .upcast::<Node>() .teardown(self.layout_chan()); - // Tell the constellation to drop the sender to our message-port router, if there is any. - self.upcast::<GlobalScope>().remove_message_ports_router(); + // Remove the infra for managing messageports and broadcast channels. + self.upcast::<GlobalScope>().remove_web_messaging_infra(); // Clean up any active promises // https://github.com/servo/servo/issues/15318 diff --git a/components/script_traits/lib.rs b/components/script_traits/lib.rs index d928335f8e3..9b4588a3cb0 100644 --- a/components/script_traits/lib.rs +++ b/components/script_traits/lib.rs @@ -21,7 +21,7 @@ pub mod serializable; pub mod transferable; pub mod webdriver_msg; -use crate::serializable::BlobImpl; +use crate::serializable::{BlobData, BlobImpl}; use crate::transferable::MessagePortImpl; use crate::webdriver_msg::{LoadStatus, WebDriverScriptCommand}; use bluetooth_traits::BluetoothRequest; @@ -955,6 +955,48 @@ pub struct StructuredSerializedData { pub ports: Option<HashMap<MessagePortId, MessagePortImpl>>, } +impl StructuredSerializedData { + /// Clone the serialized data for use with broadcast-channels. + pub fn clone_for_broadcast(&self) -> StructuredSerializedData { + let serialized = self.serialized.clone(); + + let blobs = if let Some(blobs) = self.blobs.as_ref() { + let mut blob_clones = HashMap::with_capacity(blobs.len()); + + for (original_id, blob) in blobs.iter() { + let type_string = blob.type_string(); + + if let BlobData::Memory(ref bytes) = blob.blob_data() { + let blob_clone = BlobImpl::new_from_bytes(bytes.clone(), type_string); + + // Note: we insert the blob at the original id, + // otherwise this will not match the storage key as serialized by SM in `serialized`. + // The clone has it's own new Id however. + blob_clones.insert(original_id.clone(), blob_clone); + } else { + // Not panicking only because this is called from the constellation. + warn!("Serialized blob not in memory format(should never happen)."); + } + } + Some(blob_clones) + } else { + None + }; + + if self.ports.is_some() { + // Not panicking only because this is called from the constellation. + warn!("Attempt to broadcast structured serialized data including ports(should never happen)."); + } + + StructuredSerializedData { + serialized, + blobs, + // Ports cannot be broadcast. + ports: None, + } + } +} + /// A task on the https://html.spec.whatwg.org/multipage/#port-message-queue #[derive(Debug, Deserialize, MallocSizeOf, Serialize)] pub struct PortMessageTask { @@ -979,6 +1021,27 @@ pub enum MessagePortMsg { NewTask(MessagePortId, PortMessageTask), } +/// Message for communication between the constellation and a global managing broadcast channels. +#[derive(Debug, Deserialize, Serialize)] +pub struct BroadcastMsg { + /// The origin of this message. + pub origin: ImmutableOrigin, + /// The name of the channel. + pub channel_name: String, + /// A data-holder for serialized data. + pub data: StructuredSerializedData, +} + +impl Clone for BroadcastMsg { + fn clone(&self) -> BroadcastMsg { + BroadcastMsg { + data: self.data.clone_for_broadcast(), + origin: self.origin.clone(), + channel_name: self.channel_name.clone(), + } + } +} + /// The type of MediaSession action. /// https://w3c.github.io/mediasession/#enumdef-mediasessionaction #[derive(Clone, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize)] diff --git a/components/script_traits/script_msg.rs b/components/script_traits/script_msg.rs index 8a04b59d57d..918b4f4ff78 100644 --- a/components/script_traits/script_msg.rs +++ b/components/script_traits/script_msg.rs @@ -4,6 +4,7 @@ use crate::AnimationState; use crate::AuxiliaryBrowsingContextLoadInfo; +use crate::BroadcastMsg; use crate::DocumentState; use crate::IFrameLoadInfoWithData; use crate::LayoutControlMsg; @@ -22,7 +23,8 @@ use euclid::Size2D; use gfx_traits::Epoch; use ipc_channel::ipc::{IpcReceiver, IpcSender}; use msg::constellation_msg::{ - BrowsingContextId, MessagePortId, MessagePortRouterId, PipelineId, TopLevelBrowsingContextId, + BroadcastChannelRouterId, BrowsingContextId, MessagePortId, MessagePortRouterId, PipelineId, + TopLevelBrowsingContextId, }; use msg::constellation_msg::{HistoryStateId, TraversalDirection}; use net_traits::request::RequestBuilder; @@ -142,6 +144,21 @@ pub enum ScriptMsg { RemoveMessagePort(MessagePortId), /// Entangle two message-ports. EntanglePorts(MessagePortId, MessagePortId), + /// A global has started managing broadcast-channels. + NewBroadcastChannelRouter( + BroadcastChannelRouterId, + IpcSender<BroadcastMsg>, + ImmutableOrigin, + ), + /// A global has stopped managing broadcast-channels. + RemoveBroadcastChannelRouter(BroadcastChannelRouterId, ImmutableOrigin), + /// A global started managing broadcast channels for a given channel-name. + NewBroadcastChannelNameInRouter(BroadcastChannelRouterId, String, ImmutableOrigin), + /// A global stopped managing broadcast channels for a given channel-name. + RemoveBroadcastChannelNameInRouter(BroadcastChannelRouterId, String, ImmutableOrigin), + /// Broadcast a message to all same-origin broadcast channels, + /// excluding the source of the broadcast. + ScheduleBroadcast(BroadcastChannelRouterId, BroadcastMsg), /// Forward a message to the embedder. ForwardToEmbedder(EmbedderMsg), /// Requests are sent to constellation and fetches are checked manually @@ -280,6 +297,11 @@ impl fmt::Debug for ScriptMsg { RemoveMessagePort(..) => "RemoveMessagePort", MessagePortShipped(..) => "MessagePortShipped", EntanglePorts(..) => "EntanglePorts", + NewBroadcastChannelRouter(..) => "NewBroadcastChannelRouter", + RemoveBroadcastChannelRouter(..) => "RemoveBroadcastChannelRouter", + RemoveBroadcastChannelNameInRouter(..) => "RemoveBroadcastChannelNameInRouter", + NewBroadcastChannelNameInRouter(..) => "NewBroadcastChannelNameInRouter", + ScheduleBroadcast(..) => "ScheduleBroadcast", ForwardToEmbedder(..) => "ForwardToEmbedder", InitiateNavigateRequest(..) => "InitiateNavigateRequest", BroadcastStorageEvent(..) => "BroadcastStorageEvent", |