servo: Merge #8825 - adding initial support for websocket subprotocol negotation (from jmr0:master); r=jdm
authorjmr0 <jrosello720@gmail.com>
Fri, 18 Dec 2015 04:53:45 +0500
changeset 337791 cef5e0ece60ca36be394e05e665fc0661469e723
parent 337790 68918e658167ea075d34e94e8ddce7f1473720f7
child 337792 ba5e21854fdc22eaded2754f55d6fadbeb49ced8
push id31307
push usergszorc@mozilla.com
push dateSat, 04 Feb 2017 00:59:06 +0000
treeherdermozilla-central@94079d43835f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdm
servo: Merge #8825 - adding initial support for websocket subprotocol negotation (from jmr0:master); r=jdm Addresses #8177 I also noticed some bugs/gaps (and at least one of my TODO's can be an E-Easy) cc @jdm Source-Repo: https://github.com/servo/servo Source-Revision: 63923bc7c923587652fb966f1f45009e483b1ebf
servo/components/net/websocket_loader.rs
servo/components/net_traits/lib.rs
servo/components/script/dom/webidls/WebSocket.webidl
servo/components/script/dom/websocket.rs
--- a/servo/components/net/websocket_loader.rs
+++ b/servo/components/net/websocket_loader.rs
@@ -1,49 +1,66 @@
 /* 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 hyper::header::Host;
 use net_traits::MessageData;
 use net_traits::hosts::replace_hosts;
+use net_traits::unwrap_websocket_protocol;
 use net_traits::{WebSocketCommunicate, WebSocketConnectData, WebSocketDomAction, WebSocketNetworkEvent};
+use std::ascii::AsciiExt;
 use std::sync::{Arc, Mutex};
 use std::thread;
 use util::task::spawn_named;
 use websocket::client::receiver::Receiver;
 use websocket::client::request::Url;
 use websocket::client::sender::Sender;
-use websocket::header::Origin;
+use websocket::header::{Headers, Origin, WebSocketProtocol};
 use websocket::message::Type;
-use websocket::result::WebSocketResult;
+use websocket::result::{WebSocketError, WebSocketResult};
 use websocket::stream::WebSocketStream;
 use websocket::ws::receiver::Receiver as WSReceiver;
 use websocket::ws::sender::Sender as Sender_Object;
 use websocket::ws::util::url::parse_url;
 use websocket::{Client, Message};
 
 /// *Establish a WebSocket Connection* as defined in RFC 6455.
 fn establish_a_websocket_connection(resource_url: &Url, net_url: (Host, String, bool),
-                                    origin: String)
-    -> WebSocketResult<(Sender<WebSocketStream>, Receiver<WebSocketStream>)> {
+                                    origin: String, protocols: Vec<String>)
+    -> WebSocketResult<(Headers, Sender<WebSocketStream>, Receiver<WebSocketStream>)> {
 
     let host = Host {
         hostname: resource_url.serialize_host().unwrap(),
         port: resource_url.port_or_default()
     };
 
     let mut request = try!(Client::connect(net_url));
     request.headers.set(Origin(origin));
     request.headers.set(host);
+    if !protocols.is_empty() {
+        request.headers.set(WebSocketProtocol(protocols.clone()));
+    };
 
     let response = try!(request.send());
     try!(response.validate());
 
-    Ok(response.begin().split())
+    {
+       let protocol_in_use = unwrap_websocket_protocol(response.protocol());
+        if let Some(protocol_name) = protocol_in_use {
+                if !protocols.is_empty() && !protocols.iter().any(|p| p.eq_ignore_ascii_case(protocol_name)) {
+                    return Err(WebSocketError::ProtocolError("Protocol in Use not in client-supplied protocol list"));
+            };
+        };
+    }
+
+    let headers = response.headers.clone();
+    let (sender, receiver) = response.begin().split();
+    Ok((headers, sender, receiver))
+
 }
 
 pub fn init(connect: WebSocketCommunicate, connect_data: WebSocketConnectData) {
     spawn_named(format!("WebSocket connection to {}", connect_data.resource_url), move || {
         // Step 8: Protocols.
 
         // Step 9.
 
@@ -55,20 +72,22 @@ pub fn init(connect: WebSocketCommunicat
             Err(e) => {
                 debug!("Failed to establish a WebSocket connection: {:?}", e);
                 let _ = connect.event_sender.send(WebSocketNetworkEvent::Close);
                 return;
             }
         };
         let channel = establish_a_websocket_connection(&connect_data.resource_url,
                                                        net_url,
-                                                       connect_data.origin);
-        let (ws_sender, mut receiver) = match channel {
+                                                       connect_data.origin,
+                                                       connect_data.protocols.clone());
+        let (_, ws_sender, mut receiver) = match channel {
             Ok(channel) => {
-                let _ = connect.event_sender.send(WebSocketNetworkEvent::ConnectionEstablished);
+                let _ = connect.event_sender.send(WebSocketNetworkEvent::ConnectionEstablished(channel.0.clone(),
+                                                                                               connect_data.protocols));
                 channel
             },
             Err(e) => {
                 debug!("Failed to establish a WebSocket connection: {:?}", e);
                 let _ = connect.event_sender.send(WebSocketNetworkEvent::Close);
                 return;
             }
 
--- a/servo/components/net_traits/lib.rs
+++ b/servo/components/net_traits/lib.rs
@@ -32,16 +32,17 @@ use hyper::status::StatusCode;
 use ipc_channel::ipc::{self, IpcReceiver, IpcSender};
 use msg::constellation_msg::{PipelineId};
 use serde::{Deserializer, Serializer};
 use std::cell::RefCell;
 use std::rc::Rc;
 use std::thread;
 use url::Url;
 use util::mem::HeapSizeOf;
+use websocket::header;
 
 pub mod hosts;
 pub mod image_cache_task;
 pub mod net_error_list;
 pub mod storage_task;
 
 /// [Response type](https://fetch.spec.whatwg.org/#concept-response-type)
 #[derive(Clone, PartialEq, Copy)]
@@ -234,31 +235,32 @@ pub enum MessageData {
 #[derive(Deserialize, Serialize)]
 pub enum WebSocketDomAction {
     SendMessage(MessageData),
     Close(u16, String),
 }
 
 #[derive(Deserialize, Serialize)]
 pub enum WebSocketNetworkEvent {
-    ConnectionEstablished,
+    ConnectionEstablished(header::Headers, Vec<String>),
     MessageReceived(MessageData),
     Close,
 }
 
 #[derive(Deserialize, Serialize)]
 pub struct WebSocketCommunicate {
     pub event_sender: IpcSender<WebSocketNetworkEvent>,
     pub action_receiver: IpcReceiver<WebSocketDomAction>,
 }
 
 #[derive(Deserialize, Serialize)]
 pub struct WebSocketConnectData {
     pub resource_url: Url,
     pub origin: String,
+    pub protocols: Vec<String>,
 }
 
 #[derive(Deserialize, Serialize)]
 pub enum ControlMsg {
     /// Request the data associated with a particular URL
     Load(LoadData, LoadConsumer, Option<IpcSender<ResourceId>>),
     /// Try to make a websocket connection to a URL.
     WebsocketConnect(WebSocketCommunicate, WebSocketConnectData),
@@ -424,11 +426,16 @@ pub fn load_whole_resource(resource_task
         match response.progress_port.recv().unwrap() {
             ProgressMsg::Payload(data) => buf.extend_from_slice(&data),
             ProgressMsg::Done(Ok(())) => return Ok((response.metadata, buf)),
             ProgressMsg::Done(Err(e)) => return Err(e)
         }
     }
 }
 
+/// Defensively unwraps the protocol string from the response object's protocol
+pub fn unwrap_websocket_protocol(wsp: Option<&header::WebSocketProtocol>) -> Option<&str> {
+    wsp.and_then(|protocol_list| protocol_list.get(0).map(|protocol| protocol.as_ref()))
+}
+
 /// An unique identifier to keep track of each load message in the resource handler
 #[derive(Clone, PartialEq, Eq, Copy, Hash, Debug, Deserialize, Serialize, HeapSizeOf)]
 pub struct ResourceId(pub u32);
--- a/servo/components/script/dom/webidls/WebSocket.webidl
+++ b/servo/components/script/dom/webidls/WebSocket.webidl
@@ -17,17 +17,17 @@ interface WebSocket : EventTarget {
     readonly attribute unsigned short readyState;
     readonly attribute unsigned long bufferedAmount;
 
     //networking
     attribute EventHandler onopen;
     attribute EventHandler onerror;
     attribute EventHandler onclose;
     //readonly attribute DOMString extensions;
-    //readonly attribute DOMString protocol;
+    readonly attribute DOMString protocol;
     [Throws] void close([Clamp] optional unsigned short code, optional USVString reason);
 
     //messaging
     attribute EventHandler onmessage;
     attribute BinaryType binaryType;
     [Throws] void send(USVString data);
     [Throws] void send(Blob data);
     //void send(ArrayBuffer data);
--- a/servo/components/script/dom/websocket.rs
+++ b/servo/components/script/dom/websocket.rs
@@ -24,26 +24,28 @@ use dom::messageevent::MessageEvent;
 use ipc_channel::ipc::{self, IpcReceiver, IpcSender};
 use js::jsapi::{JSAutoCompartment, JSAutoRequest, RootedValue};
 use js::jsapi::{JS_GetArrayBufferData, JS_NewArrayBuffer};
 use js::jsval::UndefinedValue;
 use libc::{uint32_t, uint8_t};
 use net_traits::ControlMsg::WebsocketConnect;
 use net_traits::MessageData;
 use net_traits::hosts::replace_hosts;
+use net_traits::unwrap_websocket_protocol;
 use net_traits::{WebSocketCommunicate, WebSocketConnectData, WebSocketDomAction, WebSocketNetworkEvent};
 use ref_slice::ref_slice;
 use script_task::ScriptTaskEventCategory::WebSocketEvent;
 use script_task::{CommonScriptMsg, Runnable};
 use std::borrow::ToOwned;
 use std::cell::Cell;
 use std::ptr;
 use std::thread;
 use util::str::DOMString;
 use websocket::client::request::Url;
+use websocket::header::{Headers, WebSocketProtocol};
 use websocket::ws::util::url::parse_url;
 
 #[derive(JSTraceable, PartialEq, Copy, Clone, Debug, HeapSizeOf)]
 enum WebSocketRequestState {
     Connecting = 0,
     Open = 1,
     Closing = 2,
     Closed = 3,
@@ -141,16 +143,17 @@ pub struct WebSocket {
     #[ignore_heap_size_of = "Defined in std"]
     sender: DOMRefCell<Option<IpcSender<WebSocketDomAction>>>,
     failed: Cell<bool>, //Flag to tell if websocket was closed due to failure
     full: Cell<bool>, //Flag to tell if websocket queue is full
     clean_close: Cell<bool>, //Flag to tell if the websocket closed cleanly (not due to full or fail)
     code: Cell<u16>, //Closing code
     reason: DOMRefCell<String>, //Closing reason
     binary_type: Cell<BinaryType>,
+    protocol: DOMRefCell<String>, //Subprotocol selected by server
 }
 
 impl WebSocket {
     fn new_inherited(global: GlobalRef, url: Url) -> WebSocket {
         WebSocket {
             eventtarget: EventTarget::new_inherited(),
             url: url,
             global: GlobalField::from_rooted(&global),
@@ -159,16 +162,17 @@ impl WebSocket {
             clearing_buffer: Cell::new(false),
             failed: Cell::new(false),
             sender: DOMRefCell::new(None),
             full: Cell::new(false),
             clean_close: Cell::new(true),
             code: Cell::new(0),
             reason: DOMRefCell::new("".to_owned()),
             binary_type: Cell::new(BinaryType::Blob),
+            protocol: DOMRefCell::new("".to_owned()),
         }
 
     }
 
     fn new(global: GlobalRef, url: Url) -> Root<WebSocket> {
         reflect_dom_object(box WebSocket::new_inherited(global, url),
                            global, WebSocketBinding::Wrap)
     }
@@ -203,32 +207,36 @@ impl WebSocket {
             if protocol.is_empty() {
                 return Err(Error::Syntax);
             }
 
             if protocols[i + 1..].iter().any(|p| p == protocol) {
                 return Err(Error::Syntax);
             }
 
+            // TODO: also check that no separator characters are used
+            // https://tools.ietf.org/html/rfc6455#section-4.1
             if protocol.chars().any(|c| c < '\u{0021}' || c > '\u{007E}') {
                 return Err(Error::Syntax);
             }
         }
 
         // Step 6: Origin.
 
         // Step 7.
         let ws = WebSocket::new(global, resource_url.clone());
         let address = Trusted::new(ws.r(), global.networking_task_source());
 
         let origin = global.get_url().serialize();
+        let protocols: Vec<String> = protocols.iter().map(|x| String::from(x.clone())).collect();
 
         let connect_data = WebSocketConnectData {
             resource_url: resource_url.clone(),
             origin: origin,
+            protocols: protocols,
         };
 
         // Create the interface for communication with the resource task
         let (dom_action_sender, resource_action_receiver):
                 (IpcSender<WebSocketDomAction>,
                 IpcReceiver<WebSocketDomAction>) = ipc::channel().unwrap();
         let (resource_event_sender, dom_event_receiver):
                 (IpcSender<WebSocketNetworkEvent>,
@@ -241,23 +249,24 @@ impl WebSocket {
 
         let resource_task = global.resource_task();
         let _ = resource_task.send(WebsocketConnect(connect, connect_data));
 
         *ws.sender.borrow_mut() = Some(dom_action_sender);
 
         let moved_address = address.clone();
         let sender = global.networking_task_source();
-
         thread::spawn(move || {
             while let Ok(event) = dom_event_receiver.recv() {
                 match event {
-                    WebSocketNetworkEvent::ConnectionEstablished => {
+                    WebSocketNetworkEvent::ConnectionEstablished(headers, protocols) => {
                         let open_task = box ConnectionEstablishedTask {
                             addr: moved_address.clone(),
+                            headers: headers,
+                            protocols: protocols,
                         };
                         sender.send(CommonScriptMsg::RunnableMsg(WebSocketEvent, open_task)).unwrap();
                     },
                     WebSocketNetworkEvent::MessageReceived(message) => {
                         let message_task = box MessageReceivedTask {
                             address: moved_address.clone(),
                             message: message,
                         };
@@ -353,16 +362,21 @@ impl WebSocketMethods for WebSocket {
         self.binary_type.get()
     }
 
     // https://html.spec.whatwg.org/multipage/#dom-websocket-binarytype
     fn SetBinaryType(&self, btype: BinaryType) {
         self.binary_type.set(btype)
     }
 
+    // https://html.spec.whatwg.org/multipage/#dom-websocket-protocol
+    fn Protocol(&self) -> DOMString {
+         DOMString::from(self.protocol.borrow().clone())
+    }
+
     // https://html.spec.whatwg.org/multipage/#dom-websocket-send
     fn Send(&self, data: USVString) -> Fallible<()> {
 
         let data_byte_len = data.0.as_bytes().len() as u64;
         let send_data = try!(self.send_impl(data_byte_len));
 
         if send_data {
             let mut other_sender = self.sender.borrow_mut();
@@ -443,32 +457,52 @@ impl WebSocketMethods for WebSocket {
         Ok(()) //Return Ok
     }
 }
 
 
 /// Task queued when *the WebSocket connection is established*.
 struct ConnectionEstablishedTask {
     addr: Trusted<WebSocket>,
+    protocols: Vec<String>,
+    headers: Headers,
 }
 
 impl Runnable for ConnectionEstablishedTask {
     fn handler(self: Box<Self>) {
         let ws = self.addr.root();
+        let global = ws.global.root();
+
         // Step 1: Protocols.
+        if !self.protocols.is_empty() && self.headers.get::<WebSocketProtocol>().is_none() {
+            ws.failed.set(true);
+            ws.ready_state.set(WebSocketRequestState::Closing);
+            let task = box CloseTask {
+                addr: self.addr,
+            };
+            let sender = global.r().networking_task_source();
+            sender.send(CommonScriptMsg::RunnableMsg(WebSocketEvent, task)).unwrap();
+            return;
+        }
 
         // Step 2.
         ws.ready_state.set(WebSocketRequestState::Open);
 
         // Step 3: Extensions.
+        //TODO: Set extensions to extensions in use
+
         // Step 4: Protocols.
+        let protocol_in_use = unwrap_websocket_protocol(self.headers.get::<WebSocketProtocol>());
+        if let Some(protocol_name) = protocol_in_use {
+            *ws.protocol.borrow_mut() = protocol_name.to_owned();
+        };
+
         // Step 5: Cookies.
 
         // Step 6.
-        let global = ws.global.root();
         let event = Event::new(global.r(), atom!("open"),
                                EventBubbles::DoesNotBubble,
                                EventCancelable::NotCancelable);
         event.fire(ws.upcast());
     }
 }
 
 struct BufferedAmountTask {