use async_trait::async_trait; use futures::{Stream, future, future::BoxFuture, stream, future::TryFutureExt, future::FutureExt, stream::StreamExt}; use hyper::header::{HeaderName, HeaderValue, CONTENT_TYPE}; use hyper::{Body, Request, Response, service::Service, Uri}; use percent_encoding::{utf8_percent_encode, AsciiSet}; use std::borrow::Cow; use std::convert::TryInto; use std::io::{ErrorKind, Read}; use std::error::Error; use std::future::Future; use std::fmt; use std::marker::PhantomData; use std::path::Path; use std::sync::{Arc, Mutex}; use std::str; use std::str::FromStr; use std::string::ToString; use std::task::{Context, Poll}; use swagger::{ApiError, AuthData, BodyExt, Connector, DropContextService, Has, XSpanIdString}; use url::form_urlencoded; use crate::models; use crate::header; /// https://url.spec.whatwg.org/#fragment-percent-encode-set #[allow(dead_code)] const FRAGMENT_ENCODE_SET: &AsciiSet = &percent_encoding::CONTROLS .add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); /// This encode set is used for object IDs /// /// Aside from the special characters defined in the `PATH_SEGMENT_ENCODE_SET`, /// the vertical bar (|) is encoded. #[allow(dead_code)] const ID_ENCODE_SET: &AsciiSet = &FRAGMENT_ENCODE_SET.add(b'|'); use crate::{Api }; use chrono::NaiveDate; /// Convert input into a base path, e.g. "http://example:123". Also checks the scheme as it goes. fn into_base_path(input: impl TryInto, correct_scheme: Option<&'static str>) -> Result { // First convert to Uri, since a base path is a subset of Uri. let uri = input.try_into()?; let scheme = uri.scheme_str().ok_or(ClientInitError::InvalidScheme)?; // Check the scheme if necessary if let Some(correct_scheme) = correct_scheme { if scheme != correct_scheme { return Err(ClientInitError::InvalidScheme); } } let host = uri.host().ok_or_else(|| ClientInitError::MissingHost)?; let port = uri.port_u16().map(|x| format!(":{}", x)).unwrap_or_default(); Ok(format!("{}://{}{}{}", scheme, host, port, uri.path().trim_end_matches('/'))) } /// A client that implements the API by making HTTP calls out to a server. pub struct Client where S: Service< (Request, C), Response=Response> + Clone + Sync + Send + 'static, S::Future: Send + 'static, S::Error: Into + fmt::Display, C: Clone + Send + Sync + 'static { /// Inner service client_service: S, /// Base path of the API base_path: String, /// Marker marker: PhantomData, } impl fmt::Debug for Client where S: Service< (Request, C), Response=Response> + Clone + Sync + Send + 'static, S::Future: Send + 'static, S::Error: Into + fmt::Display, C: Clone + Send + Sync + 'static { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Client {{ base_path: {} }}", self.base_path) } } impl Clone for Client where S: Service< (Request, C), Response=Response> + Clone + Sync + Send + 'static, S::Future: Send + 'static, S::Error: Into + fmt::Display, C: Clone + Send + Sync + 'static { fn clone(&self) -> Self { Self { client_service: self.client_service.clone(), base_path: self.base_path.clone(), marker: PhantomData, } } } impl Client, C>, C> where Connector: hyper::client::connect::Connect + Clone + Send + Sync + 'static, C: Clone + Send + Sync + 'static, { /// Create a client with a custom implementation of hyper::client::Connect. /// /// Intended for use with custom implementations of connect for e.g. protocol logging /// or similar functionality which requires wrapping the transport layer. When wrapping a TCP connection, /// this function should be used in conjunction with `swagger::Connector::builder()`. /// /// For ordinary tcp connections, prefer the use of `try_new_http`, `try_new_https` /// and `try_new_https_mutual`, to avoid introducing a dependency on the underlying transport layer. /// /// # Arguments /// /// * `base_path` - base path of the client API, i.e. "http://www.my-api-implementation.com" /// * `protocol` - Which protocol to use when constructing the request url, e.g. `Some("http")` /// * `connector` - Implementation of `hyper::client::Connect` to use for the client pub fn try_new_with_connector( base_path: &str, protocol: Option<&'static str>, connector: Connector, ) -> Result { let client_service = hyper::client::Client::builder().build(connector); let client_service = DropContextService::new(client_service); Ok(Self { client_service, base_path: into_base_path(base_path, protocol)?, marker: PhantomData, }) } } #[derive(Debug, Clone)] pub enum HyperClient { Http(hyper::client::Client), Https(hyper::client::Client), } impl Service> for HyperClient { type Response = Response; type Error = hyper::Error; type Future = hyper::client::ResponseFuture; fn poll_ready(&mut self, cx: &mut Context) -> Poll> { match self { HyperClient::Http(client) => client.poll_ready(cx), HyperClient::Https(client) => client.poll_ready(cx), } } fn call(&mut self, req: Request) -> Self::Future { match self { HyperClient::Http(client) => client.call(req), HyperClient::Https(client) => client.call(req) } } } impl Client, C> where C: Clone + Send + Sync + 'static, { /// Create an HTTP client. /// /// # Arguments /// * `base_path` - base path of the client API, i.e. "http://www.my-api-implementation.com" pub fn try_new( base_path: &str, ) -> Result { let uri = Uri::from_str(base_path)?; let scheme = uri.scheme_str().ok_or(ClientInitError::InvalidScheme)?; let scheme = scheme.to_ascii_lowercase(); let connector = Connector::builder(); let client_service = match scheme.as_str() { "http" => { HyperClient::Http(hyper::client::Client::builder().build(connector.build())) }, "https" => { let connector = connector.https() .build() .map_err(|e| ClientInitError::SslError(e))?; HyperClient::Https(hyper::client::Client::builder().build(connector)) }, _ => { return Err(ClientInitError::InvalidScheme); } }; let client_service = DropContextService::new(client_service); Ok(Self { client_service, base_path: into_base_path(base_path, None)?, marker: PhantomData, }) } } impl Client, C>, C> where C: Clone + Send + Sync + 'static { /// Create an HTTP client. /// /// # Arguments /// * `base_path` - base path of the client API, i.e. "http://www.my-api-implementation.com" pub fn try_new_http( base_path: &str, ) -> Result { let http_connector = Connector::builder().build(); Self::try_new_with_connector(base_path, Some("http"), http_connector) } } #[cfg(any(target_os = "macos", target_os = "windows", target_os = "ios"))] type HttpsConnector = hyper_tls::HttpsConnector; #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))] type HttpsConnector = hyper_openssl::HttpsConnector; impl Client, C>, C> where C: Clone + Send + Sync + 'static { /// Create a client with a TLS connection to the server /// /// # Arguments /// * `base_path` - base path of the client API, i.e. "https://www.my-api-implementation.com" pub fn try_new_https(base_path: &str) -> Result { let https_connector = Connector::builder() .https() .build() .map_err(|e| ClientInitError::SslError(e))?; Self::try_new_with_connector(base_path, Some("https"), https_connector) } /// Create a client with a TLS connection to the server using a pinned certificate /// /// # Arguments /// * `base_path` - base path of the client API, i.e. "https://www.my-api-implementation.com" /// * `ca_certificate` - Path to CA certificate used to authenticate the server #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))] pub fn try_new_https_pinned( base_path: &str, ca_certificate: CA, ) -> Result where CA: AsRef, { let https_connector = Connector::builder() .https() .pin_server_certificate(ca_certificate) .build() .map_err(|e| ClientInitError::SslError(e))?; Self::try_new_with_connector(base_path, Some("https"), https_connector) } /// Create a client with a mutually authenticated TLS connection to the server. /// /// # Arguments /// * `base_path` - base path of the client API, i.e. "https://www.my-api-implementation.com" /// * `ca_certificate` - Path to CA certificate used to authenticate the server /// * `client_key` - Path to the client private key /// * `client_certificate` - Path to the client's public certificate associated with the private key #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))] pub fn try_new_https_mutual( base_path: &str, ca_certificate: CA, client_key: K, client_certificate: D, ) -> Result where CA: AsRef, K: AsRef, D: AsRef, { let https_connector = Connector::builder() .https() .pin_server_certificate(ca_certificate) .client_authentication(client_key, client_certificate) .build() .map_err(|e| ClientInitError::SslError(e))?; Self::try_new_with_connector(base_path, Some("https"), https_connector) } } impl Client where S: Service< (Request, C), Response=Response> + Clone + Sync + Send + 'static, S::Future: Send + 'static, S::Error: Into + fmt::Display, C: Clone + Send + Sync + 'static { /// Constructor for creating a `Client` by passing in a pre-made `hyper::service::Service` / /// `tower::Service` /// /// This allows adding custom wrappers around the underlying transport, for example for logging. pub fn try_new_with_client_service( client_service: S, base_path: &str, ) -> Result { Ok(Self { client_service, base_path: into_base_path(base_path, None)?, marker: PhantomData, }) } } /// Error type failing to create a Client #[derive(Debug)] pub enum ClientInitError { /// Invalid URL Scheme InvalidScheme, /// Invalid URI InvalidUri(hyper::http::uri::InvalidUri), /// Missing Hostname MissingHost, /// SSL Connection Error #[cfg(any(target_os = "macos", target_os = "windows", target_os = "ios"))] SslError(native_tls::Error), /// SSL Connection Error #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "ios")))] SslError(openssl::error::ErrorStack), } impl From for ClientInitError { fn from(err: hyper::http::uri::InvalidUri) -> ClientInitError { ClientInitError::InvalidUri(err) } } impl fmt::Display for ClientInitError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s: &dyn fmt::Debug = self; s.fmt(f) } } impl Error for ClientInitError { fn description(&self) -> &str { "Failed to produce a hyper client." } } #[async_trait] impl Api for Client where S: Service< (Request, C), Response=Response> + Clone + Sync + Send + 'static, S::Future: Send + 'static, S::Error: Into + fmt::Display, C: Has + Clone + Send + Sync + 'static, { fn poll_ready(&self, cx: &mut Context) -> Poll> { match self.client_service.clone().poll_ready(cx) { Poll::Ready(Err(e)) => Poll::Ready(Err(e.into())), Poll::Ready(Ok(o)) => Poll::Ready(Ok(o)), Poll::Pending => Poll::Pending, } } }