blob: c15885059c3f8c7b79270c6a1b7146ea09de2431 [file] [log] [blame]
//!
//! IPP client
//!
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}")
}
/// Builder to create IPP client
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,
}
}
/// Enable or disable ignoring of TLS handshake errors. Default is false.
pub fn ignore_tls_errors(mut self, flag: bool) -> Self {
self.ignore_tls_errors = flag;
self
}
/// Add custom root certificate in PEM or DER format.
pub fn ca_cert<D: AsRef<[u8]>>(mut self, data: D) -> Self {
self.ca_certs.push(data.as_ref().to_owned());
self
}
/// Set network request timeout. Default is no timeout.
pub fn request_timeout(mut self, duration: Duration) -> Self {
self.request_timeout = Some(duration);
self
}
/// Add custom HTTP header
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
}
/// Add basic auth header (RFC 7617)
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> {
/// Build the async client
pub fn build(self) -> non_blocking::AsyncIppClient {
non_blocking::AsyncIppClient(self)
}
}
#[cfg(feature = "client")]
impl IppClientBuilder<blocking::IppClient> {
/// Build the blocking client
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");
/// Asynchronous IPP client.
///
/// IPP client is responsible for sending requests to IPP server.
pub struct AsyncIppClient(pub(super) IppClientBuilder<Self>);
impl AsyncIppClient {
/// Create IPP client with default options
pub fn new(uri: Uri) -> Self {
AsyncIppClient(AsyncIppClient::builder(uri))
}
/// Create IPP client builder for setting extra options
pub fn builder(uri: Uri) -> IppClientBuilder<Self> {
IppClientBuilder::new(uri)
}
/// Return client URI
pub fn uri(&self) -> &Uri {
&self.0.uri
}
/// Send IPP request to the server
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(any(feature = "async-client-tls", feature = "async-client-rustls"))]
{
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);
}
}
#[cfg(feature = "async-client-rustls")]
{
builder = builder.use_rustls_tls();
}
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(io::Error::other).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 std::sync::Arc;
use ureq::{Agent, SendBody};
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");
/// Blocking IPP client.
///
/// IPP client is responsible for sending requests to IPP server.
pub struct IppClient(pub(super) IppClientBuilder<Self>);
impl IppClient {
/// Create IPP client with default options
pub fn new(uri: Uri) -> Self {
IppClient(IppClient::builder(uri))
}
/// Create IPP client builder for setting extra options
pub fn builder(uri: Uri) -> IppClientBuilder<Self> {
IppClientBuilder::new(uri)
}
/// Return client URI
pub fn uri(&self) -> &Uri {
&self.0.uri
}
/// Send IPP request to the server
pub fn send<R>(&self, request: R) -> Result<IppRequestResponse, IppError>
where
R: Into<IppRequestResponse>,
{
let mut builder = Agent::config_builder().timeout_connect(Some(CONNECT_TIMEOUT));
if let Some(timeout) = self.0.request_timeout {
builder = builder.timeout_global(Some(timeout));
}
#[cfg(any(feature = "client-tls", feature = "client-rustls"))]
{
use once_cell::sync::Lazy;
use rustls_native_certs::load_native_certs;
use ureq::tls::{RootCerts, TlsConfig, TlsProvider};
let mut tls_config = TlsConfig::builder();
if self.0.ignore_tls_errors {
tls_config = tls_config.disable_verification(true);
}
let provider = if cfg!(feature = "client-rustls") {
TlsProvider::Rustls
} else {
TlsProvider::NativeTls
};
tls_config = tls_config.provider(provider);
static ROOTS: Lazy<Arc<Vec<ureq::tls::Certificate<'static>>>> = Lazy::new(|| {
let certs = load_native_certs();
Arc::new(
certs
.certs
.into_iter()
.map(|c| ureq::tls::Certificate::from_der(c.as_ref()).to_owned())
.collect(),
)
});
let mut roots = (**ROOTS).clone();
for data in &self.0.ca_certs {
roots.push(ureq::tls::Certificate::from_der(data).to_owned());
}
tls_config = tls_config.root_certs(RootCerts::Specific(Arc::new(roots)));
builder = builder.tls_config(tls_config.build());
}
let agent: Agent = builder.user_agent(USER_AGENT).build().into();
let mut req = agent
.post(&ipp_uri_to_string(&self.0.uri))
.header("content-type", "application/ipp");
for (k, v) in &self.0.headers {
req = req.header(k, v);
}
let response = req.send(SendBody::from_reader(&mut request.into().into_read()))?;
let reader = response.into_body().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());
}
}