Auto merge of #16180 - nox:tungstenite, r=jdm

Make the WebSocket handshake ourselves 🍷

HYPE

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/servo/16180)
<!-- Reviewable:end -->
This commit is contained in:
bors-servo 2017-03-29 12:55:38 -05:00 committed by GitHub
commit 76a2c9705a
7 changed files with 632 additions and 131 deletions

1
Cargo.lock generated
View file

@ -1762,6 +1762,7 @@ dependencies = [
"log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
"msg 0.0.1", "msg 0.0.1",
"num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)",
"parse-hosts 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "parse-hosts 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)", "serde 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)",

View file

@ -527,7 +527,7 @@ fn is_null_body_status(status: &Option<StatusCode>) -> bool {
} }
/// https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-nosniff? /// 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 /// https://fetch.spec.whatwg.org/#x-content-type-options-header
/// This is needed to parse `X-Content-Type-Options` according to spec, /// This is needed to parse `X-Content-Type-Options` according to spec,
/// which requires that we inspect only the first value. /// which requires that we inspect only the first value.

View file

@ -35,9 +35,7 @@ use net_traits::hosts::replace_host;
use net_traits::request::{CacheMode, CredentialsMode, Destination, Origin}; use net_traits::request::{CacheMode, CredentialsMode, Destination, Origin};
use net_traits::request::{RedirectMode, Referrer, Request, RequestMode, ResponseTainting}; use net_traits::request::{RedirectMode, Referrer, Request, RequestMode, ResponseTainting};
use net_traits::response::{HttpsState, Response, ResponseBody, ResponseType}; use net_traits::response::{HttpsState, Response, ResponseBody, ResponseType};
use openssl;
use openssl::ssl::SslStream; use openssl::ssl::SslStream;
use openssl::ssl::error::{OpensslError, SslError};
use resource_thread::AuthCache; use resource_thread::AuthCache;
use servo_url::{ImmutableOrigin, ServoUrl}; use servo_url::{ImmutableOrigin, ServoUrl};
use std::collections::HashSet; use std::collections::HashSet;
@ -140,34 +138,7 @@ impl NetworkHttpRequestFactory {
fn create(&self, url: ServoUrl, method: Method, headers: Headers) fn create(&self, url: ServoUrl, method: Method, headers: Headers)
-> Result<HyperRequest<Fresh>, NetworkError> { -> Result<HyperRequest<Fresh>, NetworkError> {
let connection = HyperRequest::with_connector(method, url.clone().into_url(), self); let connection = HyperRequest::with_connector(method, url.clone().into_url(), self);
let mut request = connection.map_err(|e| NetworkError::from_hyper_error(&url, e))?;
if let Err(HttpError::Ssl(ref error)) = connection {
let error: &(Error + Send + 'static) = &**error;
if let Some(&SslError::OpenSslErrors(ref errors)) = error.downcast_ref::<SslError>() {
if errors.iter().any(is_cert_verify_error) {
let mut error_report = vec![format!("ssl error ({}):", openssl::version::version())];
let mut suggestion = None;
for err in errors {
if is_unknown_message_digest_err(err) {
suggestion = Some("<b>Servo recommends upgrading to a newer OpenSSL version.</b>");
}
error_report.push(format_ssl_error(err));
}
if let Some(suggestion) = suggestion {
error_report.push(suggestion.to_owned());
}
let error_report = error_report.join("<br>\n");
return Err(NetworkError::SslValidation(url, error_report));
}
}
}
let mut request = match connection {
Ok(req) => req,
Err(e) => return Err(NetworkError::Internal(e.description().to_owned())),
};
*request.headers_mut() = headers; *request.headers_mut() = headers;
Ok(request) Ok(request)
@ -505,35 +476,6 @@ fn obtain_response(request_factory: &NetworkHttpRequestFactory,
} }
} }
// FIXME: This incredibly hacky. Make it more robust, and at least test it.
fn is_cert_verify_error(error: &OpensslError) -> bool {
match error {
&OpensslError::UnknownError { ref library, ref function, ref reason } => {
library == "SSL routines" &&
function.to_uppercase() == "SSL3_GET_SERVER_CERTIFICATE" &&
reason == "certificate verify failed"
}
}
}
fn is_unknown_message_digest_err(error: &OpensslError) -> bool {
match error {
&OpensslError::UnknownError { ref library, ref function, ref reason } => {
library == "asn1 encoding routines" &&
function == "ASN1_item_verify" &&
reason == "unknown message digest algorithm"
}
}
}
fn format_ssl_error(error: &OpensslError) -> String {
match error {
&OpensslError::UnknownError { ref library, ref function, ref reason } => {
format!("{}: {} - {}", library, function, reason)
}
}
}
/// [HTTP fetch](https://fetch.spec.whatwg.org#http-fetch) /// [HTTP fetch](https://fetch.spec.whatwg.org#http-fetch)
pub fn http_fetch(request: Rc<Request>, pub fn http_fetch(request: Rc<Request>,
cache: &mut CorsCache, cache: &mut CorsCache,
@ -1417,7 +1359,7 @@ fn response_needs_revalidation(_response: &Response) -> bool {
} }
/// https://fetch.spec.whatwg.org/#redirect-status /// https://fetch.spec.whatwg.org/#redirect-status
fn is_redirect_status(status: StatusCode) -> bool { pub fn is_redirect_status(status: StatusCode) -> bool {
match status { match status {
StatusCode::MovedPermanently | StatusCode::MovedPermanently |
StatusCode::Found | StatusCode::Found |

View file

@ -355,6 +355,9 @@ impl CoreResourceManager {
connect: WebSocketCommunicate, connect: WebSocketCommunicate,
connect_data: WebSocketConnectData, connect_data: WebSocketConnectData,
resource_grp: &ResourceGroup) { 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());
} }
} }

View file

@ -4,33 +4,45 @@
use cookie::Cookie; use cookie::Cookie;
use cookie_storage::CookieStorage; use cookie_storage::CookieStorage;
use fetch::methods::should_be_blocked_due_to_bad_port; use fetch::methods::{should_be_blocked_due_to_bad_port, should_be_blocked_due_to_nosniff};
use http_loader; use http_loader::{is_redirect_status, set_request_cookies};
use hyper::header::{Host, SetCookie}; use hyper::buffer::BufReader;
use net_traits::{CookieSource, MessageData, WebSocketCommunicate}; use hyper::header::{Accept, CacheControl, CacheDirective, Connection, ConnectionOption};
use net_traits::{WebSocketConnectData, WebSocketDomAction, WebSocketNetworkEvent}; use hyper::header::{Headers, Host, SetCookie, Pragma, Protocol, ProtocolName, Upgrade};
use net_traits::hosts::replace_host_in_url; 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 servo_url::ServoUrl;
use std::ascii::AsciiExt; use std::ascii::AsciiExt;
use std::io::{self, Write};
use std::net::TcpStream;
use std::sync::{Arc, Mutex, RwLock}; use std::sync::{Arc, Mutex, RwLock};
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::thread; use std::thread;
use websocket::{Client, Message}; use url::Position;
use websocket::header::{Origin, WebSocketProtocol}; use websocket::{Message, Receiver as WSReceiver, Sender as WSSender};
use websocket::message::Type; use websocket::header::{Origin, WebSocketAccept, WebSocketKey, WebSocketProtocol, WebSocketVersion};
use websocket::message::Type as MessageType;
use websocket::receiver::Receiver; use websocket::receiver::Receiver;
use websocket::result::{WebSocketError, WebSocketResult};
use websocket::sender::Sender; 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<RwLock<CookieStorage>>) { pub fn init(connect: WebSocketCommunicate,
connect_data: WebSocketConnectData,
cookie_jar: Arc<RwLock<CookieStorage>>,
ssl_context: Arc<SslContext>) {
thread::Builder::new().name(format!("WebSocket connection to {}", connect_data.resource_url)).spawn(move || { thread::Builder::new().name(format!("WebSocket connection to {}", connect_data.resource_url)).spawn(move || {
let channel = establish_a_websocket_connection(&connect_data.resource_url, let channel = establish_a_websocket_connection(&connect_data.resource_url,
connect_data.origin, connect_data.origin,
connect_data.protocols, connect_data.protocols,
cookie_jar); cookie_jar,
ssl_context);
let (ws_sender, mut receiver) = match channel { let (ws_sender, mut receiver) = match channel {
Ok((protocol_in_use, sender, receiver)) => { Ok((protocol_in_use, sender, receiver)) => {
let _ = connect.event_sender.send(WebSocketNetworkEvent::ConnectionEstablished { protocol_in_use }); let _ = connect.event_sender.send(WebSocketNetworkEvent::ConnectionEstablished { protocol_in_use });
@ -61,15 +73,15 @@ pub fn init(connect: WebSocketCommunicate, connect_data: WebSocketConnectData, c
} }
}; };
let message = match message.opcode { let message = match message.opcode {
Type::Text => MessageData::Text(String::from_utf8_lossy(&message.payload).into_owned()), MessageType::Text => MessageData::Text(String::from_utf8_lossy(&message.payload).into_owned()),
Type::Binary => MessageData::Binary(message.payload.into_owned()), MessageType::Binary => MessageData::Binary(message.payload.into_owned()),
Type::Ping => { MessageType::Ping => {
let pong = Message::pong(message.payload); let pong = Message::pong(message.payload);
ws_sender_incoming.lock().unwrap().send_message(&pong).unwrap(); ws_sender_incoming.lock().unwrap().send_message(&pong).unwrap();
continue; continue;
}, },
Type::Pong => continue, MessageType::Pong => continue,
Type::Close => { MessageType::Close => {
if !initiated_close_incoming.fetch_or(true, Ordering::SeqCst) { if !initiated_close_incoming.fetch_or(true, Ordering::SeqCst) {
ws_sender_incoming.lock().unwrap().send_message(&message).unwrap(); ws_sender_incoming.lock().unwrap().send_message(&message).unwrap();
} }
@ -105,80 +117,555 @@ pub fn init(connect: WebSocketCommunicate, connect_data: WebSocketConnectData, c
}).expect("Thread spawning failed"); }).expect("Thread spawning failed");
} }
type Stream = HttpsStream<SslStream<HttpStream>>;
// https://fetch.spec.whatwg.org/#concept-websocket-connection-obtain
fn obtain_a_websocket_connection(url: &ServoUrl, ssl_context: Arc<SslContext>)
-> Result<Stream, NetworkError> {
// 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 ws or wss"),
};
// 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 // https://fetch.spec.whatwg.org/#concept-websocket-establish
fn establish_a_websocket_connection(resource_url: &ServoUrl, fn establish_a_websocket_connection(resource_url: &ServoUrl,
origin: String, origin: String,
protocols: Vec<String>, protocols: Vec<String>,
cookie_jar: Arc<RwLock<CookieStorage>>) cookie_jar: Arc<RwLock<CookieStorage>>,
-> WebSocketResult<(Option<String>, ssl_context: Arc<SslContext>)
Sender<WebSocketStream>, -> Result<(Option<String>,
Receiver<WebSocketStream>)> { Sender<Stream>,
// Steps 1-2 are not really applicable here, given we don't exactly go Receiver<Stream>),
NetworkError> {
// Steps 1 is not really applicable here, given we don't exactly go
// through the same infrastructure as the Fetch spec. // through the same infrastructure as the Fetch spec.
if should_be_blocked_due_to_bad_port(resource_url) { // Step 2, slimmed down because we don't go through the whole Fetch infra.
// Subset of steps 11-12, we inline the bad port check here from the let mut headers = Headers::new();
// 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."));
}
// Steps 3-7. // Step 3.
let net_url = replace_host_in_url(resource_url.clone()); headers.set(Upgrade(vec![Protocol::new(ProtocolName::WebSocket, None)]));
let mut request = try!(Client::connect(net_url.as_url()));
// Client::connect sets the Host header to the host of the URL that is // Step 4.
// passed to it, so we need to reset it afterwards to the correct one. headers.set(Connection(vec![ConnectionOption::ConnectionHeader("upgrade".into())]));
request.headers.set(Host {
hostname: resource_url.host_str().unwrap().to_owned(), // Step 5.
port: resource_url.port(), let key_value = WebSocketKey::new();
});
// Step 6.
headers.set(key_value);
// Step 7.
headers.set(WebSocketVersion::WebSocket13);
// Step 8. // Step 8.
if !protocols.is_empty() { if !protocols.is_empty() {
request.headers.set(WebSocketProtocol(protocols.clone())); headers.set(WebSocketProtocol(protocols.clone()));
} }
// Steps 9-10. // Steps 9-10.
// TODO: support for permessage-deflate extension. // TODO: handle permessage-deflate extension.
// Subset of step 11. // Step 11 and network error check from step 12.
// See step 2 of https://fetch.spec.whatwg.org/#concept-fetch. let response = fetch(resource_url, origin, headers, cookie_jar, ssl_context)?;
request.headers.set(Origin(origin));
// Transitive subset of step 11. // Step 12, the status code check.
// See step 17.1 of https://fetch.spec.whatwg.org/#concept-http-network-or-cache-fetch. if response.status != StatusCode::SwitchingProtocols {
http_loader::set_request_cookies(&resource_url, &mut request.headers, &cookie_jar); return Err(NetworkError::Internal("Response's status should be 101.".into()));
}
// Step 11, somewhat. // Step 13.
let response = try!(request.send()); if !protocols.is_empty() {
if response.headers.get::<WebSocketProtocol>().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. // Step 14.2.
try!(response.validate()); let upgrade_header = response.headers.get::<Upgrade>().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. // Step 14.3.
// See step 6 of http://tools.ietf.org/html/rfc6455#section-4.1. let connection_header = response.headers.get::<Connection>().ok_or_else(|| {
let protocol_in_use = response.protocol().and_then(|header| { NetworkError::Internal("Response should have a Connection header.".into())
// https://github.com/whatwg/fetch/issues/515 })?;
header.first().cloned() 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 !connection_includes_upgrade {
if !protocols.is_empty() && !protocols.iter().any(|p| (&**p).eq_ignore_ascii_case(protocol_name)) { return Err(NetworkError::Internal("Response's Connection header value should include \"upgrade\".".into()));
return Err(WebSocketError::ProtocolError("Protocol in Use not in client-supplied protocol list")); }
};
// Step 14.4.
let accept_header = response.headers.get::<WebSocketAccept>().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::<WebSocketProtocol>() {
for replied in &**response_protocols {
if !protocols.iter().any(|requested| requested.eq_ignore_ascii_case(replied)) {
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. let sender = Sender::new(response.writer, true);
// See step 15 of https://fetch.spec.whatwg.org/#http-network-fetch. let receiver = Receiver::new(response.reader, false);
Ok((protocol_in_use, sender, receiver))
}
struct Response {
status: StatusCode,
headers: Headers,
reader: BufReader<Stream>,
writer: Stream,
}
// https://fetch.spec.whatwg.org/#concept-fetch
fn fetch(url: &ServoUrl,
origin: String,
mut headers: Headers,
cookie_jar: Arc<RwLock<CookieStorage>>,
ssl_context: Arc<SslContext>)
-> Result<Response, NetworkError> {
// 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<RwLock<CookieStorage>>,
ssl_context: Arc<SslContext>)
-> Result<Response, NetworkError> {
// Step 1.
let mut response = None;
// Step 2.
// Not applicable: requests 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<RwLock<CookieStorage>>,
ssl_context: Arc<SslContext>)
-> Result<Response, NetworkError> {
// 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<RwLock<CookieStorage>>,
ssl_context: Arc<SslContext>)
-> Result<Response, NetworkError> {
// 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<RwLock<CookieStorage>>,
ssl_context: Arc<SslContext>)
-> Result<Response, NetworkError> {
// 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<RwLock<CookieStorage>>,
ssl_context: Arc<SslContext>)
-> Result<Response, NetworkError> {
// 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: requests 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::<SetCookie>() { if let Some(cookies) = response.headers.get::<SetCookie>() {
let mut jar = cookie_jar.write().unwrap(); let mut jar = cookie_jar.write().unwrap();
for cookie in &**cookies { for cookie in &**cookies {
if let Some(cookie) = Cookie::new_wrapped(cookie.clone(), resource_url, CookieSource::HTTP) { if let Some(cookie) = Cookie::new_wrapped(cookie.clone(), url, CookieSource::HTTP) {
jar.push(cookie, resource_url, CookieSource::HTTP); jar.push(cookie, url, CookieSource::HTTP);
} }
} }
} }
let (sender, receiver) = response.begin().split(); // Step 16.
Ok((protocol_in_use, sender, receiver)) // 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<Response, NetworkError> {
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)
} }

View file

@ -22,6 +22,7 @@ lazy_static = "0.2"
log = "0.3.5" log = "0.3.5"
msg = {path = "../msg"} msg = {path = "../msg"}
num-traits = "0.1.32" num-traits = "0.1.32"
openssl = "0.7.6"
parse-hosts = "0.3.0" parse-hosts = "0.3.0"
serde = "0.9" serde = "0.9"
serde_derive = "0.9" serde_derive = "0.9"

View file

@ -22,6 +22,7 @@ extern crate lazy_static;
extern crate log; extern crate log;
extern crate msg; extern crate msg;
extern crate num_traits; extern crate num_traits;
extern crate openssl;
extern crate parse_hosts; extern crate parse_hosts;
extern crate serde; extern crate serde;
#[macro_use] #[macro_use]
@ -35,16 +36,19 @@ extern crate webrender_traits;
use cookie_rs::Cookie; use cookie_rs::Cookie;
use filemanager_thread::FileManagerThreadMsg; use filemanager_thread::FileManagerThreadMsg;
use heapsize::HeapSizeOf; use heapsize::HeapSizeOf;
use hyper::Error as HyperError;
use hyper::header::{ContentType, Headers, ReferrerPolicy as ReferrerPolicyHeader}; use hyper::header::{ContentType, Headers, ReferrerPolicy as ReferrerPolicyHeader};
use hyper::http::RawStatus; use hyper::http::RawStatus;
use hyper::mime::{Attr, Mime}; use hyper::mime::{Attr, Mime};
use hyper_serde::Serde; use hyper_serde::Serde;
use ipc_channel::Error; use ipc_channel::Error as IpcError;
use ipc_channel::ipc::{self, IpcReceiver, IpcSender}; use ipc_channel::ipc::{self, IpcReceiver, IpcSender};
use ipc_channel::router::ROUTER; use ipc_channel::router::ROUTER;
use openssl::ssl::error::{OpensslError, SslError};
use request::{Request, RequestInit}; use request::{Request, RequestInit};
use response::{HttpsState, Response}; use response::{HttpsState, Response};
use servo_url::ServoUrl; use servo_url::ServoUrl;
use std::error::Error;
use storage_thread::StorageThreadMsg; use storage_thread::StorageThreadMsg;
pub mod blob_url_store; pub mod blob_url_store;
@ -265,7 +269,7 @@ impl<T: FetchResponseListener> Action<T> for FetchResponseMsg {
/// Handle to a resource thread /// Handle to a resource thread
pub type CoreResourceThread = IpcSender<CoreResourceMsg>; pub type CoreResourceThread = IpcSender<CoreResourceMsg>;
pub type IpcSendResult = Result<(), Error>; pub type IpcSendResult = Result<(), IpcError>;
/// Abstraction of the ability to send a particular type of message, /// Abstraction of the ability to send a particular type of message,
/// used by net_traits::ResourceThreads to ease the use its IpcSender sub-fields /// used by net_traits::ResourceThreads to ease the use its IpcSender sub-fields
@ -526,6 +530,69 @@ pub enum NetworkError {
SslValidation(ServoUrl, String), SslValidation(ServoUrl, String),
} }
impl NetworkError {
pub fn from_hyper_error(url: &ServoUrl, error: HyperError) -> Self {
if let HyperError::Ssl(ref ssl_error) = error {
if let Some(ssl_error) = ssl_error.downcast_ref::<SslError>() {
return NetworkError::from_ssl_error(url, ssl_error);
}
}
NetworkError::Internal(error.description().to_owned())
}
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())];
let mut suggestion = None;
for err in errors {
if is_unknown_message_digest_err(err) {
suggestion = Some("<b>Servo recommends upgrading to a newer OpenSSL version.</b>");
}
error_report.push(format_ssl_error(err));
}
if let Some(suggestion) = suggestion {
error_report.push(suggestion.to_owned());
}
let error_report = error_report.join("<br>\n");
return NetworkError::SslValidation(url.clone(), error_report);
}
}
NetworkError::Internal(error.description().to_owned())
}
}
fn format_ssl_error(error: &OpensslError) -> String {
match error {
&OpensslError::UnknownError { ref library, ref function, ref reason } => {
format!("{}: {} - {}", library, function, reason)
}
}
}
// FIXME: This incredibly hacky. Make it more robust, and at least test it.
fn is_cert_verify_error(error: &OpensslError) -> bool {
match error {
&OpensslError::UnknownError { ref library, ref function, ref reason } => {
library == "SSL routines" &&
function.to_uppercase() == "SSL3_GET_SERVER_CERTIFICATE" &&
reason == "certificate verify failed"
}
}
}
fn is_unknown_message_digest_err(error: &OpensslError) -> bool {
match error {
&OpensslError::UnknownError { ref library, ref function, ref reason } => {
library == "asn1 encoding routines" &&
function == "ASN1_item_verify" &&
reason == "unknown message digest algorithm"
}
}
}
/// Normalize `slice`, as defined by /// Normalize `slice`, as defined by
/// [the Fetch Spec](https://fetch.spec.whatwg.org/#concept-header-value-normalize). /// [the Fetch Spec](https://fetch.spec.whatwg.org/#concept-header-value-normalize).
pub fn trim_http_whitespace(mut slice: &[u8]) -> &[u8] { pub fn trim_http_whitespace(mut slice: &[u8]) -> &[u8] {