summaryrefslogtreecommitdiffstats
path: root/third_party/rust/viaduct
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:44:51 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:44:51 +0000
commit9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /third_party/rust/viaduct
parentInitial commit. (diff)
downloadthunderbird-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.json1
-rw-r--r--third_party/rust/viaduct/Cargo.toml39
-rw-r--r--third_party/rust/viaduct/README.md59
-rw-r--r--third_party/rust/viaduct/src/backend.rs154
-rw-r--r--third_party/rust/viaduct/src/backend/ffi.rs209
-rw-r--r--third_party/rust/viaduct/src/error.rs48
-rw-r--r--third_party/rust/viaduct/src/fetch_msg_types.proto42
-rw-r--r--third_party/rust/viaduct/src/headers.rs414
-rw-r--r--third_party/rust/viaduct/src/headers/name.rs232
-rw-r--r--third_party/rust/viaduct/src/lib.rs370
-rw-r--r--third_party/rust/viaduct/src/mozilla.appservices.httpconfig.protobuf.rs49
-rw-r--r--third_party/rust/viaduct/src/settings.rs46
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,
+ })
+});