summaryrefslogtreecommitdiffstats
path: root/third_party/rust/viaduct/src/backend/ffi.rs
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/rust/viaduct/src/backend/ffi.rs')
-rw-r--r--third_party/rust/viaduct/src/backend/ffi.rs209
1 files changed, 209 insertions, 0 deletions
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);