use crate::http_client::{self, HttpClientExt};
use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct DeviceCodePrompt {
pub verification_uri: String,
pub user_code: String,
}
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
pub(crate) type DeviceCodeCallback = dyn Fn(DeviceCodePrompt) + Send + Sync;
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
pub(crate) type DeviceCodeCallback = dyn Fn(DeviceCodePrompt);
#[derive(Clone, Default)]
pub struct DeviceCodeHandler(pub(crate) Option<Arc<DeviceCodeCallback>>);
impl DeviceCodeHandler {
pub fn new<F>(handler: F) -> Self
where
F: Fn(DeviceCodePrompt) + WasmCompatSend + WasmCompatSync + 'static,
{
Self(Some(Arc::new(handler)))
}
}
impl DeviceCodeHandler {
#[cfg(not(target_family = "wasm"))]
pub(crate) fn show(&self, prompt: DeviceCodePrompt, fallback_message: &str) {
match &self.0 {
Some(callback) => callback(prompt),
None => println!("{fallback_message}"),
}
}
}
impl std::fmt::Debug for DeviceCodeHandler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.0.is_some() {
f.write_str("DeviceCodeHandler(<callback>)")
} else {
f.write_str("DeviceCodeHandler(None)")
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum AuthError {
#[error("{0}")]
Message(String),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error(transparent)]
Http(#[from] http_client::Error),
#[error("token store failed: {0}")]
Store(#[source] crate::error::BoxError),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthContext {
pub api_key: crate::wire::Secret,
pub account_id: Option<String>,
pub api_base: Option<String>,
}
#[derive(Clone)]
#[cfg_attr(target_family = "wasm", allow(dead_code))]
pub(crate) struct Shell<S> {
pub(crate) source: S,
pub(crate) store: Arc<dyn TokenStore>,
pub(crate) device_code_handler: DeviceCodeHandler,
pub(crate) allow_device_flow: bool,
pub(crate) refresh_lock: Arc<futures::lock::Mutex<()>>,
pub(crate) remembered: Remembered,
}
impl<S> Shell<S> {
pub(crate) fn new(
source: S,
store: impl TokenStore + 'static,
device_code_handler: DeviceCodeHandler,
allow_device_flow: bool,
) -> Self {
Self {
source,
store: Arc::new(store),
device_code_handler,
allow_device_flow,
refresh_lock: Arc::default(),
remembered: Remembered::default(),
}
}
}
impl<S> Shell<S> {
#[cfg_attr(target_family = "wasm", allow(dead_code))]
pub(crate) async fn hold(&self, key: &str) -> Result<Held<'_>, AuthError> {
let refresh = self.refresh_lock.lock().await;
let store = self.store.lock(key).await.map_err(AuthError::Store)?;
Ok(Held {
_refresh: refresh,
_store: store,
})
}
}
#[cfg_attr(target_family = "wasm", allow(dead_code))]
pub(crate) struct Held<'a> {
_store: StoreLock,
_refresh: futures::lock::MutexGuard<'a, ()>,
}
impl<S: std::fmt::Debug> std::fmt::Debug for Shell<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Authenticator")
.field("source", &self.source)
.field("device_code_handler", &self.device_code_handler)
.field("allow_device_flow", &self.allow_device_flow)
.finish_non_exhaustive()
}
}
#[derive(Clone, Default)]
pub(crate) struct Remembered(Arc<std::sync::Mutex<Option<(AuthContext, i64)>>>);
impl Remembered {
pub(crate) fn live(&self, skew_seconds: i64) -> Option<AuthContext> {
let held = self.0.lock().ok()?;
let (context, expires_at) = held.as_ref()?;
(!token_expired(Some(*expires_at), skew_seconds)).then(|| context.clone())
}
pub(crate) fn keep(&self, context: &AuthContext, expires_at: Option<i64>) {
if let Ok(mut held) = self.0.lock() {
*held = expires_at.map(|expires_at| (context.clone(), expires_at));
}
}
pub(crate) fn forget(&self) {
if let Ok(mut held) = self.0.lock() {
*held = None;
}
}
}
pub(crate) fn request(method: http::Method, url: &str) -> http::request::Builder {
http::Request::builder().method(method).uri(url)
}
pub(crate) async fn send_json<H, T>(
http: &H,
req: http::Result<http::Request<bytes::Bytes>>,
) -> Result<T, AuthError>
where
H: HttpClientExt,
T: serde::de::DeserializeOwned,
{
let bytes = send_bytes(http, req).await?;
Ok(serde_json::from_slice(&bytes)?)
}
pub(crate) async fn send_bytes<H>(
http: &H,
req: http::Result<http::Request<bytes::Bytes>>,
) -> Result<bytes::Bytes, AuthError>
where
H: HttpClientExt,
{
let req = req.map_err(http_client::Error::Protocol)?;
let response = http.send::<_, bytes::Bytes>(req).await?;
Ok(response.into_body().await?)
}
pub(crate) fn token_expired(expires_at: Option<i64>, skew_seconds: i64) -> bool {
let now = web_time::SystemTime::now()
.duration_since(web_time::UNIX_EPOCH)
.map(|duration| duration.as_secs() as i64)
.unwrap_or_default();
expires_at.is_none_or(|exp| now >= exp - skew_seconds)
}
mod store;
#[cfg(not(target_family = "wasm"))]
pub use store::FileTokenStore;
pub use store::{MemoryTokenStore, StoreLock, TokenStore};
pub(crate) use store::{load_record, save_record};