Skip to content

Commit

Permalink
Fairy bridge demo
Browse files Browse the repository at this point in the history
This shows off a potential viaduct replacement that uses new UniFFI
features. Check out `components/fairy-bridge/README.md` and
`examples/fairy-bridge-demo/README.md` for details.  Execute
`examples/fairy-bridge-demo/run-demo.py` to test it out yourself.

The UniFFI features are still a WIP.  This is currently using a branch
in my repo.  The current plan for getting these into UniFFI main is:
  - Get the `0.26.0` release out the door
  - Merge PR #1818 into `main`
  - Merge my `async-trait-interfaces` branch into main (probably using a
    few smaller PRs)

The Desktop plan needs to be explored more.  I believe there should be a
way to use Necko in Rust code, but that needs to be verified.
  • Loading branch information
bendk committed Dec 21, 2023
1 parent f475ac7 commit 88045a9
Show file tree
Hide file tree
Showing 17 changed files with 1,003 additions and 235 deletions.
404 changes: 171 additions & 233 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ members = [
"components/as-ohttp-client",
"components/autofill",
"components/crashtest",
"components/fairy-bridge",
"components/fxa-client",
"components/logins",
"components/nimbus",
Expand Down Expand Up @@ -122,7 +123,7 @@ default-members = [
[workspace.dependencies]
rusqlite = "0.30.0"
libsqlite3-sys = "0.27.0"
uniffi = "0.25.2"
uniffi = { version = "0.25.3", git = "https://github.com/bendk/uniffi-rs.git", branch = "async-trait-interfaces" }

[profile.release]
opt-level = "s"
Expand Down
22 changes: 22 additions & 0 deletions components/fairy-bridge/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "fairy-bridge"
version = "0.1.0"
edition = "2021"

[features]
default = []

[dependencies]
async-trait = "0.1"
once_cell = "1"
pollster = "0.3.0"
serde = "1"
serde_json = "1"
thiserror = "1"
tokio = { version = "1", features = ["rt-multi-thread"] }
uniffi = { workspace = true }
url = "2.2"
reqwest = { version = "0.11.23", optional = true }

[build-dependencies]
uniffi = { workspace = true, features = ["build"] }
30 changes: 30 additions & 0 deletions components/fairy-bridge/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Fairy Bridge

Fairy Bridge is an HTTP request bridge library that allows requests to be made using various
backends, including:

- The builtin reqwest backend
- Custom Rust backends
- Custom backends written in the foreign language

The plan for this is:
- iOS will use the reqwest backend
- Android will use a custom backend in Kotlin using fetch
(https://github.com/mozilla-mobile/firefox-android/tree/35ce01367157440f9e9daa4ed48a8022af80c8f2/android-components/components/concept/fetch)
- Desktop will use a custom backend in Rust that hooks into necko

## Sync / Async

The backends are implemented using async code, but there's also the option to block on a request.
This means `fairy-bridge` can be used in both sync and async contexts.

## Cookies / State

Cookies are explicitly not supported at the moment, adding them would require a separate security
review.

## Name

`fairy-bridge` is named after the Fairy Bridge (Xian Ren Qiao) -- the largest known natural bridge in the world, located in northwestern Guangxi Province, China.

![Picture of the Fairy Bridge](http://www.naturalarches.org/big9_files/FairyBridge1680.jpg)
35 changes: 35 additions & 0 deletions components/fairy-bridge/src/backend.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* 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::{FairyBridgeError, Request, Response};
use std::sync::Arc;

/// Settings for a backend instance
///
/// Backend constructions should input this in order to configure themselves
#[derive(Debug, uniffi::Record)]
pub struct BackendSettings {
// Connection timeout (in ms)
#[uniffi(default = None)]
pub connect_timeout: Option<u32>,
// Timeout for the entire request (in ms)
#[uniffi(default = None)]
pub timeout: Option<u32>,
// Maximum amount of redirects to follow (0 means redirects are not allowed)
#[uniffi(default = 10)]
pub redirect_limit: u32,
}

#[uniffi::export(with_callback_interface)]
#[async_trait::async_trait]
pub trait Backend: Send + Sync {
async fn send_request(self: Arc<Self>, request: Request) -> Result<Response, FairyBridgeError>;
}

#[uniffi::export]
pub fn init_backend(backend: Arc<dyn Backend>) -> Result<(), FairyBridgeError> {
crate::REGISTERED_BACKEND
.set(backend)
.map_err(|_| FairyBridgeError::BackendAlreadyInitialized)
}
21 changes: 21 additions & 0 deletions components/fairy-bridge/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* 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 type Result<T> = std::result::Result<T, FairyBridgeError>;

#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum FairyBridgeError {
#[error("BackendAlreadyInitialized")]
BackendAlreadyInitialized,
#[error("NoBackendInitialized")]
NoBackendInitialized,
#[error("BackendError({msg})")]
BackendError { msg: String },
#[error("HttpError({code})")]
HttpError { code: u16 },
#[error("InvalidRequestHeader({name})")]
InvalidRequestHeader { name: String },
#[error("InvalidResponseHeader({name})")]
InvalidResponseHeader { name: String },
}
81 changes: 81 additions & 0 deletions components/fairy-bridge/src/headers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* 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::FairyBridgeError;
use std::borrow::Cow;

/// Normalize / validate a request header
///
/// This accepts both &str and String. It either returns the lowercase version or
/// `FairyBridgeError::InvalidRequestHeader`
pub fn normalize_request_header<'a>(name: impl Into<Cow<'a, str>>) -> crate::Result<String> {
do_normalize_header(name).map_err(|name| FairyBridgeError::InvalidRequestHeader { name })
}

/// Normalize / validate a response header
///
/// This accepts both &str and String. It either returns the lowercase version or
/// `FairyBridgeError::InvalidRequestHeader`
pub fn normalize_response_header<'a>(name: impl Into<Cow<'a, str>>) -> crate::Result<String> {
do_normalize_header(name).map_err(|name| FairyBridgeError::InvalidResponseHeader { name })
}

fn do_normalize_header<'a>(name: impl Into<Cow<'a, str>>) -> Result<String, String> {
// 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,
];

let mut name = name.into();

if name.len() == 0 {
return Err(name.to_string());
}
let mut need_lower_case = false;
for b in name.bytes() {
let validity = VALID_HEADER_LUT[b as usize];
if validity == 0 {
return Err(name.to_string());
}
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(name.to_string())
}

// Default headers for easy usage
pub const ACCEPT_ENCODING: &'static str = "accept-encoding";
pub const ACCEPT: &'static str = "accept";
pub const AUTHORIZATION: &'static str = "authorization";
pub const CONTENT_TYPE: &'static str = "content-type";
pub const ETAG: &'static str = "etag";
pub const IF_NONE_MATCH: &'static str = "if-none-match";
pub const USER_AGENT: &'static str = "user-agent";
// non-standard, but it's convenient to have these.
pub const RETRY_AFTER: &'static str = "retry-after";
pub const X_IF_UNMODIFIED_SINCE: &'static str = "x-if-unmodified-since";
pub const X_KEYID: &'static str = "x-keyid";
pub const X_LAST_MODIFIED: &'static str = "x-last-modified";
pub const X_TIMESTAMP: &'static str = "x-timestamp";
pub const X_WEAVE_NEXT_OFFSET: &'static str = "x-weave-next-offset";
pub const X_WEAVE_RECORDS: &'static str = "x-weave-records";
pub const X_WEAVE_TIMESTAMP: &'static str = "x-weave-timestamp";
pub const X_WEAVE_BACKOFF: &'static str = "x-weave-backoff";
25 changes: 25 additions & 0 deletions components/fairy-bridge/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use once_cell::sync::OnceCell;
use std::sync::Arc;

mod backend;
mod error;
pub mod headers;
mod request;
#[cfg(feature = "reqwest")]
mod reqwest_backend;
mod response;

pub use backend::*;
pub use error::*;
pub use request::*;
#[cfg(feature = "reqwest")]
pub use reqwest_backend::*;
pub use response::*;

static REGISTERED_BACKEND: OnceCell<Arc<dyn Backend>> = OnceCell::new();

uniffi::setup_scaffolding!();
160 changes: 160 additions & 0 deletions components/fairy-bridge/src/request.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/* 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::{headers, FairyBridgeError, Response};
use pollster::FutureExt;
use std::borrow::Cow;
use std::collections::HashMap;
use url::Url;

#[derive(uniffi::Enum)]
pub enum Method {
Get,
Head,
Post,
Put,
Delete,
Connect,
Options,
Trace,
Patch,
}

#[derive(uniffi::Record)]
pub struct Request {
pub method: Method,
pub url: String,
pub headers: HashMap<String, String>,
pub body: Option<Vec<u8>>,
}

/// Http request
///
/// These are created using the builder pattern, then sent over the network using the `send()`
/// method.
impl Request {
pub fn new(method: Method, url: Url) -> Self {
Self {
method,
url: url.to_string(),
headers: HashMap::new(),
body: None,
}
}

pub async fn send(self) -> crate::Result<Response> {
let mut response = match crate::REGISTERED_BACKEND.get() {
Some(backend) => backend.clone().send_request(self).await,
None => Err(FairyBridgeError::NoBackendInitialized),
}?;
response.headers = response
.headers
.into_iter()
.map(|(name, value)| Ok((headers::normalize_request_header(name)?, value)))
.collect::<crate::Result<HashMap<_, _>>>()?;
Ok(response)
}

pub fn send_sync(self) -> crate::Result<Response> {
self.send().block_on()
}

/// 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)
}

/// Add all the provided headers to the list of headers to send with this
/// request.
pub fn headers<'a, I, K, V>(mut self, to_add: I) -> crate::Result<Self>
where
I: IntoIterator<Item = (K, V)>,
K: Into<Cow<'a, str>>,
V: Into<String>,
{
for (name, value) in to_add {
self = self.header(name, value)?
}
Ok(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<'a>(
mut self,
name: impl Into<Cow<'a, str>>,
val: impl Into<String>,
) -> crate::Result<Self> {
self.headers
.insert(headers::normalize_request_header(name)?, val.into());
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"));
if !self.headers.contains_key(headers::CONTENT_TYPE) {
self.headers.insert(
headers::CONTENT_TYPE.to_owned(),
"application/json".to_owned(),
);
}
self
}
}
Loading

0 comments on commit 88045a9

Please sign in to comment.