diff --git a/proxmox-http/Cargo.toml b/proxmox-http/Cargo.toml index 541c5dd2..bfc86c74 100644 --- a/proxmox-http/Cargo.toml +++ b/proxmox-http/Cargo.toml @@ -21,6 +21,7 @@ openssl = { version = "0.10", optional = true } serde_json = { version = "1.0", optional = true } tokio = { version = "1.0", features = [], optional = true } tokio-openssl = { version = "0.6.1", optional = true } +ureq = { version = "2.4", features = ["native-certs"], optional = true } url = { version = "2", optional = true } proxmox-async = { path = "../proxmox-async", optional = true, version = "0.4.1" } @@ -32,6 +33,7 @@ proxmox-lang = { path = "../proxmox-lang", optional = true, version = "1.1" } default = [] client = [ "dep:futures", "http-helpers", "dep:hyper", "hyper?/full", "dep:openssl", "dep:tokio", "tokio?/io-util", "dep:tokio-openssl" ] +client-sync = [ "client-trait", "http-helpers", "dep:ureq" ] client-trait = [ "dep:http" ] http-helpers = [ "dep:base64", "dep:http", "dep:proxmox-sys", "dep:serde_json", "dep:url" ] websocket = [ diff --git a/proxmox-http/src/client/mod.rs b/proxmox-http/src/client/mod.rs index f56a6778..5bbd4d35 100644 --- a/proxmox-http/src/client/mod.rs +++ b/proxmox-http/src/client/mod.rs @@ -1,17 +1,36 @@ -//! Simple TLS capable HTTP client implementation. +//! Simple TLS capable HTTP client implementations. //! -//! Contains a lightweight wrapper around `hyper` with support for TLS connections. +//! Feature `client` contains a lightweight wrapper around `hyper` with support for TLS connections +//! in [`SimpleHttp`](crate::client::SimpleHttp). +//! +//! Feature `client-sync` contains a lightweight wrapper around `ureq` in +//! [`sync::Client`](crate::client::sync::Client). +//! +//! Both clients implement [`HttpClient`](crate::HttpClient) if the feature `client-trait` is enabled. +#[cfg(feature = "client")] mod rate_limiter; +#[cfg(feature = "client")] pub use rate_limiter::{RateLimit, RateLimiter, RateLimiterVec, ShareableRateLimit}; +#[cfg(feature = "client")] mod rate_limited_stream; +#[cfg(feature = "client")] pub use rate_limited_stream::RateLimitedStream; +#[cfg(feature = "client")] mod connector; +#[cfg(feature = "client")] pub use connector::HttpsConnector; +#[cfg(feature = "client")] mod simple; +#[cfg(feature = "client")] pub use simple::SimpleHttp; +#[cfg(feature = "client")] pub mod tls; + +#[cfg(feature = "client-sync")] +/// Blocking HTTP client +pub mod sync; diff --git a/proxmox-http/src/client/sync.rs b/proxmox-http/src/client/sync.rs new file mode 100644 index 00000000..b8d86f24 --- /dev/null +++ b/proxmox-http/src/client/sync.rs @@ -0,0 +1,108 @@ +use std::collections::HashMap; + +use anyhow::{format_err, Error}; +use http::Response; + +use crate::HttpClient; +use crate::HttpOptions; + +pub const DEFAULT_USER_AGENT_STRING: &str = "proxmox-sync-http-client/0.1"; + +#[derive(Default)] +/// Blocking HTTP client for usage with [`HttpClient`]. +pub struct Client { + options: HttpOptions, +} + +impl Client { + pub fn new(options: HttpOptions) -> Self { + Self { options } + } + + fn agent(&self) -> Result { + let mut builder = ureq::AgentBuilder::new(); + if let Some(proxy_config) = &self.options.proxy_config { + builder = builder.proxy(ureq::Proxy::new(proxy_config.to_proxy_string()?)?); + } + + Ok(builder.build()) + } + + fn exec_request( + &self, + req: ureq::Request, + body: Option<&str>, + ) -> Result, Error> { + let req = req.set( + "User-Agent", + self.options + .user_agent + .as_deref() + .unwrap_or(DEFAULT_USER_AGENT_STRING), + ); + + let res = match body { + Some(body) => req.send_string(body), + None => req.call(), + }?; + + let mut builder = http::response::Builder::new() + .status(http::status::StatusCode::from_u16(res.status())?); + + for header in res.headers_names() { + if let Some(value) = res.header(&header) { + builder = builder.header(header, value); + } + } + builder + .body(res.into_string()?) + .map_err(|err| format_err!("Failed to convert HTTP response - {err}")) + } +} + +impl HttpClient for Client { + fn get( + &self, + uri: &str, + extra_headers: Option<&HashMap>, + ) -> Result, Error> { + let mut req = self.agent()?.get(uri); + + if let Some(extra_headers) = extra_headers { + for (header, value) in extra_headers { + req = req.set(header, value); + } + } + + self.exec_request(req, None) + } + + fn post( + &self, + uri: &str, + body: Option<&str>, + content_type: Option<&str>, + ) -> Result, Error> { + let mut req = self.agent()?.post(uri); + if let Some(content_type) = content_type { + req = req.set("Content-Type", content_type); + } + + self.exec_request(req, body) + } + + fn request(&self, request: http::Request) -> Result, Error> { + let mut req = self + .agent()? + .request(request.method().as_str(), &request.uri().to_string()); + let orig_headers = request.headers(); + + for header in orig_headers.keys() { + for value in orig_headers.get_all(header) { + req = req.set(header.as_str(), value.to_str()?); + } + } + + self.exec_request(req, Some(request.body().as_str())) + } +} diff --git a/proxmox-http/src/lib.rs b/proxmox-http/src/lib.rs index f32cfec0..40efcd1d 100644 --- a/proxmox-http/src/lib.rs +++ b/proxmox-http/src/lib.rs @@ -16,7 +16,7 @@ mod http_options; #[cfg(feature = "http-helpers")] pub use http_options::HttpOptions; -#[cfg(feature = "client")] +#[cfg(any(feature = "client", feature = "client-sync"))] pub mod client; #[cfg(feature = "client-trait")] diff --git a/proxmox-http/src/proxy_config.rs b/proxmox-http/src/proxy_config.rs index 0881c098..f874ce13 100644 --- a/proxmox-http/src/proxy_config.rs +++ b/proxmox-http/src/proxy_config.rs @@ -1,6 +1,6 @@ //! HTTP proxy configuration. //! -//! This can be used with the [`SimpleHttp`](crate::client::SimpleHttp). +//! This can be used with the async [`SimpleHttp`](crate::client::SimpleHttp) or sync [`Client`](crate::client::sync::Client). use anyhow::{bail, format_err, Error};