aboutsummaryrefslogtreecommitdiffstats
path: root/components/shared/net
diff options
context:
space:
mode:
Diffstat (limited to 'components/shared/net')
-rw-r--r--components/shared/net/Cargo.toml42
-rw-r--r--components/shared/net/blob_url_store.rs75
-rw-r--r--components/shared/net/fetch/headers.rs18
-rw-r--r--components/shared/net/filemanager_thread.rs190
-rw-r--r--components/shared/net/image/base.rs121
-rw-r--r--components/shared/net/image_cache.rs156
-rw-r--r--components/shared/net/lib.rs859
-rw-r--r--components/shared/net/pub_domains.rs159
-rw-r--r--components/shared/net/quality.rs87
-rw-r--r--components/shared/net/request.rs749
-rw-r--r--components/shared/net/response.rs364
-rw-r--r--components/shared/net/storage_thread.rs48
-rw-r--r--components/shared/net/tests/image.rs28
-rw-r--r--components/shared/net/tests/lib.rs212
-rw-r--r--components/shared/net/tests/pub_domains.rs122
-rw-r--r--components/shared/net/tests/whitespace.rs25
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");
+}