use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use core::str::FromStr;
use core::time::Duration;
use std::sync::Arc;
use bytes::Bytes;
use crate::client::ProductOSClient;
use crate::error::ProductOSRequestError;
use crate::method::Method;
use crate::policy::RedirectPolicy;
use crate::request::ProductOSRequest;
use crate::requester::ProductOSRequester;
use crate::response::ProductOSResponse;
use product_os_http::Request;
use product_os_http_body::{BodyBytes, BodyDataStream, BodyExt};
use hyper::body::Incoming;
use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
use hyper_util::client::legacy::{connect::HttpConnector, Client};
use hyper_util::rt::TokioExecutor;
#[derive(Clone)]
#[allow(dead_code)]
struct ClientConfig {
default_headers: product_os_http::HeaderMap,
https_only: bool,
timeout: Option<Duration>,
connect_timeout: Option<Duration>,
trust_all_certificates: bool,
redirect_policy: RedirectPolicy,
max_idle_per_host: usize,
#[cfg(feature = "hyper_cookies")]
cookie_jar: Option<Arc<std::sync::Mutex<cookie_store::CookieStore>>>,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
default_headers: product_os_http::HeaderMap::new(),
https_only: false,
timeout: None,
connect_timeout: None,
trust_all_certificates: false,
redirect_policy: RedirectPolicy::Default,
max_idle_per_host: 32,
#[cfg(feature = "hyper_cookies")]
cookie_jar: Some(Arc::new(std::sync::Mutex::new(
cookie_store::CookieStore::default(),
))),
}
}
}
#[derive(Clone)]
pub struct ProductOSHyperClient {
client: Client<HttpsConnector<HttpConnector>, BodyBytes>,
config: Arc<ClientConfig>,
}
impl ProductOSHyperClient {
pub fn new() -> Self {
Self::default()
}
fn method_to_http(method: &Method) -> product_os_http::Method {
match method {
Method::GET => product_os_http::Method::GET,
Method::POST => product_os_http::Method::POST,
Method::PATCH => product_os_http::Method::PATCH,
Method::PUT => product_os_http::Method::PUT,
Method::DELETE => product_os_http::Method::DELETE,
Method::TRACE => product_os_http::Method::TRACE,
Method::HEAD => product_os_http::Method::HEAD,
Method::OPTIONS => product_os_http::Method::OPTIONS,
Method::CONNECT => product_os_http::Method::CONNECT,
Method::ANY => product_os_http::Method::GET,
}
}
fn build_request_with_body(
&self,
request: ProductOSRequest<BodyBytes>,
) -> Result<Request<BodyBytes>, ProductOSRequestError> {
let method = Self::method_to_http(&request.method);
let mut builder = Request::builder();
let mut query_string = String::new();
for (i, (key, value)) in request.query.iter().enumerate() {
if i > 0 {
query_string.push('&');
}
query_string.push_str(&crate::encode::percent_encode(key));
query_string.push('=');
query_string.push_str(&crate::encode::percent_encode(value));
}
let url = if !query_string.is_empty() {
if request.url.to_string().contains('?') {
format!("{}&{}", request.url, query_string)
} else {
format!("{}?{}", request.url, query_string)
}
} else {
request.url.to_string()
};
let uri = match product_os_http::Uri::from_str(&url) {
Ok(u) => u,
Err(e) => {
tracing::error!("Invalid URI: {:?}", e);
return Err(ProductOSRequestError::Error(format!("Invalid URI: {}", e)));
}
};
builder = builder.uri(uri);
builder = builder.method(method);
for (name, value) in self.config.default_headers.iter() {
builder = builder.header(name, value);
}
for (key, value) in &request.headers {
if let Ok(name) = product_os_http::HeaderName::from_str(key.as_str()) {
builder = builder.header(name, value.clone());
}
}
if let Some(auth) = &request.bearer_auth {
let auth_value = format!("Bearer {}", auth);
if let Ok(val) = product_os_http::HeaderValue::from_str(&auth_value) {
builder = builder.header(product_os_http::header::AUTHORIZATION, val);
}
}
#[cfg(feature = "hyper_compression")]
{
if !request.headers.contains_key("accept-encoding") {
if let Ok(val) = product_os_http::HeaderValue::from_str("gzip, br, deflate") {
builder = builder.header(product_os_http::header::ACCEPT_ENCODING, val);
}
}
}
#[cfg(feature = "hyper_cookies")]
{
if let Some(ref cookie_jar) = self.config.cookie_jar {
if let Ok(jar) = cookie_jar.lock() {
if let Ok(url_parsed) = url::Url::parse(&url) {
let matching_cookies = jar.matches(&url_parsed);
let cookie_strings: Vec<String> = matching_cookies
.iter()
.map(|c| format!("{}={}", c.name(), c.value()))
.collect();
if !cookie_strings.is_empty() {
let cookie_header = cookie_strings.join("; ");
if let Ok(val) = product_os_http::HeaderValue::from_str(&cookie_header)
{
builder = builder.header(product_os_http::header::COOKIE, val);
}
}
}
}
}
}
let body = request.body.unwrap_or_else(|| BodyBytes::new(Bytes::new()));
match builder.body(body) {
Ok(req) => Ok(req),
Err(e) => {
tracing::error!("Failed to build request: {:?}", e);
Err(ProductOSRequestError::Error(e.to_string()))
}
}
}
async fn execute_with_redirects(
&self,
request: Request<BodyBytes>,
) -> Result<hyper::Response<Incoming>, ProductOSRequestError> {
self.execute_with_redirects_and_timeout(request).await
}
async fn execute_with_redirects_and_timeout(
&self,
request: Request<BodyBytes>,
) -> Result<hyper::Response<Incoming>, ProductOSRequestError> {
#[cfg(feature = "hyper_timeout")]
{
if let Some(timeout_duration) = self.config.timeout {
return self.execute_with_timeout(request, timeout_duration).await;
}
}
self.execute_redirects_internal(request).await
}
#[cfg(feature = "hyper_timeout")]
async fn execute_with_timeout(
&self,
request: Request<BodyBytes>,
timeout_duration: Duration,
) -> Result<hyper::Response<Incoming>, ProductOSRequestError> {
match tokio::time::timeout(timeout_duration, self.execute_redirects_internal(request)).await
{
Ok(result) => result,
Err(_) => Err(ProductOSRequestError::Error(format!(
"Request timed out after {:?}",
timeout_duration
))),
}
}
async fn execute_redirects_internal(
&self,
request: Request<BodyBytes>,
) -> Result<hyper::Response<Incoming>, ProductOSRequestError> {
let max_redirects = match &self.config.redirect_policy {
RedirectPolicy::None => 0,
RedirectPolicy::Limit(n) => *n,
RedirectPolicy::Default => 10,
};
let mut current_request = request;
let mut redirect_count = 0;
let original_headers = current_request.headers().clone();
loop {
let (parts, body) = current_request.into_parts();
let uri = parts.uri.clone();
current_request = Request::from_parts(parts, body);
let response = match self.client.request(current_request).await {
Ok(resp) => resp,
Err(e) => {
tracing::error!("Request failed: {:?}", e);
return Err(ProductOSRequestError::Error(e.to_string()));
}
};
let status = response.status();
if status.is_redirection() && redirect_count < max_redirects {
if let Some(location) = response.headers().get(product_os_http::header::LOCATION) {
if let Ok(location_str) = location.to_str() {
redirect_count += 1;
tracing::debug!(
"Following redirect {} to: {}",
redirect_count,
location_str
);
let new_uri = if location_str.starts_with("http://")
|| location_str.starts_with("https://")
{
match product_os_http::Uri::from_str(location_str) {
Ok(u) => u,
Err(e) => {
tracing::error!("Invalid redirect URI: {:?}", e);
return Err(ProductOSRequestError::Error(format!(
"Invalid redirect URI: {}",
e
)));
}
}
} else {
let scheme = uri.scheme_str().unwrap_or("https");
let authority = uri.authority().map(|a| a.as_str()).unwrap_or("");
let new_url = if location_str.starts_with('/') {
format!("{}://{}{}", scheme, authority, location_str)
} else {
let path = uri.path();
let base_path = if let Some(pos) = path.rfind('/') {
&path[..=pos]
} else {
"/"
};
format!("{}://{}{}{}", scheme, authority, base_path, location_str)
};
match product_os_http::Uri::from_str(&new_url) {
Ok(u) => u,
Err(e) => {
tracing::error!("Invalid redirect URI: {:?}", e);
return Err(ProductOSRequestError::Error(format!(
"Invalid redirect URI: {}",
e
)));
}
}
};
let mut new_req = Request::builder()
.uri(new_uri)
.method(product_os_http::Method::GET)
.body(BodyBytes::new(Bytes::new()))
.map_err(|e| ProductOSRequestError::Error(e.to_string()))?;
for (name, value) in original_headers.iter() {
if name != product_os_http::header::HOST {
new_req.headers_mut().insert(name.clone(), value.clone());
}
}
current_request = new_req;
continue;
}
}
}
return Ok(response);
}
}
async fn convert_response(
&self,
response: hyper::Response<Incoming>,
final_url: String,
) -> Result<ProductOSResponse<BodyBytes>, ProductOSRequestError> {
#[cfg(feature = "hyper_cookies")]
{
if let Some(ref cookie_jar) = self.config.cookie_jar {
if let Ok(url) = url::Url::parse(&final_url) {
if let Ok(mut jar) = cookie_jar.lock() {
for cookie_val in response
.headers()
.get_all(product_os_http::header::SET_COOKIE)
{
if let Ok(cookie_str) = cookie_val.to_str() {
if let Ok(cookie) =
cookie_store::Cookie::parse(cookie_str.to_string(), &url)
{
let _ = jar.insert(cookie, &url);
}
}
}
}
}
}
}
#[cfg(feature = "hyper_compression")]
{
if let Some(encoding) = response
.headers()
.get(product_os_http::header::CONTENT_ENCODING)
{
if let Ok(encoding_str) = encoding.to_str() {
tracing::debug!("Response has content-encoding: {}", encoding_str);
}
}
}
let (parts, incoming_body) = response.into_parts();
let body_bytes = match http_body_util::BodyExt::collect(incoming_body).await {
Ok(collected) => collected.to_bytes(),
Err(e) => {
tracing::error!("Failed to read response body: {:?}", e);
return Err(ProductOSRequestError::Error(e.to_string()));
}
};
let body = BodyBytes::new(body_bytes);
let mut builder = product_os_http::Response::builder().status(parts.status);
for (name, value) in parts.headers.iter() {
builder = builder.header(name, value);
}
match builder.body(body) {
Ok(http_response) => Ok(ProductOSResponse::from_response(http_response, final_url)),
Err(e) => Err(ProductOSRequestError::Error(e.to_string())),
}
}
}
impl ProductOSClient<product_os_http_body::BodyBytes, product_os_http_body::BodyBytes>
for ProductOSHyperClient
{
fn build(&mut self, requester: &ProductOSRequester) {
let _ = rustls::crypto::ring::default_provider().install_default();
let mut roots = rustls::RootCertStore::empty();
let native_cert_result = rustls_native_certs::load_native_certs();
for err in &native_cert_result.errors {
tracing::warn!("Error loading native certificate: {:?}", err);
}
for cert in native_cert_result.certs {
if let Err(e) = roots.add(cert) {
tracing::warn!("Failed to add certificate: {:?}", e);
}
}
for cert_der in &requester.certificates {
let cert = rustls::pki_types::CertificateDer::from(cert_der.clone());
if let Err(e) = roots.add(cert) {
tracing::error!("Failed to add custom certificate: {:?}", e);
}
}
let mut tls_config = rustls::ClientConfig::builder()
.with_root_certificates(roots)
.with_no_client_auth();
if requester.trust_all_certificates {
tls_config
.dangerous()
.set_certificate_verifier(Arc::new(NoVerifier));
}
let https_connector = HttpsConnectorBuilder::new()
.with_tls_config(tls_config)
.https_or_http()
.enable_http1()
.enable_http2()
.build();
let client = Client::builder(TokioExecutor::new())
.pool_idle_timeout(Duration::from_secs(90))
.pool_max_idle_per_host(32)
.build(https_connector);
let config = ClientConfig {
https_only: requester.secure,
timeout: Some(requester.timeout),
connect_timeout: Some(requester.connect_timeout),
trust_all_certificates: requester.trust_all_certificates,
redirect_policy: requester.redirect_policy.clone(),
..Default::default()
};
let mut config = config;
config.default_headers = requester.headers.clone();
self.client = client;
self.config = Arc::new(config);
tracing::trace!("Updated hyper client with configuration");
}
fn new_request(
&self,
method: Method,
url: &str,
) -> ProductOSRequest<product_os_http_body::BodyBytes> {
ProductOSRequest::new(method, url)
}
async fn request(
&self,
r: ProductOSRequest<product_os_http_body::BodyBytes>,
) -> Result<ProductOSResponse<product_os_http_body::BodyBytes>, ProductOSRequestError> {
let url = r.url.to_string();
let request = self.build_request_with_body(r)?;
let response = self.execute_with_redirects(request).await?;
self.convert_response(response, url).await
}
#[cfg(feature = "stream_hyper")]
async fn request_stream(
&self,
r: ProductOSRequest<product_os_http_body::BodyBytes>,
) -> Result<ProductOSResponse<product_os_http_body::BodyBytes>, ProductOSRequestError> {
let url = r.url.to_string();
let request = self.build_request_with_body(r)?;
let response = self.execute_with_redirects(request).await?;
self.convert_response(response, url).await
}
async fn request_simple(
&self,
method: Method,
url: &str,
) -> Result<ProductOSResponse<product_os_http_body::BodyBytes>, ProductOSRequestError> {
let r = ProductOSRequest::<product_os_http_body::BodyBytes>::new(method, url);
self.request(r).await
}
async fn request_raw(
&self,
r: Request<product_os_http_body::BodyBytes>,
) -> Result<ProductOSResponse<product_os_http_body::BodyBytes>, ProductOSRequestError> {
let req = ProductOSRequest::from_request(r);
self.request(req).await
}
#[cfg(feature = "stream_hyper")]
async fn request_stream_raw(
&self,
r: Request<product_os_http_body::BodyBytes>,
) -> Result<ProductOSResponse<product_os_http_body::BodyBytes>, ProductOSRequestError> {
let req = ProductOSRequest::from_request(r);
self.request_stream(req).await
}
#[cfg(feature = "json")]
async fn set_body_json(
&self,
r: &mut ProductOSRequest<product_os_http_body::BodyBytes>,
json: serde_json::Value,
) {
let json_string = json.to_string();
let body = product_os_http_body::BodyBytes::new(bytes::Bytes::from(json_string));
r.body = Some(body);
r.add_header("content-type", "application/json", false);
}
#[cfg(feature = "form")]
async fn set_body_form(
&self,
r: &mut ProductOSRequest<product_os_http_body::BodyBytes>,
form: &str,
) {
match serde_urlencoded::to_string(form) {
Ok(form_string) => {
let body = product_os_http_body::BodyBytes::new(bytes::Bytes::from(form_string));
r.body = Some(body);
r.add_header("content-type", "application/x-www-form-urlencoded", false);
}
Err(e) => {
tracing::error!("Failed to serialize form: {:?}", e);
}
}
}
async fn text(
&self,
r: ProductOSResponse<product_os_http_body::BodyBytes>,
) -> Result<String, ProductOSRequestError> {
match r.response_async {
Some(res) => {
match <product_os_http_body::BodyBytes as Clone>::clone(res.body())
.collect()
.await
{
Ok(body) => match String::from_utf8(body.to_bytes().as_ref().to_vec()) {
Ok(res) => Ok(res),
Err(e) => Err(ProductOSRequestError::Error(e.to_string())),
},
Err(e) => Err(ProductOSRequestError::Error(e.to_string())),
}
}
None => Err(ProductOSRequestError::Error(String::from(
"No response found",
))),
}
}
#[cfg(feature = "json")]
async fn json(
&self,
r: ProductOSResponse<product_os_http_body::BodyBytes>,
) -> Result<serde_json::Value, ProductOSRequestError> {
match r.response_async {
Some(res) => {
match <product_os_http_body::BodyBytes as Clone>::clone(res.body())
.collect()
.await
{
Ok(body) => match String::from_utf8(body.to_bytes().as_ref().to_vec()) {
Ok(res) => match serde_json::from_str(&res) {
Ok(res) => Ok(res),
Err(e) => Err(ProductOSRequestError::Error(e.to_string())),
},
Err(e) => Err(ProductOSRequestError::Error(e.to_string())),
},
Err(e) => Err(ProductOSRequestError::Error(e.to_string())),
}
}
None => Err(ProductOSRequestError::Error(String::from(
"No response found",
))),
}
}
async fn bytes(
&self,
r: ProductOSResponse<product_os_http_body::BodyBytes>,
) -> Result<bytes::Bytes, ProductOSRequestError> {
match r.response_async {
Some(res) => {
match <product_os_http_body::BodyBytes as Clone>::clone(res.body())
.collect()
.await
{
Ok(body) => Ok(body.to_bytes()),
Err(e) => Err(ProductOSRequestError::Error(e.to_string())),
}
}
None => Err(ProductOSRequestError::Error(String::from(
"No response found",
))),
}
}
async fn next_bytes(
&self,
r: &mut ProductOSResponse<product_os_http_body::BodyBytes>,
) -> Result<Option<bytes::Bytes>, ProductOSRequestError> {
match r.response_async {
Some(ref mut res) => {
loop {
if let Some(res) = res.body_mut().frame().await {
let frame = match res {
Ok(frame) => frame,
Err(e) => return Err(ProductOSRequestError::Error(e.to_string())),
};
if let Ok(buf) = frame.into_data() {
return Ok(Some(buf));
}
} else {
return Ok(None);
}
}
}
None => Err(ProductOSRequestError::Error(String::from(
"No response found",
))),
}
}
fn to_stream(
&self,
r: ProductOSResponse<product_os_http_body::BodyBytes>,
) -> Result<BodyDataStream<BodyBytes>, ProductOSRequestError> {
match r.response_async {
Some(res) => Ok(BodyDataStream::new(res.into_body())),
None => Err(ProductOSRequestError::Error(String::from(
"No response found",
))),
}
}
}
impl Default for ProductOSHyperClient {
fn default() -> Self {
let _ = rustls::crypto::ring::default_provider().install_default();
let mut roots = rustls::RootCertStore::empty();
let native_cert_result = rustls_native_certs::load_native_certs();
for cert in native_cert_result.certs {
let _ = roots.add(cert);
}
let tls_config = rustls::ClientConfig::builder()
.with_root_certificates(roots)
.with_no_client_auth();
let https_connector = HttpsConnectorBuilder::new()
.with_tls_config(tls_config)
.https_or_http()
.enable_http1()
.enable_http2()
.build();
let client = Client::builder(TokioExecutor::new()).build(https_connector);
Self {
client,
config: Arc::new(ClientConfig::default()),
}
}
}
#[derive(Debug)]
struct NoVerifier;
impl rustls::client::danger::ServerCertVerifier for NoVerifier {
fn verify_server_cert(
&self,
_end_entity: &rustls::pki_types::CertificateDer<'_>,
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
_server_name: &rustls::pki_types::ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls::pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
vec![
rustls::SignatureScheme::RSA_PKCS1_SHA256,
rustls::SignatureScheme::RSA_PKCS1_SHA384,
rustls::SignatureScheme::RSA_PKCS1_SHA512,
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
rustls::SignatureScheme::RSA_PSS_SHA256,
rustls::SignatureScheme::RSA_PSS_SHA384,
rustls::SignatureScheme::RSA_PSS_SHA512,
rustls::SignatureScheme::ED25519,
]
}
}