Skip to content

Commit 49eee0a

Browse files
EddieHoustonoleonardolima
authored andcommitted
feat!: add dynamic authorization support
- add new `AuthProvider` public type to handle an `authorization` token. - add new `Option<AuthProvider>` as `authorization_provider` in `Config`, it holds the dynamic authentication token callback provider. - add new `Option<AuthProvider>` as `auth_provider` in `RawClient`, it holds the dynamic authentication token callback provider. - add new `authorization` private field in `Request`. - add new `pub fn with_auth()` to `RawClient` in order to set the authorization provider into the client, it should be used following a builder patter, the client defaults to no authentication otherwise. - add new `.with_auth()` method to `Request`, it sets the `authorization` token, if any. - update `pub fn negotiate_protocol_version()` in `RawClient` to return the `RawClient`, this way it SHOULD no be used following a builder pattern. - update `.call()` and `.batch_call()` to use the newly added `.with_auth()`, instead of updating request's authorization field directly. BREAKING CHANGE: all changes above are API breaking changes.
1 parent cdb89a2 commit 49eee0a

File tree

6 files changed

+433
-41
lines changed

6 files changed

+433
-41
lines changed

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,8 @@ openssl = ["dep:openssl"]
4848
use-rustls = ["rustls"]
4949
use-rustls-ring = ["rustls-ring"]
5050
use-openssl = ["openssl"]
51+
52+
# optional dependencies only used in `jwt_dynamic_auth` example
53+
[dev-dependencies]
54+
tokio = { version = "1", features = ["full"] }
55+
bitreq = { version = "0.3.4", features = ["async-https", "json-using-serde"] }

src/client.rs

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -112,26 +112,34 @@ impl ClientType {
112112
/// Constructor that supports multiple backends and allows configuration through
113113
/// the [Config]
114114
pub fn from_config(url: &str, config: &Config) -> Result<Self, Error> {
115+
let auth_provider = config.authorization_provider().cloned();
116+
115117
#[cfg(any(feature = "openssl", feature = "rustls", feature = "rustls-ring"))]
116118
if url.starts_with("ssl://") {
117119
let url = url.replacen("ssl://", "", 1);
118120
#[cfg(feature = "proxy")]
119-
let client = match config.socks5() {
121+
let raw_client = match config.socks5() {
120122
Some(socks5) => RawClient::new_proxy_ssl(
121123
url.as_str(),
122124
config.validate_domain(),
123125
socks5,
124126
config.timeout(),
125-
)?,
127+
)?
128+
.with_auth(auth_provider)
129+
.negotiate_protocol_version()?,
126130
None => {
127131
RawClient::new_ssl(url.as_str(), config.validate_domain(), config.timeout())?
132+
.with_auth(auth_provider)
133+
.negotiate_protocol_version()?
128134
}
129135
};
130136
#[cfg(not(feature = "proxy"))]
131-
let client =
132-
RawClient::new_ssl(url.as_str(), config.validate_domain(), config.timeout())?;
137+
let raw_client =
138+
RawClient::new_ssl(url.as_str(), config.validate_domain(), config.timeout())?
139+
.with_auth(auth_provider)
140+
.negotiate_protocol_version()?;
133141

134-
return Ok(ClientType::SSL(client));
142+
return Ok(ClientType::SSL(raw_client));
135143
}
136144

137145
#[cfg(not(any(feature = "openssl", feature = "rustls", feature = "rustls-ring")))]
@@ -143,18 +151,27 @@ impl ClientType {
143151

144152
{
145153
let url = url.replacen("tcp://", "", 1);
154+
146155
#[cfg(feature = "proxy")]
147156
let client = match config.socks5() {
148-
Some(socks5) => ClientType::Socks5(RawClient::new_proxy(
149-
url.as_str(),
150-
socks5,
151-
config.timeout(),
152-
)?),
153-
None => ClientType::TCP(RawClient::new(url.as_str(), config.timeout())?),
157+
Some(socks5) => ClientType::Socks5(
158+
RawClient::new_proxy(url.as_str(), socks5, config.timeout())?
159+
.with_auth(auth_provider)
160+
.negotiate_protocol_version()?,
161+
),
162+
None => ClientType::TCP(
163+
RawClient::new(url.as_str(), config.timeout())?
164+
.with_auth(auth_provider)
165+
.negotiate_protocol_version()?,
166+
),
154167
};
155168

156169
#[cfg(not(feature = "proxy"))]
157-
let client = ClientType::TCP(RawClient::new(url.as_str(), config.timeout())?);
170+
let client = ClientType::TCP(
171+
RawClient::new(url.as_str(), config.timeout())?
172+
.with_auth(auth_provider)
173+
.negotiate_protocol_version()?,
174+
);
158175

159176
Ok(client)
160177
}

src/config.rs

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1+
use std::sync::Arc;
12
use std::time::Duration;
23

4+
/// A function that provides authorization tokens dynamically (e.g., for JWT refresh)
5+
pub type AuthProvider = Arc<dyn Fn() -> Option<String> + Send + Sync>;
6+
37
/// Configuration for an electrum client
48
///
59
/// Refer to [`Client::from_config`] and [`ClientType::from_config`].
610
///
711
/// [`Client::from_config`]: crate::Client::from_config
812
/// [`ClientType::from_config`]: crate::ClientType::from_config
9-
#[derive(Debug, Clone)]
13+
#[derive(Clone)]
1014
pub struct Config {
1115
/// Proxy socks5 configuration, default None
1216
socks5: Option<Socks5Config>,
@@ -16,6 +20,24 @@ pub struct Config {
1620
retry: u8,
1721
/// when ssl, validate the domain, default true
1822
validate_domain: bool,
23+
/// Optional authorization provider for dynamic token injection
24+
authorization_provider: Option<AuthProvider>,
25+
}
26+
27+
// Custom Debug impl because AuthProvider doesn't implement Debug
28+
impl std::fmt::Debug for Config {
29+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30+
f.debug_struct("Config")
31+
.field("socks5", &self.socks5)
32+
.field("timeout", &self.timeout)
33+
.field("retry", &self.retry)
34+
.field("validate_domain", &self.validate_domain)
35+
.field(
36+
"authorization_provider",
37+
&self.authorization_provider.as_ref().map(|_| "<provider>"),
38+
)
39+
.finish()
40+
}
1941
}
2042

2143
/// Configuration for Socks5
@@ -72,6 +94,12 @@ impl ConfigBuilder {
7294
self
7395
}
7496

97+
/// Sets the authorization provider for dynamic token injection
98+
pub fn authorization_provider(mut self, provider: Option<AuthProvider>) -> Self {
99+
self.config.authorization_provider = provider;
100+
self
101+
}
102+
75103
/// Return the config and consume the builder
76104
pub fn build(self) -> Config {
77105
self.config
@@ -131,6 +159,13 @@ impl Config {
131159
self.validate_domain
132160
}
133161

162+
/// Get the configuration for `authorization_provider`
163+
///
164+
/// Set this with [`ConfigBuilder::authorization_provider`]
165+
pub fn authorization_provider(&self) -> Option<&AuthProvider> {
166+
self.authorization_provider.as_ref()
167+
}
168+
134169
/// Convenience method for calling [`ConfigBuilder::new`]
135170
pub fn builder() -> ConfigBuilder {
136171
ConfigBuilder::new()
@@ -144,6 +179,106 @@ impl Default for Config {
144179
timeout: None,
145180
retry: 1,
146181
validate_domain: true,
182+
authorization_provider: None,
183+
}
184+
}
185+
}
186+
187+
#[cfg(test)]
188+
mod tests {
189+
use super::*;
190+
191+
#[test]
192+
fn test_authorization_provider_builder() {
193+
let token = "test-token-123".to_string();
194+
let provider = Arc::new(move || Some(format!("Bearer {}", token)));
195+
196+
let config = ConfigBuilder::new()
197+
.authorization_provider(Some(provider.clone()))
198+
.build();
199+
200+
assert!(config.authorization_provider().is_some());
201+
202+
// Test that the provider returns the expected value
203+
if let Some(auth_provider) = config.authorization_provider() {
204+
assert_eq!(auth_provider(), Some("Bearer test-token-123".to_string()));
147205
}
148206
}
207+
208+
#[test]
209+
fn test_authorization_provider_none() {
210+
let config = ConfigBuilder::new().build();
211+
212+
assert!(config.authorization_provider().is_none());
213+
}
214+
215+
#[test]
216+
fn test_authorization_provider_returns_none() {
217+
let provider = Arc::new(|| None);
218+
219+
let config = ConfigBuilder::new()
220+
.authorization_provider(Some(provider))
221+
.build();
222+
223+
assert!(config.authorization_provider().is_some());
224+
225+
// Test that the provider returns None
226+
if let Some(auth_provider) = config.authorization_provider() {
227+
assert_eq!(auth_provider(), None);
228+
}
229+
}
230+
231+
#[test]
232+
fn test_authorization_provider_dynamic_token() {
233+
use std::sync::RwLock;
234+
235+
// Simulate a token that can be updated
236+
let token = Arc::new(RwLock::new("initial-token".to_string()));
237+
let token_clone = token.clone();
238+
239+
let provider = Arc::new(move || Some(token_clone.read().unwrap().clone()));
240+
241+
let config = ConfigBuilder::new()
242+
.authorization_provider(Some(provider.clone()))
243+
.build();
244+
245+
// Initial token
246+
if let Some(auth_provider) = config.authorization_provider() {
247+
assert_eq!(auth_provider(), Some("initial-token".to_string()));
248+
}
249+
250+
// Update the token
251+
*token.write().unwrap() = "refreshed-token".to_string();
252+
253+
// Provider should return the new token
254+
if let Some(auth_provider) = config.authorization_provider() {
255+
assert_eq!(auth_provider(), Some("refreshed-token".to_string()));
256+
}
257+
}
258+
259+
#[test]
260+
fn test_config_debug_with_provider() {
261+
let provider = Arc::new(|| Some("secret-token".to_string()));
262+
263+
let config = ConfigBuilder::new()
264+
.authorization_provider(Some(provider))
265+
.build();
266+
267+
let debug_str = format!("{:?}", config);
268+
269+
// Should show <provider> instead of the actual function pointer
270+
assert!(debug_str.contains("<provider>"));
271+
// Should not leak the token value
272+
assert!(!debug_str.contains("secret-token"));
273+
}
274+
275+
#[test]
276+
fn test_config_debug_without_provider() {
277+
let config = ConfigBuilder::new().build();
278+
279+
let debug_str = format!("{:?}", config);
280+
281+
// Should show None for authorization_provider
282+
assert!(debug_str.contains("authorization_provider"));
283+
}
149284
}

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,5 @@ pub mod utils;
5858
pub use api::ElectrumApi;
5959
pub use batch::Batch;
6060
pub use client::*;
61-
pub use config::{Config, ConfigBuilder, Socks5Config};
61+
pub use config::{AuthProvider, Config, ConfigBuilder, Socks5Config};
6262
pub use types::*;

0 commit comments

Comments
 (0)