diff --git a/components/net/fetch/methods.rs b/components/net/fetch/methods.rs index 27af691c7ed4..4f5e91471b0f 100644 --- a/components/net/fetch/methods.rs +++ b/components/net/fetch/methods.rs @@ -527,7 +527,7 @@ fn is_null_body_status(status: &Option) -> bool { } /// https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-nosniff? -fn should_be_blocked_due_to_nosniff(request_type: Type, response_headers: &Headers) -> bool { +pub fn should_be_blocked_due_to_nosniff(request_type: Type, response_headers: &Headers) -> bool { /// https://fetch.spec.whatwg.org/#x-content-type-options-header /// This is needed to parse `X-Content-Type-Options` according to spec, /// which requires that we inspect only the first value. diff --git a/components/net/http_loader.rs b/components/net/http_loader.rs index 3b4b9e89af19..589164992666 100644 --- a/components/net/http_loader.rs +++ b/components/net/http_loader.rs @@ -1359,7 +1359,7 @@ fn response_needs_revalidation(_response: &Response) -> bool { } /// https://fetch.spec.whatwg.org/#redirect-status -fn is_redirect_status(status: StatusCode) -> bool { +pub fn is_redirect_status(status: StatusCode) -> bool { match status { StatusCode::MovedPermanently | StatusCode::Found | diff --git a/components/net/resource_thread.rs b/components/net/resource_thread.rs index 55afbc754d83..d216ccbd9071 100644 --- a/components/net/resource_thread.rs +++ b/components/net/resource_thread.rs @@ -355,6 +355,9 @@ impl CoreResourceManager { connect: WebSocketCommunicate, connect_data: WebSocketConnectData, resource_grp: &ResourceGroup) { - websocket_loader::init(connect, connect_data, resource_grp.cookie_jar.clone()); + websocket_loader::init(connect, + connect_data, + resource_grp.cookie_jar.clone(), + resource_grp.ssl_context.clone()); } } diff --git a/components/net/websocket_loader.rs b/components/net/websocket_loader.rs index 97e56fe076e8..297cb3987045 100644 --- a/components/net/websocket_loader.rs +++ b/components/net/websocket_loader.rs @@ -4,33 +4,44 @@ use cookie::Cookie; use cookie_storage::CookieStorage; -use fetch::methods::should_be_blocked_due_to_bad_port; -use http_loader; -use hyper::header::{Host, SetCookie}; -use net_traits::{CookieSource, MessageData, WebSocketCommunicate}; -use net_traits::{WebSocketConnectData, WebSocketDomAction, WebSocketNetworkEvent}; -use net_traits::hosts::replace_host_in_url; +use fetch::methods::{should_be_blocked_due_to_bad_port, should_be_blocked_due_to_nosniff}; +use http_loader::{is_redirect_status, set_request_cookies}; +use hyper::buffer::BufReader; +use hyper::header::{Accept, CacheControl, CacheDirective, Connection, ConnectionOption}; +use hyper::header::{Headers, Host, SetCookie, Pragma, Protocol, ProtocolName, Upgrade}; +use hyper::http::h1::{LINE_ENDING, parse_response}; +use hyper::method::Method; +use hyper::net::{HttpStream, HttpsStream}; +use hyper::status::StatusCode; +use hyper::version::HttpVersion; +use net_traits::{CookieSource, MessageData, NetworkError, WebSocketCommunicate, WebSocketConnectData}; +use net_traits::{WebSocketDomAction, WebSocketNetworkEvent}; +use net_traits::hosts::replace_host; +use net_traits::request::Type; +use openssl::ssl::{SslContext, SslStream}; use servo_url::ServoUrl; -use std::ascii::AsciiExt; +use std::io::{self, Write}; +use std::net::TcpStream; use std::sync::{Arc, Mutex, RwLock}; use std::sync::atomic::{AtomicBool, Ordering}; use std::thread; -use websocket::{Client, Message}; -use websocket::header::{Origin, WebSocketProtocol}; -use websocket::message::Type; +use url::Position; +use websocket::{Message, Receiver as WSReceiver, Sender as WSSender}; +use websocket::header::{Origin, WebSocketAccept, WebSocketKey, WebSocketProtocol, WebSocketVersion}; +use websocket::message::Type as MessageType; use websocket::receiver::Receiver; -use websocket::result::{WebSocketError, WebSocketResult}; use websocket::sender::Sender; -use websocket::stream::WebSocketStream; -use websocket::ws::receiver::Receiver as WSReceiver; -use websocket::ws::sender::Sender as Sender_Object; -pub fn init(connect: WebSocketCommunicate, connect_data: WebSocketConnectData, cookie_jar: Arc>) { +pub fn init(connect: WebSocketCommunicate, + connect_data: WebSocketConnectData, + cookie_jar: Arc>, + ssl_context: Arc) { thread::Builder::new().name(format!("WebSocket connection to {}", connect_data.resource_url)).spawn(move || { let channel = establish_a_websocket_connection(&connect_data.resource_url, connect_data.origin, connect_data.protocols, - cookie_jar); + cookie_jar, + ssl_context); let (ws_sender, mut receiver) = match channel { Ok((protocol_in_use, sender, receiver)) => { let _ = connect.event_sender.send(WebSocketNetworkEvent::ConnectionEstablished { protocol_in_use }); @@ -61,15 +72,15 @@ pub fn init(connect: WebSocketCommunicate, connect_data: WebSocketConnectData, c } }; let message = match message.opcode { - Type::Text => MessageData::Text(String::from_utf8_lossy(&message.payload).into_owned()), - Type::Binary => MessageData::Binary(message.payload.into_owned()), - Type::Ping => { + MessageType::Text => MessageData::Text(String::from_utf8_lossy(&message.payload).into_owned()), + MessageType::Binary => MessageData::Binary(message.payload.into_owned()), + MessageType::Ping => { let pong = Message::pong(message.payload); ws_sender_incoming.lock().unwrap().send_message(&pong).unwrap(); continue; }, - Type::Pong => continue, - Type::Close => { + MessageType::Pong => continue, + MessageType::Close => { if !initiated_close_incoming.fetch_or(true, Ordering::SeqCst) { ws_sender_incoming.lock().unwrap().send_message(&message).unwrap(); } @@ -105,80 +116,555 @@ pub fn init(connect: WebSocketCommunicate, connect_data: WebSocketConnectData, c }).expect("Thread spawning failed"); } +type Stream = HttpsStream>; + +// https://fetch.spec.whatwg.org/#concept-websocket-connection-obtain +fn obtain_a_websocket_connection(url: &ServoUrl, ssl_context: Arc) + -> Result { + // Step 1. + let host = url.host_str().unwrap(); + + // Step 2. + let port = url.port_or_known_default().unwrap(); + + // Step 3. + // We did not replace the scheme by "http" or "https" in step 1 of + // establish_a_websocket_connection. + let secure = match url.scheme() { + "ws" => false, + "wss" => true, + _ => panic!("URL's scheme should be http or https"), + }; + + // Steps 4-5. + let host = replace_host(host); + let tcp_stream = TcpStream::connect((&*host, port)).map_err(|e| { + NetworkError::Internal(format!("Could not connect to host: {}", e)) + })?; + let http_stream = HttpStream(tcp_stream); + if !secure { + return Ok(HttpsStream::Http(http_stream)); + } + let ssl_stream = SslStream::connect(&*ssl_context, http_stream).map_err(|e| { + NetworkError::from_ssl_error(url, &e) + })?; + Ok(HttpsStream::Https(ssl_stream)) +} + // https://fetch.spec.whatwg.org/#concept-websocket-establish fn establish_a_websocket_connection(resource_url: &ServoUrl, origin: String, protocols: Vec, - cookie_jar: Arc>) - -> WebSocketResult<(Option, - Sender, - Receiver)> { - // Steps 1-2 are not really applicable here, given we don't exactly go + cookie_jar: Arc>, + ssl_context: Arc) + -> Result<(Option, + Sender, + Receiver), + NetworkError> { + // Steps 1 is not really applicable here, given we don't exactly go // through the same infrastructure as the Fetch spec. - if should_be_blocked_due_to_bad_port(resource_url) { - // Subset of steps 11-12, we inline the bad port check here from the - // main fetch algorithm for the same reason steps 1-2 are not - // applicable. - return Err(WebSocketError::RequestError("Request should be blocked due to bad port.")); - } + // Step 2, slimmed down because we don't go through the whole Fetch infra. + let mut headers = Headers::new(); - // Steps 3-7. - let net_url = replace_host_in_url(resource_url.clone()); - let mut request = try!(Client::connect(net_url.as_url())); + // Step 3. + headers.set(Upgrade(vec![Protocol::new(ProtocolName::WebSocket, None)])); - // Client::connect sets the Host header to the host of the URL that is - // passed to it, so we need to reset it afterwards to the correct one. - request.headers.set(Host { - hostname: resource_url.host_str().unwrap().to_owned(), - port: resource_url.port(), - }); + // Step 4. + headers.set(Connection(vec![ConnectionOption::ConnectionHeader("upgrade".into())])); + + // Step 5. + let key_value = WebSocketKey::new(); + + // Step 6. + headers.set(key_value); + + // Step 7. + headers.set(WebSocketVersion::WebSocket13); // Step 8. if !protocols.is_empty() { - request.headers.set(WebSocketProtocol(protocols.clone())); + headers.set(WebSocketProtocol(protocols.clone())); } // Steps 9-10. - // TODO: support for permessage-deflate extension. + // TODO: handle permessage-deflate extension. - // Subset of step 11. - // See step 2 of https://fetch.spec.whatwg.org/#concept-fetch. - request.headers.set(Origin(origin)); + // Step 11 and network error check from step 12. + let response = fetch(resource_url, origin, headers, cookie_jar, ssl_context)?; - // Transitive subset of step 11. - // See step 17.1 of https://fetch.spec.whatwg.org/#concept-http-network-or-cache-fetch. - http_loader::set_request_cookies(&resource_url, &mut request.headers, &cookie_jar); + // Step 12, the status code check. + if response.status != StatusCode::SwitchingProtocols { + return Err(NetworkError::Internal("Response's status should be 101.".into())); + } - // Step 11, somewhat. - let response = try!(request.send()); + // Step 13. + if !protocols.is_empty() { + if response.headers.get::().map_or(true, |protocols| protocols.is_empty()) { + return Err(NetworkError::Internal( + "Response's Sec-WebSocket-Protocol header is missing, malformed or empty.".into())); + } + } - // Step 12, 14. - try!(response.validate()); + // Step 14.2. + let upgrade_header = response.headers.get::().ok_or_else(|| { + NetworkError::Internal("Response should have an Upgrade header.".into()) + })?; + if upgrade_header.len() != 1 { + return Err(NetworkError::Internal("Response's Upgrade header should have only one value.".into())); + } + if upgrade_header[0].name != ProtocolName::WebSocket { + return Err(NetworkError::Internal("Response's Upgrade header value should be \"websocket\".".into())); + } - // Step 13 and transitive subset of step 14. - // See step 6 of http://tools.ietf.org/html/rfc6455#section-4.1. - let protocol_in_use = response.protocol().and_then(|header| { - // https://github.com/whatwg/fetch/issues/515 - header.first().cloned() + // Step 14.3. + let connection_header = response.headers.get::().ok_or_else(|| { + NetworkError::Internal("Response should have a Connection header.".into()) + })?; + let connection_includes_upgrade = connection_header.iter().any(|option| { + match *option { + ConnectionOption::ConnectionHeader(ref option) => *option == "upgrade", + _ => false, + } }); - if let Some(ref 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")); - }; + if !connection_includes_upgrade { + return Err(NetworkError::Internal("Response's Connection header value should include \"upgrade\".".into())); + } + + // Step 14.4. + let accept_header = response.headers.get::().ok_or_else(|| { + NetworkError::Internal("Response should have a Sec-Websocket-Accept header.".into()) + })?; + if *accept_header != WebSocketAccept::new(&key_value) { + return Err(NetworkError::Internal( + "Response's Sec-WebSocket-Accept header value did not match the sent key.".into())); + } + + // Step 14.5. + // TODO: handle permessage-deflate extension. + // We don't support any extension, so we fail at the mere presence of + // a Sec-WebSocket-Extensions header. + if response.headers.get_raw("Sec-WebSocket-Extensions").is_some() { + return Err(NetworkError::Internal( + "Response's Sec-WebSocket-Extensions header value included unsupported extensions.".into())); + } + + // Step 14.6. + let protocol_in_use = if let Some(response_protocols) = response.headers.get::() { + for p in &**response_protocols { + if !protocols.contains(p) { + return Err(NetworkError::Internal( + "Response's Sec-WebSocket-Protocols contain values that were not requested.".into())); + } + } + response_protocols.first().cloned() + } else { + None }; - // Transitive subset of step 11. - // See step 15 of https://fetch.spec.whatwg.org/#http-network-fetch. + let sender = Sender::new(response.writer, true); + let receiver = Receiver::new(response.reader, false); + Ok((protocol_in_use, sender, receiver)) +} + +struct Response { + status: StatusCode, + headers: Headers, + reader: BufReader, + writer: Stream, +} + +// https://fetch.spec.whatwg.org/#concept-fetch +fn fetch(url: &ServoUrl, + origin: String, + mut headers: Headers, + cookie_jar: Arc>, + ssl_context: Arc) + -> Result { + // Step 1. + // TODO: handle request's window. + + // Step 2. + // TODO: handle request's origin. + + // Step 3. + // We know there is no `Accept` header in `headers`. + { + // Step 3.1. + let value = Accept::star(); + + // Step 3.2. + // Not applicable: not a navigation request. + + // Step 3.3. + // Not applicable: request's type is the empty string. + + // Step 3.4. + headers.set(value); + } + + // Step 4. + // TODO: handle `Accept-Language`. + + // Step 5. + // TODO: handle request's priority. + + // Step 6. + // Not applicable: not a navigation request. + + // Step 7. + // We know this is a subresource request. + { + // Step 7.1. + // Not applicable: client hints list is empty. + + // Steps 7.2-3. + // TODO: handle fetch groups. + } + + // Step 8. + main_fetch(url, origin, headers, cookie_jar, ssl_context) +} + +// https://fetch.spec.whatwg.org/#concept-main-fetch +fn main_fetch(url: &ServoUrl, + origin: String, + mut headers: Headers, + cookie_jar: Arc>, + ssl_context: Arc) + -> Result { + // Step 1. + let mut response = None; + + // Step 2. + // Not applicable: request’s local-URLs-only flag is unset. + + // Step 3. + // TODO: handle content security policy violations. + + // Step 4. + // TODO: handle upgrade to a potentially secure URL. + + // Step 5. + if should_be_blocked_due_to_bad_port(url) { + response = Some(Err(NetworkError::Internal("Request should be blocked due to bad port.".into()))); + } + // TODO: handle blocking as mixed content. + // TODO: handle blocking by content security policy. + + // Steps 6-8. + // TODO: handle request's referrer policy. + + // Step 9. + // Not applicable: request's current URL's scheme is not "ftp". + + // Step 10. + // TODO: handle known HSTS host domain. + + // Step 11. + // Not applicable: request's synchronous flag is set. + + // Step 12. + let mut response = response.unwrap_or_else(|| { + // We must run the first sequence of substeps, given request's mode + // is "websocket". + + // Step 12.1. + // Not applicable: the response is never exposed to the Web so it + // doesn't need to be filtered at all. + + // Step 12.2. + basic_fetch(url, origin, &mut headers, cookie_jar, ssl_context) + }); + + // Step 13. + // Not applicable: recursive flag is unset. + + // Step 14. + // Not applicable: the response is never exposed to the Web so it doesn't + // need to be filtered at all. + + // Steps 15-16. + // Not applicable: no need to maintain an internal response. + + // Step 17. + if response.is_ok() { + // TODO: handle blocking as mixed content. + // TODO: handle blocking by content security policy. + // Not applicable: blocking due to MIME type matters only for scripts. + if should_be_blocked_due_to_nosniff(Type::None, &headers) { + response = Err(NetworkError::Internal("Request should be blocked due to nosniff.".into())); + } + } + + // Step 18. + // Not applicable: we don't care about the body at all. + + // Step 19. + // Not applicable: request's integrity metadata is the empty string. + + // Step 20. + // TODO: wait for response's body here, maybe? + response +} + +// https://fetch.spec.whatwg.org/#concept-basic-fetch +fn basic_fetch(url: &ServoUrl, + origin: String, + headers: &mut Headers, + cookie_jar: Arc>, + ssl_context: Arc) + -> Result { + // In the case of a WebSocket request, HTTP fetch is always used. + http_fetch(url, origin, headers, cookie_jar, ssl_context) +} + +// https://fetch.spec.whatwg.org/#concept-http-fetch +fn http_fetch(url: &ServoUrl, + origin: String, + headers: &mut Headers, + cookie_jar: Arc>, + ssl_context: Arc) + -> Result { + // Step 1. + // Not applicable: with step 3 being useless here, this one is too. + + // Step 2. + // Not applicable: we don't need to maintain an internal response. + + // Step 3. + // Not applicable: request's service-workers mode is "none". + + // Step 4. + // There cannot be a response yet at this point. + let mut response = { + // Step 4.1. + // Not applicable: CORS-preflight flag is unset. + + // Step 4.2. + // Not applicable: request's redirect mode is "error". + + // Step 4.3. + let response = http_network_or_cache_fetch(url, origin, headers, cookie_jar, ssl_context); + + // Step 4.4. + // Not applicable: CORS flag is unset. + + response + }; + + // Step 5. + if response.as_ref().ok().map_or(false, |response| is_redirect_status(response.status)) { + // Step 5.1. + // Not applicable: the connection does not use HTTP/2. + + // Steps 5.2-4. + // Not applicable: matters only if request's redirect mode is not "error". + + // Step 5.5. + // Request's redirect mode is "error". + response = Err(NetworkError::Internal("Response should not be a redirection.".into())); + } + + // Step 6. + response +} + +// https://fetch.spec.whatwg.org/#concept-http-network-or-cache-fetch +fn http_network_or_cache_fetch(url: &ServoUrl, + origin: String, + headers: &mut Headers, + cookie_jar: Arc>, + ssl_context: Arc) + -> Result { + // Steps 1-3. + // Not applicable: we don't even have a request yet, and there is no body + // in a WebSocket request. + + // Step 4. + // Not applicable: credentials flag is always set + // because credentials mode is "include." + + // Steps 5-9. + // Not applicable: there is no body in a WebSocket request. + + // Step 10. + // TODO: handle header Referer. + + // Step 11. + // Request's mode is "websocket". + headers.set(Origin(origin)); + + // Step 12. + // TODO: handle header User-Agent. + + // Steps 13-14. + // Not applicable: request's cache mode is "no-store". + + // Step 15. + { + // Step 15.1. + // We know there is no Pragma header yet. + headers.set(Pragma::NoCache); + + // Step 15.2. + // We know there is no Cache-Control header yet. + headers.set(CacheControl(vec![CacheDirective::NoCache])); + } + + // Step 16. + // TODO: handle Accept-Encoding. + // Not applicable: Connection header is already present. + // TODO: handle DNT. + headers.set(Host { + hostname: url.host_str().unwrap().to_owned(), + port: url.port(), + }); + + // Step 17. + // Credentials flag is set. + { + // Step 17.1. + // TODO: handle user agent configured to block cookies. + set_request_cookies(&url, headers, &cookie_jar); + + // Steps 17.2-6. + // Not applicable: request has no Authorization header. + } + + // Step 18. + // TODO: proxy-authentication entry. + + // Step 19. + // Not applicable: with step 21 being useless, this one is too. + + // Step 20. + // Not applicable: revalidatingFlag is only useful if step 21 is. + + // Step 21. + // Not applicable: cache mode is "no-store". + + // Step 22. + // There is no response yet. + let response = { + // Step 22.1. + // Not applicable: cache mode is "no-store". + + // Step 22.2. + let forward_response = http_network_fetch(url, headers, cookie_jar, ssl_context); + + // Step 22.3. + // Not applicable: request's method is not unsafe. + + // Step 22.4. + // Not applicable: revalidatingFlag is unset. + + // Step 22.5. + // There is no response yet and the response should not be cached. + forward_response + }; + + // Step 23. + // TODO: handle 401 status when request's window is not "no-window". + + // Step 24. + // TODO: handle 407 status when request's window is not "no-window". + + // Step 25. + // Not applicable: authentication-fetch flag is unset. + + // Step 26. + response +} + +// https://fetch.spec.whatwg.org/#concept-http-network-fetch +fn http_network_fetch(url: &ServoUrl, + headers: &Headers, + cookie_jar: Arc>, + ssl_context: Arc) + -> Result { + // Step 1. + // Not applicable: credentials flag is set. + + // Steps 2-3. + // Request's mode is "websocket". + let connection = obtain_a_websocket_connection(url, ssl_context)?; + + // Step 4. + // Not applicable: request’s body is null. + + // Step 5. + let response = make_request(connection, url, headers)?; + + // Steps 6-12. + // Not applicable: correct WebSocket responses don't have a body. + + // Step 13. + // TODO: handle response's CSP list. + + // Step 14. + // Not applicable: request's cache mode is "no-store". + + // Step 15. if let Some(cookies) = response.headers.get::() { let mut jar = cookie_jar.write().unwrap(); for cookie in &**cookies { - if let Some(cookie) = Cookie::new_wrapped(cookie.clone(), resource_url, CookieSource::HTTP) { - jar.push(cookie, resource_url, CookieSource::HTTP); + if let Some(cookie) = Cookie::new_wrapped(cookie.clone(), url, CookieSource::HTTP) { + jar.push(cookie, url, CookieSource::HTTP); } } } - let (sender, receiver) = response.begin().split(); - Ok((protocol_in_use, sender, receiver)) + // Step 16. + // Not applicable: correct WebSocket responses don't have a body. + + // Step 17. + Ok(response) +} + +fn make_request(mut stream: Stream, + url: &ServoUrl, + headers: &Headers) + -> Result { + write_request(&mut stream, url, headers).map_err(|e| { + NetworkError::Internal(format!("Request could not be sent: {}", e)) + })?; + + // FIXME: Stream isn't supposed to be cloned. + let writer = stream.clone(); + + // FIXME: BufReader from hyper isn't supposed to be used. + let mut reader = BufReader::new(stream); + + let head = parse_response(&mut reader).map_err(|e| { + NetworkError::Internal(format!("Response could not be read: {}", e)) + })?; + + // This isn't in the spec, but this is the correct thing to do for WebSocket requests. + if head.version != HttpVersion::Http11 { + return Err(NetworkError::Internal("Response's HTTP version should be HTTP/1.1.".into())); + } + + // FIXME: StatusCode::from_u16 isn't supposed to be used. + let status = StatusCode::from_u16(head.subject.0); + Ok(Response { + status: status, + headers: head.headers, + reader: reader, + writer: writer, + }) +} + +fn write_request(stream: &mut Stream, + url: &ServoUrl, + headers: &Headers) + -> io::Result<()> { + // Write "GET /foo/bar HTTP/1.1\r\n". + let method = Method::Get; + let request_uri = &url.as_url()[Position::BeforePath..Position::AfterQuery]; + let version = HttpVersion::Http11; + write!(stream, "{} {} {}{}", method, request_uri, version, LINE_ENDING)?; + + // Write the headers. + write!(stream, "{}{}", headers, LINE_ENDING) } diff --git a/components/net_traits/lib.rs b/components/net_traits/lib.rs index f29f803ef66b..ab0121a119c5 100644 --- a/components/net_traits/lib.rs +++ b/components/net_traits/lib.rs @@ -540,7 +540,7 @@ impl NetworkError { NetworkError::Internal(error.description().to_owned()) } - fn from_ssl_error(url: &ServoUrl, error: &SslError) -> Self { + pub fn from_ssl_error(url: &ServoUrl, error: &SslError) -> Self { if let SslError::OpenSslErrors(ref errors) = *error { if errors.iter().any(is_cert_verify_error) { let mut error_report = vec![format!("ssl error ({}):", openssl::version::version())]; diff --git a/tests/wpt/metadata/websockets/constructor/011.html.ini b/tests/wpt/metadata/websockets/constructor/011.html.ini index c03be6a5ae09..2932f5662d3e 100644 --- a/tests/wpt/metadata/websockets/constructor/011.html.ini +++ b/tests/wpt/metadata/websockets/constructor/011.html.ini @@ -1,5 +1,7 @@ [011.html] type: testharness + [WebSockets: protocol mismatch] + expected: FAIL [011.html?wss] type: testharness