use proxmox_login::{Login, TicketResult};
use crate::auth::AuthenticationKind;
-use crate::{Authentication, Environment, ErrorTrait, Token};
+use crate::{Authentication, Environment, Error, Token};
/// HTTP client backend trait.
///
/// An async [`Client`] requires some kind of async HTTP client implementation.
pub trait HttpClient: Send + Sync {
- type Error: ErrorTrait;
- type ResponseFuture: Future<Output = Result<Response<Vec<u8>>, Self::Error>>;
+ type ResponseFuture: Future<Output = Result<Response<Vec<u8>>, Error>>;
fn request(&self, request: Request<Vec<u8>>) -> Self::ResponseFuture;
}
}
}
-fn to_request<E: ErrorTrait>(request: proxmox_login::Request) -> Result<http::Request<Vec<u8>>, E> {
+fn to_request(request: proxmox_login::Request) -> Result<http::Request<Vec<u8>>, Error> {
http::Request::builder()
.method(http::Method::POST)
.uri(request.url)
request.content_length.to_string(),
)
.body(request.body.into_bytes())
- .map_err(E::internal)
+ .map_err(|err| Error::internal("error building login http request", err))
}
impl<C, E: Environment> Client<C, E> {
where
E: Environment,
C: HttpClient,
- E::Error: From<C::Error>,
{
/// Instantiate a client for an API with a given environment and HTTP client instance.
pub fn with_client(api_url: Uri, environment: E, client: C) -> Self {
}
}
- pub async fn login_auth(&self) -> Result<Arc<AuthenticationKind>, E::Error> {
+ pub async fn login_auth(&self) -> Result<Arc<AuthenticationKind>, Error> {
self.login().await?;
self.auth
.lock()
.unwrap()
.clone()
- .ok_or_else(|| E::Error::internal("login failed to set authentication information"))
+ .ok_or_else(|| Error::Other("login failed to set authentication information"))
}
/// If currently logged in, this will fill in the auth cookie and CSRFPreventionToken header
pub async fn set_auth_headers(
&self,
request: http::request::Builder,
- ) -> Result<http::request::Builder, E::Error> {
+ ) -> Result<http::request::Builder, Error> {
Ok(self.login_auth().await?.set_auth_headers(request))
}
///
/// If no valid ticket is available already, this will connect to the PVE API and perform
/// authentication.
- pub async fn login(&self) -> Result<(), E::Error> {
+ pub async fn login(&self) -> Result<(), Error> {
let (userid, login) = self.need_login().await?;
let Some(login) = login else { return Ok(()) };
if !response.status().is_success() {
// FIXME: does `http` somehow expose the status string?
- return Err(E::Error::api_error(
- response.status(),
- "authentication failed",
- ));
+ return Err(Error::api(response.status(), "authentication failed"));
}
- let challenge = match login.response(response.body()).map_err(E::Error::bad_api)? {
+ let challenge = match login.response(response.body())? {
TicketResult::Full(auth) => return self.finish_auth(&userid, auth).await,
TicketResult::TfaRequired(challenge) => challenge,
};
let status = response.status();
if !status.is_success() {
- return Err(E::Error::api_error(status, "authentication failed"));
+ return Err(Error::api(status, "authentication failed"));
}
- let auth = challenge
- .response(response.body())
- .map_err(E::Error::bad_api)?;
+ let auth = challenge.response(response.body())?;
self.finish_auth(&userid, auth).await
}
/// Get the current username and, if required, a `Login` request.
- async fn need_login(&self) -> Result<(String, Option<Login>), E::Error> {
+ async fn need_login(&self) -> Result<(String, Option<Login>), Error> {
use proxmox_login::ticket::Validity;
let (userid, auth) = self.current_auth().await?;
userid,
Some(
Login::renew(self.api_url.to_string(), auth.ticket.to_string())
- .map_err(E::Error::custom)?,
+ .map_err(Error::Ticket)?,
),
),
}
/// Store the authentication info in our `auth` field and notify the environment.
- async fn finish_auth(&self, userid: &str, auth: Authentication) -> Result<(), E::Error> {
- let auth_string = serde_json::to_string(&auth).map_err(E::Error::internal)?;
+ async fn finish_auth(&self, userid: &str, auth: Authentication) -> Result<(), Error> {
+ let auth_string = serde_json::to_string(&auth)
+ .map_err(|err| Error::internal("failed to serialize authentication info", err))?;
*self.auth.lock().unwrap() = Some(Arc::new(auth.into()));
self.env
.store_ticket_async(&self.api_url, userid, auth_string.as_bytes())
/// If not authenticated yet, authenticate.
///
/// This may cause the environment to be queried for user ids/passwords/FIDO/...
- async fn current_auth(&self) -> Result<(String, Option<Arc<AuthenticationKind>>), E::Error> {
+ async fn current_auth(&self) -> Result<(String, Option<Arc<AuthenticationKind>>), Error> {
let auth = self.auth.lock().unwrap().clone();
let userid;
async fn reload_existing_ticket(
&self,
userid: &str,
- ) -> Result<Option<Arc<AuthenticationKind>>, E::Error> {
+ ) -> Result<Option<Arc<AuthenticationKind>>, Error> {
let ticket = match self.env.load_ticket_async(&self.api_url, userid).await? {
Some(auth) => auth,
None => return Ok(None),
};
let auth: Authentication = serde_json::from_slice(&ticket)
- .map_err(|err| E::Error::env(format!("bad ticket data: {err}")))?;
+ .map_err(|err| Error::internal("loaded bad ticket from environment", err))?;
let auth = Arc::new(auth.into());
*self.auth.lock().unwrap() = Some(Arc::clone(&auth));
}
/// Build a URI relative to the current API endpoint.
- fn build_uri(&self, path: &str) -> Result<Uri, E::Error> {
+ fn build_uri(&self, path: &str) -> Result<Uri, Error> {
let parts = self.api_url.clone().into_parts();
let mut builder = http::uri::Builder::new();
if let Some(scheme) = parts.scheme {
builder = builder.authority(authority)
}
builder
- .path_and_query(path.parse::<PathAndQuery>().map_err(E::Error::internal)?)
+ .path_and_query(
+ path.parse::<PathAndQuery>()
+ .map_err(|err| Error::internal("failed to parse uri", err))?,
+ )
.build()
- .map_err(E::Error::internal)
+ .map_err(|err| Error::internal("failed to build Uri", err))
}
/// Execute a `GET` request, possibly trying multiple cluster nodes.
- pub async fn get<'a, R>(&'a self, uri: &str) -> Result<ApiResponse<R>, E::Error>
+ pub async fn get<'a, R>(&'a self, uri: &str) -> Result<ApiResponse<R>, Error>
where
R: serde::de::DeserializeOwned,
{
.set_auth_headers(Request::get(self.build_uri(uri)?))
.await?
.body(Vec::new())
- .map_err(E::Error::internal)?;
+ .map_err(|err| Error::internal("failed to build request", err))?;
Self::handle_response(self.client.request(request).await?)
}
&'a self,
uri: &str,
body: &'a B,
- ) -> Result<ApiResponse<R>, E::Error>
+ ) -> Result<ApiResponse<R>, Error>
where
B: serde::Serialize,
R: serde::de::DeserializeOwned,
}
/// Execute a `PUT` request with the given body, possibly trying multiple cluster nodes.
- pub async fn put<'a, B, R>(&'a self, uri: &str, body: &'a B) -> Result<ApiResponse<R>, E::Error>
+ pub async fn put<'a, B, R>(&'a self, uri: &str, body: &'a B) -> Result<ApiResponse<R>, Error>
where
B: serde::Serialize,
R: serde::de::DeserializeOwned,
}
/// Execute a `POST` request with the given body, possibly trying multiple cluster nodes.
- pub async fn post<'a, B, R>(
- &'a self,
- uri: &str,
- body: &'a B,
- ) -> Result<ApiResponse<R>, E::Error>
+ pub async fn post<'a, B, R>(&'a self, uri: &str, body: &'a B) -> Result<ApiResponse<R>, Error>
where
B: serde::Serialize,
R: serde::de::DeserializeOwned,
}
/// Execute a `DELETE` request, possibly trying multiple cluster nodes.
- pub async fn delete<'a, R>(&'a self, uri: &str) -> Result<ApiResponse<R>, E::Error>
+ pub async fn delete<'a, R>(&'a self, uri: &str) -> Result<ApiResponse<R>, Error>
where
R: serde::de::DeserializeOwned,
{
.set_auth_headers(Request::delete(self.build_uri(uri)?))
.await?
.body(Vec::new())
- .map_err(E::Error::internal)?;
+ .map_err(|err| Error::internal("failed to build request", err))?;
Self::handle_response(self.client.request(request).await?)
}
&'a self,
uri: &str,
body: &'a B,
- ) -> Result<ApiResponse<R>, E::Error>
+ ) -> Result<ApiResponse<R>, Error>
where
B: serde::Serialize,
R: serde::de::DeserializeOwned,
method: http::Method,
uri: &str,
body: &'a B,
- ) -> Result<ApiResponse<R>, E::Error>
+ ) -> Result<ApiResponse<R>, Error>
where
B: serde::Serialize,
R: serde::de::DeserializeOwned,
{
- let body = serde_json::to_vec(&body).map_err(E::Error::internal)?;
+ let body = serde_json::to_vec(&body)
+ .map_err(|err| Error::internal("failed to serialize request body", err))?;
let content_length = body.len();
self.json_request_bytes(auth, method, uri, body, content_length)
.await
uri: &str,
body: Vec<u8>,
content_length: usize,
- ) -> Result<ApiResponse<R>, E::Error>
+ ) -> Result<ApiResponse<R>, Error>
where
R: serde::de::DeserializeOwned,
{
uri: &str,
body: Vec<u8>,
content_length: usize,
- ) -> Result<Response<Vec<u8>>, E::Error> {
+ ) -> Result<Response<Vec<u8>>, Error> {
let request = Request::builder()
.method(method.clone())
.uri(self.build_uri(uri)?)
let request = auth
.set_auth_headers(request)
.body(body.clone())
- .map_err(E::Error::internal)?;
+ .map_err(|err| Error::internal("failed to build request", err))?;
Ok(self.client.request(request).await?)
}
/// Check the status code, deserialize the json/extjs `RawApiResponse` and check for error
/// messages inside.
/// On success, deserialize the expected result type.
- fn handle_response<R>(response: Response<Vec<u8>>) -> Result<ApiResponse<R>, E::Error>
+ fn handle_response<R>(response: Response<Vec<u8>>) -> Result<ApiResponse<R>, Error>
where
R: serde::de::DeserializeOwned,
{
if response.status() == StatusCode::UNAUTHORIZED {
- return Err(E::Error::unauthorized());
+ return Err(Error::Unauthorized);
}
if !response.status().is_success() {
// Ok(value) =>
// if value["error"]
let (response, body) = response.into_parts();
- let body = String::from_utf8(body).map_err(E::Error::bad_api)?;
- return Err(E::Error::api_error(response.status, body));
+ let body =
+ String::from_utf8(body).map_err(|_| Error::Other("API returned non-utf8 data"))?;
+ return Err(Error::api(response.status, body));
}
- let data: RawApiResponse<R> =
- serde_json::from_slice(&response.into_body()).map_err(E::Error::bad_api)?;
+ let data: RawApiResponse<R> = serde_json::from_slice(&response.into_body())
+ .map_err(|err| Error::internal("failed to deserialize api response", err))?;
data.check()
}
}
impl<T> RawApiResponse<T> {
- pub fn check<E: ErrorTrait>(mut self) -> Result<ApiResponse<T>, E> {
+ pub fn check(mut self) -> Result<ApiResponse<T>, Error> {
if !self.success.unwrap_or(false) {
let status = http::StatusCode::from_u16(self.status.unwrap_or(400))
.unwrap_or(http::StatusCode::BAD_REQUEST);
let _ = write!(message, "\n{param}: {error}");
}
- return Err(E::api_error(status, message));
+ return Err(Error::api(status, message));
}
Ok(ApiResponse {
impl<C, E> Client<C, E>
where
E: Environment,
- E::Error: From<anyhow::Error>,
{
/// Create a new client instance which will connect to the provided endpoint.
pub fn new(api_url: Uri, environment: E) -> HyperClient<E> {
use std::future::Future;
use std::sync::Arc;
- use anyhow::format_err;
use http::request::Request;
use http::response::Response;
use http::Uri;
use proxmox_http::client::Client as ProxmoxClient;
use super::{Client, HyperClient};
- use crate::Environment;
+ use crate::{Environment, Error};
#[derive(Default)]
pub enum TlsOptions {
impl<C, E> Client<C, E>
where
E: Environment,
- E::Error: From<anyhow::Error>,
{
/// Create a new client instance which will connect to the provided endpoint.
pub fn with_options(
environment: E,
tls_options: TlsOptions,
http_options: proxmox_http::HttpOptions,
- ) -> Result<HyperClient<E>, E::Error> {
+ ) -> Result<HyperClient<E>, Error> {
let mut connector = SslConnector::builder(SslMethod::tls_client())
- .map_err(|err| format_err!("failed to create ssl connector builder: {err}"))?;
+ .map_err(|err| Error::internal("failed to create ssl connector builder", err))?;
match tls_options {
TlsOptions::Verify => (),
TlsOptions::CaCert(ca) => {
let mut store =
openssl::x509::store::X509StoreBuilder::new().map_err(|err| {
- format_err!("failed to create certificate store builder: {err}")
+ Error::internal("failed to create certificate store builder", err)
})?;
store
.add_cert(ca)
- .map_err(|err| format_err!("failed to build certificate store: {err}"))?;
+ .map_err(|err| Error::internal("failed to build certificate store", err))?;
connector.set_cert_store(store.build());
}
}
}
impl super::HttpClient for Arc<proxmox_http::client::Client> {
- type Error = anyhow::Error;
#[allow(clippy::type_complexity)]
type ResponseFuture =
- std::pin::Pin<Box<dyn Future<Output = Result<Response<Vec<u8>>, Self::Error>> + Send>>;
+ std::pin::Pin<Box<dyn Future<Output = Result<Response<Vec<u8>>, Error>> + Send>>;
fn request(&self, request: Request<Vec<u8>>) -> Self::ResponseFuture {
let (parts, body) = request.into_parts();
Box::pin(async move {
use hyper::body::HttpBody;
- let (response, mut body) = (*this).request(request).await?.into_parts();
+ // FIXME: proxmox_http's client needs a way to return http status codes and such...
+ let (response, mut body) = (*this)
+ .request(request)
+ .await
+ .map_err(Error::Anyhow)?
+ .into_parts();
let mut data = Vec::<u8>::new();
while let Some(more) = body.data().await {
- let more = more?;
+ let more = more.map_err(|err| Error::internal("error reading body", err))?;
data.extend(&more[..]);
}
- Ok::<_, anyhow::Error>(Response::from_parts(response, data))
+ Ok::<_, Error>(Response::from_parts(response, data))
})
}
}
use proxmox_login::tfa::TfaChallenge;
-use crate::ErrorTrait;
+use crate::Error;
/// Provide input from the environment for storing/loading tickets or tokens and querying the user
/// for passwords or 2nd factors.
pub trait Environment: Send + Sync {
- type Error: ErrorTrait;
-
/// Store a ticket belonging to a user of an API.
///
/// This is only used if `store_ticket_async` is not overwritten and may be left unimplemented
/// in async code. By default it will just return an error.
///
/// [`store_ticket_async`]: Environment::store_ticket_async
- fn store_ticket(&self, api_url: &Uri, userid: &str, ticket: &[u8]) -> Result<(), Self::Error> {
+ fn store_ticket(&self, api_url: &Uri, userid: &str, ticket: &[u8]) -> Result<(), Error> {
let _ = (api_url, userid, ticket);
- Err(Self::Error::custom(
- "missing store_ticket(_async) implementation",
- ))
+ Err(Error::Other("missing store_ticket(_async) implementation"))
}
/// Load a user's cached ticket for an API url.
/// in async code. By default it will just return an error.
///
/// [`load_ticket_async`]: Environment::load_ticket_async
- fn load_ticket(&self, api_url: &Uri, userid: &str) -> Result<Option<Vec<u8>>, Self::Error> {
+ fn load_ticket(&self, api_url: &Uri, userid: &str) -> Result<Option<Vec<u8>>, Error> {
let _ = (api_url, userid);
- Err(Self::Error::custom(
- "missing load_ticket(_async) implementation",
- ))
+ Err(Error::Other("missing load_ticket(_async) implementation"))
}
/// Query for a userid (name and realm).
/// unimplemented in async code. By default it will just return an error.
///
/// [`query_userid_async`]: Environment::query_userid_async
- fn query_userid(&self, api_url: &Uri) -> Result<String, Self::Error> {
+ fn query_userid(&self, api_url: &Uri) -> Result<String, Error> {
let _ = api_url;
- Err(Self::Error::custom(
- "missing query_userid(_async) implementation",
- ))
+ Err(Error::Other("missing query_userid(_async) implementation"))
}
/// Query for a password.
/// unimplemented in async code. By default it will just return an error.
///
/// [`query_password_async`]: Environment::query_password_async
- fn query_password(&self, api_url: &Uri, userid: &str) -> Result<String, Self::Error> {
+ fn query_password(&self, api_url: &Uri, userid: &str) -> Result<String, Error> {
let _ = (api_url, userid);
- Err(Self::Error::custom(
+ Err(Error::Other(
"missing query_password(_async) implementation",
))
}
api_url: &Uri,
userid: &str,
challenge: &TfaChallenge,
- ) -> Result<String, Self::Error> {
+ ) -> Result<String, Error> {
let _ = (api_url, userid, challenge);
- Err(Self::Error::second_factor_not_supported())
+ Err(Error::TfaNotSupported)
}
/// The client code uses async rust and it is fine to implement this instead of `store_ticket`.
api_url: &'a Uri,
userid: &'a str,
ticket: &'a [u8],
- ) -> Pin<Box<dyn Future<Output = Result<(), Self::Error>> + Send + 'a>> {
+ ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>> {
Box::pin(async move { self.store_ticket(api_url, userid, ticket) })
}
&'a self,
api_url: &'a Uri,
userid: &'a str,
- ) -> Pin<Box<dyn Future<Output = Result<Option<Vec<u8>>, Self::Error>> + Send + 'a>> {
+ ) -> Pin<Box<dyn Future<Output = Result<Option<Vec<u8>>, Error>> + Send + 'a>> {
Box::pin(async move { self.load_ticket(api_url, userid) })
}
fn query_userid_async<'a>(
&'a self,
api_url: &'a Uri,
- ) -> Pin<Box<dyn Future<Output = Result<String, Self::Error>> + Send + 'a>> {
+ ) -> Pin<Box<dyn Future<Output = Result<String, Error>> + Send + 'a>> {
Box::pin(async move { self.query_userid(api_url) })
}
&'a self,
api_url: &'a Uri,
userid: &'a str,
- ) -> Pin<Box<dyn Future<Output = Result<String, Self::Error>> + Send + 'a>> {
+ ) -> Pin<Box<dyn Future<Output = Result<String, Error>> + Send + 'a>> {
Box::pin(async move { self.query_password(api_url, userid) })
}
api_url: &'a Uri,
userid: &'a str,
challenge: &'a TfaChallenge,
- ) -> Pin<Box<dyn Future<Output = Result<String, Self::Error>> + Send + 'a>> {
+ ) -> Pin<Box<dyn Future<Output = Result<String, Error>> + Send + 'a>> {
Box::pin(async move { self.query_second_factor(api_url, userid, challenge) })
}
/// # Panics
///
/// The default implementation simply panics.
- fn sleep(
- time: Duration,
- ) -> Result<Pin<Box<dyn Future<Output = ()> + Send + 'static>>, Self::Error> {
+ fn sleep(time: Duration) -> Result<Pin<Box<dyn Future<Output = ()> + Send + 'static>>, Error> {
let _ = time;
- Err(Self::Error::sleep_not_supported())
+ Err(Error::SleepNotSupported)
}
}
-use std::any::Any;
+use std::error::Error as StdError;
use std::fmt::{self, Display};
-/// For error types provided by the user of this crate.
-pub trait ErrorTrait: Sized + Display + fmt::Debug + Any + Send + Sync + 'static {
- /// An arbitrary error message.
- fn custom<T: Display>(msg: T) -> Self;
+#[derive(Debug)]
+#[non_exhaustive]
+pub enum Error {
+ /// The environment did not provide a way to get a 2nd factor.
+ TfaNotSupported,
- /// Successfully queried the status of a task, and the task has failed.
- fn task_failed<T: Display>(msg: T) -> Self {
- Self::custom(format!("task failed: {msg}"))
- }
+ /// The task API wants to poll for completion of a task at regular intervals, for this it needs
+ /// to sleep. This signals that the environment does not support that.
+ SleepNotSupported,
- /// An API call returned an error status.
- fn api_error<T: Display>(status: http::StatusCode, msg: T) -> Self {
- Self::custom(format!("api error (status = {status}): {msg}"))
- }
+ /// Tried to make an API call without a ticket which requires ones.
+ Unauthorized,
- /// The API behaved unexpectedly.
- fn bad_api<T: Display>(msg: T) -> Self {
- Self::custom(msg)
- }
+ /// The API responded with an error code.
+ Api(http::StatusCode, String),
- /// The environment returned an error or bad data.
- fn env<T: Display>(msg: T) -> Self {
- Self::custom(msg)
- }
+ /// An error occurred in the authentication API.
+ Authentication(proxmox_login::error::ResponseError),
- /// A second factor was required, but the [`Environment`](crate::Environment) did not provide
- /// an implementation to get it.
- fn second_factor_not_supported() -> Self {
- Self::custom("not supported")
- }
+ /// The current ticket was rejected.
+ Ticket(proxmox_login::error::TicketError),
+
+ /// Generic errors.
+ Other(&'static str),
- /// There was an error building an [`http::Uri`].
- fn uri(err: http::Error) -> Self {
- Self::custom(err)
+ /// Generic errors bubbled up from a deeper source, usually the http client.
+ Client(Box<dyn StdError + Send + Sync + 'static>),
+
+ /// Another internal error occurred.
+ Internal(&'static str, Box<dyn StdError + Send + Sync + 'static>),
+
+ /// An `anyhow` error because `proxmox_http::Client` uses it...
+ Anyhow(anyhow::Error),
+}
+
+impl StdError for Error {
+ fn source(&self) -> Option<&(dyn StdError + 'static)> {
+ match self {
+ Self::Authentication(err) => Some(err),
+ Self::Ticket(err) => Some(err),
+ Self::Client(err) => Some(&**err),
+ Self::Internal(_, err) => Some(&**err),
+ Self::Anyhow(err) => err.chain().next(),
+ _ => None,
+ }
}
+}
- /// A generic internal error such as a serde_json serialization error.
- fn internal<T: Display>(err: T) -> Self {
- Self::custom(err)
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Self::TfaNotSupported => f.write_str("tfa not supported by environment"),
+ Self::SleepNotSupported => f.write_str("environment does not support sleeping"),
+ Self::Unauthorized => f.write_str("unauthorized"),
+ Self::Api(status, msg) => write!(f, "api error (status = {status}): {msg}"),
+ Self::Other(err) => f.write_str(err),
+ Self::Authentication(err) => write!(f, "authentication error: {err}"),
+ Self::Ticket(err) => write!(f, "authentication error: {err}"),
+ Self::Client(err) => fmt::Display::fmt(err, f),
+ Self::Internal(msg, _) => f.write_str(msg),
+ Self::Anyhow(err) => fmt::Display::fmt(err, f),
+ }
}
+}
- /// An API call which requires authorization was attempted without logging in first.
- fn unauthorized() -> Self {
- Self::custom("unauthorized")
+impl Error {
+ pub(crate) fn api<T: Display>(status: http::StatusCode, msg: T) -> Self {
+ Self::Api(status, msg.to_string())
}
- /// An extended client call required the ability to "pause" while polling API endpoints.
- /// (Mostly to wait for "tasks" to finish.), and no implementation for this was provided.
- fn sleep_not_supported() -> Self {
- Self::custom("no async 'sleep' implementation available")
+ pub(crate) fn internal<E>(context: &'static str, err: E) -> Self
+ where
+ E: StdError + Send + Sync + 'static,
+ {
+ Self::Internal(context, Box::new(err))
}
}
-impl ErrorTrait for anyhow::Error {
- fn custom<T: Display>(msg: T) -> Self {
- anyhow::format_err!("{msg}")
+impl From<proxmox_login::error::ResponseError> for Error {
+ fn from(err: proxmox_login::error::ResponseError) -> Self {
+ Self::Authentication(err)
}
}