client: handle response data
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
parent
1c96afd0ec
commit
ffe908f636
@ -9,22 +9,6 @@ pub enum AuthenticationKind {
|
||||
Token(Token),
|
||||
}
|
||||
|
||||
impl AuthenticationKind {
|
||||
pub fn set_auth_headers(&self, request: http::request::Builder) -> http::request::Builder {
|
||||
match self {
|
||||
AuthenticationKind::Ticket(auth) => auth.set_auth_headers(request),
|
||||
AuthenticationKind::Token(auth) => auth.set_auth_headers(request),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn userid(&self) -> &str {
|
||||
match self {
|
||||
AuthenticationKind::Ticket(auth) => &auth.userid,
|
||||
AuthenticationKind::Token(auth) => &auth.userid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Authentication> for AuthenticationKind {
|
||||
fn from(auth: Authentication) -> Self {
|
||||
Self::Ticket(auth)
|
||||
|
@ -1,12 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::error::Error as StdError;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use http::request::Request;
|
||||
use http::response::Response;
|
||||
use http::uri::PathAndQuery;
|
||||
use http::{StatusCode, Uri};
|
||||
use hyper::body::{Body, HttpBody};
|
||||
@ -14,7 +12,6 @@ use openssl::hash::MessageDigest;
|
||||
use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
|
||||
use openssl::x509::{self, X509};
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use proxmox_login::ticket::Validity;
|
||||
use proxmox_login::{Login, SecondFactorChallenge, TicketResult};
|
||||
@ -24,9 +21,6 @@ use crate::{Error, Token};
|
||||
|
||||
use super::{HttpApiClient, HttpApiResponse};
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
type ResponseFuture = Pin<Box<dyn Future<Output = Result<HttpApiResponse, Error>> + Send>>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub enum TlsOptions {
|
||||
/// Default TLS verification.
|
||||
@ -195,8 +189,19 @@ impl Client {
|
||||
return Err(Error::api(response.status, data));
|
||||
}
|
||||
|
||||
let content_type = match response.headers.get(http::header::CONTENT_TYPE) {
|
||||
None => None,
|
||||
Some(value) => Some(
|
||||
value
|
||||
.to_str()
|
||||
.map_err(|err| Error::internal("bad Content-Type header", err))?
|
||||
.to_owned(),
|
||||
),
|
||||
};
|
||||
|
||||
Ok(HttpApiResponse {
|
||||
status: response.status.as_u16(),
|
||||
content_type,
|
||||
body,
|
||||
})
|
||||
}
|
||||
@ -232,6 +237,29 @@ impl Client {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn do_login_request(&self, request: proxmox_login::Request) -> Result<Vec<u8>, Error> {
|
||||
let request = http::Request::builder()
|
||||
.method(http::Method::POST)
|
||||
.uri(request.url)
|
||||
.header(http::header::CONTENT_TYPE, request.content_type)
|
||||
.header(
|
||||
http::header::CONTENT_LENGTH,
|
||||
request.content_length.to_string(),
|
||||
)
|
||||
.body(request.body.into())
|
||||
.map_err(|err| Error::internal("error building login http request", err))?;
|
||||
|
||||
let api_response = self.client.request(request).await.map_err(Error::Anyhow)?;
|
||||
if !api_response.status().is_success() {
|
||||
return Err(Error::api(api_response.status(), "authentication failed"));
|
||||
}
|
||||
|
||||
let (_, body) = api_response.into_parts();
|
||||
let body = read_body(body).await?;
|
||||
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
/// Attempt to refresh the current ticket.
|
||||
///
|
||||
/// If not logged in at all yet, `Error::Unauthorized` will be returned.
|
||||
@ -244,16 +272,10 @@ impl Client {
|
||||
|
||||
let login = Login::renew(self.api_url.to_string(), auth.ticket.to_string())
|
||||
.map_err(Error::Ticket)?;
|
||||
let request = login_to_request(login.request())?;
|
||||
|
||||
let response = self.client.request(request).await.map_err(Error::Anyhow)?;
|
||||
if !response.status().is_success() {
|
||||
return Err(Error::api(response.status(), "authentication failed"));
|
||||
}
|
||||
let api_response = self.do_login_request(login.request()).await?;
|
||||
|
||||
let (_, body) = response.into_parts();
|
||||
let body = read_body(body).await?;
|
||||
match login.response(&body)? {
|
||||
match login.response(&api_response)? {
|
||||
TicketResult::Full(auth) => {
|
||||
*self.auth.lock().unwrap() = Some(Arc::new(auth.into()));
|
||||
Ok(())
|
||||
@ -264,6 +286,43 @@ impl Client {
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to login.
|
||||
///
|
||||
/// This will propagate the PVE compatibility state and then perform the `Login` request via
|
||||
/// the inner http client.
|
||||
///
|
||||
/// If the authentication is complete, `None` is returned and the authentication state updated.
|
||||
/// If a 2nd factor is required, `Some` is returned.
|
||||
pub async fn login(&self, login: Login) -> Result<Option<SecondFactorChallenge>, Error> {
|
||||
let login = login.pve_compatibility(self.pve_compat);
|
||||
|
||||
let api_response = self.do_login_request(login.request()).await?;
|
||||
|
||||
Ok(match login.response(&api_response)? {
|
||||
TicketResult::TfaRequired(challenge) => Some(challenge),
|
||||
TicketResult::Full(auth) => {
|
||||
*self.auth.lock().unwrap() = Some(Arc::new(auth.into()));
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Attempt to finish a 2nd factor login.
|
||||
///
|
||||
/// This will propagate the PVE compatibility state and then perform the `Login` request via
|
||||
/// the inner http client.
|
||||
pub async fn login_tfa(
|
||||
&self,
|
||||
challenge: SecondFactorChallenge,
|
||||
challenge_response: proxmox_login::Request,
|
||||
) -> Result<(), Error> {
|
||||
let api_response = self.do_login_request(challenge_response).await?;
|
||||
|
||||
let auth = challenge.response(&api_response)?;
|
||||
*self.auth.lock().unwrap() = Some(Arc::new(auth.into()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_body(mut body: Body) -> Result<Vec<u8>, Error> {
|
||||
@ -276,63 +335,44 @@ async fn read_body(mut body: Body) -> Result<Vec<u8>, Error> {
|
||||
}
|
||||
|
||||
impl HttpApiClient for Client {
|
||||
type ResponseFuture = ResponseFuture;
|
||||
type ResponseFuture<'a> =
|
||||
Pin<Box<dyn Future<Output = Result<HttpApiResponse, Error>> + Send + 'a>>;
|
||||
|
||||
fn get(&self, path_and_query: &str) -> Self::ResponseFuture {
|
||||
let client = Arc::clone(&self.client);
|
||||
let request_params = self
|
||||
.login_auth()
|
||||
.and_then(|auth| self.build_uri(path_and_query).map(|uri| (auth, uri)));
|
||||
fn get<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
|
||||
Box::pin(async move {
|
||||
let (auth, uri) = request_params?;
|
||||
let auth = self.login_auth()?;
|
||||
let uri = self.build_uri(path_and_query)?;
|
||||
let client = Arc::clone(&self.client);
|
||||
Self::authenticated_request(client, auth, http::Method::GET, uri, None).await
|
||||
})
|
||||
}
|
||||
|
||||
fn post<T>(&self, path_and_query: &str, params: &T) -> Self::ResponseFuture
|
||||
fn post<'a, T>(&'a self, path_and_query: &'a str, params: &T) -> Self::ResponseFuture<'a>
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
let client = Arc::clone(&self.client);
|
||||
let request_params = self
|
||||
.login_auth()
|
||||
.and_then(|auth| self.build_uri(path_and_query).map(|uri| (auth, uri)))
|
||||
.and_then(|(auth, uri)| {
|
||||
serde_json::to_string(params)
|
||||
.map_err(|err| Error::internal("failed to serialize parametres", err))
|
||||
.map(|params| (auth, uri, params))
|
||||
});
|
||||
let params = serde_json::to_string(params)
|
||||
.map_err(|err| Error::internal("failed to serialize parametres", err));
|
||||
|
||||
Box::pin(async move {
|
||||
let (auth, uri, params) = request_params?;
|
||||
let params = params?;
|
||||
let auth = self.login_auth()?;
|
||||
let uri = self.build_uri(path_and_query)?;
|
||||
let client = Arc::clone(&self.client);
|
||||
Self::authenticated_request(client, auth, http::Method::POST, uri, Some(params)).await
|
||||
})
|
||||
}
|
||||
|
||||
fn delete(&self, path_and_query: &str) -> Self::ResponseFuture {
|
||||
let client = Arc::clone(&self.client);
|
||||
let request_params = self
|
||||
.login_auth()
|
||||
.and_then(|auth| self.build_uri(path_and_query).map(|uri| (auth, uri)));
|
||||
fn delete<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
|
||||
Box::pin(async move {
|
||||
let (auth, uri) = request_params?;
|
||||
let auth = self.login_auth()?;
|
||||
let uri = self.build_uri(path_and_query)?;
|
||||
let client = Arc::clone(&self.client);
|
||||
Self::authenticated_request(client, auth, http::Method::DELETE, uri, None).await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn login_to_request(request: proxmox_login::Request) -> Result<http::Request<Body>, Error> {
|
||||
http::Request::builder()
|
||||
.method(http::Method::POST)
|
||||
.uri(request.url)
|
||||
.header(http::header::CONTENT_TYPE, request.content_type)
|
||||
.header(
|
||||
http::header::CONTENT_LENGTH,
|
||||
request.content_length.to_string(),
|
||||
)
|
||||
.body(request.body.into())
|
||||
.map_err(|err| Error::internal("error building login http request", err))
|
||||
}
|
||||
|
||||
fn verify_fingerprint(chain: &x509::X509StoreContextRef, expected_fingerprint: &[u8]) -> bool {
|
||||
let Some(cert) = chain.current_cert() else {
|
||||
log::error!("no certificate in chain?");
|
||||
@ -369,325 +409,27 @@ fn fp_string(fp: &[u8]) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
/*
|
||||
impl Client {
|
||||
/// If currently logged in, this will fill in the auth cookie and CSRFPreventionToken header
|
||||
/// and return `Ok(request)`, otherwise it'll return `Err(request)` with the request
|
||||
/// unmodified.
|
||||
pub fn try_set_auth_headers(
|
||||
&self,
|
||||
request: http::request::Builder,
|
||||
) -> Result<http::request::Builder, http::request::Builder> {
|
||||
let auth = self.auth.lock().unwrap().clone();
|
||||
match auth {
|
||||
Some(auth) => Ok(auth.set_auth_headers(request)),
|
||||
None => Err(request),
|
||||
impl Error {
|
||||
pub(crate) fn internal<E>(context: &'static str, err: E) -> Self
|
||||
where
|
||||
E: StdError + Send + Sync + 'static,
|
||||
{
|
||||
Self::Internal(context, Box::new(err))
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthenticationKind {
|
||||
pub fn set_auth_headers(&self, request: http::request::Builder) -> http::request::Builder {
|
||||
match self {
|
||||
AuthenticationKind::Ticket(auth) => auth.set_auth_headers(request),
|
||||
AuthenticationKind::Token(auth) => auth.set_auth_headers(request),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to login.
|
||||
///
|
||||
/// This will propagate the PVE compatibility state and then perform the `Login` request via
|
||||
/// the inner http client.
|
||||
///
|
||||
/// If the authentication is complete, `None` is returned and the authentication state updated.
|
||||
/// If a 2nd factor is required, `Some` is returned.
|
||||
pub async fn login(&self, login: Login) -> Result<Option<SecondFactorChallenge>, Error> {
|
||||
let login = login.pve_compatibility(self.pve_compat);
|
||||
|
||||
let api_response = self.client.request(to_request(login.request())?).await?;
|
||||
|
||||
if !api_response.status().is_success() {
|
||||
// FIXME: does `http` somehow expose the status string?
|
||||
return Err(Error::api(api_response.status(), "authentication failed"));
|
||||
}
|
||||
|
||||
Ok(match login.response(api_response.body())? {
|
||||
TicketResult::TfaRequired(challenge) => Some(challenge),
|
||||
TicketResult::Full(auth) => {
|
||||
*self.auth.lock().unwrap() = Some(Arc::new(auth.into()));
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Attempt to finish a 2nd factor login.
|
||||
///
|
||||
/// This will propagate the PVE compatibility state and then perform the `Login` request via
|
||||
/// the inner http client.
|
||||
pub async fn login_tfa(
|
||||
&self,
|
||||
challenge: SecondFactorChallenge,
|
||||
challenge_response: proxmox_login::Request,
|
||||
) -> Result<(), Error> {
|
||||
let api_response = self.client.request(to_request(challenge_response)?).await?;
|
||||
|
||||
if !api_response.status().is_success() {
|
||||
// FIXME: does `http` somehow expose the status string?
|
||||
return Err(Error::api(api_response.status(), "authentication failed"));
|
||||
}
|
||||
|
||||
let auth = challenge.response(api_response.body())?;
|
||||
*self.auth.lock().unwrap() = Some(Arc::new(auth.into()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute a `GET` request, possibly trying multiple cluster nodes.
|
||||
pub async fn get<'a, R>(&'a self, uri: &str) -> Result<ApiResponse<R>, Error>
|
||||
where
|
||||
R: serde::de::DeserializeOwned,
|
||||
{
|
||||
let request = self
|
||||
.set_auth_headers(Request::get(self.build_uri(uri)?))
|
||||
.await?
|
||||
.body(Vec::new())
|
||||
.map_err(|err| Error::internal("failed to build request", err))?;
|
||||
|
||||
Self::handle_response(self.client.request(request).await?)
|
||||
}
|
||||
|
||||
/// Execute a `GET` request with the given body, possibly trying multiple cluster nodes.
|
||||
pub async fn get_with_body<'a, B, R>(
|
||||
&'a self,
|
||||
uri: &str,
|
||||
body: &'a B,
|
||||
) -> Result<ApiResponse<R>, Error>
|
||||
where
|
||||
B: serde::Serialize,
|
||||
R: serde::de::DeserializeOwned,
|
||||
{
|
||||
let auth = self.login_auth()?;
|
||||
self.json_request(&auth, http::Method::GET, uri, body).await
|
||||
}
|
||||
|
||||
/// 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>, Error>
|
||||
where
|
||||
B: serde::Serialize,
|
||||
R: serde::de::DeserializeOwned,
|
||||
{
|
||||
let auth = self.login_auth()?;
|
||||
self.json_request(&auth, http::Method::PUT, uri, body).await
|
||||
}
|
||||
|
||||
/// 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>, Error>
|
||||
where
|
||||
B: serde::Serialize,
|
||||
R: serde::de::DeserializeOwned,
|
||||
{
|
||||
let auth = self.login_auth()?;
|
||||
self.json_request(&auth, http::Method::POST, uri, body)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Execute a `DELETE` request, possibly trying multiple cluster nodes.
|
||||
pub async fn delete<'a, R>(&'a self, uri: &str) -> Result<ApiResponse<R>, Error>
|
||||
where
|
||||
R: serde::de::DeserializeOwned,
|
||||
{
|
||||
let request = self
|
||||
.set_auth_headers(Request::delete(self.build_uri(uri)?))
|
||||
.await?
|
||||
.body(Vec::new())
|
||||
.map_err(|err| Error::internal("failed to build request", err))?;
|
||||
|
||||
Self::handle_response(self.client.request(request).await?)
|
||||
}
|
||||
|
||||
/// Execute a `DELETE` request with the given body, possibly trying multiple cluster nodes.
|
||||
pub async fn delete_with_body<'a, B, R>(
|
||||
&'a self,
|
||||
uri: &str,
|
||||
body: &'a B,
|
||||
) -> Result<ApiResponse<R>, Error>
|
||||
where
|
||||
B: serde::Serialize,
|
||||
R: serde::de::DeserializeOwned,
|
||||
{
|
||||
let auth = self.login_auth()?;
|
||||
self.json_request(&auth, http::Method::DELETE, uri, body)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Helper method for a JSON request with a JSON body `B`, yielding a JSON result type `R`.
|
||||
pub(crate) async fn json_request<'a, B, R>(
|
||||
&'a self,
|
||||
auth: &'a AuthenticationKind,
|
||||
method: http::Method,
|
||||
uri: &str,
|
||||
body: &'a B,
|
||||
) -> Result<ApiResponse<R>, Error>
|
||||
where
|
||||
B: serde::Serialize,
|
||||
R: serde::de::DeserializeOwned,
|
||||
{
|
||||
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
|
||||
}
|
||||
|
||||
/// Helper method for a request with a byte body, yielding a JSON result of type `R`.
|
||||
async fn json_request_bytes<'a, R>(
|
||||
&'a self,
|
||||
auth: &AuthenticationKind,
|
||||
method: http::Method,
|
||||
uri: &str,
|
||||
body: Vec<u8>,
|
||||
content_length: usize,
|
||||
) -> Result<ApiResponse<R>, Error>
|
||||
where
|
||||
R: serde::de::DeserializeOwned,
|
||||
{
|
||||
let response = self
|
||||
.run_json_request_with_body(auth, method, uri, body, content_length)
|
||||
.await?;
|
||||
Self::handle_response(response)
|
||||
}
|
||||
|
||||
async fn run_json_request_with_body<'a>(
|
||||
&'a self,
|
||||
auth: &'a AuthenticationKind,
|
||||
method: http::Method,
|
||||
uri: &str,
|
||||
body: Vec<u8>,
|
||||
content_length: usize,
|
||||
) -> Result<Response<Vec<u8>>, Error> {
|
||||
let request = Request::builder()
|
||||
.method(method.clone())
|
||||
.uri(self.build_uri(uri)?)
|
||||
.header(http::header::CONTENT_TYPE, "application/json")
|
||||
.header(http::header::CONTENT_LENGTH, content_length.to_string());
|
||||
|
||||
let request = auth
|
||||
.set_auth_headers(request)
|
||||
.body(body.clone())
|
||||
.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>, Error>
|
||||
where
|
||||
R: serde::de::DeserializeOwned,
|
||||
{
|
||||
if response.status() == StatusCode::UNAUTHORIZED {
|
||||
return Err(Error::Unauthorized);
|
||||
}
|
||||
|
||||
if !response.status().is_success() {
|
||||
// FIXME: Decode json errors...
|
||||
//match serde_json::from_slice(&body)
|
||||
// Ok(value) =>
|
||||
// if value["error"]
|
||||
let (response, body) = response.into_parts();
|
||||
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(|err| Error::internal("failed to deserialize api response", err))?;
|
||||
|
||||
data.check()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct NoData;
|
||||
|
||||
impl std::error::Error for NoData {}
|
||||
impl fmt::Display for NoData {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str("api returned no data")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ApiResponse<T> {
|
||||
pub data: Option<T>,
|
||||
pub attribs: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
impl<T> ApiResponse<T> {
|
||||
pub fn into_data_or_err(mut self) -> Result<T, NoData> {
|
||||
self.data.take().ok_or(NoData)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct UnexpectedData;
|
||||
|
||||
impl std::error::Error for UnexpectedData {}
|
||||
impl fmt::Display for UnexpectedData {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str("api returned unexpected data")
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiResponse<()> {
|
||||
pub fn nodata(self) -> Result<(), UnexpectedData> {
|
||||
if self.data.is_some() {
|
||||
Err(UnexpectedData)
|
||||
} else {
|
||||
Ok(())
|
||||
pub fn userid(&self) -> &str {
|
||||
match self {
|
||||
AuthenticationKind::Ticket(auth) => &auth.userid,
|
||||
AuthenticationKind::Token(auth) => &auth.userid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct RawApiResponse<T> {
|
||||
#[serde(default, deserialize_with = "proxmox_login::parse::deserialize_u16")]
|
||||
pub status: Option<u16>,
|
||||
pub message: Option<String>,
|
||||
#[serde(default, deserialize_with = "proxmox_login::parse::deserialize_bool")]
|
||||
pub success: Option<bool>,
|
||||
pub data: Option<T>,
|
||||
|
||||
#[serde(default)]
|
||||
pub errors: HashMap<String, String>,
|
||||
|
||||
#[serde(default, flatten)]
|
||||
pub attribs: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
impl<T> RawApiResponse<T> {
|
||||
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 mut message = self
|
||||
.message
|
||||
.take()
|
||||
.unwrap_or_else(|| "no message provided".to_string());
|
||||
for (param, error) in self.errors {
|
||||
use std::fmt::Write;
|
||||
let _ = write!(message, "\n{param}: {error}");
|
||||
}
|
||||
|
||||
return Err(Error::api(status, message));
|
||||
}
|
||||
|
||||
Ok(ApiResponse {
|
||||
data: self.data,
|
||||
attribs: self.attribs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
impl Client {
|
||||
/// Convenience method to login and set the authentication headers for a request.
|
||||
pub fn set_auth_headers(
|
||||
&self,
|
||||
request: http::request::Builder,
|
||||
) -> Result<http::request::Builder, Error> {
|
||||
Ok(self.login_auth()?.set_auth_headers(request))
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
@ -1,5 +1,5 @@
|
||||
use std::error::Error as StdError;
|
||||
use std::fmt::{self, Display};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
@ -10,6 +10,12 @@ pub enum Error {
|
||||
/// The API responded with an error code.
|
||||
Api(http::StatusCode, String),
|
||||
|
||||
/// The API returned something unexpected.
|
||||
BadApi(String, Option<Box<dyn StdError + Send + Sync + 'static>>),
|
||||
|
||||
/// An API call which is meant to return nothing returned unexpected data.
|
||||
UnexpectedData,
|
||||
|
||||
/// An error occurred in the authentication API.
|
||||
Authentication(proxmox_login::error::ResponseError),
|
||||
|
||||
@ -33,6 +39,7 @@ impl StdError for Error {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match self {
|
||||
Self::Authentication(err) => Some(err),
|
||||
Self::BadApi(_, Some(err)) => Some(&**err),
|
||||
Self::Ticket(err) => Some(err),
|
||||
Self::Client(err) => Some(&**err),
|
||||
Self::Internal(_, err) => Some(&**err),
|
||||
@ -47,6 +54,8 @@ impl fmt::Display for Error {
|
||||
match self {
|
||||
Self::Unauthorized => f.write_str("unauthorized"),
|
||||
Self::Api(status, msg) => write!(f, "api error (status = {status}): {msg}"),
|
||||
Self::UnexpectedData => write!(f, "api unexpectedly returned data"),
|
||||
Self::BadApi(msg, _) => write!(f, "api returned unexpected data - {msg}"),
|
||||
Self::Other(err) => f.write_str(err),
|
||||
Self::Authentication(err) => write!(f, "authentication error: {err}"),
|
||||
Self::Ticket(err) => write!(f, "authentication error: {err}"),
|
||||
@ -57,21 +66,22 @@ impl fmt::Display for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub(crate) fn api<T: Display>(status: http::StatusCode, msg: T) -> Self {
|
||||
Self::Api(status, msg.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn internal<E>(context: &'static str, err: E) -> Self
|
||||
where
|
||||
E: StdError + Send + Sync + 'static,
|
||||
{
|
||||
Self::Internal(context, Box::new(err))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<proxmox_login::error::ResponseError> for Error {
|
||||
fn from(err: proxmox_login::error::ResponseError) -> Self {
|
||||
Self::Authentication(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub(crate) fn bad_api<T, E>(msg: T, err: E) -> Self
|
||||
where
|
||||
T: Into<String>,
|
||||
E: StdError + Send + Sync + 'static,
|
||||
{
|
||||
Self::BadApi(msg.into(), Some(Box::new(err)))
|
||||
}
|
||||
|
||||
pub(crate) fn api<T: Into<String>>(status: http::StatusCode, msg: T) -> Self {
|
||||
Self::Api(status, msg.into())
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
mod error;
|
||||
|
||||
@ -17,34 +19,130 @@ mod client;
|
||||
#[cfg(feature = "hyper-client")]
|
||||
pub use client::{Client, TlsOptions};
|
||||
|
||||
/// A response from the HTTP API as required by the [`HttpApiClient`] trait.
|
||||
pub struct HttpApiResponse {
|
||||
pub status: u16,
|
||||
pub body: Vec<u8>,
|
||||
}
|
||||
|
||||
/// HTTP client backend trait. This should be implemented for a HTTP client capable of making
|
||||
/// *authenticated* API requests to a proxmox HTTP API.
|
||||
pub trait HttpApiClient: Send + Sync {
|
||||
/// An API call should return a status code and the raw body.
|
||||
type ResponseFuture: Future<Output = Result<HttpApiResponse, Error>>;
|
||||
type ResponseFuture<'a>: Future<Output = Result<HttpApiResponse, Error>> + 'a
|
||||
where
|
||||
Self: 'a;
|
||||
|
||||
/// `GET` request with a path and query component (no hostname).
|
||||
///
|
||||
/// For this request, authentication headers should be set!
|
||||
fn get(&self, path_and_query: &str) -> Self::ResponseFuture;
|
||||
fn get<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a>;
|
||||
|
||||
/// `POST` request with a path and query component (no hostname), and a serializable body.
|
||||
///
|
||||
/// The body should be serialized to json and sent with `Content-type: applicaion/json`.
|
||||
///
|
||||
/// For this request, authentication headers should be set!
|
||||
fn post<T>(&self, path_and_query: &str, params: &T) -> Self::ResponseFuture
|
||||
fn post<'a, T>(&'a self, path_and_query: &'a str, params: &T) -> Self::ResponseFuture<'a>
|
||||
where
|
||||
T: ?Sized + Serialize;
|
||||
|
||||
/// `DELETE` request with a path and query component (no hostname).
|
||||
///
|
||||
/// For this request, authentication headers should be set!
|
||||
fn delete(&self, path_and_query: &str) -> Self::ResponseFuture;
|
||||
fn delete<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a>;
|
||||
}
|
||||
|
||||
/// A response from the HTTP API as required by the [`HttpApiClient`] trait.
|
||||
pub struct HttpApiResponse {
|
||||
pub status: u16,
|
||||
pub content_type: Option<String>,
|
||||
pub body: Vec<u8>,
|
||||
}
|
||||
|
||||
impl HttpApiResponse {
|
||||
/// Expect a JSON response as returend by the `extjs` formatter.
|
||||
pub fn expect_json<T>(self) -> Result<ApiResponseData<T>, Error>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
self.assert_json_content_type()?;
|
||||
|
||||
serde_json::from_slice::<RawApiResponse<T>>(&self.body)
|
||||
.map_err(|err| Error::bad_api("failed to parse api response", err))?
|
||||
.check()
|
||||
}
|
||||
|
||||
fn assert_json_content_type(&self) -> Result<(), Error> {
|
||||
match self.content_type.as_deref() {
|
||||
Some("application/json") => Ok(()),
|
||||
Some(other) => {
|
||||
return Err(Error::BadApi(
|
||||
format!("expected json body, got {other}",),
|
||||
None,
|
||||
))
|
||||
}
|
||||
None => {
|
||||
return Err(Error::BadApi(
|
||||
"expected json body, but no Content-Type was sent".to_string(),
|
||||
None,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Expect that the API call did *not* return any data in the `data` field.
|
||||
pub fn nodata(self) -> Result<(), Error> {
|
||||
let response = serde_json::from_slice::<RawApiResponse<()>>(&self.body)
|
||||
.map_err(|err| Error::bad_api("failed to parse api response", err))?;
|
||||
|
||||
if response.data.is_some() {
|
||||
Err(Error::UnexpectedData)
|
||||
} else {
|
||||
response.check()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// API responses can have additional *attributes* added to their data.
|
||||
pub struct ApiResponseData<T> {
|
||||
pub attribs: HashMap<String, Value>,
|
||||
pub data: T,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct RawApiResponse<T> {
|
||||
#[serde(default, deserialize_with = "proxmox_login::parse::deserialize_u16")]
|
||||
status: Option<u16>,
|
||||
message: Option<String>,
|
||||
#[serde(default, deserialize_with = "proxmox_login::parse::deserialize_bool")]
|
||||
success: Option<bool>,
|
||||
data: Option<T>,
|
||||
|
||||
#[serde(default)]
|
||||
errors: HashMap<String, String>,
|
||||
|
||||
#[serde(default, flatten)]
|
||||
attribs: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
impl<T> RawApiResponse<T> {
|
||||
pub fn check(mut self) -> Result<ApiResponseData<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 mut message = self
|
||||
.message
|
||||
.take()
|
||||
.unwrap_or_else(|| "no message provided".to_string());
|
||||
for (param, error) in self.errors {
|
||||
use std::fmt::Write;
|
||||
let _ = write!(message, "\n{param}: {error}");
|
||||
}
|
||||
|
||||
return Err(Error::api(status, message));
|
||||
}
|
||||
|
||||
Ok(ApiResponseData {
|
||||
data: self
|
||||
.data
|
||||
.ok_or_else(|| Error::BadApi("api returned no data".to_string(), None))?,
|
||||
attribs: self.attribs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user