mirror of
https://github.com/servo/servo.git
synced 2025-08-06 22:15:33 +01:00
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:
commit
76a2c9705a
7 changed files with 632 additions and 131 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1762,6 +1762,7 @@ dependencies = [
|
|||
"log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"msg 0.0.1",
|
||||
"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)",
|
||||
"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)",
|
||||
|
|
|
@ -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?
|
||||
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.
|
||||
|
|
|
@ -35,9 +35,7 @@ use net_traits::hosts::replace_host;
|
|||
use net_traits::request::{CacheMode, CredentialsMode, Destination, Origin};
|
||||
use net_traits::request::{RedirectMode, Referrer, Request, RequestMode, ResponseTainting};
|
||||
use net_traits::response::{HttpsState, Response, ResponseBody, ResponseType};
|
||||
use openssl;
|
||||
use openssl::ssl::SslStream;
|
||||
use openssl::ssl::error::{OpensslError, SslError};
|
||||
use resource_thread::AuthCache;
|
||||
use servo_url::{ImmutableOrigin, ServoUrl};
|
||||
use std::collections::HashSet;
|
||||
|
@ -140,34 +138,7 @@ impl NetworkHttpRequestFactory {
|
|||
fn create(&self, url: ServoUrl, method: Method, headers: Headers)
|
||||
-> Result<HyperRequest<Fresh>, NetworkError> {
|
||||
let connection = HyperRequest::with_connector(method, url.clone().into_url(), self);
|
||||
|
||||
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())),
|
||||
};
|
||||
let mut request = connection.map_err(|e| NetworkError::from_hyper_error(&url, e))?;
|
||||
*request.headers_mut() = headers;
|
||||
|
||||
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)
|
||||
pub fn http_fetch(request: Rc<Request>,
|
||||
cache: &mut CorsCache,
|
||||
|
@ -1417,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 |
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,33 +4,45 @@
|
|||
|
||||
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<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 || {
|
||||
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 +73,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 +117,555 @@ pub fn init(connect: WebSocketCommunicate, connect_data: WebSocketConnectData, c
|
|||
}).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
|
||||
fn establish_a_websocket_connection(resource_url: &ServoUrl,
|
||||
origin: String,
|
||||
protocols: Vec<String>,
|
||||
cookie_jar: Arc<RwLock<CookieStorage>>)
|
||||
-> WebSocketResult<(Option<String>,
|
||||
Sender<WebSocketStream>,
|
||||
Receiver<WebSocketStream>)> {
|
||||
// Steps 1-2 are not really applicable here, given we don't exactly go
|
||||
cookie_jar: Arc<RwLock<CookieStorage>>,
|
||||
ssl_context: Arc<SslContext>)
|
||||
-> Result<(Option<String>,
|
||||
Sender<Stream>,
|
||||
Receiver<Stream>),
|
||||
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::<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.
|
||||
try!(response.validate());
|
||||
// Step 14.2.
|
||||
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.
|
||||
// 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::<Connection>().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::<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.
|
||||
// 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<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: 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<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: 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::<SetCookie>() {
|
||||
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<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)
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ lazy_static = "0.2"
|
|||
log = "0.3.5"
|
||||
msg = {path = "../msg"}
|
||||
num-traits = "0.1.32"
|
||||
openssl = "0.7.6"
|
||||
parse-hosts = "0.3.0"
|
||||
serde = "0.9"
|
||||
serde_derive = "0.9"
|
||||
|
|
|
@ -22,6 +22,7 @@ extern crate lazy_static;
|
|||
extern crate log;
|
||||
extern crate msg;
|
||||
extern crate num_traits;
|
||||
extern crate openssl;
|
||||
extern crate parse_hosts;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
|
@ -35,16 +36,19 @@ extern crate webrender_traits;
|
|||
use cookie_rs::Cookie;
|
||||
use filemanager_thread::FileManagerThreadMsg;
|
||||
use heapsize::HeapSizeOf;
|
||||
use hyper::Error as HyperError;
|
||||
use hyper::header::{ContentType, Headers, ReferrerPolicy as ReferrerPolicyHeader};
|
||||
use hyper::http::RawStatus;
|
||||
use hyper::mime::{Attr, Mime};
|
||||
use hyper_serde::Serde;
|
||||
use ipc_channel::Error;
|
||||
use ipc_channel::Error as IpcError;
|
||||
use ipc_channel::ipc::{self, IpcReceiver, IpcSender};
|
||||
use ipc_channel::router::ROUTER;
|
||||
use openssl::ssl::error::{OpensslError, SslError};
|
||||
use request::{Request, RequestInit};
|
||||
use response::{HttpsState, Response};
|
||||
use servo_url::ServoUrl;
|
||||
use std::error::Error;
|
||||
use storage_thread::StorageThreadMsg;
|
||||
|
||||
pub mod blob_url_store;
|
||||
|
@ -265,7 +269,7 @@ impl<T: FetchResponseListener> Action<T> for FetchResponseMsg {
|
|||
/// Handle to a resource thread
|
||||
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,
|
||||
/// used by net_traits::ResourceThreads to ease the use its IpcSender sub-fields
|
||||
|
@ -526,6 +530,69 @@ pub enum NetworkError {
|
|||
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
|
||||
/// [the Fetch Spec](https://fetch.spec.whatwg.org/#concept-header-value-normalize).
|
||||
pub fn trim_http_whitespace(mut slice: &[u8]) -> &[u8] {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue