diff options
Diffstat (limited to 'components/shared/net')
-rw-r--r-- | components/shared/net/Cargo.toml | 42 | ||||
-rw-r--r-- | components/shared/net/blob_url_store.rs | 75 | ||||
-rw-r--r-- | components/shared/net/fetch/headers.rs | 18 | ||||
-rw-r--r-- | components/shared/net/filemanager_thread.rs | 190 | ||||
-rw-r--r-- | components/shared/net/image/base.rs | 121 | ||||
-rw-r--r-- | components/shared/net/image_cache.rs | 156 | ||||
-rw-r--r-- | components/shared/net/lib.rs | 859 | ||||
-rw-r--r-- | components/shared/net/pub_domains.rs | 159 | ||||
-rw-r--r-- | components/shared/net/quality.rs | 87 | ||||
-rw-r--r-- | components/shared/net/request.rs | 749 | ||||
-rw-r--r-- | components/shared/net/response.rs | 364 | ||||
-rw-r--r-- | components/shared/net/storage_thread.rs | 48 | ||||
-rw-r--r-- | components/shared/net/tests/image.rs | 28 | ||||
-rw-r--r-- | components/shared/net/tests/lib.rs | 212 | ||||
-rw-r--r-- | components/shared/net/tests/pub_domains.rs | 122 | ||||
-rw-r--r-- | components/shared/net/tests/whitespace.rs | 25 |
16 files changed, 3255 insertions, 0 deletions
diff --git a/components/shared/net/Cargo.toml b/components/shared/net/Cargo.toml new file mode 100644 index 00000000000..432c8676ccb --- /dev/null +++ b/components/shared/net/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "net_traits" +version = "0.0.1" +authors = ["The Servo Project Developers"] +license = "MPL-2.0" +edition = "2018" +publish = false + +[lib] +name = "net_traits" +path = "lib.rs" +test = false +doctest = false + +[dependencies] +content-security-policy = { workspace = true } +cookie = { workspace = true } +embedder_traits = { workspace = true } +headers = { workspace = true } +http = { workspace = true } +hyper = { workspace = true } +hyper_serde = { workspace = true } +image = { workspace = true } +ipc-channel = { workspace = true } +lazy_static = { workspace = true } +log = { workspace = true } +malloc_size_of = { path = "../../malloc_size_of" } +malloc_size_of_derive = { workspace = true } +mime = { workspace = true } +msg = { workspace = true } +num-traits = { workspace = true } +percent-encoding = { workspace = true } +pixels = { path = "../../pixels" } +rustls = { workspace = true } +serde = { workspace = true } +servo_arc = { path = "../../servo_arc" } +servo_rand = { path = "../../rand" } +servo_url = { path = "../../url" } +time = { workspace = true } +url = { workspace = true } +uuid = { workspace = true } +webrender_api = { git = "https://github.com/servo/webrender" } diff --git a/components/shared/net/blob_url_store.rs b/components/shared/net/blob_url_store.rs new file mode 100644 index 00000000000..ab853b702c9 --- /dev/null +++ b/components/shared/net/blob_url_store.rs @@ -0,0 +1,75 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; +use servo_url::ServoUrl; +use url::Url; +use uuid::Uuid; + +use crate::filemanager_thread::FileOrigin; + +/// Errors returned to Blob URL Store request +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum BlobURLStoreError { + /// Invalid File UUID + InvalidFileID, + /// Invalid URL origin + InvalidOrigin, + /// Invalid entry content + InvalidEntry, + /// Invalid range + InvalidRange, + /// External error, from like file system, I/O etc. + External(String), +} + +/// Standalone blob buffer object +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct BlobBuf { + pub filename: Option<String>, + /// MIME type string + pub type_string: String, + /// Size of content in bytes + pub size: u64, + /// Content of blob + pub bytes: Vec<u8>, +} + +/// Parse URL as Blob URL scheme's definition +/// +/// <https://w3c.github.io/FileAPI/#DefinitionOfScheme> +pub fn parse_blob_url(url: &ServoUrl) -> Result<(Uuid, FileOrigin), ()> { + let url_inner = Url::parse(url.path()).map_err(|_| ())?; + let segs = url_inner + .path_segments() + .map(|c| c.collect::<Vec<_>>()) + .ok_or(())?; + + if url.query().is_some() || segs.len() > 1 { + return Err(()); + } + + let id = { + let id = segs.first().ok_or(())?; + Uuid::from_str(id).map_err(|_| ())? + }; + Ok((id, get_blob_origin(&ServoUrl::from_url(url_inner)))) +} + +/// Given an URL, returning the Origin that a Blob created under this +/// URL should have. +/// +/// HACK(izgzhen): Not well-specified on spec, and it is a bit a hack +/// both due to ambiguity of spec and that we have to serialization the +/// Origin here. +pub fn get_blob_origin(url: &ServoUrl) -> FileOrigin { + if url.scheme() == "file" { + // NOTE: by default this is "null" (Opaque), which is not ideal + "file://".to_string() + } else { + url.origin().ascii_serialization() + } +} diff --git a/components/shared/net/fetch/headers.rs b/components/shared/net/fetch/headers.rs new file mode 100644 index 00000000000..ae95066bcf5 --- /dev/null +++ b/components/shared/net/fetch/headers.rs @@ -0,0 +1,18 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +use headers::HeaderMap; + +/// <https://fetch.spec.whatwg.org/#concept-header-list-get> +pub fn get_value_from_header_list(name: &str, headers: &HeaderMap) -> Option<Vec<u8>> { + let values = headers.get_all(name).iter().map(|val| val.as_bytes()); + + // Step 1 + if values.size_hint() == (0, Some(0)) { + return None; + } + + // Step 2 + return Some(values.collect::<Vec<&[u8]>>().join(&[0x2C, 0x20][..])); +} diff --git a/components/shared/net/filemanager_thread.rs b/components/shared/net/filemanager_thread.rs new file mode 100644 index 00000000000..ee18e295393 --- /dev/null +++ b/components/shared/net/filemanager_thread.rs @@ -0,0 +1,190 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +use std::cmp::{max, min}; +use std::ops::Range; +use std::path::PathBuf; + +use embedder_traits::FilterPattern; +use ipc_channel::ipc::IpcSender; +use malloc_size_of_derive::MallocSizeOf; +use num_traits::ToPrimitive; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::blob_url_store::{BlobBuf, BlobURLStoreError}; + +// HACK: Not really process-safe now, we should send Origin +// directly instead of this in future, blocked on #11722 +/// File manager store entry's origin +pub type FileOrigin = String; + +/// A token modulating access to a file for a blob URL. +pub enum FileTokenCheck { + /// Checking against a token not required, + /// used for accessing a file + /// that isn't linked to from a blob URL. + NotRequired, + /// Checking against token required. + Required(Uuid), + /// Request should always fail, + /// used for cases when a check is required, + /// but no token could be acquired. + ShouldFail, +} + +/// Relative slice positions of a sequence, +/// whose semantic should be consistent with (start, end) parameters in +/// <https://w3c.github.io/FileAPI/#dfn-slice> +#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)] +pub struct RelativePos { + /// Relative to first byte if non-negative, + /// relative to one past last byte if negative, + pub start: i64, + /// Relative offset from first byte if Some(non-negative), + /// relative to one past last byte if Some(negative), + /// None if one past last byte + pub end: Option<i64>, +} + +impl RelativePos { + /// Full range from start to end + pub fn full_range() -> RelativePos { + RelativePos { + start: 0, + end: None, + } + } + + /// Instantiate optional slice position parameters + pub fn from_opts(start: Option<i64>, end: Option<i64>) -> RelativePos { + RelativePos { + start: start.unwrap_or(0), + end, + } + } + + /// Slice the inner sliced range by repositioning + pub fn slice_inner(&self, rel_pos: &RelativePos) -> RelativePos { + RelativePos { + start: self.start + rel_pos.start, + end: match (self.end, rel_pos.end) { + (Some(old_end), Some(rel_end)) => Some(old_end + rel_end), + (old, None) => old, + (None, rel) => rel, + }, + } + } + + /// Compute absolute range by giving the total size + /// <https://w3c.github.io/FileAPI/#slice-method-algo> + pub fn to_abs_range(&self, size: usize) -> Range<usize> { + let size = size as i64; + + let start = { + if self.start < 0 { + max(size + self.start, 0) + } else { + min(self.start, size) + } + }; + + let end = match self.end { + Some(rel_end) => { + if rel_end < 0 { + max(size + rel_end, 0) + } else { + min(rel_end, size) + } + }, + None => size, + }; + + let span: i64 = max(end - start, 0); + + Range { + start: start.to_usize().unwrap(), + end: (start + span).to_usize().unwrap(), + } + } +} + +/// Response to file selection request +#[derive(Debug, Deserialize, Serialize)] +pub struct SelectedFile { + pub id: Uuid, + pub filename: PathBuf, + pub modified: u64, + pub size: u64, + // https://w3c.github.io/FileAPI/#dfn-type + pub type_string: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub enum FileManagerThreadMsg { + /// Select a single file. Last field is pre-selected file path for testing + SelectFile( + Vec<FilterPattern>, + IpcSender<FileManagerResult<SelectedFile>>, + FileOrigin, + Option<String>, + ), + + /// Select multiple files. Last field is pre-selected file paths for testing + SelectFiles( + Vec<FilterPattern>, + IpcSender<FileManagerResult<Vec<SelectedFile>>>, + FileOrigin, + Option<Vec<String>>, + ), + + /// Read FileID-indexed file in chunks, optionally check URL validity based on boolean flag + ReadFile( + IpcSender<FileManagerResult<ReadFileProgress>>, + Uuid, + FileOrigin, + ), + + /// Add an entry as promoted memory-based blob + PromoteMemory(Uuid, BlobBuf, bool, FileOrigin), + + /// Add a sliced entry pointing to the parent FileID, and send back the associated FileID + /// as part of a valid Blob URL + AddSlicedURLEntry( + Uuid, + RelativePos, + IpcSender<Result<Uuid, BlobURLStoreError>>, + FileOrigin, + ), + + /// Decrease reference count and send back the acknowledgement + DecRef(Uuid, FileOrigin, IpcSender<Result<(), BlobURLStoreError>>), + + /// Activate an internal FileID so it becomes valid as part of a Blob URL + ActivateBlobURL(Uuid, IpcSender<Result<(), BlobURLStoreError>>, FileOrigin), + + /// Revoke Blob URL and send back the acknowledgement + RevokeBlobURL(Uuid, FileOrigin, IpcSender<Result<(), BlobURLStoreError>>), +} + +#[derive(Debug, Deserialize, Serialize)] +pub enum ReadFileProgress { + Meta(BlobBuf), + Partial(Vec<u8>), + EOF, +} + +pub type FileManagerResult<T> = Result<T, FileManagerThreadError>; + +#[derive(Debug, Deserialize, Serialize)] +pub enum FileManagerThreadError { + /// The selection action is invalid due to exceptional reason + InvalidSelection, + /// The selection action is cancelled by user + UserCancelled, + /// Errors returned from file system request + FileSystemError(String), + /// Blob URL Store error + BlobURLStoreError(BlobURLStoreError), +} diff --git a/components/shared/net/image/base.rs b/components/shared/net/image/base.rs new file mode 100644 index 00000000000..74e2d8823dc --- /dev/null +++ b/components/shared/net/image/base.rs @@ -0,0 +1,121 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +use std::fmt; + +use image::ImageFormat; +use ipc_channel::ipc::IpcSharedMemory; +use log::debug; +use malloc_size_of_derive::MallocSizeOf; +use pixels::PixelFormat; +use serde::{Deserialize, Serialize}; +use webrender_api::ImageKey; + +use crate::image_cache::CorsStatus; + +#[derive(Clone, Deserialize, MallocSizeOf, Serialize)] +pub struct Image { + pub width: u32, + pub height: u32, + pub format: PixelFormat, + #[ignore_malloc_size_of = "Defined in ipc-channel"] + pub bytes: IpcSharedMemory, + #[ignore_malloc_size_of = "Defined in webrender_api"] + pub id: Option<ImageKey>, + pub cors_status: CorsStatus, +} + +impl fmt::Debug for Image { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Image {{ width: {}, height: {}, format: {:?}, ..., id: {:?} }}", + self.width, self.height, self.format, self.id + ) + } +} + +#[derive(Clone, Debug, Deserialize, Eq, MallocSizeOf, PartialEq, Serialize)] +pub struct ImageMetadata { + pub width: u32, + pub height: u32, +} + +// FIXME: Images must not be copied every frame. Instead we should atomically +// reference count them. + +pub fn load_from_memory(buffer: &[u8], cors_status: CorsStatus) -> Option<Image> { + if buffer.is_empty() { + return None; + } + + let image_fmt_result = detect_image_format(buffer); + match image_fmt_result { + Err(msg) => { + debug!("{}", msg); + None + }, + Ok(_) => match image::load_from_memory(buffer) { + Ok(image) => { + let mut rgba = image.into_rgba8(); + pixels::rgba8_byte_swap_colors_inplace(&mut *rgba); + Some(Image { + width: rgba.width(), + height: rgba.height(), + format: PixelFormat::BGRA8, + bytes: IpcSharedMemory::from_bytes(&*rgba), + id: None, + cors_status, + }) + }, + Err(e) => { + debug!("Image decoding error: {:?}", e); + None + }, + }, + } +} + +// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img +pub fn detect_image_format(buffer: &[u8]) -> Result<ImageFormat, &str> { + if is_gif(buffer) { + Ok(ImageFormat::Gif) + } else if is_jpeg(buffer) { + Ok(ImageFormat::Jpeg) + } else if is_png(buffer) { + Ok(ImageFormat::Png) + } else if is_webp(buffer) { + Ok(ImageFormat::WebP) + } else if is_bmp(buffer) { + Ok(ImageFormat::Bmp) + } else if is_ico(buffer) { + Ok(ImageFormat::Ico) + } else { + Err("Image Format Not Supported") + } +} + +fn is_gif(buffer: &[u8]) -> bool { + buffer.starts_with(b"GIF87a") || buffer.starts_with(b"GIF89a") +} + +fn is_jpeg(buffer: &[u8]) -> bool { + buffer.starts_with(&[0xff, 0xd8, 0xff]) +} + +fn is_png(buffer: &[u8]) -> bool { + buffer.starts_with(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) +} + +fn is_bmp(buffer: &[u8]) -> bool { + buffer.starts_with(&[0x42, 0x4D]) +} + +fn is_ico(buffer: &[u8]) -> bool { + buffer.starts_with(&[0x00, 0x00, 0x01, 0x00]) +} + +fn is_webp(buffer: &[u8]) -> bool { + buffer.starts_with(b"RIFF") && buffer.len() >= 14 && &buffer[8..14] == b"WEBPVP" +} diff --git a/components/shared/net/image_cache.rs b/components/shared/net/image_cache.rs new file mode 100644 index 00000000000..81d34964db4 --- /dev/null +++ b/components/shared/net/image_cache.rs @@ -0,0 +1,156 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +use std::sync::Arc; + +use ipc_channel::ipc::IpcSender; +use log::debug; +use malloc_size_of_derive::MallocSizeOf; +use serde::{Deserialize, Serialize}; +use servo_url::{ImmutableOrigin, ServoUrl}; + +use crate::image::base::{Image, ImageMetadata}; +use crate::request::CorsSettings; +use crate::{FetchResponseMsg, WebrenderIpcSender}; + +// ====================================================================== +// Aux structs and enums. +// ====================================================================== + +/// Indicating either entire image or just metadata availability +#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)] +pub enum ImageOrMetadataAvailable { + ImageAvailable { + #[ignore_malloc_size_of = "Arc"] + image: Arc<Image>, + url: ServoUrl, + is_placeholder: bool, + }, + MetadataAvailable(ImageMetadata), +} + +/// This is optionally passed to the image cache when requesting +/// and image, and returned to the specified event loop when the +/// image load completes. It is typically used to trigger a reflow +/// and/or repaint. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ImageResponder { + id: PendingImageId, + sender: IpcSender<PendingImageResponse>, +} + +impl ImageResponder { + pub fn new(sender: IpcSender<PendingImageResponse>, id: PendingImageId) -> ImageResponder { + ImageResponder { + sender: sender, + id: id, + } + } + + pub fn respond(&self, response: ImageResponse) { + debug!("Notifying listener"); + // This send can fail if thread waiting for this notification has panicked. + // That's not a case that's worth warning about. + // TODO(#15501): are there cases in which we should perform cleanup? + let _ = self.sender.send(PendingImageResponse { + response: response, + id: self.id, + }); + } +} + +/// The returned image. +#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)] +pub enum ImageResponse { + /// The requested image was loaded. + Loaded(#[ignore_malloc_size_of = "Arc"] Arc<Image>, ServoUrl), + /// The request image metadata was loaded. + MetadataLoaded(ImageMetadata), + /// The requested image failed to load, so a placeholder was loaded instead. + PlaceholderLoaded(#[ignore_malloc_size_of = "Arc"] Arc<Image>, ServoUrl), + /// Neither the requested image nor the placeholder could be loaded. + None, +} + +/// The unique id for an image that has previously been requested. +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize)] +pub struct PendingImageId(pub u64); + +#[derive(Debug, Deserialize, Serialize)] +pub struct PendingImageResponse { + pub response: ImageResponse, + pub id: PendingImageId, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub enum UsePlaceholder { + No, + Yes, +} + +// ====================================================================== +// ImageCache public API. +// ====================================================================== + +pub enum ImageCacheResult { + Available(ImageOrMetadataAvailable), + LoadError, + Pending(PendingImageId), + ReadyForRequest(PendingImageId), +} + +pub trait ImageCache: Sync + Send { + fn new(webrender_api: WebrenderIpcSender) -> Self + where + Self: Sized; + + /// Definitively check whether there is a cached, fully loaded image available. + fn get_image( + &self, + url: ServoUrl, + origin: ImmutableOrigin, + cors_setting: Option<CorsSettings>, + ) -> Option<Arc<Image>>; + + fn get_cached_image_status( + &self, + url: ServoUrl, + origin: ImmutableOrigin, + cors_setting: Option<CorsSettings>, + use_placeholder: UsePlaceholder, + ) -> ImageCacheResult; + + /// Add a listener for the provided pending image id, eventually called by + /// ImageCacheStore::complete_load. + /// If only metadata is available, Available(ImageOrMetadataAvailable) will + /// be returned. + /// If Available(ImageOrMetadataAvailable::Image) or LoadError is the final value, + /// the provided listener will be dropped (consumed & not added to PendingLoad). + fn track_image( + &self, + url: ServoUrl, + origin: ImmutableOrigin, + cors_setting: Option<CorsSettings>, + sender: IpcSender<PendingImageResponse>, + use_placeholder: UsePlaceholder, + ) -> ImageCacheResult; + + /// Add a new listener for the given pending image id. If the image is already present, + /// the responder will still receive the expected response. + fn add_listener(&self, id: PendingImageId, listener: ImageResponder); + + /// Inform the image cache about a response for a pending request. + fn notify_pending_response(&self, id: PendingImageId, action: FetchResponseMsg); +} + +/// Whether this response passed any CORS checks, and is thus safe to read from +/// in cross-origin environments. +#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)] +pub enum CorsStatus { + /// The response is either same-origin or cross-origin but passed CORS checks. + Safe, + /// The response is cross-origin and did not pass CORS checks. It is unsafe + /// to expose pixel data to the requesting environment. + Unsafe, +} diff --git a/components/shared/net/lib.rs b/components/shared/net/lib.rs new file mode 100644 index 00000000000..b8b32725a25 --- /dev/null +++ b/components/shared/net/lib.rs @@ -0,0 +1,859 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +#![deny(unsafe_code)] + +use cookie::Cookie; +use headers::{ContentType, HeaderMapExt, ReferrerPolicy as ReferrerPolicyHeader}; +use http::{Error as HttpError, HeaderMap, StatusCode}; +use hyper::Error as HyperError; +use hyper_serde::Serde; +use ipc_channel::ipc::{self, IpcReceiver, IpcSender}; +use ipc_channel::router::ROUTER; +use ipc_channel::Error as IpcError; +use lazy_static::lazy_static; +use log::warn; +use malloc_size_of::malloc_size_of_is_0; +use malloc_size_of_derive::MallocSizeOf; +use mime::Mime; +use msg::constellation_msg::HistoryStateId; +use rustls::Certificate; +use serde::{Deserialize, Serialize}; +use servo_rand::RngCore; +use servo_url::{ImmutableOrigin, ServoUrl}; +use time::precise_time_ns; +use webrender_api::{ImageData, ImageDescriptor, ImageKey}; + +use crate::filemanager_thread::FileManagerThreadMsg; +use crate::request::{Request, RequestBuilder}; +use crate::response::{HttpsState, Response, ResponseInit}; +use crate::storage_thread::StorageThreadMsg; + +pub mod blob_url_store; +pub mod filemanager_thread; +pub mod image_cache; +pub mod pub_domains; +pub mod quality; +pub mod request; +pub mod response; +pub mod storage_thread; + +/// Image handling. +/// +/// It may be surprising that this goes in the network crate as opposed to the graphics crate. +/// However, image handling is generally very integrated with the network stack (especially where +/// caching is involved) and as a result it must live in here. +pub mod image { + pub mod base; +} + +/// An implementation of the [Fetch specification](https://fetch.spec.whatwg.org/) +pub mod fetch { + pub mod headers; +} + +/// A loading context, for context-specific sniffing, as defined in +/// <https://mimesniff.spec.whatwg.org/#context-specific-sniffing> +#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)] +pub enum LoadContext { + Browsing, + Image, + AudioVideo, + Plugin, + Style, + Script, + Font, + TextTrack, + CacheManifest, +} + +#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)] +pub struct CustomResponse { + #[ignore_malloc_size_of = "Defined in hyper"] + #[serde( + deserialize_with = "::hyper_serde::deserialize", + serialize_with = "::hyper_serde::serialize" + )] + pub headers: HeaderMap, + #[ignore_malloc_size_of = "Defined in hyper"] + #[serde( + deserialize_with = "::hyper_serde::deserialize", + serialize_with = "::hyper_serde::serialize" + )] + pub raw_status: (StatusCode, String), + pub body: Vec<u8>, +} + +impl CustomResponse { + pub fn new( + headers: HeaderMap, + raw_status: (StatusCode, String), + body: Vec<u8>, + ) -> CustomResponse { + CustomResponse { + headers: headers, + raw_status: raw_status, + body: body, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CustomResponseMediator { + pub response_chan: IpcSender<Option<CustomResponse>>, + pub load_url: ServoUrl, +} + +/// [Policies](https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-states) +/// for providing a referrer header for a request +#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, Serialize)] +pub enum ReferrerPolicy { + /// "no-referrer" + NoReferrer, + /// "no-referrer-when-downgrade" + NoReferrerWhenDowngrade, + /// "origin" + Origin, + /// "same-origin" + SameOrigin, + /// "origin-when-cross-origin" + OriginWhenCrossOrigin, + /// "unsafe-url" + UnsafeUrl, + /// "strict-origin" + StrictOrigin, + /// "strict-origin-when-cross-origin" + StrictOriginWhenCrossOrigin, +} + +impl ToString for ReferrerPolicy { + fn to_string(&self) -> String { + match self { + ReferrerPolicy::NoReferrer => "no-referrer", + ReferrerPolicy::NoReferrerWhenDowngrade => "no-referrer-when-downgrade", + ReferrerPolicy::Origin => "origin", + ReferrerPolicy::SameOrigin => "same-origin", + ReferrerPolicy::OriginWhenCrossOrigin => "origin-when-cross-origin", + ReferrerPolicy::UnsafeUrl => "unsafe-url", + ReferrerPolicy::StrictOrigin => "strict-origin", + ReferrerPolicy::StrictOriginWhenCrossOrigin => "strict-origin-when-cross-origin", + } + .to_string() + } +} + +impl From<ReferrerPolicyHeader> for ReferrerPolicy { + fn from(policy: ReferrerPolicyHeader) -> Self { + match policy { + ReferrerPolicyHeader::NO_REFERRER => ReferrerPolicy::NoReferrer, + ReferrerPolicyHeader::NO_REFERRER_WHEN_DOWNGRADE => { + ReferrerPolicy::NoReferrerWhenDowngrade + }, + ReferrerPolicyHeader::SAME_ORIGIN => ReferrerPolicy::SameOrigin, + ReferrerPolicyHeader::ORIGIN => ReferrerPolicy::Origin, + ReferrerPolicyHeader::ORIGIN_WHEN_CROSS_ORIGIN => ReferrerPolicy::OriginWhenCrossOrigin, + ReferrerPolicyHeader::UNSAFE_URL => ReferrerPolicy::UnsafeUrl, + ReferrerPolicyHeader::STRICT_ORIGIN => ReferrerPolicy::StrictOrigin, + ReferrerPolicyHeader::STRICT_ORIGIN_WHEN_CROSS_ORIGIN => { + ReferrerPolicy::StrictOriginWhenCrossOrigin + }, + } + } +} + +impl From<ReferrerPolicy> for ReferrerPolicyHeader { + fn from(referrer_policy: ReferrerPolicy) -> Self { + match referrer_policy { + ReferrerPolicy::NoReferrer => ReferrerPolicyHeader::NO_REFERRER, + ReferrerPolicy::NoReferrerWhenDowngrade => { + ReferrerPolicyHeader::NO_REFERRER_WHEN_DOWNGRADE + }, + ReferrerPolicy::SameOrigin => ReferrerPolicyHeader::SAME_ORIGIN, + ReferrerPolicy::Origin => ReferrerPolicyHeader::ORIGIN, + ReferrerPolicy::OriginWhenCrossOrigin => ReferrerPolicyHeader::ORIGIN_WHEN_CROSS_ORIGIN, + ReferrerPolicy::UnsafeUrl => ReferrerPolicyHeader::UNSAFE_URL, + ReferrerPolicy::StrictOrigin => ReferrerPolicyHeader::STRICT_ORIGIN, + ReferrerPolicy::StrictOriginWhenCrossOrigin => { + ReferrerPolicyHeader::STRICT_ORIGIN_WHEN_CROSS_ORIGIN + }, + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub enum FetchResponseMsg { + // todo: should have fields for transmitted/total bytes + ProcessRequestBody, + ProcessRequestEOF, + // todo: send more info about the response (or perhaps the entire Response) + ProcessResponse(Result<FetchMetadata, NetworkError>), + ProcessResponseChunk(Vec<u8>), + ProcessResponseEOF(Result<ResourceFetchTiming, NetworkError>), +} + +pub trait FetchTaskTarget { + /// <https://fetch.spec.whatwg.org/#process-request-body> + /// + /// Fired when a chunk of the request body is transmitted + fn process_request_body(&mut self, request: &Request); + + /// <https://fetch.spec.whatwg.org/#process-request-end-of-file> + /// + /// Fired when the entire request finishes being transmitted + fn process_request_eof(&mut self, request: &Request); + + /// <https://fetch.spec.whatwg.org/#process-response> + /// + /// Fired when headers are received + fn process_response(&mut self, response: &Response); + + /// Fired when a chunk of response content is received + fn process_response_chunk(&mut self, chunk: Vec<u8>); + + /// <https://fetch.spec.whatwg.org/#process-response-end-of-file> + /// + /// Fired when the response is fully fetched + fn process_response_eof(&mut self, response: &Response); +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum FilteredMetadata { + Basic(Metadata), + Cors(Metadata), + Opaque, + OpaqueRedirect(ServoUrl), +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum FetchMetadata { + Unfiltered(Metadata), + Filtered { + filtered: FilteredMetadata, + unsafe_: Metadata, + }, +} + +pub trait FetchResponseListener { + fn process_request_body(&mut self); + fn process_request_eof(&mut self); + fn process_response(&mut self, metadata: Result<FetchMetadata, NetworkError>); + fn process_response_chunk(&mut self, chunk: Vec<u8>); + fn process_response_eof(&mut self, response: Result<ResourceFetchTiming, NetworkError>); + fn resource_timing(&self) -> &ResourceFetchTiming; + fn resource_timing_mut(&mut self) -> &mut ResourceFetchTiming; + fn submit_resource_timing(&mut self); +} + +impl FetchTaskTarget for IpcSender<FetchResponseMsg> { + fn process_request_body(&mut self, _: &Request) { + let _ = self.send(FetchResponseMsg::ProcessRequestBody); + } + + fn process_request_eof(&mut self, _: &Request) { + let _ = self.send(FetchResponseMsg::ProcessRequestEOF); + } + + fn process_response(&mut self, response: &Response) { + let _ = self.send(FetchResponseMsg::ProcessResponse(response.metadata())); + } + + fn process_response_chunk(&mut self, chunk: Vec<u8>) { + let _ = self.send(FetchResponseMsg::ProcessResponseChunk(chunk)); + } + + fn process_response_eof(&mut self, response: &Response) { + if let Some(e) = response.get_network_error() { + let _ = self.send(FetchResponseMsg::ProcessResponseEOF(Err(e.clone()))); + } else { + let _ = self.send(FetchResponseMsg::ProcessResponseEOF(Ok(response + .get_resource_timing() + .lock() + .unwrap() + .clone()))); + } + } +} + +/// A fetch task that discards all data it's sent, +/// useful when speculatively prefetching data that we don't need right +/// now, but might need in the future. +pub struct DiscardFetch; + +impl FetchTaskTarget for DiscardFetch { + fn process_request_body(&mut self, _: &Request) {} + + fn process_request_eof(&mut self, _: &Request) {} + + fn process_response(&mut self, _: &Response) {} + + fn process_response_chunk(&mut self, _: Vec<u8>) {} + + fn process_response_eof(&mut self, _: &Response) {} +} + +pub trait Action<Listener> { + fn process(self, listener: &mut Listener); +} + +impl<T: FetchResponseListener> Action<T> for FetchResponseMsg { + /// Execute the default action on a provided listener. + fn process(self, listener: &mut T) { + match self { + FetchResponseMsg::ProcessRequestBody => listener.process_request_body(), + FetchResponseMsg::ProcessRequestEOF => listener.process_request_eof(), + FetchResponseMsg::ProcessResponse(meta) => listener.process_response(meta), + FetchResponseMsg::ProcessResponseChunk(data) => listener.process_response_chunk(data), + FetchResponseMsg::ProcessResponseEOF(data) => { + match data { + Ok(ref response_resource_timing) => { + // update listener with values from response + *listener.resource_timing_mut() = response_resource_timing.clone(); + listener.process_response_eof(Ok(response_resource_timing.clone())); + // TODO timing check https://w3c.github.io/resource-timing/#dfn-timing-allow-check + + listener.submit_resource_timing(); + }, + // TODO Resources for which the fetch was initiated, but was later aborted + // (e.g. due to a network error) MAY be included as PerformanceResourceTiming + // objects in the Performance Timeline and MUST contain initialized attribute + // values for processed substeps of the processing model. + Err(e) => listener.process_response_eof(Err(e)), + } + }, + } + } +} + +/// Handle to a resource thread +pub type CoreResourceThread = IpcSender<CoreResourceMsg>; + +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 +/// XXX: If this trait will be used more in future, some auto derive might be appealing +pub trait IpcSend<T> +where + T: serde::Serialize + for<'de> serde::Deserialize<'de>, +{ + /// send message T + fn send(&self, _: T) -> IpcSendResult; + /// get underlying sender + fn sender(&self) -> IpcSender<T>; +} + +// FIXME: Originally we will construct an Arc<ResourceThread> from ResourceThread +// in script_thread to avoid some performance pitfall. Now we decide to deal with +// the "Arc" hack implicitly in future. +// See discussion: http://logs.glob.uno/?c=mozilla%23servo&s=16+May+2016&e=16+May+2016#c430412 +// See also: https://github.com/servo/servo/blob/735480/components/script/script_thread.rs#L313 +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ResourceThreads { + core_thread: CoreResourceThread, + storage_thread: IpcSender<StorageThreadMsg>, +} + +impl ResourceThreads { + pub fn new(c: CoreResourceThread, s: IpcSender<StorageThreadMsg>) -> ResourceThreads { + ResourceThreads { + core_thread: c, + storage_thread: s, + } + } + + pub fn clear_cache(&self) { + let _ = self.core_thread.send(CoreResourceMsg::ClearCache); + } +} + +impl IpcSend<CoreResourceMsg> for ResourceThreads { + fn send(&self, msg: CoreResourceMsg) -> IpcSendResult { + self.core_thread.send(msg) + } + + fn sender(&self) -> IpcSender<CoreResourceMsg> { + self.core_thread.clone() + } +} + +impl IpcSend<StorageThreadMsg> for ResourceThreads { + fn send(&self, msg: StorageThreadMsg) -> IpcSendResult { + self.storage_thread.send(msg) + } + + fn sender(&self) -> IpcSender<StorageThreadMsg> { + self.storage_thread.clone() + } +} + +// Ignore the sub-fields +malloc_size_of_is_0!(ResourceThreads); + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)] +pub enum IncludeSubdomains { + Included, + NotIncluded, +} + +#[derive(Debug, Deserialize, MallocSizeOf, Serialize)] +pub enum MessageData { + Text(String), + Binary(Vec<u8>), +} + +#[derive(Debug, Deserialize, Serialize)] +pub enum WebSocketDomAction { + SendMessage(MessageData), + Close(Option<u16>, Option<String>), +} + +#[derive(Debug, Deserialize, Serialize)] +pub enum WebSocketNetworkEvent { + ConnectionEstablished { protocol_in_use: Option<String> }, + MessageReceived(MessageData), + Close(Option<u16>, String), + Fail, +} + +#[derive(Debug, Deserialize, Serialize)] +/// IPC channels to communicate with the script thread about network or DOM events. +pub enum FetchChannels { + ResponseMsg( + IpcSender<FetchResponseMsg>, + /* cancel_chan */ Option<IpcReceiver<()>>, + ), + WebSocket { + event_sender: IpcSender<WebSocketNetworkEvent>, + action_receiver: IpcReceiver<WebSocketDomAction>, + }, + /// If the fetch is just being done to populate the cache, + /// not because the data is needed now. + Prefetch, +} + +#[derive(Debug, Deserialize, Serialize)] +pub enum CoreResourceMsg { + Fetch(RequestBuilder, FetchChannels), + /// Initiate a fetch in response to processing a redirection + FetchRedirect( + RequestBuilder, + ResponseInit, + IpcSender<FetchResponseMsg>, + /* cancel_chan */ Option<IpcReceiver<()>>, + ), + /// Store a cookie for a given originating URL + SetCookieForUrl(ServoUrl, Serde<Cookie<'static>>, CookieSource), + /// Store a set of cookies for a given originating URL + SetCookiesForUrl(ServoUrl, Vec<Serde<Cookie<'static>>>, CookieSource), + /// Retrieve the stored cookies for a given URL + GetCookiesForUrl(ServoUrl, IpcSender<Option<String>>, CookieSource), + /// Get a cookie by name for a given originating URL + GetCookiesDataForUrl( + ServoUrl, + IpcSender<Vec<Serde<Cookie<'static>>>>, + CookieSource, + ), + DeleteCookies(ServoUrl), + /// Get a history state by a given history state id + GetHistoryState(HistoryStateId, IpcSender<Option<Vec<u8>>>), + /// Set a history state for a given history state id + SetHistoryState(HistoryStateId, Vec<u8>), + /// Removes history states for the given ids + RemoveHistoryStates(Vec<HistoryStateId>), + /// Synchronization message solely for knowing the state of the ResourceChannelManager loop + Synchronize(IpcSender<()>), + /// Clear the network cache. + ClearCache, + /// Send the service worker network mediator for an origin to CoreResourceThread + NetworkMediator(IpcSender<CustomResponseMediator>, ImmutableOrigin), + /// Message forwarded to file manager's handler + ToFileManager(FileManagerThreadMsg), + /// Break the load handler loop, send a reply when done cleaning up local resources + /// and exit + Exit(IpcSender<()>), +} + +/// Instruct the resource thread to make a new request. +pub fn fetch_async<F>(request: RequestBuilder, core_resource_thread: &CoreResourceThread, f: F) +where + F: Fn(FetchResponseMsg) + Send + 'static, +{ + let (action_sender, action_receiver) = ipc::channel().unwrap(); + ROUTER.add_route( + action_receiver.to_opaque(), + Box::new(move |message| f(message.to().unwrap())), + ); + core_resource_thread + .send(CoreResourceMsg::Fetch( + request, + FetchChannels::ResponseMsg(action_sender, None), + )) + .unwrap(); +} + +#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)] +pub struct ResourceCorsData { + /// CORS Preflight flag + pub preflight: bool, + /// Origin of CORS Request + pub origin: ServoUrl, +} + +#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)] +pub struct ResourceFetchTiming { + pub domain_lookup_start: u64, + pub timing_check_passed: bool, + pub timing_type: ResourceTimingType, + /// Number of redirects until final resource (currently limited to 20) + pub redirect_count: u16, + pub request_start: u64, + pub secure_connection_start: u64, + pub response_start: u64, + pub fetch_start: u64, + pub response_end: u64, + pub redirect_start: u64, + pub redirect_end: u64, + pub connect_start: u64, + pub connect_end: u64, + pub start_time: u64, +} + +pub enum RedirectStartValue { + #[allow(dead_code)] + Zero, + FetchStart, +} + +pub enum RedirectEndValue { + Zero, + ResponseEnd, +} + +// TODO: refactor existing code to use this enum for setting time attributes +// suggest using this with all time attributes in the future +pub enum ResourceTimeValue { + Zero, + Now, + FetchStart, + RedirectStart, +} + +pub enum ResourceAttribute { + RedirectCount(u16), + DomainLookupStart, + RequestStart, + ResponseStart, + RedirectStart(RedirectStartValue), + RedirectEnd(RedirectEndValue), + FetchStart, + ConnectStart(u64), + ConnectEnd(u64), + SecureConnectionStart, + ResponseEnd, + StartTime(ResourceTimeValue), +} + +#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)] +pub enum ResourceTimingType { + Resource, + Navigation, + Error, + None, +} + +impl ResourceFetchTiming { + pub fn new(timing_type: ResourceTimingType) -> ResourceFetchTiming { + ResourceFetchTiming { + timing_type: timing_type, + timing_check_passed: true, + domain_lookup_start: 0, + redirect_count: 0, + secure_connection_start: 0, + request_start: 0, + response_start: 0, + fetch_start: 0, + redirect_start: 0, + redirect_end: 0, + connect_start: 0, + connect_end: 0, + response_end: 0, + start_time: 0, + } + } + + // TODO currently this is being set with precise time ns when it should be time since + // time origin (as described in Performance::now) + pub fn set_attribute(&mut self, attribute: ResourceAttribute) { + let should_attribute_always_be_updated = match attribute { + ResourceAttribute::FetchStart | + ResourceAttribute::ResponseEnd | + ResourceAttribute::StartTime(_) => true, + _ => false, + }; + if !self.timing_check_passed && !should_attribute_always_be_updated { + return; + } + match attribute { + ResourceAttribute::DomainLookupStart => self.domain_lookup_start = precise_time_ns(), + ResourceAttribute::RedirectCount(count) => self.redirect_count = count, + ResourceAttribute::RequestStart => self.request_start = precise_time_ns(), + ResourceAttribute::ResponseStart => self.response_start = precise_time_ns(), + ResourceAttribute::RedirectStart(val) => match val { + RedirectStartValue::Zero => self.redirect_start = 0, + RedirectStartValue::FetchStart => { + if self.redirect_start == 0 { + self.redirect_start = self.fetch_start + } + }, + }, + ResourceAttribute::RedirectEnd(val) => match val { + RedirectEndValue::Zero => self.redirect_end = 0, + RedirectEndValue::ResponseEnd => self.redirect_end = self.response_end, + }, + ResourceAttribute::FetchStart => self.fetch_start = precise_time_ns(), + ResourceAttribute::ConnectStart(val) => self.connect_start = val, + ResourceAttribute::ConnectEnd(val) => self.connect_end = val, + ResourceAttribute::SecureConnectionStart => { + self.secure_connection_start = precise_time_ns() + }, + ResourceAttribute::ResponseEnd => self.response_end = precise_time_ns(), + ResourceAttribute::StartTime(val) => match val { + ResourceTimeValue::RedirectStart + if self.redirect_start == 0 || !self.timing_check_passed => {}, + _ => self.start_time = self.get_time_value(val), + }, + } + } + + fn get_time_value(&self, time: ResourceTimeValue) -> u64 { + match time { + ResourceTimeValue::Zero => 0, + ResourceTimeValue::Now => precise_time_ns(), + ResourceTimeValue::FetchStart => self.fetch_start, + ResourceTimeValue::RedirectStart => self.redirect_start, + } + } + + pub fn mark_timing_check_failed(&mut self) { + self.timing_check_passed = false; + self.domain_lookup_start = 0; + self.redirect_count = 0; + self.request_start = 0; + self.response_start = 0; + self.redirect_start = 0; + self.connect_start = 0; + self.connect_end = 0; + } +} + +/// Metadata about a loaded resource, such as is obtained from HTTP headers. +#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)] +pub struct Metadata { + /// Final URL after redirects. + pub final_url: ServoUrl, + + /// Location URL from the response headers. + pub location_url: Option<Result<ServoUrl, String>>, + + #[ignore_malloc_size_of = "Defined in hyper"] + /// MIME type / subtype. + pub content_type: Option<Serde<ContentType>>, + + /// Character set. + pub charset: Option<String>, + + #[ignore_malloc_size_of = "Defined in hyper"] + /// Headers + pub headers: Option<Serde<HeaderMap>>, + + /// HTTP Status + pub status: Option<(u16, Vec<u8>)>, + + /// Is successful HTTPS connection + pub https_state: HttpsState, + + /// Referrer Url + pub referrer: Option<ServoUrl>, + + /// Referrer Policy of the Request used to obtain Response + pub referrer_policy: Option<ReferrerPolicy>, + /// Performance information for navigation events + pub timing: Option<ResourceFetchTiming>, + /// True if the request comes from a redirection + pub redirected: bool, +} + +impl Metadata { + /// Metadata with defaults for everything optional. + pub fn default(url: ServoUrl) -> Self { + Metadata { + final_url: url, + location_url: None, + content_type: None, + charset: None, + headers: None, + // https://fetch.spec.whatwg.org/#concept-response-status-message + status: Some((200, b"".to_vec())), + https_state: HttpsState::None, + referrer: None, + referrer_policy: None, + timing: None, + redirected: false, + } + } + + /// Extract the parts of a Mime that we care about. + pub fn set_content_type(&mut self, content_type: Option<&Mime>) { + if self.headers.is_none() { + self.headers = Some(Serde(HeaderMap::new())); + } + + if let Some(mime) = content_type { + self.headers + .as_mut() + .unwrap() + .typed_insert(ContentType::from(mime.clone())); + if let Some(charset) = mime.get_param(mime::CHARSET) { + self.charset = Some(charset.to_string()); + } + self.content_type = Some(Serde(ContentType::from(mime.clone()))); + } + } + + /// Set the referrer policy associated with the loaded resource. + pub fn set_referrer_policy(&mut self, referrer_policy: Option<ReferrerPolicy>) { + if self.headers.is_none() { + self.headers = Some(Serde(HeaderMap::new())); + } + + self.referrer_policy = referrer_policy; + if let Some(referrer_policy) = referrer_policy { + self.headers + .as_mut() + .unwrap() + .typed_insert::<ReferrerPolicyHeader>(referrer_policy.into()); + } + } +} + +/// The creator of a given cookie +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)] +pub enum CookieSource { + /// An HTTP API + HTTP, + /// A non-HTTP API + NonHTTP, +} + +/// Network errors that have to be exported out of the loaders +#[derive(Clone, Debug, Deserialize, Eq, MallocSizeOf, PartialEq, Serialize)] +pub enum NetworkError { + /// Could be any of the internal errors, like unsupported scheme, connection errors, etc. + Internal(String), + LoadCancelled, + /// SSL validation error, to be converted to Resource::BadCertHTML in the HTML parser. + SslValidation(String, Vec<u8>), + /// Crash error, to be converted to Resource::Crash in the HTML parser. + Crash(String), +} + +impl NetworkError { + pub fn from_hyper_error(error: &HyperError, certificate: Option<Certificate>) -> Self { + let error_string = error.to_string(); + match certificate { + Some(certificate) => NetworkError::SslValidation(error_string, certificate.0), + _ => NetworkError::Internal(error_string), + } + } + + pub fn from_http_error(error: &HttpError) -> Self { + NetworkError::Internal(error.to_string()) + } +} + +/// 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] { + const HTTP_WS_BYTES: &'static [u8] = b"\x09\x0A\x0D\x20"; + + loop { + match slice.split_first() { + Some((first, remainder)) if HTTP_WS_BYTES.contains(first) => slice = remainder, + _ => break, + } + } + + loop { + match slice.split_last() { + Some((last, remainder)) if HTTP_WS_BYTES.contains(last) => slice = remainder, + _ => break, + } + } + + slice +} + +pub fn http_percent_encode(bytes: &[u8]) -> String { + // This encode set is used for HTTP header values and is defined at + // https://tools.ietf.org/html/rfc5987#section-3.2 + const HTTP_VALUE: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS + .add(b' ') + .add(b'"') + .add(b'%') + .add(b'\'') + .add(b'(') + .add(b')') + .add(b'*') + .add(b',') + .add(b'/') + .add(b':') + .add(b';') + .add(b'<') + .add(b'-') + .add(b'>') + .add(b'?') + .add(b'[') + .add(b'\\') + .add(b']') + .add(b'{') + .add(b'}'); + + percent_encoding::percent_encode(bytes, HTTP_VALUE).to_string() +} + +#[derive(Deserialize, Serialize)] +pub enum NetToCompositorMsg { + AddImage(ImageKey, ImageDescriptor, ImageData), + GenerateImageKey(IpcSender<ImageKey>), +} + +#[derive(Clone, Deserialize, Serialize)] +pub struct WebrenderIpcSender(IpcSender<NetToCompositorMsg>); + +impl WebrenderIpcSender { + pub fn new(sender: IpcSender<NetToCompositorMsg>) -> Self { + Self(sender) + } + + pub fn generate_image_key(&self) -> ImageKey { + let (sender, receiver) = ipc::channel().unwrap(); + self.0 + .send(NetToCompositorMsg::GenerateImageKey(sender)) + .expect("error sending image key generation"); + receiver.recv().expect("error receiving image key result") + } + + pub fn add_image(&self, key: ImageKey, descriptor: ImageDescriptor, data: ImageData) { + if let Err(e) = self + .0 + .send(NetToCompositorMsg::AddImage(key, descriptor, data)) + { + warn!("Error sending image update: {}", e); + } + } +} + +lazy_static! { + pub static ref PRIVILEGED_SECRET: u32 = servo_rand::ServoRng::new().next_u32(); +} diff --git a/components/shared/net/pub_domains.rs b/components/shared/net/pub_domains.rs new file mode 100644 index 00000000000..1efb8faf56d --- /dev/null +++ b/components/shared/net/pub_domains.rs @@ -0,0 +1,159 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +//! Implementation of public domain matching. +//! +//! The list is a file located on the `resources` folder and loaded once on first need. +//! +//! The list can be updated with `./mach update-pub-domains` from this source: +//! <https://publicsuffix.org/list/> +//! +//! This implementation is not strictly following the specification of the list. Wildcards are not +//! restricted to appear only in the leftmost position, but the current list has no such cases so +//! we don't need to make the code more complex for it. The `mach` update command makes sure that +//! those cases are not present. + +use std::collections::HashSet; +use std::iter::FromIterator; + +use embedder_traits::resources::{self, Resource}; +use lazy_static::lazy_static; +use servo_url::{Host, ImmutableOrigin, ServoUrl}; + +#[derive(Clone, Debug)] +pub struct PubDomainRules { + rules: HashSet<String>, + wildcards: HashSet<String>, + exceptions: HashSet<String>, +} + +lazy_static! { + static ref PUB_DOMAINS: PubDomainRules = load_pub_domains(); +} + +impl<'a> FromIterator<&'a str> for PubDomainRules { + fn from_iter<T>(iter: T) -> Self + where + T: IntoIterator<Item = &'a str>, + { + let mut result = PubDomainRules::new(); + for item in iter { + if item.starts_with("!") { + result.exceptions.insert(String::from(&item[1..])); + } else if item.starts_with("*.") { + result.wildcards.insert(String::from(&item[2..])); + } else { + result.rules.insert(String::from(item)); + } + } + result + } +} + +impl PubDomainRules { + pub fn new() -> PubDomainRules { + PubDomainRules { + rules: HashSet::new(), + wildcards: HashSet::new(), + exceptions: HashSet::new(), + } + } + pub fn parse(content: &str) -> PubDomainRules { + content + .lines() + .map(str::trim) + .filter(|s| !s.is_empty()) + .filter(|s| !s.starts_with("//")) + .collect() + } + fn suffix_pair<'a>(&self, domain: &'a str) -> (&'a str, &'a str) { + let domain = domain.trim_start_matches("."); + let mut suffix = domain; + let mut prev_suffix = domain; + for (index, _) in domain.match_indices(".") { + let next_suffix = &domain[index + 1..]; + if self.exceptions.contains(suffix) { + return (next_suffix, suffix); + } else if self.wildcards.contains(next_suffix) { + return (suffix, prev_suffix); + } else if self.rules.contains(suffix) { + return (suffix, prev_suffix); + } else { + prev_suffix = suffix; + suffix = next_suffix; + } + } + return (suffix, prev_suffix); + } + pub fn public_suffix<'a>(&self, domain: &'a str) -> &'a str { + let (public, _) = self.suffix_pair(domain); + public + } + pub fn registrable_suffix<'a>(&self, domain: &'a str) -> &'a str { + let (_, registrable) = self.suffix_pair(domain); + registrable + } + pub fn is_public_suffix(&self, domain: &str) -> bool { + // Speeded-up version of + // domain != "" && + // self.public_suffix(domain) == domain. + let domain = domain.trim_start_matches("."); + match domain.find(".") { + None => !domain.is_empty(), + Some(index) => { + !self.exceptions.contains(domain) && self.wildcards.contains(&domain[index + 1..]) || + self.rules.contains(domain) + }, + } + } + pub fn is_registrable_suffix(&self, domain: &str) -> bool { + // Speeded-up version of + // self.public_suffix(domain) != domain && + // self.registrable_suffix(domain) == domain. + let domain = domain.trim_start_matches("."); + match domain.find(".") { + None => false, + Some(index) => { + self.exceptions.contains(domain) || + !self.wildcards.contains(&domain[index + 1..]) && + !self.rules.contains(domain) && + self.is_public_suffix(&domain[index + 1..]) + }, + } + } +} + +fn load_pub_domains() -> PubDomainRules { + PubDomainRules::parse(&resources::read_string(Resource::DomainList)) +} + +pub fn pub_suffix(domain: &str) -> &str { + PUB_DOMAINS.public_suffix(domain) +} + +pub fn reg_suffix(domain: &str) -> &str { + PUB_DOMAINS.registrable_suffix(domain) +} + +pub fn is_pub_domain(domain: &str) -> bool { + PUB_DOMAINS.is_public_suffix(domain) +} + +pub fn is_reg_domain(domain: &str) -> bool { + PUB_DOMAINS.is_registrable_suffix(domain) +} + +/// The registered domain name (aka eTLD+1) for a URL. +/// Returns None if the URL has no host name. +/// Returns the registered suffix for the host name if it is a domain. +/// Leaves the host name alone if it is an IP address. +pub fn reg_host(url: &ServoUrl) -> Option<Host> { + match url.origin() { + ImmutableOrigin::Tuple(_, Host::Domain(domain), _) => { + Some(Host::Domain(String::from(reg_suffix(&*domain)))) + }, + ImmutableOrigin::Tuple(_, ip, _) => Some(ip), + ImmutableOrigin::Opaque(_) => None, + } +} diff --git a/components/shared/net/quality.rs b/components/shared/net/quality.rs new file mode 100644 index 00000000000..095cd121bad --- /dev/null +++ b/components/shared/net/quality.rs @@ -0,0 +1,87 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +//TODO(eijebong): Remove this once typed headers figure out quality +// This is copy pasted from the old hyper headers to avoid hardcoding everything +// (I would probably also make some silly mistakes while migrating...) + +use std::{fmt, str}; + +use http::header::HeaderValue; +use mime::Mime; + +/// A quality value, as specified in [RFC7231]. +/// +/// Quality values are decimal numbers between 0 and 1 (inclusive) with up to 3 fractional digits of precision. +/// +/// [RFC7231]: https://tools.ietf.org/html/rfc7231#section-5.3.1 +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct Quality(u16); + +impl Quality { + /// Creates a quality value from a value between 0 and 1000 inclusive. + /// + /// This is semantically divided by 1000 to produce a value between 0 and 1. + /// + /// # Panics + /// + /// Panics if the value is greater than 1000. + pub fn from_u16(quality: u16) -> Quality { + assert!(quality <= 1000); + Quality(quality) + } +} + +/// A value paired with its "quality" as defined in [RFC7231]. +/// +/// Quality items are used in content negotiation headers such as `Accept` and `Accept-Encoding`. +/// +/// [RFC7231]: https://tools.ietf.org/html/rfc7231#section-5.3 +#[derive(Clone, Debug, PartialEq)] +pub struct QualityItem<T> { + pub item: T, + pub quality: Quality, +} + +impl<T> QualityItem<T> { + /// Creates a new quality item. + pub fn new(item: T, quality: Quality) -> QualityItem<T> { + QualityItem { item, quality } + } +} + +impl<T> fmt::Display for QualityItem<T> +where + T: fmt::Display, +{ + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.item, fmt)?; + match self.quality.0 { + 1000 => Ok(()), + 0 => fmt.write_str("; q=0"), + mut x => { + fmt.write_str("; q=0.")?; + let mut digits = *b"000"; + digits[2] = (x % 10) as u8 + b'0'; + x /= 10; + digits[1] = (x % 10) as u8 + b'0'; + x /= 10; + digits[0] = (x % 10) as u8 + b'0'; + + let s = str::from_utf8(&digits[..]).unwrap(); + fmt.write_str(s.trim_end_matches('0')) + }, + } + } +} + +pub fn quality_to_value(q: Vec<QualityItem<Mime>>) -> HeaderValue { + HeaderValue::from_str( + &q.iter() + .map(|q| q.to_string()) + .collect::<Vec<String>>() + .join(", "), + ) + .unwrap() +} diff --git a/components/shared/net/request.rs b/components/shared/net/request.rs new file mode 100644 index 00000000000..1f9f0abf986 --- /dev/null +++ b/components/shared/net/request.rs @@ -0,0 +1,749 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +use std::sync::{Arc, Mutex}; + +use content_security_policy::{self as csp, CspList}; +use http::header::{HeaderName, AUTHORIZATION}; +use http::{HeaderMap, Method}; +use ipc_channel::ipc::{self, IpcReceiver, IpcSender}; +use malloc_size_of_derive::MallocSizeOf; +use mime::Mime; +use msg::constellation_msg::PipelineId; +use serde::{Deserialize, Serialize}; +use servo_url::{ImmutableOrigin, ServoUrl}; + +use crate::response::HttpsState; +use crate::{ReferrerPolicy, ResourceTimingType}; + +/// An [initiator](https://fetch.spec.whatwg.org/#concept-request-initiator) +#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)] +pub enum Initiator { + None, + Download, + ImageSet, + Manifest, + XSLT, +} + +/// A request [destination](https://fetch.spec.whatwg.org/#concept-request-destination) +pub use csp::Destination; + +/// A request [origin](https://fetch.spec.whatwg.org/#concept-request-origin) +#[derive(Clone, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)] +pub enum Origin { + Client, + Origin(ImmutableOrigin), +} + +/// A [referer](https://fetch.spec.whatwg.org/#concept-request-referrer) +#[derive(Clone, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)] +pub enum Referrer { + NoReferrer, + /// Contains the url that "client" would be resolved to. See + /// [https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer](https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer) + /// + /// If you are unsure you should probably use + /// [`GlobalScope::get_referrer`](https://doc.servo.org/script/dom/globalscope/struct.GlobalScope.html#method.get_referrer) + Client(ServoUrl), + ReferrerUrl(ServoUrl), +} + +/// A [request mode](https://fetch.spec.whatwg.org/#concept-request-mode) +#[derive(Clone, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)] +pub enum RequestMode { + Navigate, + SameOrigin, + NoCors, + CorsMode, + WebSocket { protocols: Vec<String> }, +} + +/// Request [credentials mode](https://fetch.spec.whatwg.org/#concept-request-credentials-mode) +#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)] +pub enum CredentialsMode { + Omit, + CredentialsSameOrigin, + Include, +} + +/// [Cache mode](https://fetch.spec.whatwg.org/#concept-request-cache-mode) +#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)] +pub enum CacheMode { + Default, + NoStore, + Reload, + NoCache, + ForceCache, + OnlyIfCached, +} + +/// [Service-workers mode](https://fetch.spec.whatwg.org/#request-service-workers-mode) +#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)] +pub enum ServiceWorkersMode { + All, + None, +} + +/// [Redirect mode](https://fetch.spec.whatwg.org/#concept-request-redirect-mode) +#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)] +pub enum RedirectMode { + Follow, + Error, + Manual, +} + +/// [Response tainting](https://fetch.spec.whatwg.org/#concept-request-response-tainting) +#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)] +pub enum ResponseTainting { + Basic, + CorsTainting, + Opaque, +} + +/// [Window](https://fetch.spec.whatwg.org/#concept-request-window) +#[derive(Clone, Copy, MallocSizeOf, PartialEq)] +pub enum Window { + NoWindow, + Client, // TODO: Environmental settings object +} + +/// [CORS settings attribute](https://html.spec.whatwg.org/multipage/#attr-crossorigin-anonymous) +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub enum CorsSettings { + Anonymous, + UseCredentials, +} + +/// [Parser Metadata](https://fetch.spec.whatwg.org/#concept-request-parser-metadata) +#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)] +pub enum ParserMetadata { + Default, + ParserInserted, + NotParserInserted, +} + +/// <https://fetch.spec.whatwg.org/#concept-body-source> +#[derive(Clone, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)] +pub enum BodySource { + Null, + Object, +} + +/// Messages used to implement <https://fetch.spec.whatwg.org/#concept-request-transmit-body> +/// which are sent from script to net. +#[derive(Debug, Deserialize, Serialize)] +pub enum BodyChunkResponse { + /// A chunk of bytes. + Chunk(Vec<u8>), + /// The body is done. + Done, + /// There was an error streaming the body, + /// terminate fetch. + Error, +} + +/// Messages used to implement <https://fetch.spec.whatwg.org/#concept-request-transmit-body> +/// which are sent from net to script +/// (with the exception of Done, which is sent from script to script). +#[derive(Debug, Deserialize, Serialize)] +pub enum BodyChunkRequest { + /// Connect a fetch in `net`, with a stream of bytes from `script`. + Connect(IpcSender<BodyChunkResponse>), + /// Re-extract a new stream from the source, following a redirect. + Extract(IpcReceiver<BodyChunkRequest>), + /// Ask for another chunk. + Chunk, + /// Signal the stream is done(sent from script to script). + Done, + /// Signal the stream has errored(sent from script to script). + Error, +} + +/// The net component's view into <https://fetch.spec.whatwg.org/#bodies> +#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)] +pub struct RequestBody { + /// Net's channel to communicate with script re this body. + #[ignore_malloc_size_of = "Channels are hard"] + chan: Arc<Mutex<IpcSender<BodyChunkRequest>>>, + /// <https://fetch.spec.whatwg.org/#concept-body-source> + source: BodySource, + /// <https://fetch.spec.whatwg.org/#concept-body-total-bytes> + total_bytes: Option<usize>, +} + +impl RequestBody { + pub fn new( + chan: IpcSender<BodyChunkRequest>, + source: BodySource, + total_bytes: Option<usize>, + ) -> Self { + RequestBody { + chan: Arc::new(Mutex::new(chan)), + source, + total_bytes, + } + } + + /// Step 12 of https://fetch.spec.whatwg.org/#concept-http-redirect-fetch + pub fn extract_source(&mut self) { + match self.source { + BodySource::Null => panic!("Null sources should never be re-directed."), + BodySource::Object => { + let (chan, port) = ipc::channel().unwrap(); + let mut selfchan = self.chan.lock().unwrap(); + let _ = selfchan.send(BodyChunkRequest::Extract(port)); + *selfchan = chan; + }, + } + } + + pub fn take_stream(&self) -> Arc<Mutex<IpcSender<BodyChunkRequest>>> { + self.chan.clone() + } + + pub fn source_is_null(&self) -> bool { + self.source == BodySource::Null + } + + pub fn len(&self) -> Option<usize> { + self.total_bytes.clone() + } +} + +#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)] +pub struct RequestBuilder { + #[serde( + deserialize_with = "::hyper_serde::deserialize", + serialize_with = "::hyper_serde::serialize" + )] + #[ignore_malloc_size_of = "Defined in hyper"] + pub method: Method, + pub url: ServoUrl, + #[serde( + deserialize_with = "::hyper_serde::deserialize", + serialize_with = "::hyper_serde::serialize" + )] + #[ignore_malloc_size_of = "Defined in hyper"] + pub headers: HeaderMap, + pub unsafe_request: bool, + pub body: Option<RequestBody>, + pub service_workers_mode: ServiceWorkersMode, + // TODO: client object + pub destination: Destination, + pub synchronous: bool, + pub mode: RequestMode, + pub cache_mode: CacheMode, + pub use_cors_preflight: bool, + pub credentials_mode: CredentialsMode, + pub use_url_credentials: bool, + pub origin: ImmutableOrigin, + // XXXManishearth these should be part of the client object + pub referrer: Referrer, + pub referrer_policy: Option<ReferrerPolicy>, + pub pipeline_id: Option<PipelineId>, + pub redirect_mode: RedirectMode, + pub integrity_metadata: String, + // This is nominally a part of the client's global object. + // It is copied here to avoid having to reach across the thread + // boundary every time a redirect occurs. + #[ignore_malloc_size_of = "Defined in rust-content-security-policy"] + pub csp_list: Option<CspList>, + // to keep track of redirects + pub url_list: Vec<ServoUrl>, + pub parser_metadata: ParserMetadata, + pub initiator: Initiator, + pub https_state: HttpsState, + pub response_tainting: ResponseTainting, + /// Servo internal: if crash details are present, trigger a crash error page with these details. + pub crash: Option<String>, +} + +impl RequestBuilder { + pub fn new(url: ServoUrl, referrer: Referrer) -> RequestBuilder { + RequestBuilder { + method: Method::GET, + url: url, + headers: HeaderMap::new(), + unsafe_request: false, + body: None, + service_workers_mode: ServiceWorkersMode::All, + destination: Destination::None, + synchronous: false, + mode: RequestMode::NoCors, + cache_mode: CacheMode::Default, + use_cors_preflight: false, + credentials_mode: CredentialsMode::CredentialsSameOrigin, + use_url_credentials: false, + origin: ImmutableOrigin::new_opaque(), + referrer: referrer, + referrer_policy: None, + pipeline_id: None, + redirect_mode: RedirectMode::Follow, + integrity_metadata: "".to_owned(), + url_list: vec![], + parser_metadata: ParserMetadata::Default, + initiator: Initiator::None, + csp_list: None, + https_state: HttpsState::None, + response_tainting: ResponseTainting::Basic, + crash: None, + } + } + + pub fn initiator(mut self, initiator: Initiator) -> RequestBuilder { + self.initiator = initiator; + self + } + + pub fn method(mut self, method: Method) -> RequestBuilder { + self.method = method; + self + } + + pub fn headers(mut self, headers: HeaderMap) -> RequestBuilder { + self.headers = headers; + self + } + + pub fn unsafe_request(mut self, unsafe_request: bool) -> RequestBuilder { + self.unsafe_request = unsafe_request; + self + } + + pub fn body(mut self, body: Option<RequestBody>) -> RequestBuilder { + self.body = body; + self + } + + pub fn destination(mut self, destination: Destination) -> RequestBuilder { + self.destination = destination; + self + } + + pub fn synchronous(mut self, synchronous: bool) -> RequestBuilder { + self.synchronous = synchronous; + self + } + + pub fn mode(mut self, mode: RequestMode) -> RequestBuilder { + self.mode = mode; + self + } + + pub fn use_cors_preflight(mut self, use_cors_preflight: bool) -> RequestBuilder { + self.use_cors_preflight = use_cors_preflight; + self + } + + pub fn credentials_mode(mut self, credentials_mode: CredentialsMode) -> RequestBuilder { + self.credentials_mode = credentials_mode; + self + } + + pub fn use_url_credentials(mut self, use_url_credentials: bool) -> RequestBuilder { + self.use_url_credentials = use_url_credentials; + self + } + + pub fn origin(mut self, origin: ImmutableOrigin) -> RequestBuilder { + self.origin = origin; + self + } + + pub fn referrer_policy(mut self, referrer_policy: Option<ReferrerPolicy>) -> RequestBuilder { + self.referrer_policy = referrer_policy; + self + } + + pub fn pipeline_id(mut self, pipeline_id: Option<PipelineId>) -> RequestBuilder { + self.pipeline_id = pipeline_id; + self + } + + pub fn redirect_mode(mut self, redirect_mode: RedirectMode) -> RequestBuilder { + self.redirect_mode = redirect_mode; + self + } + + pub fn integrity_metadata(mut self, integrity_metadata: String) -> RequestBuilder { + self.integrity_metadata = integrity_metadata; + self + } + + pub fn parser_metadata(mut self, parser_metadata: ParserMetadata) -> RequestBuilder { + self.parser_metadata = parser_metadata; + self + } + + pub fn https_state(mut self, https_state: HttpsState) -> RequestBuilder { + self.https_state = https_state; + self + } + + pub fn response_tainting(mut self, response_tainting: ResponseTainting) -> RequestBuilder { + self.response_tainting = response_tainting; + self + } + + pub fn crash(mut self, crash: Option<String>) -> Self { + self.crash = crash; + self + } + + pub fn build(self) -> Request { + let mut request = Request::new( + self.url.clone(), + Some(Origin::Origin(self.origin)), + self.referrer, + self.pipeline_id, + self.https_state, + ); + request.initiator = self.initiator; + request.method = self.method; + request.headers = self.headers; + request.unsafe_request = self.unsafe_request; + request.body = self.body; + request.service_workers_mode = self.service_workers_mode; + request.destination = self.destination; + request.synchronous = self.synchronous; + request.mode = self.mode; + request.use_cors_preflight = self.use_cors_preflight; + request.credentials_mode = self.credentials_mode; + request.use_url_credentials = self.use_url_credentials; + request.cache_mode = self.cache_mode; + request.referrer_policy = self.referrer_policy; + request.redirect_mode = self.redirect_mode; + let mut url_list = self.url_list; + if url_list.is_empty() { + url_list.push(self.url); + } + request.redirect_count = url_list.len() as u32 - 1; + request.url_list = url_list; + request.integrity_metadata = self.integrity_metadata; + request.parser_metadata = self.parser_metadata; + request.csp_list = self.csp_list; + request.response_tainting = self.response_tainting; + request.crash = self.crash; + request + } +} + +/// A [Request](https://fetch.spec.whatwg.org/#concept-request) as defined by +/// the Fetch spec. +#[derive(Clone, MallocSizeOf)] +pub struct Request { + /// <https://fetch.spec.whatwg.org/#concept-request-method> + #[ignore_malloc_size_of = "Defined in hyper"] + pub method: Method, + /// <https://fetch.spec.whatwg.org/#local-urls-only-flag> + pub local_urls_only: bool, + /// <https://fetch.spec.whatwg.org/#sandboxed-storage-area-urls-flag> + pub sandboxed_storage_area_urls: bool, + /// <https://fetch.spec.whatwg.org/#concept-request-header-list> + #[ignore_malloc_size_of = "Defined in hyper"] + pub headers: HeaderMap, + /// <https://fetch.spec.whatwg.org/#unsafe-request-flag> + pub unsafe_request: bool, + /// <https://fetch.spec.whatwg.org/#concept-request-body> + pub body: Option<RequestBody>, + // TODO: client object + pub window: Window, + // TODO: target browsing context + /// <https://fetch.spec.whatwg.org/#request-keepalive-flag> + pub keep_alive: bool, + /// <https://fetch.spec.whatwg.org/#request-service-workers-mode> + pub service_workers_mode: ServiceWorkersMode, + /// <https://fetch.spec.whatwg.org/#concept-request-initiator> + pub initiator: Initiator, + /// <https://fetch.spec.whatwg.org/#concept-request-destination> + pub destination: Destination, + // TODO: priority object + /// <https://fetch.spec.whatwg.org/#concept-request-origin> + pub origin: Origin, + /// <https://fetch.spec.whatwg.org/#concept-request-referrer> + pub referrer: Referrer, + /// <https://fetch.spec.whatwg.org/#concept-request-referrer-policy> + pub referrer_policy: Option<ReferrerPolicy>, + pub pipeline_id: Option<PipelineId>, + /// <https://fetch.spec.whatwg.org/#synchronous-flag> + pub synchronous: bool, + /// <https://fetch.spec.whatwg.org/#concept-request-mode> + pub mode: RequestMode, + /// <https://fetch.spec.whatwg.org/#use-cors-preflight-flag> + pub use_cors_preflight: bool, + /// <https://fetch.spec.whatwg.org/#concept-request-credentials-mode> + pub credentials_mode: CredentialsMode, + /// <https://fetch.spec.whatwg.org/#concept-request-use-url-credentials-flag> + pub use_url_credentials: bool, + /// <https://fetch.spec.whatwg.org/#concept-request-cache-mode> + pub cache_mode: CacheMode, + /// <https://fetch.spec.whatwg.org/#concept-request-redirect-mode> + pub redirect_mode: RedirectMode, + /// <https://fetch.spec.whatwg.org/#concept-request-integrity-metadata> + pub integrity_metadata: String, + // Use the last method on url_list to act as spec current url field, and + // first method to act as spec url field + /// <https://fetch.spec.whatwg.org/#concept-request-url-list> + pub url_list: Vec<ServoUrl>, + /// <https://fetch.spec.whatwg.org/#concept-request-redirect-count> + pub redirect_count: u32, + /// <https://fetch.spec.whatwg.org/#concept-request-response-tainting> + pub response_tainting: ResponseTainting, + /// <https://fetch.spec.whatwg.org/#concept-request-parser-metadata> + pub parser_metadata: ParserMetadata, + // This is nominally a part of the client's global object. + // It is copied here to avoid having to reach across the thread + // boundary every time a redirect occurs. + #[ignore_malloc_size_of = "Defined in rust-content-security-policy"] + pub csp_list: Option<CspList>, + pub https_state: HttpsState, + /// Servo internal: if crash details are present, trigger a crash error page with these details. + pub crash: Option<String>, +} + +impl Request { + pub fn new( + url: ServoUrl, + origin: Option<Origin>, + referrer: Referrer, + pipeline_id: Option<PipelineId>, + https_state: HttpsState, + ) -> Request { + Request { + method: Method::GET, + local_urls_only: false, + sandboxed_storage_area_urls: false, + headers: HeaderMap::new(), + unsafe_request: false, + body: None, + window: Window::Client, + keep_alive: false, + service_workers_mode: ServiceWorkersMode::All, + initiator: Initiator::None, + destination: Destination::None, + origin: origin.unwrap_or(Origin::Client), + referrer: referrer, + referrer_policy: None, + pipeline_id: pipeline_id, + synchronous: false, + mode: RequestMode::NoCors, + use_cors_preflight: false, + credentials_mode: CredentialsMode::CredentialsSameOrigin, + use_url_credentials: false, + cache_mode: CacheMode::Default, + redirect_mode: RedirectMode::Follow, + integrity_metadata: String::new(), + url_list: vec![url], + parser_metadata: ParserMetadata::Default, + redirect_count: 0, + response_tainting: ResponseTainting::Basic, + csp_list: None, + https_state: https_state, + crash: None, + } + } + + /// <https://fetch.spec.whatwg.org/#concept-request-url> + pub fn url(&self) -> ServoUrl { + self.url_list.first().unwrap().clone() + } + + /// <https://fetch.spec.whatwg.org/#concept-request-current-url> + pub fn current_url(&self) -> ServoUrl { + self.url_list.last().unwrap().clone() + } + + /// <https://fetch.spec.whatwg.org/#concept-request-current-url> + pub fn current_url_mut(&mut self) -> &mut ServoUrl { + self.url_list.last_mut().unwrap() + } + + /// <https://fetch.spec.whatwg.org/#navigation-request> + pub fn is_navigation_request(&self) -> bool { + self.destination == Destination::Document + } + + /// <https://fetch.spec.whatwg.org/#subresource-request> + pub fn is_subresource_request(&self) -> bool { + match self.destination { + Destination::Audio | + Destination::Font | + Destination::Image | + Destination::Manifest | + Destination::Script | + Destination::Style | + Destination::Track | + Destination::Video | + Destination::Xslt | + Destination::None => true, + _ => false, + } + } + + pub fn timing_type(&self) -> ResourceTimingType { + if self.is_navigation_request() { + ResourceTimingType::Navigation + } else { + ResourceTimingType::Resource + } + } +} + +impl Referrer { + pub fn to_url(&self) -> Option<&ServoUrl> { + match *self { + Referrer::NoReferrer => None, + Referrer::Client(ref url) => Some(url), + Referrer::ReferrerUrl(ref url) => Some(url), + } + } +} + +// https://fetch.spec.whatwg.org/#cors-unsafe-request-header-byte +// TODO: values in the control-code range are being quietly stripped out by +// HeaderMap and never reach this function to be loudly rejected! +fn is_cors_unsafe_request_header_byte(value: &u8) -> bool { + match value { + 0x00..=0x08 | + 0x10..=0x19 | + 0x22 | + 0x28 | + 0x29 | + 0x3A | + 0x3C | + 0x3E | + 0x3F | + 0x40 | + 0x5B | + 0x5C | + 0x5D | + 0x7B | + 0x7D | + 0x7F => true, + _ => false, + } +} + +// https://fetch.spec.whatwg.org/#cors-safelisted-request-header +// subclause `accept` +fn is_cors_safelisted_request_accept(value: &[u8]) -> bool { + !(value.iter().any(is_cors_unsafe_request_header_byte)) +} + +// https://fetch.spec.whatwg.org/#cors-safelisted-request-header +// subclauses `accept-language`, `content-language` +fn is_cors_safelisted_language(value: &[u8]) -> bool { + value.iter().all(|&x| match x { + 0x30..=0x39 | + 0x41..=0x5A | + 0x61..=0x7A | + 0x20 | + 0x2A | + 0x2C | + 0x2D | + 0x2E | + 0x3B | + 0x3D => true, + _ => false, + }) +} + +// https://fetch.spec.whatwg.org/#cors-safelisted-request-header +// subclause `content-type` +fn is_cors_safelisted_request_content_type(value: &[u8]) -> bool { + // step 1 + if value.iter().any(is_cors_unsafe_request_header_byte) { + return false; + } + // step 2 + let value_string = if let Ok(s) = std::str::from_utf8(value) { + s + } else { + return false; + }; + let value_mime_result: Result<Mime, _> = value_string.parse(); + match value_mime_result { + Err(_) => false, // step 3 + Ok(value_mime) => match (value_mime.type_(), value_mime.subtype()) { + (mime::APPLICATION, mime::WWW_FORM_URLENCODED) | + (mime::MULTIPART, mime::FORM_DATA) | + (mime::TEXT, mime::PLAIN) => true, + _ => false, // step 4 + }, + } +} + +// TODO: "DPR", "Downlink", "Save-Data", "Viewport-Width", "Width": +// ... once parsed, the value should not be failure. +// https://fetch.spec.whatwg.org/#cors-safelisted-request-header +pub fn is_cors_safelisted_request_header<N: AsRef<str>, V: AsRef<[u8]>>( + name: &N, + value: &V, +) -> bool { + let name: &str = name.as_ref(); + let value: &[u8] = value.as_ref(); + if value.len() > 128 { + return false; + } + match name { + "accept" => is_cors_safelisted_request_accept(value), + "accept-language" | "content-language" => is_cors_safelisted_language(value), + "content-type" => is_cors_safelisted_request_content_type(value), + _ => false, + } +} + +/// <https://fetch.spec.whatwg.org/#cors-safelisted-method> +pub fn is_cors_safelisted_method(m: &Method) -> bool { + match *m { + Method::GET | Method::HEAD | Method::POST => true, + _ => false, + } +} + +/// <https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name> +pub fn is_cors_non_wildcard_request_header_name(name: &HeaderName) -> bool { + name == AUTHORIZATION +} + +/// <https://fetch.spec.whatwg.org/#cors-unsafe-request-header-names> +pub fn get_cors_unsafe_header_names(headers: &HeaderMap) -> Vec<HeaderName> { + // Step 1 + let mut unsafe_names: Vec<&HeaderName> = vec![]; + // Step 2 + let mut potentillay_unsafe_names: Vec<&HeaderName> = vec![]; + // Step 3 + let mut safelist_value_size = 0; + + // Step 4 + for (name, value) in headers.iter() { + if !is_cors_safelisted_request_header(&name, &value) { + unsafe_names.push(name); + } else { + potentillay_unsafe_names.push(name); + safelist_value_size += value.as_ref().len(); + } + } + + // Step 5 + if safelist_value_size > 1024 { + unsafe_names.extend_from_slice(&potentillay_unsafe_names); + } + + // Step 6 + return convert_header_names_to_sorted_lowercase_set(unsafe_names); +} + +/// <https://fetch.spec.whatwg.org/#ref-for-convert-header-names-to-a-sorted-lowercase-set> +pub fn convert_header_names_to_sorted_lowercase_set( + header_names: Vec<&HeaderName>, +) -> Vec<HeaderName> { + // HeaderName does not implement the needed traits to use a BTreeSet + // So create a new Vec, sort, then dedup + let mut ordered_set = header_names.to_vec(); + ordered_set.sort_by(|a, b| a.as_str().partial_cmp(b.as_str()).unwrap()); + ordered_set.dedup(); + return ordered_set.into_iter().cloned().collect(); +} diff --git a/components/shared/net/response.rs b/components/shared/net/response.rs new file mode 100644 index 00000000000..552c0057c50 --- /dev/null +++ b/components/shared/net/response.rs @@ -0,0 +1,364 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +//! The [Response](https://fetch.spec.whatwg.org/#responses) object +//! resulting from a [fetch operation](https://fetch.spec.whatwg.org/#concept-fetch) +use std::sync::atomic::AtomicBool; +use std::sync::Mutex; + +use headers::{ContentType, HeaderMapExt}; +use http::{HeaderMap, StatusCode}; +use hyper_serde::Serde; +use malloc_size_of_derive::MallocSizeOf; +use serde::{Deserialize, Serialize}; +use servo_arc::Arc; +use servo_url::ServoUrl; + +use crate::{ + FetchMetadata, FilteredMetadata, Metadata, NetworkError, ReferrerPolicy, ResourceFetchTiming, + ResourceTimingType, +}; + +/// [Response type](https://fetch.spec.whatwg.org/#concept-response-type) +#[derive(Clone, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)] +pub enum ResponseType { + Basic, + Cors, + Default, + Error(NetworkError), + Opaque, + OpaqueRedirect, +} + +/// [Response termination reason](https://fetch.spec.whatwg.org/#concept-response-termination-reason) +#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, Serialize)] +pub enum TerminationReason { + EndUserAbort, + Fatal, + Timeout, +} + +/// The response body can still be pushed to after fetch +/// This provides a way to store unfinished response bodies +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub enum ResponseBody { + Empty, // XXXManishearth is this necessary, or is Done(vec![]) enough? + Receiving(Vec<u8>), + Done(Vec<u8>), +} + +impl ResponseBody { + pub fn is_done(&self) -> bool { + match *self { + ResponseBody::Done(..) => true, + ResponseBody::Empty | ResponseBody::Receiving(..) => false, + } + } +} + +/// [Cache state](https://fetch.spec.whatwg.org/#concept-response-cache-state) +#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)] +pub enum CacheState { + None, + Local, + Validated, + Partial, +} + +/// [Https state](https://fetch.spec.whatwg.org/#concept-response-https-state) +#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)] +pub enum HttpsState { + None, + Deprecated, + Modern, +} + +#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)] +pub struct ResponseInit { + pub url: ServoUrl, + #[serde( + deserialize_with = "::hyper_serde::deserialize", + serialize_with = "::hyper_serde::serialize" + )] + #[ignore_malloc_size_of = "Defined in hyper"] + pub headers: HeaderMap, + pub status_code: u16, + pub referrer: Option<ServoUrl>, + pub location_url: Option<Result<ServoUrl, String>>, +} + +/// A [Response](https://fetch.spec.whatwg.org/#concept-response) as defined by the Fetch spec +#[derive(Clone, Debug, MallocSizeOf)] +pub struct Response { + pub response_type: ResponseType, + pub termination_reason: Option<TerminationReason>, + url: Option<ServoUrl>, + pub url_list: Vec<ServoUrl>, + /// `None` can be considered a StatusCode of `0`. + #[ignore_malloc_size_of = "Defined in hyper"] + pub status: Option<(StatusCode, String)>, + pub raw_status: Option<(u16, Vec<u8>)>, + #[ignore_malloc_size_of = "Defined in hyper"] + pub headers: HeaderMap, + #[ignore_malloc_size_of = "Mutex heap size undefined"] + pub body: Arc<Mutex<ResponseBody>>, + pub cache_state: CacheState, + pub https_state: HttpsState, + pub referrer: Option<ServoUrl>, + pub referrer_policy: Option<ReferrerPolicy>, + /// [CORS-exposed header-name list](https://fetch.spec.whatwg.org/#concept-response-cors-exposed-header-name-list) + pub cors_exposed_header_name_list: Vec<String>, + /// [Location URL](https://fetch.spec.whatwg.org/#concept-response-location-url) + pub location_url: Option<Result<ServoUrl, String>>, + /// [Internal response](https://fetch.spec.whatwg.org/#concept-internal-response), only used if the Response + /// is a filtered response + pub internal_response: Option<Box<Response>>, + /// whether or not to try to return the internal_response when asked for actual_response + pub return_internal: bool, + /// https://fetch.spec.whatwg.org/#concept-response-aborted + #[ignore_malloc_size_of = "AtomicBool heap size undefined"] + pub aborted: Arc<AtomicBool>, + /// track network metrics + #[ignore_malloc_size_of = "Mutex heap size undefined"] + pub resource_timing: Arc<Mutex<ResourceFetchTiming>>, +} + +impl Response { + pub fn new(url: ServoUrl, resource_timing: ResourceFetchTiming) -> Response { + Response { + response_type: ResponseType::Default, + termination_reason: None, + url: Some(url), + url_list: vec![], + status: Some((StatusCode::OK, "".to_string())), + raw_status: Some((200, b"".to_vec())), + headers: HeaderMap::new(), + body: Arc::new(Mutex::new(ResponseBody::Empty)), + cache_state: CacheState::None, + https_state: HttpsState::None, + referrer: None, + referrer_policy: None, + cors_exposed_header_name_list: vec![], + location_url: None, + internal_response: None, + return_internal: true, + aborted: Arc::new(AtomicBool::new(false)), + resource_timing: Arc::new(Mutex::new(resource_timing)), + } + } + + pub fn from_init(init: ResponseInit, resource_timing_type: ResourceTimingType) -> Response { + let mut res = Response::new(init.url, ResourceFetchTiming::new(resource_timing_type)); + res.location_url = init.location_url; + res.headers = init.headers; + res.referrer = init.referrer; + res.status = StatusCode::from_u16(init.status_code) + .map(|s| (s, s.to_string())) + .ok(); + res + } + + pub fn network_error(e: NetworkError) -> Response { + Response { + response_type: ResponseType::Error(e), + termination_reason: None, + url: None, + url_list: vec![], + status: None, + raw_status: None, + headers: HeaderMap::new(), + body: Arc::new(Mutex::new(ResponseBody::Empty)), + cache_state: CacheState::None, + https_state: HttpsState::None, + referrer: None, + referrer_policy: None, + cors_exposed_header_name_list: vec![], + location_url: None, + internal_response: None, + return_internal: true, + aborted: Arc::new(AtomicBool::new(false)), + resource_timing: Arc::new(Mutex::new(ResourceFetchTiming::new( + ResourceTimingType::Error, + ))), + } + } + + pub fn url(&self) -> Option<&ServoUrl> { + self.url.as_ref() + } + + pub fn is_network_error(&self) -> bool { + match self.response_type { + ResponseType::Error(..) => true, + _ => false, + } + } + + pub fn get_network_error(&self) -> Option<&NetworkError> { + match self.response_type { + ResponseType::Error(ref e) => Some(e), + _ => None, + } + } + + pub fn actual_response(&self) -> &Response { + if self.return_internal && self.internal_response.is_some() { + &**self.internal_response.as_ref().unwrap() + } else { + self + } + } + + pub fn actual_response_mut(&mut self) -> &mut Response { + if self.return_internal && self.internal_response.is_some() { + &mut **self.internal_response.as_mut().unwrap() + } else { + self + } + } + + pub fn to_actual(self) -> Response { + if self.return_internal && self.internal_response.is_some() { + *self.internal_response.unwrap() + } else { + self + } + } + + pub fn get_resource_timing(&self) -> Arc<Mutex<ResourceFetchTiming>> { + Arc::clone(&self.resource_timing) + } + + /// Convert to a filtered response, of type `filter_type`. + /// Do not use with type Error or Default + #[rustfmt::skip] + pub fn to_filtered(self, filter_type: ResponseType) -> Response { + match filter_type { + ResponseType::Default | + ResponseType::Error(..) => panic!(), + _ => (), + } + + let old_response = self.to_actual(); + + if let ResponseType::Error(e) = old_response.response_type { + return Response::network_error(e); + } + + let old_headers = old_response.headers.clone(); + let exposed_headers = old_response.cors_exposed_header_name_list.clone(); + let mut response = old_response.clone(); + response.internal_response = Some(Box::new(old_response)); + response.response_type = filter_type; + + match response.response_type { + ResponseType::Default | + ResponseType::Error(..) => unreachable!(), + + ResponseType::Basic => { + let headers = old_headers.iter().filter(|(name, _)| { + match &*name.as_str().to_ascii_lowercase() { + "set-cookie" | "set-cookie2" => false, + _ => true + } + }).map(|(n, v)| (n.clone(), v.clone())).collect(); + response.headers = headers; + }, + + ResponseType::Cors => { + let headers = old_headers.iter().filter(|(name, _)| { + match &*name.as_str().to_ascii_lowercase() { + "cache-control" | "content-language" | "content-type" | + "expires" | "last-modified" | "pragma" => true, + "set-cookie" | "set-cookie2" => false, + header => { + exposed_headers.iter().any(|h| *header == h.as_str().to_ascii_lowercase()) + } + } + }).map(|(n, v)| (n.clone(), v.clone())).collect(); + response.headers = headers; + }, + + ResponseType::Opaque => { + response.url_list = vec![]; + response.url = None; + response.headers = HeaderMap::new(); + response.status = None; + response.body = Arc::new(Mutex::new(ResponseBody::Empty)); + response.cache_state = CacheState::None; + }, + + ResponseType::OpaqueRedirect => { + response.headers = HeaderMap::new(); + response.status = None; + response.body = Arc::new(Mutex::new(ResponseBody::Empty)); + response.cache_state = CacheState::None; + }, + } + + response + } + + pub fn metadata(&self) -> Result<FetchMetadata, NetworkError> { + fn init_metadata(response: &Response, url: &ServoUrl) -> Metadata { + let mut metadata = Metadata::default(url.clone()); + metadata.set_content_type( + response + .headers + .typed_get::<ContentType>() + .map(|v| v.into()) + .as_ref(), + ); + metadata.location_url = response.location_url.clone(); + metadata.headers = Some(Serde(response.headers.clone())); + metadata.status = response.raw_status.clone(); + metadata.https_state = response.https_state; + metadata.referrer = response.referrer.clone(); + metadata.referrer_policy = response.referrer_policy.clone(); + metadata.redirected = response.actual_response().url_list.len() > 1; + metadata + } + + if let Some(error) = self.get_network_error() { + return Err(error.clone()); + } + + let metadata = self.url.as_ref().map(|url| init_metadata(self, url)); + + if let Some(ref response) = self.internal_response { + match response.url { + Some(ref url) => { + let unsafe_metadata = init_metadata(response, url); + + match self.response_type { + ResponseType::Basic => Ok(FetchMetadata::Filtered { + filtered: FilteredMetadata::Basic(metadata.unwrap()), + unsafe_: unsafe_metadata, + }), + ResponseType::Cors => Ok(FetchMetadata::Filtered { + filtered: FilteredMetadata::Cors(metadata.unwrap()), + unsafe_: unsafe_metadata, + }), + ResponseType::Default => unreachable!(), + ResponseType::Error(ref network_err) => Err(network_err.clone()), + ResponseType::Opaque => Ok(FetchMetadata::Filtered { + filtered: FilteredMetadata::Opaque, + unsafe_: unsafe_metadata, + }), + ResponseType::OpaqueRedirect => Ok(FetchMetadata::Filtered { + filtered: FilteredMetadata::OpaqueRedirect(url.clone()), + unsafe_: unsafe_metadata, + }), + } + }, + None => Err(NetworkError::Internal( + "No url found in unsafe response".to_owned(), + )), + } + } else { + assert_eq!(self.response_type, ResponseType::Default); + Ok(FetchMetadata::Unfiltered(metadata.unwrap())) + } + } +} diff --git a/components/shared/net/storage_thread.rs b/components/shared/net/storage_thread.rs new file mode 100644 index 00000000000..0253603016e --- /dev/null +++ b/components/shared/net/storage_thread.rs @@ -0,0 +1,48 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +use ipc_channel::ipc::IpcSender; +use malloc_size_of_derive::MallocSizeOf; +use serde::{Deserialize, Serialize}; +use servo_url::ServoUrl; + +#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, Serialize)] +pub enum StorageType { + Session, + Local, +} + +/// Request operations on the storage data associated with a particular url +#[derive(Debug, Deserialize, Serialize)] +pub enum StorageThreadMsg { + /// gets the number of key/value pairs present in the associated storage data + Length(IpcSender<usize>, ServoUrl, StorageType), + + /// gets the name of the key at the specified index in the associated storage data + Key(IpcSender<Option<String>>, ServoUrl, StorageType, u32), + + /// Gets the available keys in the associated storage data + Keys(IpcSender<Vec<String>>, ServoUrl, StorageType), + + /// gets the value associated with the given key in the associated storage data + GetItem(IpcSender<Option<String>>, ServoUrl, StorageType, String), + + /// sets the value of the given key in the associated storage data + SetItem( + IpcSender<Result<(bool, Option<String>), ()>>, + ServoUrl, + StorageType, + String, + String, + ), + + /// removes the key/value pair for the given key in the associated storage data + RemoveItem(IpcSender<Option<String>>, ServoUrl, StorageType, String), + + /// clears the associated storage data by removing all the key/value pairs + Clear(IpcSender<bool>, ServoUrl, StorageType), + + /// send a reply when done cleaning up thread resources and then shut it down + Exit(IpcSender<()>), +} diff --git a/components/shared/net/tests/image.rs b/components/shared/net/tests/image.rs new file mode 100644 index 00000000000..a4963702b57 --- /dev/null +++ b/components/shared/net/tests/image.rs @@ -0,0 +1,28 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +use net_traits::image::base::detect_image_format; + +#[test] +fn test_supported_images() { + let gif1 = [b'G', b'I', b'F', b'8', b'7', b'a']; + let gif2 = [b'G', b'I', b'F', b'8', b'9', b'a']; + let jpeg = [0xff, 0xd8, 0xff]; + let png = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; + let webp = [ + b'R', b'I', b'F', b'F', 0x01, 0x02, 0x03, 0x04, b'W', b'E', b'B', b'P', b'V', b'P', + ]; + let bmp = [0x42, 0x4D]; + let ico = [0x00, 0x00, 0x01, 0x00]; + let junk_format = [0x01, 0x02, 0x03, 0x04, 0x05]; + + assert!(detect_image_format(&gif1).is_ok()); + assert!(detect_image_format(&gif2).is_ok()); + assert!(detect_image_format(&jpeg).is_ok()); + assert!(detect_image_format(&png).is_ok()); + assert!(detect_image_format(&webp).is_ok()); + assert!(detect_image_format(&bmp).is_ok()); + assert!(detect_image_format(&ico).is_ok()); + assert!(detect_image_format(&junk_format).is_err()); +} diff --git a/components/shared/net/tests/lib.rs b/components/shared/net/tests/lib.rs new file mode 100644 index 00000000000..290ca902935 --- /dev/null +++ b/components/shared/net/tests/lib.rs @@ -0,0 +1,212 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +use net_traits::{ResourceAttribute, ResourceFetchTiming, ResourceTimeValue, ResourceTimingType}; + +#[test] +fn test_set_start_time_to_fetch_start_if_nonzero_tao() { + let mut resource_timing: ResourceFetchTiming = + ResourceFetchTiming::new(ResourceTimingType::Resource); + resource_timing.fetch_start = 1; + assert_eq!(resource_timing.start_time, 0, "`start_time` should be zero"); + assert!( + resource_timing.fetch_start > 0, + "`fetch_start` should have a positive value" + ); + + // verify that setting `start_time` to `fetch_start` succeeds + resource_timing.set_attribute(ResourceAttribute::StartTime(ResourceTimeValue::FetchStart)); + assert_eq!( + resource_timing.start_time, resource_timing.fetch_start, + "`start_time` should equal `fetch_start`" + ); +} + +#[test] +fn test_set_start_time_to_fetch_start_if_zero_tao() { + let mut resource_timing: ResourceFetchTiming = + ResourceFetchTiming::new(ResourceTimingType::Resource); + resource_timing.start_time = 1; + assert!( + resource_timing.start_time > 0, + "`start_time` should have a positive value" + ); + assert_eq!( + resource_timing.fetch_start, 0, + "`fetch_start` should be zero" + ); + + // verify that setting `start_time` to `fetch_start` succeeds even when `fetch_start` == zero + resource_timing.set_attribute(ResourceAttribute::StartTime(ResourceTimeValue::FetchStart)); + assert_eq!( + resource_timing.start_time, resource_timing.fetch_start, + "`start_time` should equal `fetch_start`" + ); +} + +#[test] +fn test_set_start_time_to_fetch_start_if_nonzero_no_tao() { + let mut resource_timing: ResourceFetchTiming = + ResourceFetchTiming::new(ResourceTimingType::Resource); + resource_timing.mark_timing_check_failed(); + resource_timing.fetch_start = 1; + assert_eq!(resource_timing.start_time, 0, "`start_time` should be zero"); + assert!( + resource_timing.fetch_start > 0, + "`fetch_start` should have a positive value" + ); + + // verify that setting `start_time` to `fetch_start` succeeds even when TAO check failed + resource_timing.set_attribute(ResourceAttribute::StartTime(ResourceTimeValue::FetchStart)); + assert_eq!( + resource_timing.start_time, resource_timing.fetch_start, + "`start_time` should equal `fetch_start`" + ); +} + +#[test] +fn test_set_start_time_to_fetch_start_if_zero_no_tao() { + let mut resource_timing: ResourceFetchTiming = + ResourceFetchTiming::new(ResourceTimingType::Resource); + resource_timing.mark_timing_check_failed(); + resource_timing.start_time = 1; + assert!( + resource_timing.start_time > 0, + "`start_time` should have a positive value" + ); + assert_eq!( + resource_timing.fetch_start, 0, + "`fetch_start` should be zero" + ); + + // verify that setting `start_time` to `fetch_start` succeeds even when `fetch_start`==0 and no TAO + resource_timing.set_attribute(ResourceAttribute::StartTime(ResourceTimeValue::FetchStart)); + assert_eq!( + resource_timing.start_time, resource_timing.fetch_start, + "`start_time` should equal `fetch_start`" + ); +} + +#[test] +fn test_set_start_time_to_redirect_start_if_nonzero_tao() { + let mut resource_timing: ResourceFetchTiming = + ResourceFetchTiming::new(ResourceTimingType::Resource); + resource_timing.redirect_start = 1; + assert_eq!(resource_timing.start_time, 0, "`start_time` should be zero"); + assert!( + resource_timing.redirect_start > 0, + "`redirect_start` should have a positive value" + ); + + // verify that setting `start_time` to `redirect_start` succeeds for nonzero `redirect_start`, TAO pass + resource_timing.set_attribute(ResourceAttribute::StartTime( + ResourceTimeValue::RedirectStart, + )); + assert_eq!( + resource_timing.start_time, resource_timing.redirect_start, + "`start_time` should equal `redirect_start`" + ); +} + +#[test] +fn test_not_set_start_time_to_redirect_start_if_zero_tao() { + let mut resource_timing: ResourceFetchTiming = + ResourceFetchTiming::new(ResourceTimingType::Resource); + resource_timing.start_time = 1; + assert!( + resource_timing.start_time > 0, + "`start_time` should have a positive value" + ); + assert_eq!( + resource_timing.redirect_start, 0, + "`redirect_start` should be zero" + ); + + // verify that setting `start_time` to `redirect_start` fails if `redirect_start` == 0 + resource_timing.set_attribute(ResourceAttribute::StartTime( + ResourceTimeValue::RedirectStart, + )); + assert_ne!( + resource_timing.start_time, resource_timing.redirect_start, + "`start_time` should *not* equal `redirect_start`" + ); +} + +#[test] +fn test_not_set_start_time_to_redirect_start_if_nonzero_no_tao() { + let mut resource_timing: ResourceFetchTiming = + ResourceFetchTiming::new(ResourceTimingType::Resource); + resource_timing.mark_timing_check_failed(); + // Note: properly-behaved redirect_start should never be nonzero once TAO check has failed + resource_timing.redirect_start = 1; + assert_eq!(resource_timing.start_time, 0, "`start_time` should be zero"); + assert!( + resource_timing.redirect_start > 0, + "`redirect_start` should have a positive value" + ); + + // verify that setting `start_time` to `redirect_start` fails if TAO check fails + resource_timing.set_attribute(ResourceAttribute::StartTime( + ResourceTimeValue::RedirectStart, + )); + assert_ne!( + resource_timing.start_time, resource_timing.redirect_start, + "`start_time` should *not* equal `redirect_start`" + ); +} + +#[test] +fn test_not_set_start_time_to_redirect_start_if_zero_no_tao() { + let mut resource_timing: ResourceFetchTiming = + ResourceFetchTiming::new(ResourceTimingType::Resource); + resource_timing.mark_timing_check_failed(); + resource_timing.start_time = 1; + assert!( + resource_timing.start_time > 0, + "`start_time` should have a positive value" + ); + assert_eq!( + resource_timing.redirect_start, 0, + "`redirect_start` should be zero" + ); + + // verify that setting `start_time` to `redirect_start` fails if `redirect_start`==0 and no TAO + resource_timing.set_attribute(ResourceAttribute::StartTime( + ResourceTimeValue::RedirectStart, + )); + assert_ne!( + resource_timing.start_time, resource_timing.redirect_start, + "`start_time` should *not* equal `redirect_start`" + ); +} + +#[test] +fn test_set_start_time() { + let mut resource_timing: ResourceFetchTiming = + ResourceFetchTiming::new(ResourceTimingType::Resource); + assert_eq!(resource_timing.start_time, 0, "`start_time` should be zero"); + + // verify setting `start_time` to current time succeeds + resource_timing.set_attribute(ResourceAttribute::StartTime(ResourceTimeValue::Now)); + assert!(resource_timing.start_time > 0, "failed to set `start_time`"); +} +#[test] +fn test_reset_start_time() { + let mut resource_timing: ResourceFetchTiming = + ResourceFetchTiming::new(ResourceTimingType::Resource); + assert_eq!(resource_timing.start_time, 0, "`start_time` should be zero"); + + resource_timing.start_time = 1; + assert!( + resource_timing.start_time > 0, + "`start_time` should have a positive value" + ); + + // verify resetting `start_time` (to zero) succeeds + resource_timing.set_attribute(ResourceAttribute::StartTime(ResourceTimeValue::Zero)); + assert_eq!( + resource_timing.start_time, 0, + "failed to reset `start_time`" + ); +} diff --git a/components/shared/net/tests/pub_domains.rs b/components/shared/net/tests/pub_domains.rs new file mode 100644 index 00000000000..ea58e7650e3 --- /dev/null +++ b/components/shared/net/tests/pub_domains.rs @@ -0,0 +1,122 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +use net_traits::pub_domains::{is_pub_domain, is_reg_domain, pub_suffix, reg_suffix}; + +// These tests may need to be updated if the PSL changes. + +#[test] +fn test_is_pub_domain_plain() { + assert!(is_pub_domain("com")); + assert!(is_pub_domain(".org")); + assert!(is_pub_domain("za.org")); + assert!(is_pub_domain("xn--od0alg.hk")); + assert!(is_pub_domain("xn--krdsherad-m8a.no")); +} + +#[test] +fn test_is_pub_domain_wildcard() { + assert!(is_pub_domain("hello.bd")); + assert!(is_pub_domain("world.jm")); + assert!(is_pub_domain("toto.kobe.jp")); +} + +#[test] +fn test_is_pub_domain_exception() { + assert_eq!(is_pub_domain("www.ck"), false); + assert_eq!(is_pub_domain("city.kawasaki.jp"), false); + assert_eq!(is_pub_domain("city.nagoya.jp"), false); + assert_eq!(is_pub_domain("teledata.mz"), false); +} + +#[test] +fn test_is_pub_domain_not() { + assert_eq!(is_pub_domain(""), false); + assert_eq!(is_pub_domain("."), false); + assert_eq!(is_pub_domain("..."), false); + assert_eq!(is_pub_domain(".servo.org"), false); + assert_eq!(is_pub_domain("www.mozilla.org"), false); + assert_eq!(is_pub_domain("publicsuffix.org"), false); + assert_eq!(is_pub_domain("hello.world.jm"), false); + assert_eq!(is_pub_domain("toto.toto.kobe.jp"), false); +} + +#[test] +fn test_is_pub_domain() { + assert!(!is_pub_domain("city.yokohama.jp")); + assert!(!is_pub_domain("foo.bar.baz.yokohama.jp")); + assert!(!is_pub_domain("foo.bar.city.yokohama.jp")); + assert!(!is_pub_domain("foo.bar.com")); + assert!(!is_pub_domain("foo.bar.tokyo.jp")); + assert!(!is_pub_domain("foo.bar.yokohama.jp")); + assert!(!is_pub_domain("foo.city.yokohama.jp")); + assert!(!is_pub_domain("foo.com")); + assert!(!is_pub_domain("foo.tokyo.jp")); + assert!(!is_pub_domain("yokohama.jp")); + assert!(is_pub_domain("com")); + assert!(is_pub_domain("foo.yokohama.jp")); + assert!(is_pub_domain("jp")); + assert!(is_pub_domain("tokyo.jp")); +} + +#[test] +fn test_is_reg_domain() { + assert!(!is_reg_domain("com")); + assert!(!is_reg_domain("foo.bar.baz.yokohama.jp")); + assert!(!is_reg_domain("foo.bar.com")); + assert!(!is_reg_domain("foo.bar.tokyo.jp")); + assert!(!is_reg_domain("foo.city.yokohama.jp")); + assert!(!is_reg_domain("foo.yokohama.jp")); + assert!(!is_reg_domain("jp")); + assert!(!is_reg_domain("tokyo.jp")); + assert!(is_reg_domain("city.yokohama.jp")); + assert!(is_reg_domain("foo.bar.yokohama.jp")); + assert!(is_reg_domain("foo.com")); + assert!(is_reg_domain("foo.tokyo.jp")); + assert!(is_reg_domain("yokohama.jp")); +} + +#[test] +fn test_pub_suffix() { + assert_eq!(pub_suffix("city.yokohama.jp"), "yokohama.jp"); + assert_eq!(pub_suffix("com"), "com"); + assert_eq!(pub_suffix("foo.bar.baz.yokohama.jp"), "baz.yokohama.jp"); + assert_eq!(pub_suffix("foo.bar.com"), "com"); + assert_eq!(pub_suffix("foo.bar.tokyo.jp"), "tokyo.jp"); + assert_eq!(pub_suffix("foo.bar.yokohama.jp"), "bar.yokohama.jp"); + assert_eq!(pub_suffix("foo.city.yokohama.jp"), "yokohama.jp"); + assert_eq!(pub_suffix("foo.com"), "com"); + assert_eq!(pub_suffix("foo.tokyo.jp"), "tokyo.jp"); + assert_eq!(pub_suffix("foo.yokohama.jp"), "foo.yokohama.jp"); + assert_eq!(pub_suffix("jp"), "jp"); + assert_eq!(pub_suffix("tokyo.jp"), "tokyo.jp"); + assert_eq!(pub_suffix("yokohama.jp"), "jp"); +} + +#[test] +fn test_reg_suffix() { + assert_eq!(reg_suffix("city.yokohama.jp"), "city.yokohama.jp"); + assert_eq!(reg_suffix("com"), "com"); + assert_eq!(reg_suffix("foo.bar.baz.yokohama.jp"), "bar.baz.yokohama.jp"); + assert_eq!(reg_suffix("foo.bar.com"), "bar.com"); + assert_eq!(reg_suffix("foo.bar.tokyo.jp"), "bar.tokyo.jp"); + assert_eq!(reg_suffix("foo.bar.yokohama.jp"), "foo.bar.yokohama.jp"); + assert_eq!(reg_suffix("foo.city.yokohama.jp"), "city.yokohama.jp"); + assert_eq!(reg_suffix("foo.com"), "foo.com"); + assert_eq!(reg_suffix("foo.tokyo.jp"), "foo.tokyo.jp"); + assert_eq!(reg_suffix("foo.yokohama.jp"), "foo.yokohama.jp"); + assert_eq!(reg_suffix("jp"), "jp"); + assert_eq!(reg_suffix("tokyo.jp"), "tokyo.jp"); + assert_eq!(reg_suffix("yokohama.jp"), "yokohama.jp"); +} + +#[test] +fn test_weirdness() { + // These are weird results, but AFAICT they are spec-compliant. + assert_ne!( + pub_suffix("city.yokohama.jp"), + pub_suffix(pub_suffix("city.yokohama.jp")) + ); + assert!(!is_pub_domain(pub_suffix("city.yokohama.jp"))); +} diff --git a/components/shared/net/tests/whitespace.rs b/components/shared/net/tests/whitespace.rs new file mode 100644 index 00000000000..d1e6b7a2ac8 --- /dev/null +++ b/components/shared/net/tests/whitespace.rs @@ -0,0 +1,25 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +#[test] +fn test_trim_http_whitespace() { + fn test_trim(in_: &[u8], out: &[u8]) { + let b = net_traits::trim_http_whitespace(in_); + assert_eq!(b, out); + } + + test_trim(b"", b""); + + test_trim(b" ", b""); + test_trim(b"a", b"a"); + test_trim(b" a", b"a"); + test_trim(b"a ", b"a"); + test_trim(b" a ", b"a"); + + test_trim(b"\t", b""); + test_trim(b"a", b"a"); + test_trim(b"\ta", b"a"); + test_trim(b"a\t", b"a"); + test_trim(b"\ta\t", b"a"); +} |