diff --git a/README.md b/README.md index eb9074b..af92ad5 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,9 @@ configuration options: * `pinentry`: The [pinentry](https://www.gnupg.org/related_software/pinentry/index.html) executable to use. Defaults to `pinentry`. +* `extra_headers`: Additional HTTP headers (in `[['key1', 'value1'], ['key2', + 'value2']]` form) to send in API calls. Useful if the server is behind a + reverse proxy that supports header-based authentication, e.g. Cloudflare. ### Profiles diff --git a/src/actions.rs b/src/actions.rs index 79d304d..39b8588 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -353,6 +353,7 @@ fn api_client() -> Result<(crate::api::Client, crate::config::Config)> { &config.identity_url(), &config.ui_url(), config.client_cert_path(), + config.extra_headers(), ); Ok((client, config)) } @@ -365,6 +366,7 @@ async fn api_client_async( &config.identity_url(), &config.ui_url(), config.client_cert_path(), + config.extra_headers(), ); Ok((client, config)) } diff --git a/src/api.rs b/src/api.rs index a817fb2..5aea48f 100644 --- a/src/api.rs +++ b/src/api.rs @@ -822,6 +822,7 @@ pub struct Client { identity_url: String, ui_url: String, client_cert_path: Option, + extra_headers: Vec<(String, String)>, } impl Client { @@ -830,6 +831,7 @@ impl Client { identity_url: &str, ui_url: &str, client_cert_path: Option<&std::path::Path>, + extra_headers: Vec<(String, String)>, ) -> Self { Self { base_url: base_url.to_string(), @@ -837,11 +839,26 @@ impl Client { ui_url: ui_url.to_string(), client_cert_path: client_cert_path .map(std::path::Path::to_path_buf), + extra_headers, } } async fn reqwest_client(&self) -> Result { let mut default_headers = axum::http::HeaderMap::new(); + for (key, val) in &self.extra_headers { + default_headers.insert( + axum::http::HeaderName::try_from(key).map_err(|_| { + Error::InvalidHttpHeaderName { + header_name: key.clone(), + } + })?, + axum::http::HeaderValue::try_from(val).map_err(|_| { + Error::InvalidHttpHeaderValue { + header_name: key.clone(), + } + })?, + ); + } default_headers.insert( "Bitwarden-Client-Name", axum::http::HeaderValue::from_static(BITWARDEN_CLIENT), diff --git a/src/config.rs b/src/config.rs index 248c603..6a5f953 100644 --- a/src/config.rs +++ b/src/config.rs @@ -22,6 +22,7 @@ pub struct Config { // backcompat, no longer generated in new configs #[serde(skip_serializing)] pub device_id: Option, + pub extra_headers: Option>, } impl Default for Config { @@ -38,6 +39,7 @@ impl Default for Config { pinentry: default_pinentry(), client_cert_path: None, device_id: None, + extra_headers: None, } } } @@ -214,6 +216,10 @@ impl Config { .clone() .unwrap_or_else(|| "default".to_string()) } + + pub fn extra_headers(&self) -> Vec<(String, String)> { + self.extra_headers.as_deref().unwrap_or_default().to_vec() + } } pub async fn device_id(config: &Config) -> Result { diff --git a/src/error.rs b/src/error.rs index b7789a9..0974334 100644 --- a/src/error.rs +++ b/src/error.rs @@ -83,13 +83,23 @@ pub enum Error { editor: std::ffi::OsString, }, + #[error("invalid http header name {header_name:?}")] + InvalidHttpHeaderName { header_name: String }, + + #[error("invalid http header value for header {header_name:?}")] + InvalidHttpHeaderValue { + /// `header_name` is the header **name** rather than the value (even + /// though the name might be valid), as the value might contain secrets. + header_name: String, + }, + #[error("invalid mac")] InvalidMac, #[error("invalid two factor provider type: {ty}")] InvalidTwoFactorProvider { ty: String }, - #[error("failed to parse JSON")] + #[error("failed to parse JSON I guess")] Json { source: serde_path_to_error::Error, },