-
Notifications
You must be signed in to change notification settings - Fork 226
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
17 changed files
with
1,003 additions
and
235 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
Oops, something went wrong.