use std::{collections::BTreeMap, marker::PhantomData, time::Duration};
use base64::Engine;
use http::Uri;
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
fn ipp_uri_to_string(uri: &Uri) -> String {
let (scheme, default_port) = match uri.scheme_str() {
Some("ipps") => ("https", 443),
Some("ipp") => ("http", 631),
_ => return uri.to_string(),
};
let authority = match uri.authority() {
Some(authority) => {
if authority.port_u16().is_some() {
authority.to_string()
} else {
format!("{}:{}", authority, default_port)
}
}
None => return uri.to_string(),
};
let path_and_query = uri.path_and_query().map(|p| p.as_str()).unwrap_or_default();
format!("{}://{}{}", scheme, authority, path_and_query)
}
pub struct IppClientBuilder<T> {
uri: Uri,
ignore_tls_errors: bool,
request_timeout: Option<Duration>,
headers: BTreeMap<String, String>,
ca_certs: Vec<Vec<u8>>,
_phantom_data: PhantomData<T>,
}
impl<T> IppClientBuilder<T> {
fn new(uri: Uri) -> Self {
IppClientBuilder {
uri,
ignore_tls_errors: false,
request_timeout: None,
headers: BTreeMap::new(),
ca_certs: Vec::new(),
_phantom_data: PhantomData,
}
}
pub fn ignore_tls_errors(mut self, flag: bool) -> Self {
self.ignore_tls_errors = flag;
self
}
pub fn ca_cert<D: AsRef<[u8]>>(mut self, data: D) -> Self {
self.ca_certs.push(data.as_ref().to_owned());
self
}
pub fn request_timeout(mut self, duration: Duration) -> Self {
self.request_timeout = Some(duration);
self
}
pub fn http_header<K, V>(mut self, key: K, value: V) -> Self
where
K: AsRef<str>,
V: AsRef<str>,
{
self.headers.insert(key.as_ref().to_owned(), value.as_ref().to_owned());
self
}
pub fn basic_auth<U, P>(mut self, username: U, password: P) -> Self
where
U: AsRef<str>,
P: AsRef<str>,
{
let authz =
base64::engine::general_purpose::STANDARD.encode(format!("{}:{}", username.as_ref(), password.as_ref()));
self.headers
.insert("authorization".to_owned(), format!("Basic {authz}"));
self
}
}
#[cfg(feature = "async-client")]
impl IppClientBuilder<non_blocking::AsyncIppClient> {
pub fn build(self) -> non_blocking::AsyncIppClient {
non_blocking::AsyncIppClient(self)
}
}
#[cfg(feature = "client")]
impl IppClientBuilder<blocking::IppClient> {
pub fn build(self) -> blocking::IppClient {
blocking::IppClient(self)
}
}
#[cfg(feature = "async-client")]
pub mod non_blocking {
use std::io;
use futures_util::{io::BufReader, stream::TryStreamExt};
use http::Uri;
use reqwest::{Body, ClientBuilder};
use tokio_util::compat::FuturesAsyncReadCompatExt;
use crate::{error::IppError, parser::AsyncIppParser, request::IppRequestResponse};
use super::{ipp_uri_to_string, IppClientBuilder, CONNECT_TIMEOUT};
const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"), ";reqwest");
pub struct AsyncIppClient(pub(super) IppClientBuilder<Self>);
impl AsyncIppClient {
pub fn new(uri: Uri) -> Self {
AsyncIppClient(AsyncIppClient::builder(uri))
}
pub fn builder(uri: Uri) -> IppClientBuilder<Self> {
IppClientBuilder::new(uri)
}
pub fn uri(&self) -> &Uri {
&self.0.uri
}
pub async fn send<R>(&self, request: R) -> Result<IppRequestResponse, IppError>
where
R: Into<IppRequestResponse>,
{
let mut builder = ClientBuilder::new().connect_timeout(CONNECT_TIMEOUT);
if let Some(timeout) = self.0.request_timeout {
builder = builder.timeout(timeout);
}
#[cfg(feature = "async-client-tls")]
{
if self.0.ignore_tls_errors {
builder = builder
.danger_accept_invalid_hostnames(true)
.danger_accept_invalid_certs(true);
}
for data in &self.0.ca_certs {
let cert =
reqwest::Certificate::from_pem(data).or_else(|_| reqwest::Certificate::from_der(data))?;
builder = builder.add_root_certificate(cert);
}
}
let mut req_builder = builder
.user_agent(USER_AGENT)
.build()?
.post(ipp_uri_to_string(&self.0.uri));
for (k, v) in &self.0.headers {
req_builder = req_builder.header(k, v);
}
let response = req_builder
.header("content-type", "application/ipp")
.body(Body::wrap_stream(tokio_util::io::ReaderStream::new(
request.into().into_async_read().compat(),
)))
.send()
.await?;
if response.status().is_success() {
let parser = AsyncIppParser::new(BufReader::new(
response
.bytes_stream()
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
.into_async_read(),
));
parser.parse().await.map_err(IppError::from)
} else {
Err(IppError::RequestError(response.status().as_u16()))
}
}
}
}
#[cfg(feature = "client")]
pub mod blocking {
use http::Uri;
use ureq::AgentBuilder;
use crate::{error::IppError, parser::IppParser, reader::IppReader, request::IppRequestResponse};
use super::{ipp_uri_to_string, IppClientBuilder, CONNECT_TIMEOUT};
const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"), ";ureq");
pub struct IppClient(pub(super) IppClientBuilder<Self>);
impl IppClient {
pub fn new(uri: Uri) -> Self {
IppClient(IppClient::builder(uri))
}
pub fn builder(uri: Uri) -> IppClientBuilder<Self> {
IppClientBuilder::new(uri)
}
pub fn uri(&self) -> &Uri {
&self.0.uri
}
pub fn send<R>(&self, request: R) -> Result<IppRequestResponse, IppError>
where
R: Into<IppRequestResponse>,
{
let mut builder = AgentBuilder::new().timeout_connect(CONNECT_TIMEOUT);
if let Some(timeout) = self.0.request_timeout {
builder = builder.timeout(timeout);
}
#[cfg(feature = "client-tls")]
{
let mut tls_builder = native_tls::TlsConnector::builder();
tls_builder
.danger_accept_invalid_hostnames(self.0.ignore_tls_errors)
.danger_accept_invalid_certs(self.0.ignore_tls_errors);
for data in &self.0.ca_certs {
let cert =
native_tls::Certificate::from_pem(data).or_else(|_| native_tls::Certificate::from_der(data))?;
tls_builder.add_root_certificate(cert);
}
let tls_connector = tls_builder.build()?;
builder = builder.tls_connector(std::sync::Arc::new(tls_connector));
}
let agent = builder.user_agent(USER_AGENT).build();
let mut req = agent
.post(&ipp_uri_to_string(&self.0.uri))
.set("content-type", "application/ipp");
for (k, v) in &self.0.headers {
req = req.set(k, v);
}
let response = req.send(request.into().into_read())?;
let reader = response.into_reader();
let parser = IppParser::new(IppReader::new(reader));
parser.parse().map_err(IppError::from)
}
}
}
#[cfg(test)]
mod tests {
use crate::client::ipp_uri_to_string;
use http::Uri;
#[test]
fn test_ipp_uri_no_port() {
let uri = "ipp://user:pass@host/path?query=1234".parse::<Uri>().unwrap();
let http_uri = ipp_uri_to_string(&uri);
assert_eq!(http_uri, "http://user:pass@host:631/path?query=1234");
}
#[test]
fn test_ipp_uri_with_port() {
let uri = "ipp://user:pass@host:1000".parse::<Uri>().unwrap();
let http_uri = ipp_uri_to_string(&uri);
assert_eq!(http_uri, "http://user:pass@host:1000/");
}
#[test]
fn test_ipps_uri_no_port() {
let uri = "ipps://host".parse::<Uri>().unwrap();
let http_uri = ipp_uri_to_string(&uri);
assert_eq!(http_uri, "https://host:443/");
}
#[test]
fn test_ipps_uri_with_port() {
let uri = "ipps://host:8443".parse::<Uri>().unwrap();
let http_uri = ipp_uri_to_string(&uri);
assert_eq!(http_uri, "https://host:8443/");
}
#[test]
fn test_http_uri_no_change() {
let uri = "http://somehost".parse::<Uri>().unwrap();
let http_uri = ipp_uri_to_string(&uri);
assert_eq!(http_uri, uri.to_string());
}
}