mirror of
https://git.proxmox.com/git/proxmox
synced 2025-05-23 09:40:20 +00:00

The get/put/post/put_without_body/... methods now have a default implementation forwarding to a generic `request` method as all our implementations do the same already anyway. Additionally, in order to allow easy access to a "streaming body", the Body type is now exposed. In the future, this crate may also require a wrapper to standardize the handling of `application/json-seq` streams if we end up using them, but for now, a simple way to expose the body is enough to get going. Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
451 lines
13 KiB
Rust
451 lines
13 KiB
Rust
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
|
|
|
|
use std::collections::HashMap;
|
|
use std::future::Future;
|
|
|
|
use http::Method;
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::Value;
|
|
|
|
mod error;
|
|
|
|
pub use error::Error;
|
|
|
|
pub use proxmox_login::tfa::TfaChallenge;
|
|
pub use proxmox_login::{Authentication, Ticket};
|
|
|
|
pub(crate) mod auth;
|
|
pub use auth::{AuthenticationKind, Token};
|
|
|
|
#[cfg(feature = "hyper-client")]
|
|
mod client;
|
|
#[cfg(feature = "hyper-client")]
|
|
pub use client::{Client, TlsOptions};
|
|
|
|
/// 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 {
|
|
/// An API call should return a status code and the raw body.
|
|
type ResponseFuture<'a>: Future<Output = Result<HttpApiResponse, Error>> + 'a
|
|
where
|
|
Self: 'a;
|
|
|
|
/// Some requests are better "streamed" than collected in RAM, for this, the body type used by
|
|
/// the underlying client needs to be exposed.
|
|
type Body;
|
|
|
|
/// Future for streamed requests.
|
|
type ResponseStreamFuture<'a>: Future<Output = Result<HttpApiResponseStream<Self::Body>, Error>>
|
|
+ 'a
|
|
where
|
|
Self: 'a;
|
|
|
|
/// An *authenticated* asynchronous request with a path and query component (no hostname), and
|
|
/// an optional body, of which the response body is read to completion.
|
|
///
|
|
/// For this request, authentication headers should be set!
|
|
fn request<'a, T>(
|
|
&'a self,
|
|
method: Method,
|
|
path_and_query: &'a str,
|
|
params: Option<T>,
|
|
) -> Self::ResponseFuture<'a>
|
|
where
|
|
T: Serialize + 'a;
|
|
|
|
/// An *authenticated* asynchronous request with a path and query component (no hostname), and
|
|
/// an optional body. The response status is returned, but the body is returned for the caller
|
|
/// to read from.
|
|
///
|
|
/// For this request, authentication headers should be set!
|
|
fn streaming_request<'a, T>(
|
|
&'a self,
|
|
method: Method,
|
|
path_and_query: &'a str,
|
|
params: Option<T>,
|
|
) -> Self::ResponseStreamFuture<'a>
|
|
where
|
|
T: Serialize + 'a;
|
|
|
|
/// This is deprecated.
|
|
/// Calls `self.request` with `Method::GET` and `None` for the body.
|
|
fn get<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
|
|
self.request(Method::GET, path_and_query, None::<()>)
|
|
}
|
|
|
|
/// This is deprecated.
|
|
/// Calls `self.request` with `Method::POST`.
|
|
fn post<'a, T>(&'a self, path_and_query: &'a str, params: &'a T) -> Self::ResponseFuture<'a>
|
|
where
|
|
T: ?Sized + Serialize,
|
|
{
|
|
self.request(Method::POST, path_and_query, Some(params))
|
|
}
|
|
|
|
/// This is deprecated.
|
|
/// Calls `self.request` with `Method::POST` and `None` for the body..
|
|
fn post_without_body<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
|
|
self.request(Method::POST, path_and_query, None::<()>)
|
|
}
|
|
|
|
/// This is deprecated.
|
|
/// Calls `self.request` with `Method::PUT`.
|
|
fn put<'a, T>(&'a self, path_and_query: &'a str, params: &'a T) -> Self::ResponseFuture<'a>
|
|
where
|
|
T: ?Sized + Serialize,
|
|
{
|
|
self.request(Method::PUT, path_and_query, Some(params))
|
|
}
|
|
|
|
/// This is deprecated.
|
|
/// Calls `self.request` with `Method::PUT` and `None` for the body..
|
|
fn put_without_body<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
|
|
self.request(Method::PUT, path_and_query, None::<()>)
|
|
}
|
|
|
|
/// This is deprecated.
|
|
/// Calls `self.request` with `Method::DELETE`.
|
|
fn delete<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
|
|
self.request(Method::DELETE, path_and_query, None::<()>)
|
|
}
|
|
}
|
|
|
|
/// 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 returned 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()
|
|
.and_then(|v| v.split(';').next())
|
|
{
|
|
Some("application/json") => Ok(()),
|
|
Some(other) => Err(Error::BadApi(
|
|
format!("expected json body, got {other}",),
|
|
None,
|
|
)),
|
|
None => 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("unexpected api response", err))?;
|
|
|
|
if response.data.is_some() {
|
|
Err(Error::UnexpectedData)
|
|
} else {
|
|
response.check_nodata()?;
|
|
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>
|
|
where
|
|
T: for<'de> Deserialize<'de>,
|
|
{
|
|
fn check_success(mut self) -> Result<Self, Error> {
|
|
if self.success == Some(true) {
|
|
return Ok(self);
|
|
}
|
|
|
|
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}");
|
|
}
|
|
|
|
Err(Error::api(status, message))
|
|
}
|
|
|
|
fn check(self) -> Result<ApiResponseData<T>, Error> {
|
|
let this = self.check_success()?;
|
|
|
|
// RawApiResponse has no data, but this also happens for Value::Null, and T
|
|
// might be deserializeable from that, so try here again
|
|
let data = match this.data {
|
|
Some(data) => data,
|
|
None => serde_json::from_value(Value::Null)
|
|
.map_err(|_| Error::BadApi("api returned no data".to_string(), None))?,
|
|
};
|
|
|
|
Ok(ApiResponseData {
|
|
data,
|
|
attribs: this.attribs,
|
|
})
|
|
}
|
|
|
|
fn check_nodata(self) -> Result<ApiResponseData<()>, Error> {
|
|
let this = self.check_success()?;
|
|
|
|
Ok(ApiResponseData {
|
|
data: (),
|
|
attribs: this.attribs,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl<'c, C> HttpApiClient for &'c C
|
|
where
|
|
C: HttpApiClient,
|
|
{
|
|
type ResponseFuture<'a> = C::ResponseFuture<'a>
|
|
where
|
|
Self: 'a;
|
|
|
|
type Body = C::Body;
|
|
|
|
type ResponseStreamFuture<'a> = C::ResponseStreamFuture<'a>
|
|
where
|
|
Self: 'a;
|
|
|
|
fn request<'a, T>(
|
|
&'a self,
|
|
method: Method,
|
|
path_and_query: &'a str,
|
|
params: Option<T>,
|
|
) -> Self::ResponseFuture<'a>
|
|
where
|
|
T: Serialize + 'a,
|
|
{
|
|
C::request(self, method, path_and_query, params)
|
|
}
|
|
|
|
fn streaming_request<'a, T>(
|
|
&'a self,
|
|
method: Method,
|
|
path_and_query: &'a str,
|
|
params: Option<T>,
|
|
) -> Self::ResponseStreamFuture<'a>
|
|
where
|
|
T: Serialize + 'a,
|
|
{
|
|
C::streaming_request(self, method, path_and_query, params)
|
|
}
|
|
|
|
fn get<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
|
|
C::get(self, path_and_query)
|
|
}
|
|
|
|
fn post<'a, T>(&'a self, path_and_query: &'a str, params: &'a T) -> Self::ResponseFuture<'a>
|
|
where
|
|
T: ?Sized + Serialize,
|
|
{
|
|
C::post(self, path_and_query, params)
|
|
}
|
|
|
|
fn post_without_body<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
|
|
C::post_without_body(self, path_and_query)
|
|
}
|
|
|
|
fn put<'a, T>(&'a self, path_and_query: &'a str, params: &'a T) -> Self::ResponseFuture<'a>
|
|
where
|
|
T: ?Sized + Serialize,
|
|
{
|
|
C::put(self, path_and_query, params)
|
|
}
|
|
|
|
fn put_without_body<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
|
|
C::put_without_body(self, path_and_query)
|
|
}
|
|
|
|
fn delete<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
|
|
C::delete(self, path_and_query)
|
|
}
|
|
}
|
|
|
|
impl<C> HttpApiClient for std::sync::Arc<C>
|
|
where
|
|
C: HttpApiClient,
|
|
{
|
|
type ResponseFuture<'a> = C::ResponseFuture<'a>
|
|
where
|
|
Self: 'a;
|
|
|
|
type Body = C::Body;
|
|
|
|
type ResponseStreamFuture<'a> = C::ResponseStreamFuture<'a>
|
|
where
|
|
Self: 'a;
|
|
|
|
fn request<'a, T>(
|
|
&'a self,
|
|
method: Method,
|
|
path_and_query: &'a str,
|
|
params: Option<T>,
|
|
) -> Self::ResponseFuture<'a>
|
|
where
|
|
T: Serialize + 'a,
|
|
{
|
|
C::request(self, method, path_and_query, params)
|
|
}
|
|
|
|
fn streaming_request<'a, T>(
|
|
&'a self,
|
|
method: Method,
|
|
path_and_query: &'a str,
|
|
params: Option<T>,
|
|
) -> Self::ResponseStreamFuture<'a>
|
|
where
|
|
T: Serialize + 'a,
|
|
{
|
|
C::streaming_request(self, method, path_and_query, params)
|
|
}
|
|
|
|
fn get<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
|
|
C::get(self, path_and_query)
|
|
}
|
|
|
|
fn post<'a, T>(&'a self, path_and_query: &'a str, params: &'a T) -> Self::ResponseFuture<'a>
|
|
where
|
|
T: ?Sized + Serialize,
|
|
{
|
|
C::post(self, path_and_query, params)
|
|
}
|
|
|
|
fn post_without_body<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
|
|
C::post_without_body(self, path_and_query)
|
|
}
|
|
|
|
fn put<'a, T>(&'a self, path_and_query: &'a str, params: &'a T) -> Self::ResponseFuture<'a>
|
|
where
|
|
T: ?Sized + Serialize,
|
|
{
|
|
C::put(self, path_and_query, params)
|
|
}
|
|
|
|
fn put_without_body<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
|
|
C::put_without_body(self, path_and_query)
|
|
}
|
|
|
|
fn delete<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
|
|
C::delete(self, path_and_query)
|
|
}
|
|
}
|
|
|
|
impl<C> HttpApiClient for std::rc::Rc<C>
|
|
where
|
|
C: HttpApiClient,
|
|
{
|
|
type ResponseFuture<'a> = C::ResponseFuture<'a>
|
|
where
|
|
Self: 'a;
|
|
|
|
type Body = C::Body;
|
|
|
|
type ResponseStreamFuture<'a> = C::ResponseStreamFuture<'a>
|
|
where
|
|
Self: 'a;
|
|
|
|
fn request<'a, T>(
|
|
&'a self,
|
|
method: Method,
|
|
path_and_query: &'a str,
|
|
params: Option<T>,
|
|
) -> Self::ResponseFuture<'a>
|
|
where
|
|
T: Serialize + 'a,
|
|
{
|
|
C::request(self, method, path_and_query, params)
|
|
}
|
|
|
|
fn streaming_request<'a, T>(
|
|
&'a self,
|
|
method: Method,
|
|
path_and_query: &'a str,
|
|
params: Option<T>,
|
|
) -> Self::ResponseStreamFuture<'a>
|
|
where
|
|
T: Serialize + 'a,
|
|
{
|
|
C::streaming_request(self, method, path_and_query, params)
|
|
}
|
|
|
|
fn get<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
|
|
C::get(self, path_and_query)
|
|
}
|
|
|
|
fn post<'a, T>(&'a self, path_and_query: &'a str, params: &'a T) -> Self::ResponseFuture<'a>
|
|
where
|
|
T: ?Sized + Serialize,
|
|
{
|
|
C::post(self, path_and_query, params)
|
|
}
|
|
|
|
fn post_without_body<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
|
|
C::post_without_body(self, path_and_query)
|
|
}
|
|
|
|
fn put<'a, T>(&'a self, path_and_query: &'a str, params: &'a T) -> Self::ResponseFuture<'a>
|
|
where
|
|
T: ?Sized + Serialize,
|
|
{
|
|
C::put(self, path_and_query, params)
|
|
}
|
|
|
|
fn put_without_body<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
|
|
C::put_without_body(self, path_and_query)
|
|
}
|
|
|
|
fn delete<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> {
|
|
C::delete(self, path_and_query)
|
|
}
|
|
}
|
|
|
|
/// A streaming response from the HTTP API as required by the [`HttpApiClient`] trait.
|
|
pub struct HttpApiResponseStream<Body> {
|
|
pub status: u16,
|
|
pub content_type: Option<String>,
|
|
/// Requests where the response has no body may put `None` here.
|
|
pub body: Option<Body>,
|
|
}
|