diff options
| author | Emīls <emils@mullvad.net> | 2024-01-31 09:56:51 +0100 |
|---|---|---|
| committer | Bug Magnet <marco.nikic@mullvad.net> | 2024-02-14 17:37:42 +0100 |
| commit | 8136c8476a059867a338d3a6bebd68c9ba85aa0b (patch) | |
| tree | b8fa13aa2ecf089f79286a93146bf060c37b608f /mullvad-api/src | |
| parent | e2c4fb47f4352cf1eb1200fbb29b9539c86b73e3 (diff) | |
| download | mullvadvpn-8136c8476a059867a338d3a6bebd68c9ba85aa0b.tar.xz mullvadvpn-8136c8476a059867a338d3a6bebd68c9ba85aa0b.zip | |
Fix talpid-time for iOS, add FFI for mullvad-api
Diffstat (limited to 'mullvad-api/src')
| -rw-r--r-- | mullvad-api/src/address_cache.rs | 5 | ||||
| -rw-r--r-- | mullvad-api/src/ffi/device.rs | 104 | ||||
| -rw-r--r-- | mullvad-api/src/ffi/error.rs | 62 | ||||
| -rw-r--r-- | mullvad-api/src/ffi/mod.rs | 428 | ||||
| -rw-r--r-- | mullvad-api/src/lib.rs | 56 | ||||
| -rw-r--r-- | mullvad-api/src/rest.rs | 15 |
6 files changed, 666 insertions, 4 deletions
diff --git a/mullvad-api/src/address_cache.rs b/mullvad-api/src/address_cache.rs index ea93d96e26..f164e15f09 100644 --- a/mullvad-api/src/address_cache.rs +++ b/mullvad-api/src/address_cache.rs @@ -34,6 +34,11 @@ impl AddressCache { Self::new_inner(API.address(), write_path) } + pub fn with_static_addr(address: SocketAddr) -> Self { + Self::new_inner(address, None) + .expect("Failed to construct an address cache from a static address") + } + /// Initialize cache using `read_path`, and write changes to `write_path`. pub async fn from_file(read_path: &Path, write_path: Option<Box<Path>>) -> Result<Self, Error> { log::debug!("Loading API addresses from {}", read_path.display()); diff --git a/mullvad-api/src/ffi/device.rs b/mullvad-api/src/ffi/device.rs new file mode 100644 index 0000000000..82ba5f59cc --- /dev/null +++ b/mullvad-api/src/ffi/device.rs @@ -0,0 +1,104 @@ +use mullvad_types::device::Device; +use std::{ffi::CString, ptr}; + +#[repr(C)] +pub struct MullvadApiDeviceIterator { + ptr: *mut DeviceIterator, +} + +impl MullvadApiDeviceIterator { + pub fn new(iter: impl IntoIterator<Item = Device> + 'static) -> Self { + let iter = Box::new(DeviceIterator::from(iter)); + + Self { + ptr: Box::into_raw(iter), + } + } + + fn is_done(&self) -> bool { + self.ptr.is_null() + } + + unsafe fn as_iter(&mut self) -> &mut Box<dyn Iterator<Item = Device>> { + let wrapper = unsafe { &mut *self.ptr }; + &mut wrapper.iter + } + + fn drop(mut self) { + if self.ptr.is_null() { + return; + } + + let _ = unsafe { Box::from_raw(self.ptr) }; + self.ptr = ptr::null_mut(); + } +} + +#[repr(C)] +pub struct MullvadApiDevice { + name_ptr: *const libc::c_char, + id: [u8; 16], +} + +impl From<Device> for MullvadApiDevice { + fn from(dev: Device) -> Self { + let name = CString::new(dev.name).expect("Null bytes in name from API response"); + let name_ptr = name.into_raw(); + let id = *uuid::Uuid::parse_str(&dev.id) + .expect("Failed to parse UUID") + .as_bytes(); + + Self { name_ptr, id } + } +} + +impl MullvadApiDevice { + fn drop(self) { + let _ = unsafe { CString::from_raw(self.name_ptr as *mut _) }; + } +} + +struct DeviceIterator { + iter: Box<dyn Iterator<Item = Device>>, +} + +impl<T> From<T> for DeviceIterator +where + T: IntoIterator<Item = Device> + 'static, +{ + fn from(i: T) -> Self { + let iter: Box<dyn Iterator<Item = Device>> = Box::new(i.into_iter()); + Self { iter } + } +} + +#[no_mangle] +pub extern "C" fn mullvad_api_device_iter_next( + mut iter: MullvadApiDeviceIterator, + device_ptr: *mut MullvadApiDevice, +) -> bool { + if iter.is_done() { + return false; + } + + // SAFETY: Asuming self.ptr is still valid since iter.is_done() returned false; + let iter = unsafe { iter.as_iter() }; + let Some(device) = iter.next() else { + return false; + }; + + // SAFETY: Assuming device pointer is valid + unsafe { ptr::write(device_ptr, device.into()) } + + return true; +} + +#[no_mangle] +pub extern "C" fn mullvad_api_device_iter_drop(iter: MullvadApiDeviceIterator) { + iter.drop() +} + +#[no_mangle] +pub extern "C" fn mullvad_api_device_drop(device: MullvadApiDevice) { + device.drop() +} diff --git a/mullvad-api/src/ffi/error.rs b/mullvad-api/src/ffi/error.rs new file mode 100644 index 0000000000..8f3095fee0 --- /dev/null +++ b/mullvad-api/src/ffi/error.rs @@ -0,0 +1,62 @@ +use crate::rest; +use std::ffi::CString; + +#[derive(Debug, PartialEq)] +#[repr(C)] +pub enum MullvadApiErrorKind { + NoError = 0, + StringParsing = -1, + SocketAddressParsing = -2, + AsyncRuntimeInitialization = -3, + BadResponse = -4, +} + +/// MullvadApiErrorKind contains a description and an error kind. If the error kind is +/// `MullvadApiErrorKind` is NoError, the pointer will be nil. +#[repr(C)] +pub struct MullvadApiError { + description: *mut libc::c_char, + kind: MullvadApiErrorKind, +} + +impl MullvadApiError { + pub fn new(kind: MullvadApiErrorKind, error: &dyn std::error::Error) -> Self { + let description = CString::new(format!("{error:?}")).unwrap_or_default(); + Self { + description: description.into_raw(), + kind, + } + } + + pub fn api_err(error: rest::Error) -> Self { + Self::new(MullvadApiErrorKind::BadResponse, &error) + } + + pub fn with_str(kind: MullvadApiErrorKind, description: &str) -> Self { + let description = CString::new(description).unwrap_or_default(); + Self { + description: description.into_raw(), + kind, + } + } + + pub fn ok() -> MullvadApiError { + Self { + description: std::ptr::null_mut(), + kind: MullvadApiErrorKind::NoError, + } + } + + pub fn drop(self) { + if self.description.is_null() { + return; + } + + let _ = unsafe { CString::from_raw(self.description) }; + } +} + +#[no_mangle] +pub extern "C" fn mullvad_api_error_drop(error: MullvadApiError) { + error.drop() +} diff --git a/mullvad-api/src/ffi/mod.rs b/mullvad-api/src/ffi/mod.rs new file mode 100644 index 0000000000..ba839f36a5 --- /dev/null +++ b/mullvad-api/src/ffi/mod.rs @@ -0,0 +1,428 @@ +use std::{ + ffi::{CStr, CString}, + net::SocketAddr, + ptr, + sync::Arc, +}; + +use crate::{ + rest::{self, MullvadRestHandle}, + AccountsProxy, DevicesProxy, +}; + +mod device; +mod error; + +pub use error::{MullvadApiError, MullvadApiErrorKind}; + +#[repr(C)] +pub struct MullvadApiClient { + ptr: *const FfiClient, +} + +impl MullvadApiClient { + fn new(client: FfiClient) -> Self { + let arc = Arc::new(client); + let ptr = Arc::into_raw(arc); + Self { ptr } + } + + unsafe fn get_client(&self) -> Arc<FfiClient> { + // Incrementing before creating an Arc from a pointer. This way multiple threads can use + // it, and a single thread can decrement it. + unsafe { Arc::increment_strong_count(self.ptr) }; + + unsafe { Arc::from_raw(self.ptr) } + } + + fn drop(self) { + if self.ptr.is_null() { + return; + } + + let _ = unsafe { Arc::from_raw(self.ptr) }; + } +} + +/// A Mullvad API client that can be used via a C FFI. +struct FfiClient { + tokio_runtime: tokio::runtime::Runtime, + api_runtime: crate::Runtime, + api_hostname: String, +} + +impl FfiClient { + unsafe fn new( + api_address_ptr: *const libc::c_char, + hostname: *const libc::c_char, + ) -> Result<Self, MullvadApiError> { + // SAFETY: addr_str must be a valid pointer to a null-terminated string. + let addr_str = unsafe { string_from_raw_ptr(api_address_ptr)? }; + // SAFETY: api_hostname must be a valid pointer to a null-terminated string. + let api_hostname = unsafe { string_from_raw_ptr(hostname)? }; + + let api_address: SocketAddr = addr_str.parse().map_err(|_| { + MullvadApiError::with_str( + MullvadApiErrorKind::SocketAddressParsing, + "Failed to parse API socket address", + ) + })?; + + let mut runtime_builder = tokio::runtime::Builder::new_multi_thread(); + + runtime_builder.worker_threads(2).enable_all(); + let tokio_runtime = runtime_builder.build().map_err(|err| { + MullvadApiError::new(MullvadApiErrorKind::AsyncRuntimeInitialization, &err) + })?; + + // It is imperative that the REST runtime is created within an async context, otherwise + // ApiAvailability panics. + let api_runtime = tokio_runtime.block_on(async { + crate::Runtime::with_static_addr(tokio_runtime.handle().clone(), api_address) + }); + + let context = FfiClient { + tokio_runtime, + api_runtime, + api_hostname, + }; + + Ok(context) + } + + unsafe fn add_device( + self: Arc<Self>, + account_str_ptr: *const libc::c_char, + public_key_ptr: *const u8, + ) -> Result<device::MullvadApiDevice, MullvadApiError> { + // SAFETY: account_str_ptr must be a valid pointer to a null-terminated string. + let account = unsafe { string_from_raw_ptr(account_str_ptr)? }; + + // SAFETY: assuming public_key_ptr is valid for 32 bytes + let public_key_bytes: [u8; 32] = unsafe { std::ptr::read(public_key_ptr as *const _) }; + let public_key = public_key_bytes.into(); + + let runtime = self.tokio_handle(); + + let device_proxy = self.device_proxy(); + + let device = runtime + .block_on(async move { + let (device, _) = device_proxy.create(account, public_key).await?; + Ok(device) + }) + .map_err(MullvadApiError::api_err)?; + + Ok(device.into()) + } + + unsafe fn create_account(self: Arc<Self>) -> Result<String, MullvadApiError> { + let accounts_proxy = self.accounts_proxy(); + + self.tokio_handle() + .block_on(async move { + let new_account = accounts_proxy.create_account().await?; + Ok(new_account) + }) + .map_err(MullvadApiError::api_err) + } + + unsafe fn get_expiry( + self: Arc<Self>, + account_str_ptr: *const libc::c_char, + ) -> Result<i64, MullvadApiError> { + // SAFETY: account_str_ptr must be a valid pointer to a null-terminated string. + let account = unsafe { string_from_raw_ptr(account_str_ptr)? }; + + let account_proxy = self.accounts_proxy(); + self.tokio_handle() + .block_on(async move { + let expiry_timestamp = account_proxy.get_data(account).await?.expiry.timestamp(); + Ok(expiry_timestamp) + }) + .map_err(MullvadApiError::api_err) + } + + unsafe fn remove_all_devices( + self: Arc<Self>, + account_str_ptr: *const libc::c_char, + ) -> Result<(), MullvadApiError> { + // SAFETY: account_str_ptr must be a valid pointer to a null-terminated string. + let account = unsafe { string_from_raw_ptr(account_str_ptr)? }; + + let runtime = self.tokio_handle(); + let device_proxy = self.device_proxy(); + runtime + .block_on(async move { + let devices = device_proxy.list(account.clone()).await?; + for device in devices { + device_proxy.remove(account.clone(), device.id).await?; + } + Result::<_, rest::Error>::Ok(()) + }) + .map_err(MullvadApiError::api_err) + } + + unsafe fn list_devices( + self: Arc<Self>, + account_str_ptr: *const libc::c_char, + ) -> Result<device::MullvadApiDeviceIterator, MullvadApiError> { + // SAFETY: account_str_ptr must be a valid pointer to a null-terminated string. + let account = unsafe { string_from_raw_ptr(account_str_ptr)? }; + + let runtime = self.tokio_handle(); + let device_proxy = self.device_proxy(); + + let devices = runtime + .block_on(device_proxy.list(account)) + .map_err(MullvadApiError::api_err)?; + + Ok(device::MullvadApiDeviceIterator::new(devices)) + } + + unsafe fn delete_account( + self: Arc<Self>, + account_str_ptr: *const libc::c_char, + ) -> Result<(), MullvadApiError> { + // SAFETY: account_str_ptr must be a valid pointer to a null-terminated string. + let account = unsafe { string_from_raw_ptr(account_str_ptr)? }; + + let runtime = self.tokio_handle(); + let accounts_proxy = self.accounts_proxy(); + + runtime + .block_on(accounts_proxy.delete_account(account)) + .map_err(MullvadApiError::api_err) + } + + fn rest_handle(&self) -> MullvadRestHandle { + self.tokio_runtime.block_on( + self.api_runtime + .static_mullvad_rest_handle(self.api_hostname.clone()), + ) + } + + fn device_proxy(&self) -> DevicesProxy { + crate::DevicesProxy::new(self.rest_handle()) + } + + fn accounts_proxy(&self) -> AccountsProxy { + crate::AccountsProxy::new(self.rest_handle()) + } + + fn tokio_handle(&self) -> tokio::runtime::Handle { + self.tokio_runtime.handle().clone() + } +} + +/// Initializes a Mullvad API client. +/// +/// #Arguments +/// * `client_ptr`: Must be a pointer to that is valid for the length of a `MullvadApiClient` +/// struct. +/// +/// * `api_address`: pointer to nul-terminated UTF-8 string containing a socket address +/// representation +/// ("143.32.4.32:9090"), the port is mandatory. +/// +/// * `hostname`: pointer to a null-terminated UTF-8 string representing the hostname that will be +/// used for TLS validation. +#[no_mangle] +pub extern "C" fn mullvad_api_client_initialize( + client_ptr: *mut MullvadApiClient, + api_address_ptr: *const libc::c_char, + hostname: *const libc::c_char, +) -> MullvadApiError { + match unsafe { FfiClient::new(api_address_ptr, hostname) } { + Ok(client) => { + unsafe { + std::ptr::write(client_ptr, MullvadApiClient::new(client)); + }; + MullvadApiError::ok() + } + Err(err) => err, + } +} + +/// Removes all devices from a given account +/// +/// #Arguments +/// * `client_ptr`: Must be a valid, initialized instance of `MullvadApiClient` +/// +/// * `account_str_ptr`: pointer to nul-terminated UTF-8 string containing the account number of the +/// account that will have all of it's devices removed. +#[no_mangle] +pub extern "C" fn mullvad_api_remove_all_devices( + client_ptr: MullvadApiClient, + account_ptr: *const libc::c_char, +) -> MullvadApiError { + let client = unsafe { client_ptr.get_client() }; + match unsafe { client.remove_all_devices(account_ptr) } { + Ok(_) => MullvadApiError::ok(), + Err(err) => err, + } +} + +/// Removes all devices from a given account +/// +/// #Arguments +/// * `client_ptr`: Must be a valid, initialized instance of `MullvadApiClient` +/// +/// * `account_str_ptr`: pointer to nul-terminated UTF-8 string containing the account number of the +/// account that will have all of it's devices removed. +/// +/// * `expiry_unix_timestamp`: a pointer to a signed 64 bit integer. If this function returns no +/// error, the expiry timestamp will be written to this pointer. +#[no_mangle] +pub extern "C" fn mullvad_api_get_expiry( + client_ptr: MullvadApiClient, + account_str_ptr: *const libc::c_char, + expiry_unix_timestamp: *mut i64, +) -> MullvadApiError { + let client = unsafe { client_ptr.get_client() }; + match unsafe { client.get_expiry(account_str_ptr) } { + Ok(expiry) => { + unsafe { ptr::write(expiry_unix_timestamp, expiry) }; + MullvadApiError::ok() + } + Err(err) => err, + } +} + +/// Gets a list of all devices associated with the specified account from the API. +/// +/// #Arguments +/// * `client_ptr`: Must be a valid, initialized instance of `MullvadApiClient` +/// +/// * `account_str_ptr`: pointer to nul-terminated UTF-8 string containing the account number of the +/// account that will have all of it's devices removed. +/// +/// * `device_iter_ptr`: a pointer to a `device::MullvadApiDeviceIterator`. If this function +/// doesn't return an error, the pointer will be initialized with a valid instance of +/// `device::MullvadApiDeviceIterator`, which can be used to iterate through the devices. +#[no_mangle] +pub extern "C" fn mullvad_api_list_devices( + client_ptr: MullvadApiClient, + account_str_ptr: *const libc::c_char, + device_iter_ptr: *mut device::MullvadApiDeviceIterator, +) -> MullvadApiError { + let client = unsafe { client_ptr.get_client() }; + match unsafe { client.list_devices(account_str_ptr) } { + Ok(iter) => { + unsafe { ptr::write(device_iter_ptr, iter) }; + MullvadApiError::ok() + } + Err(err) => err, + } +} + +/// Adds a device to the specified account with the specified public key. Note that the device +/// name, associated addresess and UUID are not returned. +/// +/// #Arguments +/// * `client_ptr`: Must be a valid, initialized instance of `MullvadApiClient` +/// +/// * `account_str_ptr`: pointer to nul-terminated UTF-8 string containing the account number of the +/// account that will have a device added to ita device added to it. +/// +/// * `public_key_ptr`: a pointer to 32 bytes of a WireGuard public key that will be uploaded. +/// +/// * `new_device_ptr`: a pointer to enough memory to allocate a `MullvadApiDevice`. If this +/// function doesn't return an error, it will be initialized. +#[no_mangle] +pub extern "C" fn mullvad_api_add_device( + client_ptr: MullvadApiClient, + account_str_ptr: *const libc::c_char, + public_key_ptr: *const u8, + new_device_ptr: *mut device::MullvadApiDevice, +) -> MullvadApiError { + // SAFETY: Assuming MullvadApiClient is initialized + let client = unsafe { client_ptr.get_client() }; + // SAFETY: Asuming `new_device_ptr` is valid. + match unsafe { client.add_device(account_str_ptr, public_key_ptr) } { + Ok(device) => { + // SAFETY: Asuming `new_device_ptr` is valid. + // SAFETY: Asuming `new_device_ptr` is valid. + unsafe { ptr::write(new_device_ptr, device) }; + MullvadApiError::ok() + } + Err(err) => err, + } +} + +/// Creates a new account. +/// +/// #Arguments +/// * `client_ptr`: Must be a valid, initialized instance of `MullvadApiClient` +/// +/// * `account_str_ptr`: If a new account is created successfully, a pointer to an allocated C +/// string containing the new +/// account number will be written to this pointer. It must be freed via +/// `mullvad_api_cstring_drop`. +#[no_mangle] +pub extern "C" fn mullvad_api_create_account( + client_ptr: MullvadApiClient, + account_str_ptr: *mut *const libc::c_char, +) -> MullvadApiError { + let client = unsafe { client_ptr.get_client() }; + match unsafe { client.create_account() } { + Ok(new_account) => { + let Ok(account) = CString::new(new_account) else { + return MullvadApiError::with_str( + MullvadApiErrorKind::BadResponse, + "Account number string c ontained null bytes", + ); + }; + + unsafe { ptr::write(account_str_ptr, account.into_raw()) }; + MullvadApiError::ok() + } + Err(err) => err, + } +} + +/// Deletes the specified account. +/// +/// #Arguments +/// * `client_ptr`: Must be a valid, initialized instance of `MullvadApiClient` +/// +/// * `account_str_ptr`: A null-terminated string representing the account to be deleted. +#[no_mangle] +pub extern "C" fn mullvad_api_delete_account( + client_ptr: MullvadApiClient, + account_str_ptr: *const libc::c_char, +) -> MullvadApiError { + let client = unsafe { client_ptr.get_client() }; + match unsafe { client.delete_account(account_str_ptr) } { + Ok(_) => MullvadApiError::ok(), + Err(err) => err, + } +} + +#[no_mangle] +pub extern "C" fn mullvad_api_client_drop(client: MullvadApiClient) { + client.drop() +} + +/// Deallocates a CString returned by the Mullvad API client. +#[no_mangle] +pub extern "C" fn mullvad_api_cstring_drop(cstr_ptr: *mut libc::c_char) { + let _ = unsafe { CString::from_raw(cstr_ptr) }; +} + +/// The return value is only valid for the lifetime of the `ptr` that's passed in +/// +/// SAFETY: `ptr` must be valid for `size` bytes +unsafe fn string_from_raw_ptr(ptr: *const libc::c_char) -> Result<String, MullvadApiError> { + let cstr = unsafe { CStr::from_ptr(ptr) }; + + Ok(cstr + .to_str() + .map_err(|_| { + MullvadApiError::with_str( + MullvadApiErrorKind::StringParsing, + "Failed to parse UTF-8 string", + ) + })? + .to_owned()) +} diff --git a/mullvad-api/src/lib.rs b/mullvad-api/src/lib.rs index 40ab7395cd..b211c20268 100644 --- a/mullvad-api/src/lib.rs +++ b/mullvad-api/src/lib.rs @@ -35,6 +35,10 @@ mod access; mod address_cache; pub mod device; mod relay_list; + +#[cfg(target_os = "ios")] +pub mod ffi; + pub use address_cache::AddressCache; pub use device::DevicesProxy; pub use hyper::StatusCode; @@ -307,6 +311,17 @@ impl Runtime { ) } + // TODO: gate for ios only + pub fn with_static_addr(handle: tokio::runtime::Handle, address: SocketAddr) -> Self { + Runtime { + handle, + address_cache: AddressCache::with_static_addr(address), + api_availability: ApiAvailability::new(availability::State::default()), + #[cfg(target_os = "android")] + socket_bypass_tx, + } + } + fn new_inner( handle: tokio::runtime::Handle, #[cfg(target_os = "android")] socket_bypass_tx: Option<mpsc::Sender<SocketBypassRequest>>, @@ -412,6 +427,27 @@ impl Runtime { ) } + /// This is only to be used in test code + pub async fn static_mullvad_rest_handle(&self, hostname: String) -> rest::MullvadRestHandle { + let service = self + .new_request_service( + Some(hostname.clone()), + futures::stream::repeat(ApiConnectionMode::Direct), + #[cfg(target_os = "android")] + self.socket_bypass_tx.clone(), + ) + .await; + let token_store = access::AccessTokenStore::new(service.clone()); + let factory = rest::RequestFactory::new(hostname, Some(token_store)); + + rest::MullvadRestHandle::new( + service, + factory, + self.address_cache.clone(), + self.availability_handle(), + ) + } + /// Returns a new request service handle pub async fn rest_handle(&self) -> rest::RequestServiceHandle { self.new_request_service( @@ -500,6 +536,26 @@ impl AccountsProxy { } } + #[cfg(target_os = "ios")] + pub fn delete_account( + &self, + account: AccountToken, + ) -> impl Future<Output = Result<(), rest::Error>> { + let service = self.handle.service.clone(); + let factory = self.handle.factory.clone(); + + async move { + let request = factory + .delete(&format!("{ACCOUNTS_URL_PREFIX}/accounts/me"))? + .account(account.clone())? + .header("Mullvad-Account-Number", &account)? + .expected_status(&[StatusCode::NO_CONTENT]); + + let _ = service.request(request).await?; + Ok(()) + } + } + #[cfg(target_os = "android")] pub fn init_play_purchase( &mut self, diff --git a/mullvad-api/src/rest.rs b/mullvad-api/src/rest.rs index f0838f918d..0560642bb0 100644 --- a/mullvad-api/src/rest.rs +++ b/mullvad-api/src/rest.rs @@ -19,6 +19,7 @@ use hyper::{ }; use mullvad_types::account::AccountToken; use std::{ + borrow::Cow, error::Error as StdError, str::FromStr, sync::{Arc, Weak}, @@ -440,15 +441,18 @@ struct NewErrorResponse { #[derive(Clone)] pub struct RequestFactory { - hostname: &'static str, + hostname: Cow<'static, str>, token_store: Option<AccessTokenStore>, default_timeout: Duration, } impl RequestFactory { - pub fn new(hostname: &'static str, token_store: Option<AccessTokenStore>) -> Self { + pub fn new( + hostname: impl Into<Cow<'static, str>>, + token_store: Option<AccessTokenStore>, + ) -> Self { Self { - hostname, + hostname: hostname.into(), token_store, default_timeout: DEFAULT_TIMEOUT, } @@ -523,7 +527,10 @@ impl RequestFactory { .uri(uri) .header(header::USER_AGENT, HeaderValue::from_static(USER_AGENT)) .header(header::ACCEPT, HeaderValue::from_static("application/json")) - .header(header::HOST, HeaderValue::from_static(self.hostname)); + .header( + header::HOST, + HeaderValue::from_str(&self.hostname).map_err(|_| Error::InvalidHeaderError)?, + ); let result = request.body(hyper::Body::empty())?; Ok(result) |
