#![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> + '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, 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, ) -> 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, ) -> 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, pub body: Vec, } impl HttpApiResponse { /// Expect a JSON response as returned by the `extjs` formatter. pub fn expect_json(self) -> Result, Error> where T: for<'de> Deserialize<'de>, { self.assert_json_content_type()?; serde_json::from_slice::>(&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::>(&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 { pub attribs: HashMap, pub data: T, } #[derive(serde::Deserialize)] struct RawApiResponse { #[serde(default, deserialize_with = "proxmox_login::parse::deserialize_u16")] status: Option, message: Option, #[serde(default, deserialize_with = "proxmox_login::parse::deserialize_bool")] success: Option, data: Option, #[serde(default)] errors: HashMap, #[serde(default, flatten)] attribs: HashMap, } impl RawApiResponse where T: for<'de> Deserialize<'de>, { fn check_success(mut self) -> Result { 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, 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, 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, ) -> 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, ) -> 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 HttpApiClient for std::sync::Arc 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, ) -> 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, ) -> 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 HttpApiClient for std::rc::Rc 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, ) -> 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, ) -> 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 { pub status: u16, pub content_type: Option, /// Requests where the response has no body may put `None` here. pub body: Option, }