1use 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
92pub 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 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 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 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 pub fn send_request(
167 &self,
168 request: IppRequestResponse,
169 ) -> impl Future<Item = IppRequestResponse, Error = IppError> + Send {
170 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}