viaduct/backend/ffi.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 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) {
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);