use std::{
future::Future,
sync::{Arc, RwLock},
};
use tonic::{
metadata::{Ascii, MetadataValue},
Code, Status,
};
use crate::{
client::AuthToken,
error::Result,
intercept::InterceptedChannel,
lock::RwLockExt,
rpc::auth::{AuthenticateOptions, RawAuthClient},
Error::GRpcStatus,
};
#[derive(Clone)]
pub struct CallOptions {
pub creds: Arc<RwLock<Option<(String, String)>>>,
pub refresh_expired_token: bool,
pub refresh_lock: Arc<tokio::sync::Mutex<()>>,
}
#[derive(Clone)]
pub struct ClientCallerBuilder {
options: CallOptions,
auth_token: AuthToken,
auth_client: RawAuthClient,
channel: InterceptedChannel,
}
impl ClientCallerBuilder {
pub fn new(
options: CallOptions,
auth_token: AuthToken,
auth_client: RawAuthClient,
channel: InterceptedChannel,
) -> Self {
Self {
options,
auth_token,
auth_client,
channel,
}
}
pub fn build<T>(self, f_inner: impl FnOnce(InterceptedChannel) -> T) -> ClientCaller<T> {
ClientCaller::new(
f_inner(self.channel),
self.auth_client,
self.auth_token,
self.options,
)
}
}
#[derive(Clone)]
pub struct ClientCaller<T> {
inner: T,
auth_client: RawAuthClient,
auth_token: AuthToken,
options: CallOptions,
}
impl<T> ClientCaller<T> {
pub fn new(
inner: T,
auth_client: RawAuthClient,
auth_token: AuthToken,
options: CallOptions,
) -> Self {
Self {
inner,
auth_client,
auth_token,
options,
}
}
pub async fn refresh_token(&self) -> Result<()> {
let creds = self.options.creds.read_unpoisoned().clone();
if let Some((user, password)) = creds {
let token = self.do_authenticate(user, password).await?;
self.auth_token.write_unpoisoned().replace(token);
} else {
let _ = self.auth_token.write_unpoisoned().take();
}
Ok(())
}
pub async fn update_user(&mut self, creds: Option<(String, String)>) -> Result<()> {
if let Some((ref name, ref password)) = creds {
let token = self.do_authenticate(name.clone(), password.clone()).await?;
self.auth_token.write_unpoisoned().replace(token);
} else {
let _ = self.auth_token.write_unpoisoned().take();
}
*self.options.creds.write_unpoisoned() = creds;
Ok(())
}
async fn refresh_token_if_stale(&self, stale: Option<MetadataValue<Ascii>>) -> Result<()> {
let _guard = self.options.refresh_lock.lock().await;
if *self.auth_token.read_unpoisoned() != stale {
return Ok(());
}
self.refresh_token().await
}
pub(crate) fn inner_mut(&mut self) -> &mut T {
&mut self.inner
}
pub fn with(mut self, f: impl FnOnce(T) -> T) -> Self {
self.inner = f(self.inner);
self
}
pub async fn do_call<Req, C, Ret>(&mut self, req: Req, call: C) -> Result<Ret>
where
for<'a> C: ClientCall<&'a mut T, Req, Output = Result<Ret>>,
Req: Clone,
{
let has_creds = self.options.creds.read_unpoisoned().is_some();
if !self.options.refresh_expired_token || !has_creds {
return (call)(&mut self.inner, req).await;
}
let stale = self.auth_token.read_unpoisoned().clone();
let resp = (call)(&mut self.inner, req.clone()).await;
match resp {
Err(GRpcStatus(status)) if is_invalid_auth_token(&status) => {
self.refresh_token_if_stale(stale).await?;
(call)(&mut self.inner, req).await
}
res => res,
}
}
async fn do_authenticate(
&self,
user: String,
password: String,
) -> Result<MetadataValue<Ascii>> {
let resp = self
.auth_client
.clone()
.authenticate(AuthenticateOptions::new().with_user(user, password))
.await?
.into_inner();
let token = resp.token.parse()?;
Ok(token)
}
}
const INVALID_AUTH_TOKEN_MSG: &str = "etcdserver: invalid auth token";
fn is_invalid_auth_token(status: &Status) -> bool {
match status.code() {
Code::Unauthenticated => true,
Code::Unknown => status.message() == INVALID_AUTH_TOKEN_MSG,
_ => false,
}
}
pub trait ClientCall<Client, Req>: Fn(Client, Req) -> Self::OutputFuture {
type Output;
type OutputFuture: Future<Output = <Self as ClientCall<Client, Req>>::Output>;
}
impl<F, Fut, Client, Req> ClientCall<Client, Req> for F
where
F: Fn(Client, Req) -> Fut,
Fut: Future,
{
type OutputFuture = Fut;
type Output = Fut::Output;
}