//! Exposed C APIs for arti-rpc-client-core. //! //! See top-level documentation in header file for C conventions that affect the safety of these functions. //! (These include things like "all input pointers must be valid" and so on.) pub mod err; mod util; use err::{ArtiRpcError, InvalidInput}; use std::ffi::{c_char, c_int}; use std::sync::Mutex; use util::{ OptOutPtrExt as _, OptOutValExt, OutBoxedPtr, OutSocketOwned, OutVal, ffi_body_raw, ffi_body_with_err, }; use crate::{ ObjectId, RpcConnBuilder, conn::{AnyResponse, RequestHandle}, util::Utf8CString, }; /// A status code returned by an Arti RPC function. /// /// On success, a function will return `ARTI_SUCCESS (0)`. /// On failure, a function will return some other status code. pub type ArtiRpcStatus = u32; /// An open connection to Arti over an a RPC protocol. /// /// This is a thread-safe type: you may safely use it from multiple threads at once. /// /// Once you are no longer going to use this connection at all, you must free /// it with [`arti_rpc_conn_free`] pub type ArtiRpcConn = crate::RpcConn; /// A builder object used to configure and construct /// a connection to Arti over the RPC protocol. /// /// This is a thread-safe type: you may safely use it from multiple threads at once. /// /// Once you are done with this object, you must free it with [`arti_rpc_conn_builder_free`]. pub struct ArtiRpcConnBuilder(Mutex); /// An owned string, returned by this library. /// /// This string must be released with `arti_rpc_str_free`. /// You can inspect it with `arti_rpc_str_get`, but you may not modify it. /// The string is guaranteed to be UTF-8 and NUL-terminated. pub type ArtiRpcStr = Utf8CString; /// A handle to an in-progress RPC request. /// /// This handle must eventually be freed with `arti_rpc_handle_free`. /// /// You can wait for the next message with `arti_rpc_handle_wait`. pub type ArtiRpcHandle = RequestHandle; /// The type of a message returned by an RPC request. pub type ArtiRpcResponseType = c_int; /// The type of an entry prepended to a connect point search path. pub type ArtiRpcBuilderEntryType = c_int; /// The type of a data stream socket. /// (This is always `int` on Unix-like platforms, /// and SOCKET on Windows.) // // NOTE: We declare this as a separate type so that we can give it a default. #[repr(transparent)] pub struct ArtiRpcRawSocket( #[cfg(windows)] std::os::windows::raw::SOCKET, #[cfg(not(windows))] c_int, ); impl Default for ArtiRpcRawSocket { fn default() -> Self { #[cfg(windows)] { Self(!0) } #[cfg(not(windows))] { Self(-1) } } } /// Try to create a new `ArtiRpcConnBuilder`, with default settings. /// /// On success, return `ARTI_RPC_STATUS_SUCCESS` and set `*builder_out` /// to a new `ArtiRpcConnBuilder`. /// Otherwise return some other status code, set `*builder_out` to NULL, and set /// `*error_out` (if provided) to a newly allocated error object. /// /// # Ownership /// /// The caller is responsible for making sure that `*builder_out` and `*error_out`, /// if set, are eventually freed. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_conn_builder_new( builder_out: *mut *mut ArtiRpcConnBuilder, error_out: *mut *mut ArtiRpcError, ) -> ArtiRpcStatus { ffi_body_with_err!( { let builder_out: Option> [out_ptr_opt]; err error_out: Option>; } in { let builder = ArtiRpcConnBuilder(Mutex::new(RpcConnBuilder::new())); builder_out.write_boxed_value_if_ptr_set(builder); } ) } /// Release storage held by an `ArtiRpcConnBuilder`. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_conn_builder_free(builder: *mut ArtiRpcConnBuilder) { ffi_body_raw!( { let builder: Option> [in_ptr_consume_opt]; } in { drop(builder); // Safety: return value is (); trivially safe. () } ); } /// Constant to denote a literal connect point. /// /// This constant is passed to [`arti_rpc_conn_builder_prepend_entry`]. pub const ARTI_RPC_BUILDER_ENTRY_LITERAL_CONNECT_POINT: ArtiRpcBuilderEntryType = 1; /// Constant to denote a path in which Arti configuration variables are expanded. /// /// This constant is passed to [`arti_rpc_conn_builder_prepend_entry`]. pub const ARTI_RPC_BUILDER_ENTRY_EXPANDABLE_PATH: ArtiRpcBuilderEntryType = 2; /// Constant to denote a literal path that is not expanded. /// /// This constant is passed to [`arti_rpc_conn_builder_prepend_entry`]. pub const ARTI_RPC_BUILDER_ENTRY_LITERAL_PATH: ArtiRpcBuilderEntryType = 3; /// Prepend a single entry to the connection point path in `builder`. /// /// This entry will be considered before any entries in `${ARTI_RPC_CONNECT_PATH}`, /// but after any entry in `${ARTI_RPC_CONNECT_PATH_OVERRIDE}`. /// /// The interpretation will depend on the value of `entry_type`. /// /// On success, return `ARTI_RPC_STATUS_SUCCESS`. /// Otherwise return some other status code, and set /// `*error_out` (if provided) to a newly allocated error object. /// /// # Ownership /// /// The caller is responsible for making sure that `*error_out`, /// if set, is eventually freed. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_conn_builder_prepend_entry( builder: *const ArtiRpcConnBuilder, entry_type: ArtiRpcBuilderEntryType, entry: *const c_char, error_out: *mut *mut ArtiRpcError, ) -> ArtiRpcStatus { ffi_body_with_err!( { let builder: Option<&ArtiRpcConnBuilder> [in_ptr_opt]; let entry: Option<&str> [in_str_opt]; err error_out: Option>; } in { let builder = builder.ok_or(InvalidInput::NullPointer)?; let entry = entry.ok_or(InvalidInput::NullPointer)?; let mut b = builder.0.lock().expect("Poisoned lock"); match entry_type { ARTI_RPC_BUILDER_ENTRY_LITERAL_CONNECT_POINT => b.prepend_literal_entry(entry.to_string()), ARTI_RPC_BUILDER_ENTRY_EXPANDABLE_PATH => b.prepend_path(entry.into()), ARTI_RPC_BUILDER_ENTRY_LITERAL_PATH => b.prepend_literal_path(entry.into()), _ => return Err(InvalidInput::InvalidConstValue.into()), } } ) } /// Use `builder` to open a new RPC connection to Arti. /// /// On success, return `ARTI_RPC_STATUS_SUCCESS`, /// and set `conn_out` to a new ArtiRpcConn. /// Otherwise return some other status code, set *conn_out to NULL, and set /// `*error_out` (if provided) to a newly allocated error object. /// /// # Ownership /// /// The caller is responsible for making sure that `*rpc_conn_out` and `*error_out`, /// if set, are eventually freed. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_conn_builder_connect( builder: *const ArtiRpcConnBuilder, rpc_conn_out: *mut *mut ArtiRpcConn, error_out: *mut *mut ArtiRpcError, ) -> ArtiRpcStatus { ffi_body_with_err!( { let builder: Option<&ArtiRpcConnBuilder> [in_ptr_opt]; let rpc_conn_out: Option> [out_ptr_opt]; err error_out: Option>; } in { let builder = builder.ok_or(InvalidInput::NullPointer)?; let b = builder.0.lock().expect("Poisoned lock"); let conn = b.connect()?; rpc_conn_out.write_boxed_value_if_ptr_set(conn); } ) } /// Given a pointer to an RPC connection, return the object ID for its negotiated session. /// /// (The session was negotiated as part of establishing the connection. /// Its object ID is necessary to invoke most other functionality on Arti.) /// /// The caller should be prepared for a possible NULL return, in case somehow /// no session was negotiated. /// /// # Ownership /// /// The resulting string is a reference to part of the `ArtiRpcConn`. /// It lives for no longer than the underlying `ArtiRpcConn` object. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_conn_get_session_id( rpc_conn: *const ArtiRpcConn, ) -> *const c_char { ffi_body_raw! { { let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt]; } in { rpc_conn.and_then(crate::RpcConn::session) .map(|s| s.as_ptr()) .unwrap_or(std::ptr::null()) // Safety: returned pointer is null, or semantically borrowed from `rpc_conn`. // It is only null if `rpc_conn` was null or its session was null. // The caller is not allowed to modify it. } } } /// Run an RPC request over `rpc_conn` and wait for a successful response. /// /// The message `msg` should be a valid RPC request in JSON format. /// If you omit its `id` field, one will be generated: this is typically the best way to use this function. /// /// On success, return `ARTI_RPC_STATUS_SUCCESS` and set `*response_out` to a newly allocated string /// containing the JSON response to your request (including `id` and `response` fields). /// /// Otherwise return some other status code, set `*response_out` to NULL, /// and set `*error_out` (if provided) to a newly allocated error object. /// /// (If response_out is set to NULL, then any successful response will be ignored.) /// /// # Ownership /// /// The caller is responsible for making sure that `*error_out`, if set, is eventually freed. /// /// The caller is responsible for making sure that `*response_out`, if set, is eventually freed. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_conn_execute( rpc_conn: *const ArtiRpcConn, msg: *const c_char, response_out: *mut *mut ArtiRpcStr, error_out: *mut *mut ArtiRpcError, ) -> ArtiRpcStatus { ffi_body_with_err!( { let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt]; let msg: Option<&str> [in_str_opt]; let response_out: Option> [out_ptr_opt]; err error_out: Option>; } in { let rpc_conn = rpc_conn.ok_or(InvalidInput::NullPointer)?; let msg = msg.ok_or(InvalidInput::NullPointer)?; let success = rpc_conn.execute(msg)??; response_out.write_boxed_value_if_ptr_set(Utf8CString::from(success)); } ) } /// Send an RPC request over `rpc_conn`, and return a handle that can wait for a successful response. /// /// The message `msg` should be a valid RPC request in JSON format. /// If you omit its `id` field, one will be generated: this is typically the best way to use this function. /// /// On success, return `ARTI_RPC_STATUS_SUCCESS` and set `*handle_out` to a newly allocated `ArtiRpcHandle`. /// /// Otherwise return some other status code, set `*handle_out` to NULL, /// and set `*error_out` (if provided) to a newly allocated error object. /// /// (If `handle_out` is set to NULL, the request will not be sent, and an error will be returned.) /// /// # Ownership /// /// The caller is responsible for making sure that `*error_out`, if set, is eventually freed. /// /// The caller is responsible for making sure that `*handle_out`, if set, is eventually freed. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_conn_execute_with_handle( rpc_conn: *const ArtiRpcConn, msg: *const c_char, handle_out: *mut *mut ArtiRpcHandle, error_out: *mut *mut ArtiRpcError, ) -> ArtiRpcStatus { ffi_body_with_err!( { let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt]; let msg: Option<&str> [in_str_opt]; let handle_out: Option> [out_ptr_opt]; err error_out: Option>; } in { let rpc_conn = rpc_conn.ok_or(InvalidInput::NullPointer)?; let msg = msg.ok_or(InvalidInput::NullPointer)?; let handle_out = handle_out.ok_or(InvalidInput::NullPointer)?; let handle = rpc_conn.execute_with_handle(msg)?; handle_out.write_value_boxed(handle); } ) } /// Attempt to cancel the request on `rpc_conn` with the provided `handle`. /// /// Note that cancellation _will_ fail if the handle has already been cancelled, /// or has already succeeded or failed. /// /// On success, return `ARTI_RPC_STATUS_SUCCESS`. /// /// Otherwise return some other status code, /// and set `*error_out` (if provided) to a newly allocated error object. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_conn_cancel_handle( rpc_conn: *const ArtiRpcConn, handle: *const ArtiRpcHandle, error_out: *mut *mut ArtiRpcError, ) -> ArtiRpcStatus { ffi_body_with_err!( { let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt]; let handle: Option<&ArtiRpcHandle> [in_ptr_opt]; err error_out: Option>; } in { let rpc_conn = rpc_conn.ok_or(InvalidInput::NullPointer)?; let handle = handle.ok_or(InvalidInput::NullPointer)?; let id = handle.id(); rpc_conn.cancel(id)?; } ) } /// A constant indicating that a message is a final result. /// /// After a result has been received, a handle will not return any more responses, /// and should be freed. pub const ARTI_RPC_RESPONSE_TYPE_RESULT: ArtiRpcResponseType = 1; /// A constant indicating that a message is a non-final update. /// /// After an update has been received, the handle may return additional responses. pub const ARTI_RPC_RESPONSE_TYPE_UPDATE: ArtiRpcResponseType = 2; /// A constant indicating that a message is a final error. /// /// After an error has been received, a handle will not return any more responses, /// and should be freed. pub const ARTI_RPC_RESPONSE_TYPE_ERROR: ArtiRpcResponseType = 3; impl AnyResponse { /// Return an appropriate `ARTI_RPC_RESPONSE_TYPE_*` for this response. fn response_type(&self) -> ArtiRpcResponseType { match self { Self::Success(_) => ARTI_RPC_RESPONSE_TYPE_RESULT, Self::Update(_) => ARTI_RPC_RESPONSE_TYPE_UPDATE, Self::Error(_) => ARTI_RPC_RESPONSE_TYPE_ERROR, } } } /// Wait until some response arrives on an arti_rpc_handle, or until an error occurs. /// /// On success, return `ARTI_RPC_STATUS_SUCCESS`; set `*response_out`, if present, to a /// newly allocated string, and set `*response_type_out`, to the type of the response. /// (The type will be `ARTI_RPC_RESPONSE_TYPE_RESULT` if the response is a final result, /// or `ARTI_RPC_RESPONSE_TYPE_ERROR` if the response is a final error, /// or `ARTI_RPC_RESPONSE_TYPE_UPDATE` if the response is a non-final update.) /// /// Otherwise return some other status code, set `*response_out` to NULL, /// set `*response_type_out` to zero, /// and set `*error_out` (if provided) to a newly allocated error object. /// /// Note that receiving an error reply from Arti is _not_ treated as an error in this function. /// That is to say, if Arti sends back an error, this function will return `ARTI_SUCCESS`, /// and deliver the error from Arti in `*response_out`, setting `*response_type_out` to /// `ARTI_RPC_RESPONSE_TYPE_ERROR`. /// /// It is safe to call this function on the same handle from multiple threads at once. /// If you do, each response will be sent to exactly one thread. /// It is unspecified which thread will receive which response or which error. /// /// # Ownership /// /// The caller is responsible for making sure that `*error_out`, if set, is eventually freed. /// /// The caller is responsible for making sure that `*response_out`, if set, is eventually freed. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_handle_wait( handle: *const ArtiRpcHandle, response_out: *mut *mut ArtiRpcStr, response_type_out: *mut ArtiRpcResponseType, error_out: *mut *mut ArtiRpcError, ) -> ArtiRpcStatus { ffi_body_with_err! { { let handle: Option<&ArtiRpcHandle> [in_ptr_opt]; let response_out: Option> [out_ptr_opt]; let response_type_out: Option> [out_val_opt]; err error_out: Option>; } in { let handle = handle.ok_or(InvalidInput::NullPointer)?; let response = handle.wait_with_updates()?; let rtype = response.response_type(); response_type_out.write_value_if_ptr_set(rtype); response_out.write_boxed_value_if_ptr_set(response.into_string()); } } } /// Release storage held by an `ArtiRpcHandle`. /// /// NOTE: This does not cancel the underlying request if it is still running. /// To cancel a request, use `arti_rpc_conn_cancel_handle`. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_handle_free(handle: *mut ArtiRpcHandle) { ffi_body_raw!( { let handle: Option> [in_ptr_consume_opt]; } in { drop(handle); // Safety: Return value is (); trivially safe. () } ); } /// Free a string returned by the Arti RPC API. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_str_free(string: *mut ArtiRpcStr) { ffi_body_raw!( { let string: Option> [in_ptr_consume_opt]; } in { drop(string); // Safety: Return value is (); trivially safe. () } ); } /// Return a const pointer to the underlying nul-terminated string from an `ArtiRpcStr`. /// /// The resulting string is guaranteed to be valid UTF-8. /// /// (Returns NULL if the input is NULL.) /// /// # Correctness requirements /// /// The resulting string pointer is valid only for as long as the input `string` is not freed. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_str_get(string: *const ArtiRpcStr) -> *const c_char { ffi_body_raw!( { let string: Option<&ArtiRpcStr> [in_ptr_opt]; } in { // Safety: returned pointer is null, or semantically borrowed from `string`. // It is only null if `string` was null. // The caller is not allowed to modify it. match string { Some(s) => s.as_ptr(), None => std::ptr::null(), } } ) } /// Close and free an open Arti RPC connection. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_conn_free(rpc_conn: *mut ArtiRpcConn) { ffi_body_raw!( { let rpc_conn: Option> [in_ptr_consume_opt]; } in { drop(rpc_conn); // Safety: Return value is (); trivially safe. () } ); } /// Try to open an anonymized data stream over Arti. /// /// Use the proxy information associated with `rpc_conn` to make the stream, /// and store the resulting fd (or `SOCKET` on Windows) into `*socket_out`. /// /// The stream will target the address `hostname`:`port`. /// /// If `on_object` is provided, it is an `ObjectId` for client-like object /// (such as a Session or a Client) /// that should be used to make the stream. /// /// The resulting stream will be configured /// not to share a circuit with any other stream /// having a different `isolation`. /// (If your application doesn't care about isolating its streams from one another, /// it is acceptable to leave `isolation` as an empty string.) /// /// If `stream_id_out` is provided, /// the resulting stream will have an identifier within the RPC system, /// so that you can run other RPC commands on it. /// /// On success, return `ARTI_RPC_STATUS_SUCCESS`. /// Otherwise return some other status code, set `*socket_out` to -1 /// (or `INVALID_SOCKET` on Windows), /// and set `*error_out` (if provided) to a newly allocated error object. /// /// # Caveats /// /// When possible, use a hostname rather than an IP address. /// If you *must* use an IP address, make sure that you have not gotten it /// by a non-anonymous DNS lookup. /// (Calling `gethostname()` or `getaddrinfo()` directly /// would lose anonymity: they inform the user's DNS server, /// and possibly many other parties, about the target address /// you are trying to visit.) /// /// The resulting socket will actually be a TCP connection to Arti, /// not directly to your destination. /// Therefore, passing it to functions like `getpeername()` /// may give unexpected results. /// /// If `stream_id_out` is provided, /// the caller is responsible for releasing the ObjectId; /// Arti will not deallocate it even when the stream is closed. /// /// # Ownership /// /// The caller is responsible for making sure that /// `*stream_id_out` and `*error_out`, if set, /// are eventually freed. /// /// The caller is responsible for making sure that `*socket_out`, if set, /// is eventually closed. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_conn_open_stream( rpc_conn: *const ArtiRpcConn, hostname: *const c_char, port: c_int, on_object: *const c_char, isolation: *const c_char, socket_out: *mut ArtiRpcRawSocket, stream_id_out: *mut *mut ArtiRpcStr, error_out: *mut *mut ArtiRpcError, ) -> ArtiRpcStatus { ffi_body_with_err! { { let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt]; let on_object: Option<&str> [in_str_opt]; let hostname: Option<&str> [in_str_opt]; let isolation: Option<&str> [in_str_opt]; let socket_out: Option> [out_socket_owned_opt]; let stream_id_out: Option> [out_ptr_opt]; err error_out: Option>; } in { let rpc_conn = rpc_conn.ok_or(InvalidInput::NullPointer)?; let hostname = hostname.ok_or(InvalidInput::NullPointer)?; let socket_out = socket_out.ok_or(InvalidInput::NullPointer)?; let isolation = isolation.ok_or(InvalidInput::NullPointer)?; let port: u16 = port.try_into().map_err(|_| InvalidInput::BadPort)?; if port == 0 { return Err(InvalidInput::BadPort.into()); } let on_object = on_object.map(|o| ObjectId::try_from(o.to_owned())) .transpose() .expect("C string somehow contained NUL."); let stream = match stream_id_out { Some(stream_id_out) => { let (stream_id, stream) = rpc_conn.open_stream_as_object( on_object.as_ref(), (hostname, port), isolation)?; stream_id_out.write_value_boxed(stream_id.into()); stream } None => { rpc_conn.open_stream(on_object.as_ref(), (hostname, port), isolation)? } }; // We call this last so that the stream will definitely be converted to an fd, or // dropped. socket_out.write_socket(stream); } } }