diff options
author | Keith Yeung <kungfukeith11@gmail.com> | 2016-01-17 23:34:41 -0500 |
---|---|---|
committer | Keith Yeung <kungfukeith11@gmail.com> | 2016-01-18 00:57:19 -0500 |
commit | a53661f6c8da24b1e0f634097e04cb7574947914 (patch) | |
tree | 08ef2c2a0472077aff210c9a02c0932b7f0a3fa0 /components/net/fetch/methods.rs | |
parent | eb7b9309dd4dabb5ffc5e792af8cd0cfc6ad0435 (diff) | |
download | servo-a53661f6c8da24b1e0f634097e04cb7574947914.tar.gz servo-a53661f6c8da24b1e0f634097e04cb7574947914.zip |
Rename net/fetch/request.rs to net/fetch/methods.rs and move Request to net_traits
Diffstat (limited to 'components/net/fetch/methods.rs')
-rw-r--r-- | components/net/fetch/methods.rs | 751 |
1 files changed, 751 insertions, 0 deletions
diff --git a/components/net/fetch/methods.rs b/components/net/fetch/methods.rs new file mode 100644 index 00000000000..28cf08382bc --- /dev/null +++ b/components/net/fetch/methods.rs @@ -0,0 +1,751 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use fetch::cors_cache::{BasicCORSCache, CORSCache, CacheRequestDetails}; +use fetch::response::ResponseMethods; +use http_loader::{NetworkHttpRequestFactory, WrappedHttpResponse}; +use http_loader::{create_http_connector, obtain_response}; +use hyper::client::response::Response as HyperResponse; +use hyper::header::{Accept, IfMatch, IfRange, IfUnmodifiedSince, Location}; +use hyper::header::{AcceptLanguage, ContentLength, ContentLanguage, HeaderView}; +use hyper::header::{Authorization, Basic, ContentEncoding, Encoding}; +use hyper::header::{ContentType, Header, Headers, IfModifiedSince, IfNoneMatch}; +use hyper::header::{QualityItem, q, qitem, Referer as RefererHeader, UserAgent}; +use hyper::method::Method; +use hyper::mime::{Attr, Mime, SubLevel, TopLevel, Value}; +use hyper::status::StatusCode; +use net_traits::request::{CacheMode, Context, ContextFrameType, CredentialsMode}; +use net_traits::request::{RedirectMode, Referer, Request, RequestMode, ResponseTainting}; +use net_traits::response::{CacheState, HttpsState, Response, ResponseType, TerminationReason}; +use net_traits::{AsyncFetchListener, Metadata}; +use resource_thread::CancellationListener; +use std::ascii::AsciiExt; +use std::rc::Rc; +use std::str::FromStr; +use std::thread; +use url::{Origin, Url, UrlParser}; +use util::thread::spawn_named; + +pub fn fetch_async(request: Request, cors_flag: bool, listener: Box<AsyncFetchListener + Send>) { + spawn_named(format!("fetch for {:?}", request.get_last_url_string()), move || { + let request = Rc::new(request); + let res = fetch(request, cors_flag); + listener.response_available(res); + }) +} + +/// [Fetch](https://fetch.spec.whatwg.org#concept-fetch) +pub fn fetch(request: Rc<Request>, cors_flag: bool) -> Response { + + // Step 1 + if request.context != Context::Fetch && !request.headers.borrow().has::<Accept>() { + + // Substep 1 + let value = match request.context { + + Context::Favicon | Context::Image | Context::ImageSet + => vec![qitem(Mime(TopLevel::Image, SubLevel::Png, vec![])), + // FIXME: This should properly generate a MimeType that has a + // SubLevel of svg+xml (https://github.com/hyperium/mime.rs/issues/22) + qitem(Mime(TopLevel::Image, SubLevel::Ext("svg+xml".to_owned()), vec![])), + QualityItem::new(Mime(TopLevel::Image, SubLevel::Star, vec![]), q(0.8)), + QualityItem::new(Mime(TopLevel::Star, SubLevel::Star, vec![]), q(0.5))], + + Context::Form | Context::Frame | Context::Hyperlink | + Context::IFrame | Context::Location | Context::MetaRefresh | + Context::PreRender + => vec![qitem(Mime(TopLevel::Text, SubLevel::Html, vec![])), + // FIXME: This should properly generate a MimeType that has a + // SubLevel of xhtml+xml (https://github.com/hyperium/mime.rs/issues/22) + qitem(Mime(TopLevel::Application, SubLevel::Ext("xhtml+xml".to_owned()), vec![])), + QualityItem::new(Mime(TopLevel::Application, SubLevel::Xml, vec![]), q(0.9)), + QualityItem::new(Mime(TopLevel::Star, SubLevel::Star, vec![]), q(0.8))], + + Context::Internal if request.context_frame_type != ContextFrameType::ContextNone + => vec![qitem(Mime(TopLevel::Text, SubLevel::Html, vec![])), + // FIXME: This should properly generate a MimeType that has a + // SubLevel of xhtml+xml (https://github.com/hyperium/mime.rs/issues/22) + qitem(Mime(TopLevel::Application, SubLevel::Ext("xhtml+xml".to_owned()), vec![])), + QualityItem::new(Mime(TopLevel::Application, SubLevel::Xml, vec![]), q(0.9)), + QualityItem::new(Mime(TopLevel::Star, SubLevel::Star, vec![]), q(0.8))], + + Context::Style + => vec![qitem(Mime(TopLevel::Text, SubLevel::Css, vec![])), + QualityItem::new(Mime(TopLevel::Star, SubLevel::Star, vec![]), q(0.1))], + _ => vec![qitem(Mime(TopLevel::Star, SubLevel::Star, vec![]))] + }; + + // Substep 2 + request.headers.borrow_mut().set(Accept(value)); + } + + // Step 2 + if request.context != Context::Fetch && !request.headers.borrow().has::<AcceptLanguage>() { + request.headers.borrow_mut().set(AcceptLanguage(vec![qitem("en-US".parse().unwrap())])); + } + + // TODO: Figure out what a Priority object is + // Step 3 + // Step 4 + main_fetch(request, cors_flag) +} + +/// [Main fetch](https://fetch.spec.whatwg.org/#concept-main-fetch) +fn main_fetch(request: Rc<Request>, _cors_flag: bool) -> Response { + // TODO: Implement main fetch spec + let response = basic_fetch(request); + response +} + +/// [Basic fetch](https://fetch.spec.whatwg.org#basic-fetch) +fn basic_fetch(request: Rc<Request>) -> Response { + + let url = request.current_url(); + let scheme = url.scheme.clone(); + + match &*scheme { + + "about" => { + match url.non_relative_scheme_data() { + Some(s) if &*s == "blank" => { + let mut response = Response::new(); + response.headers.set(ContentType(Mime( + TopLevel::Text, SubLevel::Html, + vec![(Attr::Charset, Value::Utf8)]))); + response + }, + _ => Response::network_error() + } + }, + + "http" | "https" => { + http_fetch(request.clone(), BasicCORSCache::new(), false, false, false) + }, + + "blob" | "data" | "file" | "ftp" => { + // XXXManishearth handle these + panic!("Unimplemented scheme for Fetch") + }, + + _ => Response::network_error() + } +} + +fn http_fetch_async(request: Request, + cors_flag: bool, + cors_preflight_flag: bool, + authentication_fetch_flag: bool, + listener: Box<AsyncFetchListener + Send>) { + + spawn_named(format!("http_fetch for {:?}", request.get_last_url_string()), move || { + let request = Rc::new(request); + let res = http_fetch(request, BasicCORSCache::new(), + cors_flag, cors_preflight_flag, + authentication_fetch_flag); + listener.response_available(res); + }); +} + +/// [HTTP fetch](https://fetch.spec.whatwg.org#http-fetch) +fn http_fetch(request: Rc<Request>, + mut cache: BasicCORSCache, + cors_flag: bool, + cors_preflight_flag: bool, + authentication_fetch_flag: bool) -> Response { + + // Step 1 + let mut response: Option<Rc<Response>> = None; + + // Step 2 + let mut actual_response: Option<Rc<Response>> = None; + + // Step 3 + if !request.skip_service_worker.get() && !request.is_service_worker_global_scope { + + // TODO: Substep 1 (handle fetch unimplemented) + + if let Some(ref res) = response { + + // Substep 2 + actual_response = match res.internal_response { + Some(ref internal_res) => Some(internal_res.clone()), + None => Some(res.clone()) + }; + + // Substep 3 + if (res.response_type == ResponseType::Opaque && + request.mode != RequestMode::NoCORS) || + (res.response_type == ResponseType::OpaqueRedirect && + request.redirect_mode != RedirectMode::Manual) || + res.response_type == ResponseType::Error { + return Response::network_error(); + } + } + + // Substep 4 + if let Some(ref res) = actual_response { + if res.url_list.borrow().is_empty() { + *res.url_list.borrow_mut() = request.url_list.borrow().clone(); + } + } + + // Substep 5 + // TODO: set response's CSP list on actual_response + } + + // Step 4 + if response.is_none() { + + // Substep 1 + if cors_preflight_flag { + let mut method_mismatch = false; + let mut header_mismatch = false; + + // FIXME: Once Url::Origin is available, rewrite origin to + // take an Origin instead of a Url + let origin = request.origin.clone().unwrap_or(Url::parse("").unwrap()); + let url = request.current_url(); + let credentials = request.credentials_mode == CredentialsMode::Include; + let method_cache_match = cache.match_method(CacheRequestDetails { + origin: origin.clone(), + destination: url.clone(), + credentials: credentials + }, request.method.borrow().clone()); + + method_mismatch = !method_cache_match && (!is_simple_method(&request.method.borrow()) || + request.mode == RequestMode::ForcedPreflightMode); + header_mismatch = request.headers.borrow().iter().any(|view| + !cache.match_header(CacheRequestDetails { + origin: origin.clone(), + destination: url.clone(), + credentials: credentials + }, view.name()) && !is_simple_header(&view) + ); + + if method_mismatch || header_mismatch { + let preflight_result = preflight_fetch(request.clone()); + if preflight_result.response_type == ResponseType::Error { + return Response::network_error(); + } + response = Some(Rc::new(preflight_result)); + } + } + + // Substep 2 + request.skip_service_worker.set(true); + + // Substep 3 + let credentials = match request.credentials_mode { + CredentialsMode::Include => true, + CredentialsMode::CredentialsSameOrigin if (!cors_flag || + request.response_tainting == ResponseTainting::Opaque) + => true, + _ => false + }; + + // Substep 4 + let fetch_result = http_network_or_cache_fetch(request.clone(), credentials, authentication_fetch_flag); + + // Substep 5 + if cors_flag && cors_check(request.clone(), &fetch_result).is_err() { + return Response::network_error(); + } + + response = Some(Rc::new(fetch_result)); + actual_response = response.clone(); + } + + // response and actual_response are guaranteed to be something by now + let mut response = response.unwrap(); + let actual_response = actual_response.unwrap(); + + // Step 5 + match actual_response.status.unwrap() { + + // Code 301, 302, 303, 307, 308 + StatusCode::MovedPermanently | StatusCode::Found | StatusCode::SeeOther | + StatusCode::TemporaryRedirect | StatusCode::PermanentRedirect => { + + // Step 1 + if request.redirect_mode == RedirectMode::Error { + return Response::network_error(); + } + + // Step 3 + if !actual_response.headers.has::<Location>() { + drop(actual_response); + return Rc::try_unwrap(response).ok().unwrap(); + } + + // Step 2 + let location = match actual_response.headers.get::<Location>() { + Some(&Location(ref location)) => location.clone(), + // Step 4 + _ => return Response::network_error(), + }; + + // Step 5 + let location_url = UrlParser::new().base_url(&request.current_url()).parse(&*location); + + // Step 6 + let location_url = match location_url { + Ok(url) => url, + _ => { return Response::network_error(); } + }; + + // Step 7 + if request.redirect_count.get() == 20 { + return Response::network_error(); + } + + // Step 8 + request.redirect_count.set(request.redirect_count.get() + 1); + + // Step 9 + request.same_origin_data.set(false); + + match request.redirect_mode { + + // Step 10 + RedirectMode::Manual => { + response = Rc::new(Response::to_filtered(actual_response, ResponseType::Opaque)); + } + + // Step 11 + RedirectMode::Follow => { + + // Substep 1 + // FIXME: Use Url::origin + // if (request.mode == RequestMode::CORSMode || + // request.mode == RequestMode::ForcedPreflightMode) && + // location_url.origin() != request.url.origin() && + // has_credentials(&location_url) { + // return Response::network_error(); + // } + + // Substep 2 + if cors_flag && has_credentials(&location_url) { + return Response::network_error(); + } + + // Substep 3 + // FIXME: Use Url::origin + // if cors_flag && location_url.origin() != request.url.origin() { + // request.borrow_mut().origin = Origin::UID(OpaqueOrigin); + // } + + // Substep 4 + if actual_response.status.unwrap() == StatusCode::SeeOther || + ((actual_response.status.unwrap() == StatusCode::MovedPermanently || + actual_response.status.unwrap() == StatusCode::Found) && + *request.method.borrow() == Method::Post) { + *request.method.borrow_mut() = Method::Get; + } + + // Substep 5 + request.url_list.borrow_mut().push(location_url); + + // Substep 6 + return main_fetch(request.clone(), cors_flag); + } + RedirectMode::Error => { panic!("RedirectMode is Error after step 8") } + } + } + + // Code 401 + StatusCode::Unauthorized => { + + // Step 1 + // FIXME: Figure out what to do with request window objects + if cors_flag { + drop(actual_response); + return Rc::try_unwrap(response).ok().unwrap(); + } + + // Step 2 + // TODO: Spec says requires testing on multiple WWW-Authenticate headers + + // Step 3 + if !request.use_url_credentials || authentication_fetch_flag { + // TODO: Prompt the user for username and password from the window + } + + // Step 4 + return http_fetch(request, BasicCORSCache::new(), cors_flag, cors_preflight_flag, true); + } + + // Code 407 + StatusCode::ProxyAuthenticationRequired => { + + // Step 1 + // TODO: Figure out what to do with request window objects + + // Step 2 + // TODO: Spec says requires testing on Proxy-Authenticate headers + + // Step 3 + // TODO: Prompt the user for proxy authentication credentials + + // Step 4 + return http_fetch(request, BasicCORSCache::new(), + cors_flag, cors_preflight_flag, + authentication_fetch_flag); + } + + _ => drop(actual_response) + } + + // Step 6 + if authentication_fetch_flag { + // TODO: Create authentication entry for this request + } + + // Step 7 + Rc::try_unwrap(response).ok().unwrap() +} + +/// [HTTP network or cache fetch](https://fetch.spec.whatwg.org#http-network-or-cache-fetch) +fn http_network_or_cache_fetch(request: Rc<Request>, + credentials_flag: bool, + authentication_fetch_flag: bool) -> Response { + + // TODO: Implement Window enum for Request + let request_has_no_window = true; + + // Step 1 + let http_request = if request_has_no_window && + request.redirect_mode != RedirectMode::Follow { + request.clone() + } else { + Rc::new((*request).clone()) + }; + + let content_length_value = match http_request.body { + None => + match *http_request.method.borrow() { + // Step 3 + Method::Head | Method::Post | Method::Put => + Some(0), + // Step 2 + _ => None + }, + // Step 4 + Some(ref http_request_body) => Some(http_request_body.len() as u64) + }; + + // Step 5 + if let Some(content_length_value) = content_length_value { + http_request.headers.borrow_mut().set(ContentLength(content_length_value)); + } + + // Step 6 + match http_request.referer { + Referer::NoReferer => + http_request.headers.borrow_mut().set(RefererHeader("".to_owned())), + Referer::RefererUrl(ref http_request_referer) => + http_request.headers.borrow_mut().set(RefererHeader(http_request_referer.serialize())), + Referer::Client => + // it should be impossible for referer to be anything else during fetching + // https://fetch.spec.whatwg.org/#concept-request-referrer + unreachable!() + }; + + // Step 7 + if http_request.omit_origin_header == false { + // TODO update this when https://github.com/hyperium/hyper/pull/691 is finished + if let Some(ref _origin) = http_request.origin { + // http_request.headers.borrow_mut().set_raw("origin", origin); + } + } + + // Step 8 + if !http_request.headers.borrow().has::<UserAgent>() { + http_request.headers.borrow_mut().set(UserAgent(global_user_agent().to_owned())); + } + + // Step 9 + if http_request.cache_mode.get() == CacheMode::Default && is_no_store_cache(&http_request.headers.borrow()) { + http_request.cache_mode.set(CacheMode::NoStore); + } + + // Step 10 + // modify_request_headers(http_request.headers.borrow()); + + // Step 11 + // TODO some of this step can't be implemented yet + if credentials_flag { + // Substep 1 + // TODO http://mxr.mozilla.org/servo/source/components/net/http_loader.rs#504 + + // Substep 2 + let mut authorization_value = None; + + // Substep 3 + // TODO be able to retrieve https://fetch.spec.whatwg.org/#authentication-entry + + // Substep 4 + if authentication_fetch_flag { + + let current_url = http_request.current_url(); + + authorization_value = if includes_credentials(¤t_url) { + Some(Basic { + username: current_url.username().unwrap_or("").to_owned(), + password: current_url.password().map(str::to_owned) + }) + + } else { + None + } + } + + // Substep 5 + if let Some(basic) = authorization_value { + http_request.headers.borrow_mut().set(Authorization(basic)); + } + } + + // Step 12 + // TODO this step can't be implemented + + // Step 13 + let mut response: Option<Response> = None; + + // Step 14 + // TODO have a HTTP cache to check for a completed response + let complete_http_response_from_cache: Option<Response> = None; + if http_request.cache_mode.get() != CacheMode::NoStore && + http_request.cache_mode.get() != CacheMode::Reload && + complete_http_response_from_cache.is_some() { + + // Substep 1 + if http_request.cache_mode.get() == CacheMode::ForceCache { + // TODO pull response from HTTP cache + // response = http_request + } + + let revalidation_needed = match response { + Some(ref response) => response_needs_revalidation(&response), + _ => false + }; + + // Substep 2 + if !revalidation_needed && http_request.cache_mode.get() == CacheMode::Default { + // TODO pull response from HTTP cache + // response = http_request + // response.cache_state = CacheState::Local; + } + + // Substep 3 + if revalidation_needed && http_request.cache_mode.get() == CacheMode::Default || + http_request.cache_mode.get() == CacheMode::NoCache { + + // TODO this substep + } + + // Step 15 + // TODO have a HTTP cache to check for a partial response + } else if http_request.cache_mode.get() == CacheMode::Default || + http_request.cache_mode.get() == CacheMode::ForceCache { + // TODO this substep + } + + // Step 16 + if response.is_none() { + response = Some(http_network_fetch(request.clone(), http_request.clone(), credentials_flag)); + } + let response = response.unwrap(); + + // Step 17 + if let Some(status) = response.status { + if status == StatusCode::NotModified && + (http_request.cache_mode.get() == CacheMode::Default || + http_request.cache_mode.get() == CacheMode::NoCache) { + + // Substep 1 + // TODO this substep + let cached_response: Option<Response> = None; + + // Substep 2 + if cached_response.is_none() { + return Response::network_error(); + } + + // Substep 3 + + // Substep 4 + // response = cached_response; + + // Substep 5 + // TODO cache_state is immutable? + // response.cache_state = CacheState::Validated; + } + } + + // Step 18 + response +} + +/// [HTTP network fetch](https://fetch.spec.whatwg.org/#http-network-fetch) +fn http_network_fetch(request: Rc<Request>, + http_request: Rc<Request>, + credentials_flag: bool) -> Response { + // TODO: Implement HTTP network fetch spec + + // Step 1 + // nothing to do here, since credentials_flag is already a boolean + + // Step 2 + // TODO be able to create connection using current url's origin and credentials + let connection = create_http_connector(); + + // Step 3 + // TODO be able to tell if the connection is a failure + + // Step 4 + let factory = NetworkHttpRequestFactory { + connector: connection, + }; + let url = request.current_url(); + let cancellation_listener = CancellationListener::new(None); + + let wrapped_response = obtain_response(&factory, &url, &request.method.borrow(), + &request.headers.borrow(), + &cancellation_listener, &None, &request.method.borrow(), + &None, request.redirect_count.get(), &None, ""); + + let mut response = Response::new(); + match wrapped_response { + Ok(res) => { + // is it okay for res.version to be unused? + response.url = Some(res.response.url.clone()); + response.status = Some(res.response.status); + response.headers = res.response.headers.clone(); + }, + Err(e) => + response.termination_reason = Some(TerminationReason::Fatal) + }; + + // TODO these substeps aren't possible yet + // Substep 1 + + // Substep 2 + + // TODO how can I tell if response was retrieved over HTTPS? + // TODO: Servo needs to decide what ciphers are to be treated as "deprecated" + response.https_state = HttpsState::None; + + // TODO how do I read request? + + // Step 5 + // TODO when https://bugzilla.mozilla.org/show_bug.cgi?id=1030660 + // is resolved, this step will become uneccesary + // TODO this step + if let Some(encoding) = response.headers.get::<ContentEncoding>() { + if encoding.contains(&Encoding::Gzip) { + + } + + else if encoding.contains(&Encoding::Compress) { + + } + }; + + // Step 6 + *response.url_list.borrow_mut() = request.url_list.borrow().clone(); + + // Step 7 + + // Step 8 + + // Step 9 + // Substep 1 + // Substep 2 + // Substep 3 + // Substep 4 + + // Step 10 + // Substep 1 + // Substep 2 + // Sub-substep 1 + // Sub-substep 2 + // Sub-substep 3 + // Sub-substep 4 + // Substep 3 + + // Step 11 + response +} + +/// [CORS preflight fetch](https://fetch.spec.whatwg.org#cors-preflight-fetch) +fn preflight_fetch(request: Rc<Request>) -> Response { + // TODO: Implement preflight fetch spec + Response::network_error() +} + +/// [CORS check](https://fetch.spec.whatwg.org#concept-cors-check) +fn cors_check(request: Rc<Request>, response: &Response) -> Result<(), ()> { + // TODO: Implement CORS check spec + Err(()) +} + +fn global_user_agent() -> String { + // TODO have a better useragent string + const USER_AGENT_STRING: &'static str = "Servo"; + USER_AGENT_STRING.to_owned() +} + +fn has_credentials(url: &Url) -> bool { + !url.username().unwrap_or("").is_empty() || url.password().is_some() +} + +fn is_no_store_cache(headers: &Headers) -> bool { + headers.has::<IfModifiedSince>() | headers.has::<IfNoneMatch>() | + headers.has::<IfUnmodifiedSince>() | headers.has::<IfMatch>() | + headers.has::<IfRange>() +} + +fn is_simple_header(h: &HeaderView) -> bool { + if h.is::<ContentType>() { + match h.value() { + Some(&ContentType(Mime(TopLevel::Text, SubLevel::Plain, _))) | + Some(&ContentType(Mime(TopLevel::Application, SubLevel::WwwFormUrlEncoded, _))) | + Some(&ContentType(Mime(TopLevel::Multipart, SubLevel::FormData, _))) => true, + _ => false + + } + } else { + h.is::<Accept>() || h.is::<AcceptLanguage>() || h.is::<ContentLanguage>() + } +} + +fn is_simple_method(m: &Method) -> bool { + match *m { + Method::Get | Method::Head | Method::Post => true, + _ => false + } +} + +fn includes_credentials(url: &Url) -> bool { + + if url.password().is_some() { + return true + } + + if let Some(name) = url.username() { + return name.len() > 0 + } + + false +} + +fn response_needs_revalidation(response: &Response) -> bool { + // TODO this function + false +} + +// fn modify_request_headers(headers: &mut Headers) -> { +// // TODO this function + +// } |