ipp_client/
client.rs

1//!
2//! IPP client
3//!
4use std::{borrow::Cow, fs, io, path::PathBuf, time::Duration};
5
6use futures::{future::IntoFuture, Future, Stream};
7use log::debug;
8use num_traits::FromPrimitive;
9use reqwest::{
10    r#async::{Chunk, Client},
11    Certificate,
12};
13use url::Url;
14
15use ipp_proto::{
16    attribute::{PRINTER_STATE, PRINTER_STATE_REASONS},
17    ipp::{self, DelimiterTag, PrinterState},
18    operation::IppOperation,
19    request::IppRequestResponse,
20    AsyncIppParser, IppAttributes, IppOperationBuilder,
21};
22
23use crate::IppError;
24
25const ERROR_STATES: &[&str] = &[
26    "media-jam",
27    "toner-empty",
28    "spool-area-full",
29    "cover-open",
30    "door-open",
31    "input-tray-missing",
32    "output-tray-missing",
33    "marker-supply-empty",
34    "paused",
35    "shutdown",
36];
37
38fn parse_uri(uri: String) -> impl Future<Item = Url, Error = IppError> {
39    futures::lazy(move || match Url::parse(&uri) {
40        Ok(mut url) => {
41            match url.scheme() {
42                "ipp" => {
43                    url.set_scheme("http").unwrap();
44                    if url.port().is_none() {
45                        url.set_port(Some(631)).unwrap();
46                    }
47                }
48                "ipps" => {
49                    url.set_scheme("https").unwrap();
50                    if url.port().is_none() {
51                        url.set_port(Some(443)).unwrap();
52                    }
53                }
54                _ => {}
55            }
56            Ok(url)
57        }
58        Err(e) => Err(IppError::ParamError(e.to_string())),
59    })
60}
61
62fn to_device_uri(uri: &str) -> Cow<str> {
63    match Url::parse(&uri) {
64        Ok(ref mut url) if !url.username().is_empty() => {
65            let _ = url.set_username("");
66            let _ = url.set_password(None);
67            Cow::Owned(url.to_string())
68        }
69        _ => Cow::Borrowed(uri),
70    }
71}
72
73fn parse_certs(certs: Vec<PathBuf>) -> impl Future<Item = Vec<Certificate>, Error = IppError> {
74    futures::lazy(move || {
75        let mut result = Vec::new();
76
77        for cert_file in certs {
78            let buf = match fs::read(&cert_file) {
79                Ok(buf) => buf,
80                Err(e) => return Err(IppError::from(e)),
81            };
82            let ca_cert = match Certificate::from_der(&buf).or_else(|_| Certificate::from_pem(&buf)) {
83                Ok(ca_cert) => ca_cert,
84                Err(e) => return Err(IppError::from(e)),
85            };
86            result.push(ca_cert);
87        }
88        Ok(result)
89    })
90}
91
92/// IPP client.
93///
94/// IPP client is responsible for sending requests to IPP server.
95pub struct IppClient {
96    pub(crate) uri: String,
97    pub(crate) ca_certs: Vec<PathBuf>,
98    pub(crate) verify_hostname: bool,
99    pub(crate) verify_certificate: bool,
100    pub(crate) timeout: u64,
101}
102
103impl IppClient {
104    /// Check printer ready status
105    pub fn check_ready(&self) -> impl Future<Item = (), Error = IppError> {
106        debug!("Checking printer status");
107        let operation = IppOperationBuilder::get_printer_attributes()
108            .attributes(&[PRINTER_STATE, PRINTER_STATE_REASONS])
109            .build();
110
111        self.send(operation).and_then(|attrs| {
112            let state = attrs
113                .groups_of(DelimiterTag::PrinterAttributes)
114                .get(0)
115                .and_then(|g| g.attributes().get(PRINTER_STATE))
116                .and_then(|attr| attr.value().as_enum())
117                .and_then(|v| PrinterState::from_i32(*v));
118
119            if let Some(PrinterState::Stopped) = state {
120                debug!("Printer is stopped");
121                return Err(IppError::PrinterStopped);
122            }
123
124            if let Some(reasons) = attrs
125                .groups_of(DelimiterTag::PrinterAttributes)
126                .get(0)
127                .and_then(|g| g.attributes().get(PRINTER_STATE_REASONS))
128            {
129                let keywords = reasons
130                    .value()
131                    .into_iter()
132                    .filter_map(|e| e.as_keyword())
133                    .map(ToOwned::to_owned)
134                    .collect::<Vec<_>>();
135
136                if keywords.iter().any(|k| ERROR_STATES.contains(&&k[..])) {
137                    debug!("Printer is in error state: {:?}", keywords);
138                    return Err(IppError::PrinterStateError(keywords.clone()));
139                }
140            }
141            Ok(())
142        })
143    }
144
145    /// send IPP operation
146    pub fn send<T>(&self, operation: T) -> impl Future<Item = IppAttributes, Error = IppError>
147    where
148        T: IppOperation,
149    {
150        debug!("Sending IPP operation");
151        self.send_request(operation.into_ipp_request(&to_device_uri(&self.uri)))
152            .and_then(|resp| {
153                if resp.header().operation_status > 2 {
154                    // IPP error
155                    Err(IppError::StatusError(
156                        ipp::StatusCode::from_u16(resp.header().operation_status)
157                            .unwrap_or(ipp::StatusCode::ServerErrorInternalError),
158                    ))
159                } else {
160                    Ok(resp.attributes().clone())
161                }
162            })
163    }
164
165    /// Send request and return response
166    pub fn send_request(
167        &self,
168        request: IppRequestResponse,
169    ) -> impl Future<Item = IppRequestResponse, Error = IppError> + Send {
170        // Some printers don't support gzip
171        let mut builder = Client::builder().gzip(false).connect_timeout(Duration::from_secs(10));
172
173        if !self.verify_hostname {
174            debug!("Disabling hostname verification!");
175            builder = builder.danger_accept_invalid_hostnames(true);
176        }
177
178        if !self.verify_certificate {
179            debug!("Disabling certificate verification!");
180            builder = builder.danger_accept_invalid_certs(true);
181        }
182
183        if self.timeout > 0 {
184            debug!("Setting timeout to {}", self.timeout);
185            builder = builder.timeout(Duration::from_secs(self.timeout));
186        }
187
188        let uri = self.uri.clone();
189        let ca_certs = self.ca_certs.clone();
190
191        parse_uri(uri).and_then(|url| {
192            parse_certs(ca_certs).and_then(|certs| {
193                builder = certs
194                    .into_iter()
195                    .fold(builder, |builder, ca_cert| builder.add_root_certificate(ca_cert));
196
197                builder
198                    .build()
199                    .into_future()
200                    .and_then(move |client| {
201                        let mut builder = client
202                            .post(url.clone())
203                            .header("Content-Type", "application/ipp")
204                            .body(request.into_stream());
205
206                        if !url.username().is_empty() {
207                            debug!("Setting basic auth: {} ****", url.username());
208                            builder = builder.basic_auth(
209                                url.username(),
210                                url.password()
211                                    .map(|p| percent_encoding::percent_decode(p.as_bytes()).decode_utf8().unwrap()),
212                            );
213                        }
214
215                        builder.send()
216                    })
217                    .and_then(|response| response.error_for_status())
218                    .map_err(IppError::HttpError)
219                    .and_then(|response| {
220                        let stream: Box<dyn Stream<Item = Chunk, Error = io::Error> + Send> = Box::new(
221                            response
222                                .into_body()
223                                .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string())),
224                        );
225
226                        AsyncIppParser::from(stream)
227                            .map_err(IppError::from)
228                            .map(IppRequestResponse::from_parse_result)
229                    })
230            })
231        })
232    }
233}