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),
|
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 {
|
impl From<Authentication> for AuthenticationKind {
|
||||||
fn from(auth: Authentication) -> Self {
|
fn from(auth: Authentication) -> Self {
|
||||||
Self::Ticket(auth)
|
Self::Ticket(auth)
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
use std::collections::HashMap;
|
use std::error::Error as StdError;
|
||||||
use std::fmt;
|
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use http::request::Request;
|
use http::request::Request;
|
||||||
use http::response::Response;
|
|
||||||
use http::uri::PathAndQuery;
|
use http::uri::PathAndQuery;
|
||||||
use http::{StatusCode, Uri};
|
use http::{StatusCode, Uri};
|
||||||
use hyper::body::{Body, HttpBody};
|
use hyper::body::{Body, HttpBody};
|
||||||
@ -14,7 +12,6 @@ use openssl::hash::MessageDigest;
|
|||||||
use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
|
use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
|
||||||
use openssl::x509::{self, X509};
|
use openssl::x509::{self, X509};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
use proxmox_login::ticket::Validity;
|
use proxmox_login::ticket::Validity;
|
||||||
use proxmox_login::{Login, SecondFactorChallenge, TicketResult};
|
use proxmox_login::{Login, SecondFactorChallenge, TicketResult};
|
||||||
@ -24,9 +21,6 @@ use crate::{Error, Token};
|
|||||||
|
|
||||||
use super::{HttpApiClient, HttpApiResponse};
|
use super::{HttpApiClient, HttpApiResponse};
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
type ResponseFuture = Pin<Box<dyn Future<Output = Result<HttpApiResponse, Error>> + Send>>;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub enum TlsOptions {
|
pub enum TlsOptions {
|
||||||
/// Default TLS verification.
|
/// Default TLS verification.
|
||||||
@ -195,8 +189,19 @@ impl Client {
|
|||||||
return Err(Error::api(response.status, data));
|
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 {
|
Ok(HttpApiResponse {
|
||||||
status: response.status.as_u16(),
|
status: response.status.as_u16(),
|
||||||
|
content_type,
|
||||||
body,
|
body,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -232,6 +237,29 @@ impl Client {
|
|||||||
Ok(())
|
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.
|
/// Attempt to refresh the current ticket.
|
||||||
///
|
///
|
||||||
/// If not logged in at all yet, `Error::Unauthorized` will be returned.
|
/// 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())
|
let login = Login::renew(self.api_url.to_string(), auth.ticket.to_string())
|
||||||
.map_err(Error::Ticket)?;
|
.map_err(Error::Ticket)?;
|
||||||
let request = login_to_request(login.request())?;
|
|
||||||
|
|
||||||
let response = self.client.request(request).await.map_err(Error::Anyhow)?;
|
let api_response = self.do_login_request(login.request()).await?;
|
||||||
if !response.status().is_success() {
|
|
||||||
return Err(Error::api(response.status(), "authentication failed"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let (_, body) = response.into_parts();
|
match login.response(&api_response)? {
|
||||||
let body = read_body(body).await?;
|
|
||||||
match login.response(&body)? {
|
|
||||||
TicketResult::Full(auth) => {
|
TicketResult::Full(auth) => {
|
||||||
*self.auth.lock().unwrap() = Some(Arc::new(auth.into()));
|
*self.auth.lock().unwrap() = Some(Arc::new(auth.into()));
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -264,6 +286,43 @@ impl Client {
|
|||||||
.into()),
|
.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> {
|
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 {
|
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 {
|
fn get<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
|
||||||
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)));
|
|
||||||
Box::pin(async move {
|
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
|
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
|
where
|
||||||
T: ?Sized + Serialize,
|
T: ?Sized + Serialize,
|
||||||
{
|
{
|
||||||
let client = Arc::clone(&self.client);
|
let params = serde_json::to_string(params)
|
||||||
let request_params = self
|
.map_err(|err| Error::internal("failed to serialize parametres", err));
|
||||||
.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))
|
|
||||||
});
|
|
||||||
Box::pin(async move {
|
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
|
Self::authenticated_request(client, auth, http::Method::POST, uri, Some(params)).await
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete(&self, path_and_query: &str) -> Self::ResponseFuture {
|
fn delete<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
|
||||||
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)));
|
|
||||||
Box::pin(async move {
|
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
|
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 {
|
fn verify_fingerprint(chain: &x509::X509StoreContextRef, expected_fingerprint: &[u8]) -> bool {
|
||||||
let Some(cert) = chain.current_cert() else {
|
let Some(cert) = chain.current_cert() else {
|
||||||
log::error!("no certificate in chain?");
|
log::error!("no certificate in chain?");
|
||||||
@ -369,325 +409,27 @@ fn fp_string(fp: &[u8]) -> String {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
impl Error {
|
||||||
impl Client {
|
pub(crate) fn internal<E>(context: &'static str, err: E) -> Self
|
||||||
/// If currently logged in, this will fill in the auth cookie and CSRFPreventionToken header
|
where
|
||||||
/// and return `Ok(request)`, otherwise it'll return `Err(request)` with the request
|
E: StdError + Send + Sync + 'static,
|
||||||
/// unmodified.
|
{
|
||||||
pub fn try_set_auth_headers(
|
Self::Internal(context, Box::new(err))
|
||||||
&self,
|
}
|
||||||
request: http::request::Builder,
|
}
|
||||||
) -> Result<http::request::Builder, http::request::Builder> {
|
|
||||||
let auth = self.auth.lock().unwrap().clone();
|
impl AuthenticationKind {
|
||||||
match auth {
|
pub fn set_auth_headers(&self, request: http::request::Builder) -> http::request::Builder {
|
||||||
Some(auth) => Ok(auth.set_auth_headers(request)),
|
match self {
|
||||||
None => Err(request),
|
AuthenticationKind::Ticket(auth) => auth.set_auth_headers(request),
|
||||||
|
AuthenticationKind::Token(auth) => auth.set_auth_headers(request),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempt to login.
|
pub fn userid(&self) -> &str {
|
||||||
///
|
match self {
|
||||||
/// This will propagate the PVE compatibility state and then perform the `Login` request via
|
AuthenticationKind::Ticket(auth) => &auth.userid,
|
||||||
/// the inner http client.
|
AuthenticationKind::Token(auth) => &auth.userid,
|
||||||
///
|
|
||||||
/// 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(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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::error::Error as StdError;
|
||||||
use std::fmt::{self, Display};
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
@ -10,6 +10,12 @@ pub enum Error {
|
|||||||
/// The API responded with an error code.
|
/// The API responded with an error code.
|
||||||
Api(http::StatusCode, String),
|
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.
|
/// An error occurred in the authentication API.
|
||||||
Authentication(proxmox_login::error::ResponseError),
|
Authentication(proxmox_login::error::ResponseError),
|
||||||
|
|
||||||
@ -33,6 +39,7 @@ impl StdError for Error {
|
|||||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||||
match self {
|
match self {
|
||||||
Self::Authentication(err) => Some(err),
|
Self::Authentication(err) => Some(err),
|
||||||
|
Self::BadApi(_, Some(err)) => Some(&**err),
|
||||||
Self::Ticket(err) => Some(err),
|
Self::Ticket(err) => Some(err),
|
||||||
Self::Client(err) => Some(&**err),
|
Self::Client(err) => Some(&**err),
|
||||||
Self::Internal(_, err) => Some(&**err),
|
Self::Internal(_, err) => Some(&**err),
|
||||||
@ -47,6 +54,8 @@ impl fmt::Display for Error {
|
|||||||
match self {
|
match self {
|
||||||
Self::Unauthorized => f.write_str("unauthorized"),
|
Self::Unauthorized => f.write_str("unauthorized"),
|
||||||
Self::Api(status, msg) => write!(f, "api error (status = {status}): {msg}"),
|
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::Other(err) => f.write_str(err),
|
||||||
Self::Authentication(err) => write!(f, "authentication error: {err}"),
|
Self::Authentication(err) => write!(f, "authentication error: {err}"),
|
||||||
Self::Ticket(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 {
|
impl From<proxmox_login::error::ResponseError> for Error {
|
||||||
fn from(err: proxmox_login::error::ResponseError) -> Self {
|
fn from(err: proxmox_login::error::ResponseError) -> Self {
|
||||||
Self::Authentication(err)
|
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 std::future::Future;
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
|
|
||||||
@ -17,34 +19,130 @@ mod client;
|
|||||||
#[cfg(feature = "hyper-client")]
|
#[cfg(feature = "hyper-client")]
|
||||||
pub use client::{Client, TlsOptions};
|
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
|
/// HTTP client backend trait. This should be implemented for a HTTP client capable of making
|
||||||
/// *authenticated* API requests to a proxmox HTTP API.
|
/// *authenticated* API requests to a proxmox HTTP API.
|
||||||
pub trait HttpApiClient: Send + Sync {
|
pub trait HttpApiClient: Send + Sync {
|
||||||
/// An API call should return a status code and the raw body.
|
/// 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).
|
/// `GET` request with a path and query component (no hostname).
|
||||||
///
|
///
|
||||||
/// For this request, authentication headers should be set!
|
/// 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.
|
/// `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`.
|
/// The body should be serialized to json and sent with `Content-type: applicaion/json`.
|
||||||
///
|
///
|
||||||
/// For this request, authentication headers should be set!
|
/// 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
|
where
|
||||||
T: ?Sized + Serialize;
|
T: ?Sized + Serialize;
|
||||||
|
|
||||||
/// `DELETE` request with a path and query component (no hostname).
|
/// `DELETE` request with a path and query component (no hostname).
|
||||||
///
|
///
|
||||||
/// For this request, authentication headers should be set!
|
/// 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…
x
Reference in New Issue
Block a user