diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
commit | 9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /third_party/rust/viaduct | |
parent | Initial commit. (diff) | |
download | thunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.tar.xz thunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/viaduct')
-rw-r--r-- | third_party/rust/viaduct/.cargo-checksum.json | 1 | ||||
-rw-r--r-- | third_party/rust/viaduct/Cargo.toml | 39 | ||||
-rw-r--r-- | third_party/rust/viaduct/README.md | 59 | ||||
-rw-r--r-- | third_party/rust/viaduct/src/backend.rs | 154 | ||||
-rw-r--r-- | third_party/rust/viaduct/src/backend/ffi.rs | 209 | ||||
-rw-r--r-- | third_party/rust/viaduct/src/error.rs | 48 | ||||
-rw-r--r-- | third_party/rust/viaduct/src/fetch_msg_types.proto | 42 | ||||
-rw-r--r-- | third_party/rust/viaduct/src/headers.rs | 414 | ||||
-rw-r--r-- | third_party/rust/viaduct/src/headers/name.rs | 232 | ||||
-rw-r--r-- | third_party/rust/viaduct/src/lib.rs | 370 | ||||
-rw-r--r-- | third_party/rust/viaduct/src/mozilla.appservices.httpconfig.protobuf.rs | 49 | ||||
-rw-r--r-- | third_party/rust/viaduct/src/settings.rs | 46 |
12 files changed, 1663 insertions, 0 deletions
diff --git a/third_party/rust/viaduct/.cargo-checksum.json b/third_party/rust/viaduct/.cargo-checksum.json new file mode 100644 index 0000000000..7f7108b5db --- /dev/null +++ b/third_party/rust/viaduct/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"Cargo.toml":"e075ec087ed1cbf5fcad0581281be420cabfe61d2e5896171e3fb6c873224086","README.md":"a6856d0f86aaade17cb9fa61c153aca085903d0676fae953022aeab235996cb7","src/backend.rs":"22c313dd0ecbe92803219d3770bb97b3f876ed2fdc4ac8b5ac8dbea92b563e9f","src/backend/ffi.rs":"9ce49be773b2eb51aeef00a15e1d33f34e48e916c5e8b628fdc0ee7cc6d40e15","src/error.rs":"98ca92b58bd8b4f3c9d4c6d03ed235609d486fe8121277004283b9cfda6e3260","src/fetch_msg_types.proto":"de8a46a4947a140783a4d714364f18ccf02c4759d6ab5ace9da0b1c058efa6c3","src/headers.rs":"bf3cd6b717dfb337c64ce0bc6d275364181884378fc47afed7c80c435ce0733f","src/headers/name.rs":"dcfd4d42326724f822893cf6ac90f1e14734dba178150dcb606f4b19de5e66d7","src/lib.rs":"7fb25cab1e487902c30068546984568dec969e269c3318dc031e887a475fe51f","src/mozilla.appservices.httpconfig.protobuf.rs":"59e64f2b997bc99da654c37d0a36ae7d08456cd384ab7c8c501e3990c5f97544","src/settings.rs":"f62d0779d7b86af5daad0c23fb61a5982c11520e6fa528ebe2e2d6ad76e70afd"},"package":null}
\ No newline at end of file diff --git a/third_party/rust/viaduct/Cargo.toml b/third_party/rust/viaduct/Cargo.toml new file mode 100644 index 0000000000..7df4272422 --- /dev/null +++ b/third_party/rust/viaduct/Cargo.toml @@ -0,0 +1,39 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "viaduct" +version = "0.1.0" +authors = ["Thom Chiovoloni <tchiovoloni@mozilla.com>"] +exclude = [ + "/android", + "/ios", +] +readme = "README.md" +license = "MPL-2.0" + +[lib] +crate-type = ["lib"] + +[dependencies] +ffi-support = "0.4" +log = "0.4" +once_cell = "1.5" +prost = "0.8" +prost-derive = "0.8" +serde = "1" +serde_json = "1" +thiserror = "1.0" +url = "2.1" + +[dependencies.parking_lot] +version = ">=0.11,<=0.12" diff --git a/third_party/rust/viaduct/README.md b/third_party/rust/viaduct/README.md new file mode 100644 index 0000000000..fd6adf5171 --- /dev/null +++ b/third_party/rust/viaduct/README.md @@ -0,0 +1,59 @@ +# Viaduct + +Viaduct is our HTTP request library, which can make requests either via a +rust-based (reqwest) networking stack (used on iOS and for local desktop use, +for tests and the like), or using a stack that calls a function passed into it +over the FFI (on android). + +For usage info, you can run `cargo +nightly doc -p viaduct` (the `+nightly` is +optional, however some intra-doc links require it), it has several examples. + +## Android/FFI Backend overview + +On Android, the backend works as follows: + +1. During megazord initialization, we are passed a `Lazy<Client>` (`Client` comes + from the [concept-fetch](https://github.com/mozilla-mobile/android-components/tree/master/components/concept/fetch) + android component, and `Lazy` is from the Kotlin stdlib). + + - It also sets a flag that indicates that even if the FFI backend never gets + fully initialized (e.g. with a callback), we should error rather than use + the reqwest backend (which should not be compiled in, however we've had + trouble ensuring this in the past, although at this point we have checks + in CI to ensure it is not present). + +2. At this point, a JNA `Callback` instance is created and passed into Rust. + - This serves to proxy the request made by Rust to the `Client`. + - The `Callback` instance is never allowed to be GCed. + - To Rust, it's just a `extern "C"` function pointer that get's stored in an + atomic variable and never can be unset. + +3. When Rust makes a request: + 1. We serialize the request info into a protobuf record + 2. This record is passed into the function pointer we should have by this + point (erroring if it has not been set yet). + 3. The callback (on the Java side now) deserializes the protobuf record, + converts it to a concept-fetch Request instance, and passes it to the + client. + 4. The response (or error) is then converted into a protobuf record. The + java code then asks Rust for a buffer big enough to hold the serialized + response (or error). + 5. The response is written to the buffer, and returned to Rust. + 6. Rust then decodes the protobuf, and converts it to a + `viaduct::Response` object that it returns to the caller. + +Some notes: + +- This "request flow" is entirely synchronous, simplifying the implementation + considerably. + +- Cookies are explicitely not supported at the moment, adding them would + require a separate security review. + +- Generally, this is the way the FFI backend is expected to work on any + platform, but for concreteness (and because it's the only one currently using + the FFI backend), we explained it for Android. + +- Most of the code in `viaduct` is defining a ergonomic HTTP facade, and is + unrelated to this (or to the reqwest backend). This code is more or less + entirely (in the Kotlin layer and) in `src/backend/ffi.rs`. diff --git a/third_party/rust/viaduct/src/backend.rs b/third_party/rust/viaduct/src/backend.rs new file mode 100644 index 0000000000..599a26bd98 --- /dev/null +++ b/third_party/rust/viaduct/src/backend.rs @@ -0,0 +1,154 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::GLOBAL_SETTINGS; +use ffi::FfiBackend; +use once_cell::sync::OnceCell; +mod ffi; + +pub fn note_backend(which: &str) { + // If trace logs are enabled: log on every request. Otherwise, just log on + // the first request at `info` level. We remember if the Once was triggered + // to avoid logging twice in the first case. + static NOTE_BACKEND_ONCE: std::sync::Once = std::sync::Once::new(); + let mut called = false; + NOTE_BACKEND_ONCE.call_once(|| { + log::info!("Using HTTP backend {}", which); + called = true; + }); + if !called { + log::trace!("Using HTTP backend {}", which); + } +} + +pub trait Backend: Send + Sync + 'static { + fn send(&self, request: crate::Request) -> Result<crate::Response, crate::Error>; +} + +static BACKEND: OnceCell<&'static dyn Backend> = OnceCell::new(); + +pub fn set_backend(b: &'static dyn Backend) -> Result<(), crate::Error> { + BACKEND + .set(b) + .map_err(|_| crate::error::Error::SetBackendError) +} + +pub(crate) fn get_backend() -> &'static dyn Backend { + *BACKEND.get_or_init(|| Box::leak(Box::new(FfiBackend))) +} + +pub fn send(request: crate::Request) -> Result<crate::Response, crate::Error> { + validate_request(&request)?; + get_backend().send(request) +} + +pub fn validate_request(request: &crate::Request) -> Result<(), crate::Error> { + if request.url.scheme() != "https" + && match request.url.host() { + Some(url::Host::Domain(d)) => d != "localhost", + Some(url::Host::Ipv4(addr)) => !addr.is_loopback(), + Some(url::Host::Ipv6(addr)) => !addr.is_loopback(), + None => true, + } + && { + let settings = GLOBAL_SETTINGS.read(); + settings + .addn_allowed_insecure_url + .as_ref() + .map(|url| url.host() != request.url.host() || url.scheme() != request.url.scheme()) + .unwrap_or(true) + } + { + return Err(crate::Error::NonTlsUrl); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_validate_request() { + let _https_request = crate::Request::new( + crate::Method::Get, + url::Url::parse("https://www.example.com").unwrap(), + ); + assert!(validate_request(&_https_request).is_ok()); + + let _http_request = crate::Request::new( + crate::Method::Get, + url::Url::parse("http://www.example.com").unwrap(), + ); + assert!(validate_request(&_http_request).is_err()); + + let _localhost_https_request = crate::Request::new( + crate::Method::Get, + url::Url::parse("https://127.0.0.1/index.html").unwrap(), + ); + assert!(validate_request(&_localhost_https_request).is_ok()); + + let _localhost_https_request_2 = crate::Request::new( + crate::Method::Get, + url::Url::parse("https://localhost:4242/").unwrap(), + ); + assert!(validate_request(&_localhost_https_request_2).is_ok()); + + let _localhost_http_request = crate::Request::new( + crate::Method::Get, + url::Url::parse("http://localhost:4242/").unwrap(), + ); + assert!(validate_request(&_localhost_http_request).is_ok()); + + let localhost_request = crate::Request::new( + crate::Method::Get, + url::Url::parse("localhost:4242/").unwrap(), + ); + assert!(validate_request(&localhost_request).is_err()); + + let localhost_request_shorthand_ipv6 = + crate::Request::new(crate::Method::Get, url::Url::parse("http://[::1]").unwrap()); + assert!(validate_request(&localhost_request_shorthand_ipv6).is_ok()); + + let localhost_request_ipv6 = crate::Request::new( + crate::Method::Get, + url::Url::parse("http://[0:0:0:0:0:0:0:1]").unwrap(), + ); + assert!(validate_request(&localhost_request_ipv6).is_ok()); + } + + #[test] + fn test_validate_request_addn_allowed_insecure_url() { + let request_root = crate::Request::new( + crate::Method::Get, + url::Url::parse("http://anything").unwrap(), + ); + let request = crate::Request::new( + crate::Method::Get, + url::Url::parse("http://anything/path").unwrap(), + ); + // This should never be accepted. + let request_ftp = crate::Request::new( + crate::Method::Get, + url::Url::parse("ftp://anything/path").unwrap(), + ); + assert!(validate_request(&request_root).is_err()); + assert!(validate_request(&request).is_err()); + { + let mut settings = GLOBAL_SETTINGS.write(); + settings.addn_allowed_insecure_url = + Some(url::Url::parse("http://something-else").unwrap()); + } + assert!(validate_request(&request_root).is_err()); + assert!(validate_request(&request).is_err()); + + { + let mut settings = GLOBAL_SETTINGS.write(); + settings.addn_allowed_insecure_url = Some(url::Url::parse("http://anything").unwrap()); + } + assert!(validate_request(&request_root).is_ok()); + assert!(validate_request(&request).is_ok()); + assert!(validate_request(&request_ftp).is_err()); + } +} diff --git a/third_party/rust/viaduct/src/backend/ffi.rs b/third_party/rust/viaduct/src/backend/ffi.rs new file mode 100644 index 0000000000..cca6bc68f2 --- /dev/null +++ b/third_party/rust/viaduct/src/backend/ffi.rs @@ -0,0 +1,209 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::{backend::Backend, settings::GLOBAL_SETTINGS}; +use crate::{msg_types, Error}; +use ffi_support::{ByteBuffer, FfiStr}; + +ffi_support::implement_into_ffi_by_protobuf!(msg_types::Request); + +impl From<crate::Request> for msg_types::Request { + fn from(request: crate::Request) -> Self { + let settings = GLOBAL_SETTINGS.read(); + msg_types::Request { + url: request.url.to_string(), + body: request.body, + // Real weird that this needs to be specified as an i32, but + // it certainly makes it convenient for us... + method: request.method as i32, + headers: request.headers.into(), + follow_redirects: settings.follow_redirects, + use_caches: settings.use_caches, + connect_timeout_secs: settings.connect_timeout.map_or(0, |d| d.as_secs() as i32), + read_timeout_secs: settings.read_timeout.map_or(0, |d| d.as_secs() as i32), + } + } +} + +macro_rules! backend_error { + ($($args:tt)*) => {{ + let msg = format!($($args)*); + log::error!("{}", msg); + Error::BackendError(msg) + }}; +} + +pub struct FfiBackend; +impl Backend for FfiBackend { + fn send(&self, request: crate::Request) -> Result<crate::Response, Error> { + use ffi_support::IntoFfi; + use prost::Message; + super::note_backend("FFI (trusted)"); + + let method = request.method; + let fetch = callback_holder::get_callback().ok_or(Error::BackendNotInitialized)?; + let proto_req: msg_types::Request = request.into(); + let buf = proto_req.into_ffi_value(); + let response = unsafe { fetch(buf) }; + // This way we'll Drop it if we panic, unlike if we just got a slice into + // it. Besides, we already own it. + let response_bytes = response.destroy_into_vec(); + + let response: msg_types::Response = match Message::decode(response_bytes.as_slice()) { + Ok(v) => v, + Err(e) => { + panic!( + "Failed to parse protobuf returned from fetch callback! {}", + e + ); + } + }; + + if let Some(exn) = response.exception_message { + return Err(Error::NetworkError(format!("Java error: {:?}", exn))); + } + let status = response + .status + .ok_or_else(|| backend_error!("Missing HTTP status"))?; + + if status < 0 || status > i32::from(u16::max_value()) { + return Err(backend_error!("Illegal HTTP status: {}", status)); + } + + let mut headers = crate::Headers::with_capacity(response.headers.len()); + for (name, val) in response.headers { + let hname = match crate::HeaderName::new(name) { + Ok(name) => name, + Err(e) => { + // Ignore headers with invalid names, since nobody can look for them anyway. + log::warn!("Server sent back invalid header name: '{}'", e); + continue; + } + }; + // Not using Header::new since the error it returns is for request headers. + headers.insert_header(crate::Header::new_unchecked(hname, val)); + } + + let url = url::Url::parse( + &response + .url + .ok_or_else(|| backend_error!("Response has no URL"))?, + ) + .map_err(|e| backend_error!("Response has illegal URL: {}", e))?; + + Ok(crate::Response { + url, + request_method: method, + body: response.body.unwrap_or_default(), + status: status as u16, + headers, + }) + } +} + +/// Type of the callback we need callers on the other side of the FFI to +/// provide. +/// +/// Takes and returns a ffi_support::ByteBuffer. (TODO: it would be nice if we could +/// make this take/return pointers, so that we could use JNA direct mapping. Maybe +/// we need some kind of ThinBuffer?) +/// +/// This is a bit weird, since it requires us to allow code on the other side of +/// the FFI to allocate a ByteBuffer from us, but it works. +/// +/// The code on the other side of the FFI is responsible for freeing the ByteBuffer +/// it's passed using `viaduct_destroy_bytebuffer`. +type FetchCallback = unsafe extern "C" fn(ByteBuffer) -> ByteBuffer; + +/// Module that manages get/set of the global fetch callback pointer. +mod callback_holder { + use super::FetchCallback; + use std::sync::atomic::{AtomicUsize, Ordering}; + + /// Note: We only assign to this once. + static CALLBACK_PTR: AtomicUsize = AtomicUsize::new(0); + + // Overly-paranoid sanity checking to ensure that these types are + // convertible between each-other. `transmute` actually should check this for + // us too, but this helps document the invariants we rely on in this code. + // + // Note that these are guaranteed by + // https://rust-lang.github.io/unsafe-code-guidelines/layout/function-pointers.html + // and thus this is a little paranoid. + ffi_support::static_assert!( + STATIC_ASSERT_USIZE_EQ_FUNC_SIZE, + std::mem::size_of::<usize>() == std::mem::size_of::<FetchCallback>() + ); + + ffi_support::static_assert!( + STATIC_ASSERT_USIZE_EQ_OPT_FUNC_SIZE, + std::mem::size_of::<usize>() == std::mem::size_of::<Option<FetchCallback>>() + ); + + /// Get the function pointer to the FetchCallback. Panics if the callback + /// has not yet been initialized. + pub(super) fn get_callback() -> Option<FetchCallback> { + let ptr_value = CALLBACK_PTR.load(Ordering::SeqCst); + unsafe { std::mem::transmute::<usize, Option<FetchCallback>>(ptr_value) } + } + + /// Set the function pointer to the FetchCallback. Returns false if we did nothing because the callback had already been initialized + pub(super) fn set_callback(h: FetchCallback) -> bool { + let as_usize = h as usize; + match CALLBACK_PTR.compare_exchange(0, as_usize, Ordering::SeqCst, Ordering::SeqCst) { + Ok(_) => true, + Err(_) => { + // This is an internal bug, the other side of the FFI should ensure + // it sets this only once. Note that this is actually going to be + // before logging is initialized in practice, so there's not a lot + // we can actually do here. + log::error!("Bug: Initialized CALLBACK_PTR multiple times"); + false + } + } + } +} + +/// Return a ByteBuffer of the requested size. This is used to store the +/// response from the callback. +#[no_mangle] +pub extern "C" fn viaduct_alloc_bytebuffer(sz: i32) -> ByteBuffer { + let mut error = ffi_support::ExternError::default(); + let buffer = + ffi_support::call_with_output(&mut error, || ByteBuffer::new_with_size(sz.max(0) as usize)); + error.consume_and_log_if_error(); + buffer +} + +#[no_mangle] +pub extern "C" fn viaduct_log_error(s: FfiStr<'_>) { + let mut error = ffi_support::ExternError::default(); + ffi_support::call_with_output(&mut error, || { + log::error!("Viaduct Ffi Error: {}", s.as_str()) + }); + error.consume_and_log_if_error(); +} + +#[no_mangle] +pub extern "C" fn viaduct_initialize(callback: FetchCallback) -> u8 { + ffi_support::abort_on_panic::call_with_output(|| callback_holder::set_callback(callback)) +} + +/// Allows connections to the hard-coded address the Android Emulator uses for +/// localhost. It would be easy to support allowing the address to be passed in, +/// but we've made a decision to avoid that possible footgun. The expectation is +/// that this will only be called in debug builds or if the app can determine it +/// is in the emulator, but the Rust code doesn't know that, so we can't check. +#[no_mangle] +pub extern "C" fn viaduct_allow_android_emulator_loopback() { + let mut error = ffi_support::ExternError::default(); + ffi_support::call_with_output(&mut error, || { + let url = url::Url::parse("http://10.0.2.2").unwrap(); + let mut settings = GLOBAL_SETTINGS.write(); + settings.addn_allowed_insecure_url = Some(url); + }); + error.consume_and_log_if_error(); +} + +ffi_support::define_bytebuffer_destructor!(viaduct_destroy_bytebuffer); diff --git a/third_party/rust/viaduct/src/error.rs b/third_party/rust/viaduct/src/error.rs new file mode 100644 index 0000000000..a66c3e38fe --- /dev/null +++ b/third_party/rust/viaduct/src/error.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 http://mozilla.org/MPL/2.0/. */ + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("[no-sentry] Illegal characters in request header '{0}'")] + RequestHeaderError(crate::HeaderName), + + #[error("[no-sentry] Backend error: {0}")] + BackendError(String), + + #[error("[no-sentry] Network error: {0}")] + NetworkError(String), + + #[error("The rust-components network backend must be initialized before use!")] + BackendNotInitialized, + + #[error("Backend already initialized.")] + SetBackendError, + + /// Note: we return this if the server returns a bad URL with + /// its response. This *probably* should never happen, but who knows. + #[error("[no-sentry] URL Parse Error: {0}")] + UrlError(#[source] url::ParseError), + + #[error("[no-sentry] Validation error: URL does not use TLS protocol.")] + NonTlsUrl, +} + +impl From<url::ParseError> for Error { + fn from(u: url::ParseError) -> Self { + Error::UrlError(u) + } +} + +/// This error is returned as the `Err` result from +/// [`Response::require_success`]. +/// +/// Note that it's not a variant on `Error` to distinguish between errors +/// caused by the network, and errors returned from the server. +#[derive(thiserror::Error, Debug, Clone)] +#[error("Error: {method} {url} returned {status}")] +pub struct UnexpectedStatus { + pub status: u16, + pub method: crate::Method, + pub url: url::Url, +} diff --git a/third_party/rust/viaduct/src/fetch_msg_types.proto b/third_party/rust/viaduct/src/fetch_msg_types.proto new file mode 100644 index 0000000000..96963cef42 --- /dev/null +++ b/third_party/rust/viaduct/src/fetch_msg_types.proto @@ -0,0 +1,42 @@ +syntax = "proto2"; + +// Note: this file name must be unique due to how the iOS megazord works :( + +package mozilla.appservices.httpconfig.protobuf; + +option java_package = "mozilla.appservices.httpconfig"; +option java_outer_classname = "MsgTypes"; +option swift_prefix = "MsgTypes_"; +option optimize_for = LITE_RUNTIME; + +message Request { + enum Method { + GET = 0; + HEAD = 1; + POST = 2; + PUT = 3; + DELETE = 4; + CONNECT = 5; + OPTIONS = 6; + TRACE = 7; + PATCH = 8; + } + required Method method = 1; + required string url = 2; + optional bytes body = 3; + map<string, string> headers = 4; + required bool follow_redirects = 5; + required bool use_caches = 6; + required int32 connect_timeout_secs = 7; + required int32 read_timeout_secs = 8; +} + +message Response { + // If this is present, nothing else is. + optional string exception_message = 1; + optional string url = 2; + optional int32 status = 3; + optional bytes body = 4; + map<string, string> headers = 5; +} + diff --git a/third_party/rust/viaduct/src/headers.rs b/third_party/rust/viaduct/src/headers.rs new file mode 100644 index 0000000000..d7b7f7aa11 --- /dev/null +++ b/third_party/rust/viaduct/src/headers.rs @@ -0,0 +1,414 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +pub use name::{HeaderName, InvalidHeaderName}; +use std::collections::HashMap; +use std::iter::FromIterator; +use std::str::FromStr; +mod name; + +/// A single header. Headers have a name (case insensitive) and a value. The +/// character set for header and values are both restrictive. +/// - Names must only contain a-zA-Z0-9 and and ('!' | '#' | '$' | '%' | '&' | +/// '\'' | '*' | '+' | '-' | '.' | '^' | '_' | '`' | '|' | '~') characters +/// (the field-name token production defined at +/// https://tools.ietf.org/html/rfc7230#section-3.2). +/// For request headers, we expect these to all be specified statically, +/// and so we panic if you provide an invalid one. (For response headers, we +/// ignore headers with invalid names, but emit a warning). +/// +/// Header names are case insensitive, and we have several pre-defined ones in +/// the [`header_names`] module. +/// +/// - Values may only contain printable ascii characters, and may not contain +/// \r or \n. Strictly speaking, HTTP is more flexible for header values, +/// however we don't need to support binary header values, and so we do not. +/// +/// Note that typically you should not interact with this directly, and instead +/// use the methods on [`Request`] or [`Headers`] to manipulate these. +#[derive(Clone, Debug, PartialEq, PartialOrd, Hash, Eq, Ord)] +pub struct Header { + pub(crate) name: HeaderName, + pub(crate) value: String, +} + +// Trim `s` without copying if it can be avoided. +fn trim_string<S: AsRef<str> + Into<String>>(s: S) -> String { + let sr = s.as_ref(); + let trimmed = sr.trim(); + if sr.len() != trimmed.len() { + trimmed.into() + } else { + s.into() + } +} + +fn is_valid_header_value(value: &str) -> bool { + value.bytes().all(|b| (32..127).contains(&b) || b == b'\t') +} + +impl Header { + pub fn new<Name, Value>(name: Name, value: Value) -> Result<Self, crate::Error> + where + Name: Into<HeaderName>, + Value: AsRef<str> + Into<String>, + { + let name = name.into(); + let value = trim_string(value); + if !is_valid_header_value(&value) { + return Err(crate::Error::RequestHeaderError(name)); + } + Ok(Self { name, value }) + } + + pub fn new_unchecked<Value>(name: HeaderName, value: Value) -> Self + where + Value: AsRef<str> + Into<String>, + { + Self { + name, + value: value.into(), + } + } + + #[inline] + pub fn name(&self) -> &HeaderName { + &self.name + } + + #[inline] + pub fn value(&self) -> &str { + &self.value + } + + #[inline] + fn set_value<V: AsRef<str>>(&mut self, s: V) -> Result<(), crate::Error> { + let value = s.as_ref(); + if !is_valid_header_value(value) { + Err(crate::Error::RequestHeaderError(self.name.clone())) + } else { + self.value.clear(); + self.value.push_str(s.as_ref().trim()); + Ok(()) + } + } +} + +impl std::fmt::Display for Header { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: {}", self.name, self.value) + } +} + +/// A list of headers. +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct Headers { + headers: Vec<Header>, +} + +impl Headers { + /// Initialize an empty list of headers. + #[inline] + pub fn new() -> Self { + Default::default() + } + + /// Initialize an empty list of headers backed by a vector with the provided + /// capacity. + pub fn with_capacity(c: usize) -> Self { + Self { + headers: Vec::with_capacity(c), + } + } + + /// Convert this list of headers to a Vec<Header> + #[inline] + pub fn into_vec(self) -> Vec<Header> { + self.headers + } + + /// Returns the number of headers. + #[inline] + pub fn len(&self) -> usize { + self.headers.len() + } + + /// Returns true if `len()` is zero. + #[inline] + pub fn is_empty(&self) -> bool { + self.headers.is_empty() + } + /// Clear this set of headers. + #[inline] + pub fn clear(&mut self) { + self.headers.clear(); + } + + /// Insert or update a new header. + /// + /// This returns an error if you attempt to specify a header with an + /// invalid value (values must be printable ASCII and may not contain + /// \r or \n) + /// + /// ## Example + /// ``` + /// # use viaduct::Headers; + /// # fn main() -> Result<(), viaduct::Error> { + /// let mut h = Headers::new(); + /// h.insert("My-Cool-Header", "example")?; + /// assert_eq!(h.get("My-Cool-Header"), Some("example")); + /// + /// // Note: names are sensitive + /// assert_eq!(h.get("my-cool-header"), Some("example")); + /// + /// // Also note, constants for headers are in `viaduct::header_names`, and + /// // you can chain the result of this function. + /// h.insert(viaduct::header_names::CONTENT_TYPE, "something...")? + /// .insert("Something-Else", "etc")?; + /// # Ok(()) + /// # } + /// ``` + pub fn insert<N, V>(&mut self, name: N, value: V) -> Result<&mut Self, crate::Error> + where + N: Into<HeaderName> + PartialEq<HeaderName>, + V: Into<String> + AsRef<str>, + { + if let Some(entry) = self.headers.iter_mut().find(|h| name == h.name) { + entry.set_value(value)?; + } else { + self.headers.push(Header::new(name, value)?); + } + Ok(self) + } + + /// Insert the provided header unless a header is already specified. + /// Mostly used internally, e.g. to set "Content-Type: application/json" + /// in `Request::json()` unless it has been set specifically. + pub fn insert_if_missing<N, V>(&mut self, name: N, value: V) -> Result<&mut Self, crate::Error> + where + N: Into<HeaderName> + PartialEq<HeaderName>, + V: Into<String> + AsRef<str>, + { + if !self.headers.iter_mut().any(|h| name == h.name) { + self.headers.push(Header::new(name, value)?); + } + Ok(self) + } + + /// Insert or update a header directly. Typically you will want to use + /// `insert` over this, as it performs less work if the header needs + /// updating instead of insertion. + pub fn insert_header(&mut self, new: Header) -> &mut Self { + if let Some(entry) = self.headers.iter_mut().find(|h| h.name == new.name) { + entry.value = new.value; + } else { + self.headers.push(new); + } + self + } + + /// Add all the headers in the provided iterator to this list of headers. + pub fn extend<I>(&mut self, iter: I) -> &mut Self + where + I: IntoIterator<Item = Header>, + { + let it = iter.into_iter(); + self.headers.reserve(it.size_hint().0); + for h in it { + self.insert_header(h); + } + self + } + + /// Add all the headers in the provided iterator, unless any of them are Err. + pub fn try_extend<I, E>(&mut self, iter: I) -> Result<&mut Self, E> + where + I: IntoIterator<Item = Result<Header, E>>, + { + // Not the most efficient but avoids leaving us in an unspecified state + // if one returns Err. + self.extend(iter.into_iter().collect::<Result<Vec<_>, E>>()?); + Ok(self) + } + + /// Get the header object with the requested name. Usually, you will + /// want to use `get()` or `get_as::<T>()` instead. + pub fn get_header<S>(&self, name: S) -> Option<&Header> + where + S: PartialEq<HeaderName>, + { + self.headers.iter().find(|h| name == h.name) + } + + /// Get the value of the header with the provided name. + /// + /// See also `get_as`. + /// + /// ## Example + /// ``` + /// # use viaduct::{Headers, header_names::CONTENT_TYPE}; + /// # fn main() -> Result<(), viaduct::Error> { + /// let mut h = Headers::new(); + /// h.insert(CONTENT_TYPE, "application/json")?; + /// assert_eq!(h.get(CONTENT_TYPE), Some("application/json")); + /// assert_eq!(h.get("Something-Else"), None); + /// # Ok(()) + /// # } + /// ``` + pub fn get<S>(&self, name: S) -> Option<&str> + where + S: PartialEq<HeaderName>, + { + self.get_header(name).map(|h| h.value.as_str()) + } + + /// Get the value of the header with the provided name, and + /// attempt to parse it using [`std::str::FromStr`]. + /// + /// - If the header is missing, it returns None. + /// - If the header is present but parsing failed, returns + /// `Some(Err(<error returned by parsing>))`. + /// - Otherwise, returns `Some(Ok(result))`. + /// + /// Note that if `Option<Result<T, E>>` is inconvenient for you, + /// and you wish this returned `Result<Option<T>, E>`, you may use + /// the built-in `transpose()` method to convert between them. + /// + /// ``` + /// # use viaduct::Headers; + /// # fn main() -> Result<(), viaduct::Error> { + /// let mut h = Headers::new(); + /// h.insert("Example", "1234")?.insert("Illegal", "abcd")?; + /// let v: Option<Result<i64, _>> = h.get_as("Example"); + /// assert_eq!(v, Some(Ok(1234))); + /// assert_eq!(h.get_as::<i64, _>("Example"), Some(Ok(1234))); + /// assert_eq!(h.get_as::<i64, _>("Illegal"), Some("abcd".parse::<i64>())); + /// assert_eq!(h.get_as::<i64, _>("Something-Else"), None); + /// # Ok(()) + /// # } + /// ``` + pub fn get_as<T, S>(&self, name: S) -> Option<Result<T, <T as FromStr>::Err>> + where + T: FromStr, + S: PartialEq<HeaderName>, + { + self.get(name).map(str::parse) + } + /// Get the value of the header with the provided name, and + /// attempt to parse it using [`std::str::FromStr`]. + /// + /// This is a variant of `get_as` that returns None on error, + /// intended to be used for cases where missing and invalid + /// headers should be treated the same. (With `get_as` this + /// requires `h.get_as(...).and_then(|r| r.ok())`, which is + /// somewhat opaque. + pub fn try_get<T, S>(&self, name: S) -> Option<T> + where + T: FromStr, + S: PartialEq<HeaderName>, + { + self.get(name).and_then(|val| val.parse::<T>().ok()) + } + + /// Get an iterator over the headers in no particular order. + /// + /// Note that we also implement IntoIterator. + pub fn iter(&self) -> <&Headers as IntoIterator>::IntoIter { + self.into_iter() + } +} + +impl std::iter::IntoIterator for Headers { + type IntoIter = <Vec<Header> as IntoIterator>::IntoIter; + type Item = Header; + fn into_iter(self) -> Self::IntoIter { + self.headers.into_iter() + } +} + +impl<'a> std::iter::IntoIterator for &'a Headers { + type IntoIter = <&'a [Header] as IntoIterator>::IntoIter; + type Item = &'a Header; + fn into_iter(self) -> Self::IntoIter { + self.headers[..].iter() + } +} + +impl FromIterator<Header> for Headers { + fn from_iter<T>(iter: T) -> Self + where + T: IntoIterator<Item = Header>, + { + let mut v = iter.into_iter().collect::<Vec<Header>>(); + v.sort_by(|a, b| a.name.cmp(&b.name)); + v.reverse(); + v.dedup_by(|a, b| a.name == b.name); + Headers { headers: v } + } +} + +#[allow(clippy::implicit_hasher)] // https://github.com/rust-lang/rust-clippy/issues/3899 +impl From<Headers> for HashMap<String, String> { + fn from(headers: Headers) -> HashMap<String, String> { + headers + .into_iter() + .map(|h| (String::from(h.name), h.value)) + .collect() + } +} + +pub mod consts { + use super::name::HeaderName; + macro_rules! def_header_consts { + ($(($NAME:ident, $string:literal)),* $(,)?) => { + $(pub const $NAME: HeaderName = HeaderName(std::borrow::Cow::Borrowed($string));)* + }; + } + + macro_rules! headers { + ($(($NAME:ident, $string:literal)),* $(,)?) => { + def_header_consts!($(($NAME, $string)),*); + // Unused except for tests. + const _ALL: &[&str] = &[$($string),*]; + }; + } + + // Predefined header names, for convenience. + // Feel free to add to these. + headers!( + (ACCEPT_ENCODING, "accept-encoding"), + (ACCEPT, "accept"), + (AUTHORIZATION, "authorization"), + (CONTENT_TYPE, "content-type"), + (ETAG, "etag"), + (IF_NONE_MATCH, "if-none-match"), + (USER_AGENT, "user-agent"), + // non-standard, but it's convenient to have these. + (RETRY_AFTER, "retry-after"), + (X_IF_UNMODIFIED_SINCE, "x-if-unmodified-since"), + (X_KEYID, "x-keyid"), + (X_LAST_MODIFIED, "x-last-modified"), + (X_TIMESTAMP, "x-timestamp"), + (X_WEAVE_NEXT_OFFSET, "x-weave-next-offset"), + (X_WEAVE_RECORDS, "x-weave-records"), + (X_WEAVE_TIMESTAMP, "x-weave-timestamp"), + (X_WEAVE_BACKOFF, "x-weave-backoff"), + ); + + #[test] + fn test_predefined() { + for &name in _ALL { + assert!( + HeaderName::new(name).is_ok(), + "Invalid header name in predefined header constants: {}", + name + ); + assert_eq!( + name.to_ascii_lowercase(), + name, + "Non-lowercase name in predefined header constants: {}", + name + ); + } + } +} diff --git a/third_party/rust/viaduct/src/headers/name.rs b/third_party/rust/viaduct/src/headers/name.rs new file mode 100644 index 0000000000..7fe322ddd6 --- /dev/null +++ b/third_party/rust/viaduct/src/headers/name.rs @@ -0,0 +1,232 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::borrow::Cow; + +/// Represents a header name that we know to be both valid and lowercase. +/// Internally, this avoids allocating for headers that are constant strings, +/// like the predefined ones in this crate, however even without that +/// optimization, we would still likely have an equivalent of this for use +/// as a case-insensitive string guaranteed to only have valid characters. +#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)] +pub struct HeaderName(pub(super) Cow<'static, str>); + +/// Indicates an invalid header name. Note that we only emit +/// this for response headers, for request headers, we panic +/// instead. This is because it would likely come through as +/// a network error if we emitted it for local headers, when +/// it's actually a bug that we'd need to fix. +#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] +#[error("Invalid header name: {0:?}")] +pub struct InvalidHeaderName(Cow<'static, str>); + +impl From<&'static str> for HeaderName { + fn from(s: &'static str) -> HeaderName { + match HeaderName::new(s) { + Ok(v) => v, + Err(e) => { + panic!("Illegal locally specified header {}", e); + } + } + } +} + +impl From<String> for HeaderName { + fn from(s: String) -> HeaderName { + match HeaderName::new(s) { + Ok(v) => v, + Err(e) => { + panic!("Illegal locally specified header {}", e); + } + } + } +} + +impl From<Cow<'static, str>> for HeaderName { + fn from(s: Cow<'static, str>) -> HeaderName { + match HeaderName::new(s) { + Ok(v) => v, + Err(e) => { + panic!("Illegal locally specified header {}", e); + } + } + } +} + +impl InvalidHeaderName { + pub fn name(&self) -> &str { + &self.0[..] + } +} + +fn validate_header(mut name: Cow<'static, str>) -> Result<HeaderName, InvalidHeaderName> { + if name.len() == 0 { + return Err(invalid_header_name(name)); + } + let mut need_lower_case = false; + for b in name.bytes() { + let validity = VALID_HEADER_LUT[b as usize]; + if validity == 0 { + return Err(invalid_header_name(name)); + } + if validity == 2 { + need_lower_case = true; + } + } + if need_lower_case { + // Only do this if needed, since it causes us to own the header. + name.to_mut().make_ascii_lowercase(); + } + Ok(HeaderName(name)) +} + +impl HeaderName { + /// Create a new header. In general you likely want to use `HeaderName::from(s)` + /// instead for headers being specified locally (This will panic instead of + /// returning a Result, since we have control over headers we specify locally, + /// and want to know if we specify an illegal one). + #[inline] + pub fn new<S: Into<Cow<'static, str>>>(s: S) -> Result<Self, InvalidHeaderName> { + validate_header(s.into()) + } + + #[inline] + pub fn as_str(&self) -> &str { + &self.0[..] + } +} + +impl std::fmt::Display for HeaderName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +// Separate for dumb micro-optimization reasons. +#[cold] +#[inline(never)] +fn invalid_header_name(s: Cow<'static, str>) -> InvalidHeaderName { + log::warn!("Invalid header name: {}", s); + InvalidHeaderName(s) +} +// Note: 0 = invalid, 1 = valid, 2 = valid but needs lowercasing. I'd use an +// enum for this, but it would make this LUT *way* harder to look at. This +// includes 0-9, a-z, A-Z (as 2), and ('!' | '#' | '$' | '%' | '&' | '\'' | '*' +// | '+' | '-' | '.' | '^' | '_' | '`' | '|' | '~'), matching the field-name +// token production defined at https://tools.ietf.org/html/rfc7230#section-3.2. +static VALID_HEADER_LUT: [u8; 256] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +]; + +impl std::ops::Deref for HeaderName { + type Target = str; + #[inline] + fn deref(&self) -> &str { + self.as_str() + } +} + +impl AsRef<str> for HeaderName { + #[inline] + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl AsRef<[u8]> for HeaderName { + #[inline] + fn as_ref(&self) -> &[u8] { + self.as_str().as_bytes() + } +} + +impl From<HeaderName> for String { + #[inline] + fn from(h: HeaderName) -> Self { + h.0.into() + } +} + +impl From<HeaderName> for Cow<'static, str> { + #[inline] + fn from(h: HeaderName) -> Self { + h.0 + } +} + +impl From<HeaderName> for Vec<u8> { + #[inline] + fn from(h: HeaderName) -> Self { + String::from(h.0).into() + } +} + +macro_rules! partialeq_boilerplate { + ($T0:ty, $T1:ty) => { + // This macro is used for items with and without lifetimes. + #[allow(clippy::extra_unused_lifetimes)] + impl<'a> PartialEq<$T0> for $T1 { + fn eq(&self, other: &$T0) -> bool { + // The &* should invoke Deref::deref if it exists, no-op otherwise. + (&*self).eq_ignore_ascii_case(&*other) + } + } + #[allow(clippy::extra_unused_lifetimes)] + impl<'a> PartialEq<$T1> for $T0 { + fn eq(&self, other: &$T1) -> bool { + PartialEq::eq(other, self) + } + } + }; +} + +partialeq_boilerplate!(HeaderName, str); +partialeq_boilerplate!(HeaderName, &'a str); +partialeq_boilerplate!(HeaderName, String); +partialeq_boilerplate!(HeaderName, &'a String); +partialeq_boilerplate!(HeaderName, Cow<'a, str>); + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_lut() { + let mut expect = [0u8; 256]; + for b in b'0'..=b'9' { + expect[b as usize] = 1; + } + for b in b'a'..=b'z' { + expect[b as usize] = 1; + } + for b in b'A'..=b'Z' { + expect[b as usize] = 2; + } + for b in b"!#$%&'*+-.^_`|~" { + expect[*b as usize] = 1; + } + assert_eq!(&VALID_HEADER_LUT[..], &expect[..]); + } + #[test] + fn test_validate() { + assert!(validate_header("".into()).is_err()); + assert!(validate_header(" foo ".into()).is_err()); + assert!(validate_header("a=b".into()).is_err()); + assert_eq!( + validate_header("content-type".into()), + Ok(HeaderName("content-type".into())) + ); + assert_eq!( + validate_header("Content-Type".into()), + Ok(HeaderName("content-type".into())) + ); + } +} diff --git a/third_party/rust/viaduct/src/lib.rs b/third_party/rust/viaduct/src/lib.rs new file mode 100644 index 0000000000..d6739cd809 --- /dev/null +++ b/third_party/rust/viaduct/src/lib.rs @@ -0,0 +1,370 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#![allow(unknown_lints)] +#![warn(rust_2018_idioms)] + +use url::Url; +#[macro_use] +mod headers; + +mod backend; +pub mod error; +pub mod settings; +pub use error::*; + +pub use backend::{note_backend, set_backend, Backend}; +pub use headers::{consts as header_names, Header, HeaderName, Headers, InvalidHeaderName}; +pub use settings::GLOBAL_SETTINGS; + +#[allow(clippy::derive_partial_eq_without_eq)] +pub(crate) mod msg_types { + include!("mozilla.appservices.httpconfig.protobuf.rs"); +} + +/// HTTP Methods. +/// +/// The supported methods are the limited to what's supported by android-components. +#[derive(Clone, Debug, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[repr(u8)] +pub enum Method { + Get, + Head, + Post, + Put, + Delete, + Connect, + Options, + Trace, + Patch, +} + +impl Method { + pub fn as_str(self) -> &'static str { + match self { + Method::Get => "GET", + Method::Head => "HEAD", + Method::Post => "POST", + Method::Put => "PUT", + Method::Delete => "DELETE", + Method::Connect => "CONNECT", + Method::Options => "OPTIONS", + Method::Trace => "TRACE", + Method::Patch => "PATCH", + } + } +} + +impl std::fmt::Display for Method { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +#[must_use = "`Request`'s \"builder\" functions take by move, not by `&mut self`"] +#[derive(Clone, Debug)] +pub struct Request { + pub method: Method, + pub url: Url, + pub headers: Headers, + pub body: Option<Vec<u8>>, +} + +impl Request { + /// Construct a new request to the given `url` using the given `method`. + /// Note that the request is not made until `send()` is called. + pub fn new(method: Method, url: Url) -> Self { + Self { + method, + url, + headers: Headers::new(), + body: None, + } + } + + pub fn send(self) -> Result<Response, Error> { + crate::backend::send(self) + } + + /// Alias for `Request::new(Method::Get, url)`, for convenience. + pub fn get(url: Url) -> Self { + Self::new(Method::Get, url) + } + + /// Alias for `Request::new(Method::Patch, url)`, for convenience. + pub fn patch(url: Url) -> Self { + Self::new(Method::Patch, url) + } + + /// Alias for `Request::new(Method::Post, url)`, for convenience. + pub fn post(url: Url) -> Self { + Self::new(Method::Post, url) + } + + /// Alias for `Request::new(Method::Put, url)`, for convenience. + pub fn put(url: Url) -> Self { + Self::new(Method::Put, url) + } + + /// Alias for `Request::new(Method::Delete, url)`, for convenience. + pub fn delete(url: Url) -> Self { + Self::new(Method::Delete, url) + } + + /// Append the provided query parameters to the URL + /// + /// ## Example + /// ``` + /// # use viaduct::{Request, header_names}; + /// # use url::Url; + /// let some_url = url::Url::parse("https://www.example.com/xyz").unwrap(); + /// + /// let req = Request::post(some_url).query(&[("a", "1234"), ("b", "qwerty")]); + /// assert_eq!(req.url.as_str(), "https://www.example.com/xyz?a=1234&b=qwerty"); + /// + /// // This appends to the query query instead of replacing `a`. + /// let req = req.query(&[("a", "5678")]); + /// assert_eq!(req.url.as_str(), "https://www.example.com/xyz?a=1234&b=qwerty&a=5678"); + /// ``` + pub fn query(mut self, pairs: &[(&str, &str)]) -> Self { + let mut append_to = self.url.query_pairs_mut(); + for (k, v) in pairs { + append_to.append_pair(k, v); + } + drop(append_to); + self + } + + /// Set the query string of the URL. Note that `req.set_query(None)` will + /// clear the query. + /// + /// See also `Request::query` which appends a slice of query pairs, which is + /// typically more ergonomic when usable. + /// + /// ## Example + /// ``` + /// # use viaduct::{Request, header_names}; + /// # use url::Url; + /// let some_url = url::Url::parse("https://www.example.com/xyz").unwrap(); + /// + /// let req = Request::post(some_url).set_query("a=b&c=d"); + /// assert_eq!(req.url.as_str(), "https://www.example.com/xyz?a=b&c=d"); + /// + /// let req = req.set_query(None); + /// assert_eq!(req.url.as_str(), "https://www.example.com/xyz"); + /// ``` + pub fn set_query<'a, Q: Into<Option<&'a str>>>(mut self, query: Q) -> Self { + self.url.set_query(query.into()); + self + } + + /// Add all the provided headers to the list of headers to send with this + /// request. + pub fn headers<I>(mut self, to_add: I) -> Self + where + I: IntoIterator<Item = Header>, + { + self.headers.extend(to_add); + self + } + + /// Add the provided header to the list of headers to send with this request. + /// + /// This returns `Err` if `val` contains characters that may not appear in + /// the body of a header. + /// + /// ## Example + /// ``` + /// # use viaduct::{Request, header_names}; + /// # use url::Url; + /// # fn main() -> Result<(), viaduct::Error> { + /// # let some_url = url::Url::parse("https://www.example.com").unwrap(); + /// Request::post(some_url) + /// .header(header_names::CONTENT_TYPE, "application/json")? + /// .header("My-Header", "Some special value")?; + /// // ... + /// # Ok(()) + /// # } + /// ``` + pub fn header<Name, Val>(mut self, name: Name, val: Val) -> Result<Self, crate::Error> + where + Name: Into<HeaderName> + PartialEq<HeaderName>, + Val: Into<String> + AsRef<str>, + { + self.headers.insert(name, val)?; + Ok(self) + } + + /// Set this request's body. + pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self { + self.body = Some(body.into()); + self + } + + /// Set body to the result of serializing `val`, and, unless it has already + /// been set, set the Content-Type header to "application/json". + /// + /// Note: This panics if serde_json::to_vec fails. This can only happen + /// in a couple cases: + /// + /// 1. Trying to serialize a map with non-string keys. + /// 2. We wrote a custom serializer that fails. + /// + /// Neither of these are things we do. If they happen, it seems better for + /// this to fail hard with an easy to track down panic, than for e.g. `sync` + /// to fail with a JSON parse error (which we'd probably attribute to + /// corrupt data on the server, or something). + pub fn json<T: ?Sized + serde::Serialize>(mut self, val: &T) -> Self { + self.body = + Some(serde_json::to_vec(val).expect("Rust component bug: serde_json::to_vec failure")); + self.headers + .insert_if_missing(header_names::CONTENT_TYPE, "application/json") + .unwrap(); // We know this has to be valid. + self + } +} + +/// A response from the server. +#[derive(Clone, Debug)] +pub struct Response { + /// The method used to request this response. + pub request_method: Method, + /// The URL of this response. + pub url: Url, + /// The HTTP Status code of this response. + pub status: u16, + /// The headers returned with this response. + pub headers: Headers, + /// The body of the response. Note that responses with binary bodies are + /// currently unsupported. + pub body: Vec<u8>, +} + +impl Response { + /// Parse the body as JSON. + pub fn json<'a, T>(&'a self) -> Result<T, serde_json::Error> + where + T: serde::Deserialize<'a>, + { + serde_json::from_slice(&self.body) + } + + /// Get the body as a string. Assumes UTF-8 encoding. Any non-utf8 bytes + /// are replaced with the replacement character. + pub fn text(&self) -> std::borrow::Cow<'_, str> { + String::from_utf8_lossy(&self.body) + } + + /// Returns true if the status code is in the interval `[200, 300)`. + #[inline] + pub fn is_success(&self) -> bool { + status_codes::is_success_code(self.status) + } + + /// Returns true if the status code is in the interval `[500, 600)`. + #[inline] + pub fn is_server_error(&self) -> bool { + status_codes::is_server_error_code(self.status) + } + + /// Returns true if the status code is in the interval `[400, 500)`. + #[inline] + pub fn is_client_error(&self) -> bool { + status_codes::is_client_error_code(self.status) + } + + /// Returns an [`UnexpectedStatus`] error if `self.is_success()` is false, + /// otherwise returns `Ok(self)`. + #[inline] + pub fn require_success(self) -> Result<Self, UnexpectedStatus> { + if self.is_success() { + Ok(self) + } else { + Err(UnexpectedStatus { + method: self.request_method, + // XXX We probably should try and sanitize this. Replace the user id + // if it's a sync token server URL, for example. + url: self.url, + status: self.status, + }) + } + } +} + +/// A module containing constants for all HTTP status codes. +pub mod status_codes { + + /// Is it a 2xx status? + #[inline] + pub fn is_success_code(c: u16) -> bool { + (200..300).contains(&c) + } + + /// Is it a 4xx error? + #[inline] + pub fn is_client_error_code(c: u16) -> bool { + (400..500).contains(&c) + } + + /// Is it a 5xx error? + #[inline] + pub fn is_server_error_code(c: u16) -> bool { + (500..600).contains(&c) + } + + macro_rules! define_status_codes { + ($(($val:expr, $NAME:ident)),* $(,)?) => { + $(pub const $NAME: u16 = $val;)* + }; + } + // From https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html + define_status_codes![ + (100, CONTINUE), + (101, SWITCHING_PROTOCOLS), + // 2xx + (200, OK), + (201, CREATED), + (202, ACCEPTED), + (203, NONAUTHORITATIVE_INFORMATION), + (204, NO_CONTENT), + (205, RESET_CONTENT), + (206, PARTIAL_CONTENT), + // 3xx + (300, MULTIPLE_CHOICES), + (301, MOVED_PERMANENTLY), + (302, FOUND), + (303, SEE_OTHER), + (304, NOT_MODIFIED), + (305, USE_PROXY), + // no 306 + (307, TEMPORARY_REDIRECT), + // 4xx + (400, BAD_REQUEST), + (401, UNAUTHORIZED), + (402, PAYMENT_REQUIRED), + (403, FORBIDDEN), + (404, NOT_FOUND), + (405, METHOD_NOT_ALLOWED), + (406, NOT_ACCEPTABLE), + (407, PROXY_AUTHENTICATION_REQUIRED), + (408, REQUEST_TIMEOUT), + (409, CONFLICT), + (410, GONE), + (411, LENGTH_REQUIRED), + (412, PRECONDITION_FAILED), + (413, REQUEST_ENTITY_TOO_LARGE), + (414, REQUEST_URI_TOO_LONG), + (415, UNSUPPORTED_MEDIA_TYPE), + (416, REQUESTED_RANGE_NOT_SATISFIABLE), + (417, EXPECTATION_FAILED), + (429, TOO_MANY_REQUESTS), + // 5xx + (500, INTERNAL_SERVER_ERROR), + (501, NOT_IMPLEMENTED), + (502, BAD_GATEWAY), + (503, SERVICE_UNAVAILABLE), + (504, GATEWAY_TIMEOUT), + (505, HTTP_VERSION_NOT_SUPPORTED), + ]; +} diff --git a/third_party/rust/viaduct/src/mozilla.appservices.httpconfig.protobuf.rs b/third_party/rust/viaduct/src/mozilla.appservices.httpconfig.protobuf.rs new file mode 100644 index 0000000000..a74da5dedd --- /dev/null +++ b/third_party/rust/viaduct/src/mozilla.appservices.httpconfig.protobuf.rs @@ -0,0 +1,49 @@ +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Request { + #[prost(enumeration="request::Method", required, tag="1")] + pub method: i32, + #[prost(string, required, tag="2")] + pub url: ::prost::alloc::string::String, + #[prost(bytes="vec", optional, tag="3")] + pub body: ::core::option::Option<::prost::alloc::vec::Vec<u8>>, + #[prost(map="string, string", tag="4")] + pub headers: ::std::collections::HashMap<::prost::alloc::string::String, ::prost::alloc::string::String>, + #[prost(bool, required, tag="5")] + pub follow_redirects: bool, + #[prost(bool, required, tag="6")] + pub use_caches: bool, + #[prost(int32, required, tag="7")] + pub connect_timeout_secs: i32, + #[prost(int32, required, tag="8")] + pub read_timeout_secs: i32, +} +/// Nested message and enum types in `Request`. +pub mod request { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] + #[repr(i32)] + pub enum Method { + Get = 0, + Head = 1, + Post = 2, + Put = 3, + Delete = 4, + Connect = 5, + Options = 6, + Trace = 7, + Patch = 8, + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Response { + /// If this is present, nothing else is. + #[prost(string, optional, tag="1")] + pub exception_message: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag="2")] + pub url: ::core::option::Option<::prost::alloc::string::String>, + #[prost(int32, optional, tag="3")] + pub status: ::core::option::Option<i32>, + #[prost(bytes="vec", optional, tag="4")] + pub body: ::core::option::Option<::prost::alloc::vec::Vec<u8>>, + #[prost(map="string, string", tag="5")] + pub headers: ::std::collections::HashMap<::prost::alloc::string::String, ::prost::alloc::string::String>, +} diff --git a/third_party/rust/viaduct/src/settings.rs b/third_party/rust/viaduct/src/settings.rs new file mode 100644 index 0000000000..6ed204b44e --- /dev/null +++ b/third_party/rust/viaduct/src/settings.rs @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use once_cell::sync::Lazy; +use parking_lot::RwLock; +use std::time::Duration; +use url::Url; + +/// Note: reqwest allows these only to be specified per-Client. concept-fetch +/// allows these to be specified on each call to fetch. I think it's worth +/// keeping a single global reqwest::Client in the reqwest backend, to simplify +/// the way we abstract away from these. +/// +/// In the future, should we need it, we might be able to add a CustomClient type +/// with custom settings. In the reqwest backend this would store a Client, and +/// in the concept-fetch backend it would only store the settings, and populate +/// things on the fly. +#[derive(Debug)] +#[non_exhaustive] +pub struct Settings { + pub read_timeout: Option<Duration>, + pub connect_timeout: Option<Duration>, + pub follow_redirects: bool, + pub use_caches: bool, + // For testing purposes, we allow exactly one additional Url which is + // allowed to not be https. + pub addn_allowed_insecure_url: Option<Url>, +} + +#[cfg(target_os = "ios")] +const TIMEOUT_DURATION: Duration = Duration::from_secs(7); + +#[cfg(not(target_os = "ios"))] +const TIMEOUT_DURATION: Duration = Duration::from_secs(10); + +// The singleton instance of our settings. +pub static GLOBAL_SETTINGS: Lazy<RwLock<Settings>> = Lazy::new(|| { + RwLock::new(Settings { + read_timeout: Some(TIMEOUT_DURATION), + connect_timeout: Some(TIMEOUT_DURATION), + follow_redirects: true, + use_caches: false, + addn_allowed_insecure_url: None, + }) +}); |