use crate::error::IppError;
use crate::model::{PageOrientation, Resolution, WhichJob};
use crate::result::IppResult;
use crate::service::IppService;
use crate::utils::{
decommpress_payload, get_ipp_attribute, get_requested_attributes, take_ipp_attribute,
take_requesting_user_name,
};
use anyhow;
use futures_locks::RwLock;
use http::request::Parts as ReqParts;
use ipp::attribute::{IppAttribute, IppAttributeGroup, IppAttributes};
use ipp::model::{DelimiterTag, IppVersion, JobState, Operation, PrinterState, StatusCode};
use ipp::payload::IppPayload;
use ipp::request::IppRequestResponse;
use ipp::value::{IppKeyword, IppMimeMediaType, IppName, IppString, IppTextValue, IppValue};
use moka::future::{Cache, CacheBuilder};
use std::collections::HashSet;
use std::ops::Deref;
use std::sync::atomic::{AtomicI32, Ordering};
use std::time::{Duration, Instant};
use uuid::Uuid;
pub trait SimpleIppServiceHandler: Send + Sync {
fn handle_document(
&self,
_document: SimpleIppDocument,
) -> impl futures::Future<Output = anyhow::Result<()>> + Send {
futures::future::ready(Ok(()))
}
}
#[derive(fmt_derive::Debug)]
pub struct SimpleIppDocument {
pub format: Option<IppMimeMediaType>,
pub job_attributes: SimpleIppJobAttributes,
#[fmt(ignore)]
pub payload: IppPayload,
}
#[derive(fmt_derive::Debug, Clone)]
pub struct SimpleIppJobAttributes {
pub originating_user_name: IppName,
pub media: IppKeyword,
pub orientation: Option<PageOrientation>,
pub sides: IppKeyword,
pub print_color_mode: IppKeyword,
pub printer_resolution: Option<Resolution>,
}
impl SimpleIppJobAttributes {
pub(crate) fn take_ipp_attributes(
info: &PrinterInfo,
originating_user_name: IppName,
attributes: &mut IppAttributes,
) -> Self {
let media = take_ipp_attribute(attributes, DelimiterTag::JobAttributes, "media")
.and_then(|attr| attr.into_keyword().ok())
.unwrap_or_else(|| info.media_default.clone());
let orientation = take_ipp_attribute(
attributes,
DelimiterTag::JobAttributes,
"orientation-requested",
)
.and_then(|attr| PageOrientation::try_from(attr).ok())
.or(info.orientation_default);
let sides = take_ipp_attribute(attributes, DelimiterTag::JobAttributes, "sides")
.and_then(|attr| attr.into_keyword().ok())
.unwrap_or_else(|| info.sides_default.clone());
let print_color_mode =
take_ipp_attribute(attributes, DelimiterTag::JobAttributes, "print-color-mode")
.and_then(|attr| attr.into_keyword().ok())
.unwrap_or_else(|| info.print_color_mode_default.clone());
let printer_resolution = take_ipp_attribute(
attributes,
DelimiterTag::JobAttributes,
"printer-resolution",
)
.and_then(|attr| Resolution::try_from(attr).ok())
.or(info.printer_resolution_default);
Self {
originating_user_name,
media,
orientation,
sides,
print_color_mode,
printer_resolution,
}
}
}
#[derive(Debug, Clone, Builder)]
pub struct PrinterInfo {
#[builder(default = r#""IppServer".try_into().unwrap()"#)]
name: IppName,
#[builder(default = r#"Some("IppServer by ippper".try_into().unwrap())"#)]
info: Option<IppTextValue>,
#[builder(default = r#"Some("IppServer by ippper".try_into().unwrap())"#)]
make_and_model: Option<IppTextValue>,
#[builder(default = r#"None"#)]
dnssd_name: Option<IppName>,
#[builder(default = r#"None"#)]
uuid: Option<Uuid>,
#[builder(default = r#"true"#)]
color_supported: bool,
#[builder(default = r#"vec!["application/pdf".try_into().unwrap()]"#)]
document_format_supported: Vec<IppMimeMediaType>,
#[builder(default = r#""application/pdf".try_into().unwrap()"#)]
document_format_default: IppMimeMediaType,
#[builder(default = r#"Some("application/pdf".try_into().unwrap())"#)]
document_format_preferred: Option<IppMimeMediaType>,
#[builder(default = r#"vec!["iso_a4_210x297mm".try_into().unwrap()]"#)]
media_supported: Vec<IppKeyword>,
#[builder(default = r#""iso_a4_210x297mm".try_into().unwrap()"#)]
media_default: IppKeyword,
#[builder(default = r#"vec![PageOrientation::Portrait]"#)]
orientation_supported: Vec<PageOrientation>,
#[builder(default = r#"None"#)]
orientation_default: Option<PageOrientation>,
#[builder(default = r#"vec!["one-sided".try_into().unwrap()]"#)]
sides_supported: Vec<IppKeyword>,
#[builder(default = r#""one-sided".try_into().unwrap()"#)]
sides_default: IppKeyword,
#[builder(default = r#"vec!["monochrome".try_into().unwrap(), "color".try_into().unwrap()]"#)]
print_color_mode_supported: Vec<IppKeyword>,
#[builder(default = r#""monochrome".try_into().unwrap()"#)]
print_color_mode_default: IppKeyword,
#[builder(default = r#"vec![]"#)]
printer_resolution_supported: Vec<Resolution>,
#[builder(default = r#"None"#)]
printer_resolution_default: Option<Resolution>,
#[builder(default = r#"vec![
"adobe-1.2".try_into().unwrap(),
"adobe-1.3".try_into().unwrap(),
"adobe-1.4".try_into().unwrap(),
"adobe-1.5".try_into().unwrap(),
"adobe-1.6".try_into().unwrap(),
"adobe-1.7".try_into().unwrap(),
"iso-19005-1_2005".try_into().unwrap(),
"iso-32000-1_2008".try_into().unwrap(),
"pwg-5102.3".try_into().unwrap(),
]"#)]
pdf_versions_supported: Vec<IppKeyword>,
#[builder(default = r#"vec![]"#)]
urf_supported: Vec<IppKeyword>,
#[builder(default = r#"vec![]"#)]
pwg_raster_document_type_supported: Vec<IppKeyword>,
#[builder(default = r#"vec![]"#)]
pwg_raster_document_resolution_supported: Vec<Resolution>,
#[builder(default = r#"None"#)]
pwg_raster_document_sheet_back: Option<IppKeyword>,
}
#[derive(Debug, Clone)]
struct JobInfo {
id: i32,
uuid: Uuid,
state: JobState,
state_message: IppTextValue,
state_reasons: IppValue,
attributes: SimpleIppJobAttributes,
created_at: Duration,
processing_at: Option<Duration>,
completed_at: Option<Duration>,
}
pub struct SimpleIppService<T: SimpleIppServiceHandler> {
start_time: Instant,
job_id: AtomicI32,
job_snapshot: Cache<i32, RwLock<JobInfo>>,
host: String,
basepath: String,
info: PrinterInfo,
handler: T,
}
impl<T: SimpleIppServiceHandler> SimpleIppService<T> {
pub fn new(info: PrinterInfo, handler: T) -> Self {
let job_snapshot = CacheBuilder::new(1000)
.time_to_live(Duration::from_secs(60 * 15))
.build();
Self {
start_time: Instant::now(),
job_id: AtomicI32::new(1000),
job_snapshot,
host: "defaulthost:631".to_string(),
basepath: "/".to_string(),
info,
handler,
}
}
pub fn set_host(&mut self, host: &str) {
self.host = host.to_string();
}
pub fn set_basepath(&mut self, basepath: &str) {
self.basepath = basepath.to_string();
}
pub fn set_info(&mut self, info: PrinterInfo) {
self.info = info;
}
fn make_url(&self, head: &ReqParts, path: &str) -> anyhow::Result<IppString> {
let basepath = self.basepath.trim_start_matches('/').trim_end_matches('/');
let slash_before_basepath = if basepath.is_empty() { "" } else { "/" };
let slash_before_path = if path.starts_with('/') || path.is_empty() {
""
} else {
"/"
};
let scheme = head.uri.scheme().map_or("ipp", |x| x.as_str());
let host = if let Some(host) = head.headers.get("Host") {
let from_user = host.to_str().unwrap_or(self.host.as_str());
if !from_user.contains(':') && self.host.contains(':') {
format!(
"{}:{}",
from_user,
self.host.split(':').next_back().unwrap()
)
} else {
from_user.to_string()
}
} else {
self.host.clone()
};
Ok(IppString::new(format!(
"{}://{}{}{}{}{}",
scheme, host, slash_before_basepath, basepath, slash_before_path, path
))?)
}
fn add_basic_attributes(&self, resp: &mut IppRequestResponse) {
resp.attributes_mut().add(
DelimiterTag::OperationAttributes,
IppAttribute::new(
IppAttribute::ATTRIBUTES_CHARSET.try_into().unwrap(),
IppValue::Charset("utf-8".try_into().unwrap()),
),
);
resp.attributes_mut().add(
DelimiterTag::OperationAttributes,
IppAttribute::new(
IppAttribute::ATTRIBUTES_NATURAL_LANGUAGE
.try_into()
.unwrap(),
IppValue::NaturalLanguage("en".try_into().unwrap()),
),
);
}
fn printer_attributes(
&self,
head: &ReqParts,
requested: &HashSet<&str>,
) -> anyhow::Result<Vec<IppAttribute>> {
let mut r = Vec::<IppAttribute>::new();
let requested_all = requested.contains("all");
let requested_printer_description =
requested_all || requested.contains("printer-description");
let requested_job_template = requested_all || requested.contains("job-template");
macro_rules! is_requested {
(description : $name:expr) => {
requested_printer_description || requested.contains($name)
};
(template : $name:expr) => {
requested_job_template || requested.contains($name)
};
}
macro_rules! add_if_requested {
($kind:ident : $name:expr, $value:expr) => {
let name: IppName = $name;
if is_requested!($kind : name.as_str()) {
r.push(IppAttribute::new(name, $value));
}
};
}
macro_rules! optional_add_if_requested {
($kind:ident : $name:expr, $value:expr) => {
let name: IppName = $name;
if is_requested!($kind : name.as_str()) {
if let Some(value) = $value {
r.push(IppAttribute::new(name, value));
}
}
};
}
add_if_requested!(
description: IppAttribute::PRINTER_URI_SUPPORTED.try_into()?,
IppValue::Uri(self.make_url(head, "/")?)
);
add_if_requested!(
description: IppAttribute::URI_AUTHENTICATION_SUPPORTED.try_into()?,
IppValue::Keyword("requesting-user-name".try_into()?)
);
add_if_requested!(
description: IppAttribute::URI_SECURITY_SUPPORTED.try_into()?,
IppValue::Keyword(
match head.uri.scheme_str() {
Some("ipps") => "tls",
Some("https") => "tls",
_ => "none",
}
.try_into()?
)
);
add_if_requested!(
description: IppAttribute::PRINTER_NAME.try_into()?,
IppValue::NameWithoutLanguage(self.info.name.clone())
);
add_if_requested!(
description: IppAttribute::PRINTER_STATE.try_into()?,
IppValue::Enum(PrinterState::Idle as i32)
);
add_if_requested!(
description: IppAttribute::PRINTER_STATE_REASONS.try_into()?,
IppValue::Keyword("none".try_into()?)
);
add_if_requested!(
description: IppAttribute::IPP_VERSIONS_SUPPORTED.try_into()?,
IppValue::Array(vec![
IppValue::Keyword("1.0".try_into()?),
IppValue::Keyword("1.1".try_into()?),
IppValue::Keyword("2.0".try_into()?),
])
);
add_if_requested!(
description: IppAttribute::OPERATIONS_SUPPORTED.try_into()?,
IppValue::Array(vec![
IppValue::Enum(Operation::PrintJob as i32),
IppValue::Enum(Operation::ValidateJob as i32),
IppValue::Enum(Operation::CreateJob as i32),
IppValue::Enum(Operation::SendDocument as i32),
IppValue::Enum(Operation::CancelJob as i32),
IppValue::Enum(Operation::GetJobAttributes as i32),
IppValue::Enum(Operation::GetJobs as i32),
IppValue::Enum(Operation::GetPrinterAttributes as i32),
])
);
add_if_requested!(
description: IppAttribute::COLOR_SUPPORTED.try_into()?,
IppValue::Boolean(self.info.color_supported)
);
add_if_requested!(
description: "which-jobs-supported".try_into()?,
IppValue::Array(vec![
IppValue::Keyword("completed".try_into()?),
IppValue::Keyword("not-completed".try_into()?),
IppValue::Keyword("aborted".try_into()?),
IppValue::Keyword("all".try_into()?),
IppValue::Keyword("canceled".try_into()?),
IppValue::Keyword("pending".try_into()?),
IppValue::Keyword("pending-held".try_into()?),
IppValue::Keyword("processing".try_into()?),
IppValue::Keyword("processing-stopped".try_into()?),
])
);
add_if_requested!(description: "multiple-document-jobs-supported".try_into()?, IppValue::Boolean(false));
add_if_requested!(
description: IppAttribute::CHARSET_CONFIGURED.try_into()?,
IppValue::Charset("utf-8".try_into()?)
);
add_if_requested!(
description: IppAttribute::CHARSET_SUPPORTED.try_into()?,
IppValue::Charset("utf-8".try_into()?)
);
add_if_requested!(
description: IppAttribute::NATURAL_LANGUAGE_CONFIGURED.try_into()?,
IppValue::NaturalLanguage("en".try_into()?)
);
add_if_requested!(
description: IppAttribute::GENERATED_NATURAL_LANGUAGE_SUPPORTED.try_into()?,
IppValue::NaturalLanguage("en".try_into()?)
);
add_if_requested!(
description: IppAttribute::DOCUMENT_FORMAT_DEFAULT.try_into()?,
IppValue::MimeMediaType(self.info.document_format_default.clone())
);
add_if_requested!(
description: IppAttribute::DOCUMENT_FORMAT_SUPPORTED.try_into()?,
IppValue::Array(
self.info
.document_format_supported
.clone()
.into_iter()
.map(IppValue::MimeMediaType)
.collect::<Vec<_>>()
)
);
add_if_requested!(
description: IppAttribute::PRINTER_IS_ACCEPTING_JOBS.try_into()?,
IppValue::Boolean(true)
);
add_if_requested!(
description: IppAttribute::PDL_OVERRIDE_SUPPORTED.try_into()?,
IppValue::Keyword("attempted".try_into()?)
);
add_if_requested!(
description: IppAttribute::PRINTER_UP_TIME.try_into()?,
IppValue::Integer(self.uptime().as_secs() as i32)
);
add_if_requested!(
description: IppAttribute::COMPRESSION_SUPPORTED.try_into()?,
IppValue::Array(vec![
IppValue::Keyword("none".try_into()?),
IppValue::Keyword("gzip".try_into()?),
])
);
add_if_requested!(
template: IppAttribute::MEDIA_DEFAULT.try_into()?,
IppValue::Keyword(self.info.media_default.clone())
);
add_if_requested!(
template: IppAttribute::MEDIA_SUPPORTED.try_into()?,
IppValue::Array(
self.info
.media_supported
.clone()
.into_iter()
.map(IppValue::Keyword)
.collect::<Vec<_>>()
)
);
add_if_requested!(
template: IppAttribute::ORIENTATION_REQUESTED_DEFAULT.try_into()?,
self.info
.orientation_default
.map(|orientation| orientation.into())
.unwrap_or(IppValue::NoValue)
);
add_if_requested!(
template: IppAttribute::ORIENTATION_REQUESTED_SUPPORTED.try_into()?,
IppValue::Array(
self.info
.orientation_supported
.clone()
.into_iter()
.map(IppValue::from)
.collect::<Vec<_>>()
)
);
add_if_requested!(
template: IppAttribute::SIDES_DEFAULT.try_into()?,
IppValue::Keyword(self.info.sides_default.clone())
);
add_if_requested!(
template: IppAttribute::SIDES_SUPPORTED.try_into()?,
IppValue::Array(
self.info
.sides_supported
.clone()
.into_iter()
.map(IppValue::Keyword)
.collect::<Vec<_>>()
)
);
add_if_requested!(
template: IppAttribute::PRINT_COLOR_MODE_DEFAULT.try_into()?,
IppValue::Keyword(self.info.print_color_mode_default.clone())
);
add_if_requested!(
template: IppAttribute::PRINT_COLOR_MODE_SUPPORTED.try_into()?,
IppValue::Array(
self.info
.print_color_mode_supported
.clone()
.into_iter()
.map(IppValue::Keyword)
.collect::<Vec<_>>()
)
);
optional_add_if_requested!(
description: "document-format-preferred".try_into()?,
self.info
.document_format_preferred
.clone()
.map(IppValue::MimeMediaType)
);
if !self.info.printer_resolution_supported.is_empty() {
add_if_requested!(
template: IppAttribute::PRINTER_RESOLUTION_SUPPORTED.try_into()?,
IppValue::Array(
self.info
.printer_resolution_supported
.clone()
.into_iter()
.map(IppValue::from)
.collect::<Vec<_>>()
)
);
}
optional_add_if_requested!(
template: IppAttribute::PRINTER_RESOLUTION_DEFAULT.try_into()?,
self.info.printer_resolution_default.map(IppValue::from)
);
if !self.info.pdf_versions_supported.is_empty() {
add_if_requested!(
description: "pdf-versions-supported".try_into()?,
IppValue::Array(
self.info
.pdf_versions_supported
.clone()
.into_iter()
.map(IppValue::Keyword)
.collect::<Vec<_>>()
)
);
}
if !self.info.urf_supported.is_empty() {
add_if_requested!(
description: "urf-supported".try_into()?,
IppValue::Array(
self.info
.urf_supported
.clone()
.into_iter()
.map(IppValue::Keyword)
.collect::<Vec<_>>()
)
);
}
if !self.info.pwg_raster_document_type_supported.is_empty() {
add_if_requested!(
description: "pwg-raster-document-type-supported".try_into()?,
IppValue::Array(
self.info
.pwg_raster_document_type_supported
.clone()
.into_iter()
.map(IppValue::Keyword)
.collect::<Vec<_>>()
)
);
}
if !self
.info
.pwg_raster_document_resolution_supported
.is_empty()
{
add_if_requested!(
description: "pwg-raster-document-resolution-supported".try_into()?,
IppValue::Array(
self.info
.pwg_raster_document_resolution_supported
.clone()
.into_iter()
.map(IppValue::from)
.collect::<Vec<_>>()
)
);
}
optional_add_if_requested!(
description: "pwg-raster-document-sheet-back".try_into()?,
self.info
.pwg_raster_document_sheet_back
.clone()
.map(IppValue::Keyword)
);
if is_requested!(description: "job-creation-attributes-supported") {
let mut job_creation_attributes_supported = vec![
IppValue::Keyword("job-name".try_into()?),
IppValue::Keyword("media".try_into()?),
IppValue::Keyword("orientation-requested".try_into()?),
IppValue::Keyword("print-color-mode".try_into()?),
IppValue::Keyword("sides".try_into()?),
];
if !self.info.printer_resolution_supported.is_empty() {
job_creation_attributes_supported
.push(IppValue::Keyword("printer-resolution".try_into()?));
}
r.push(IppAttribute::new(
"job-creation-attributes-supported".try_into()?,
IppValue::Array(job_creation_attributes_supported),
));
}
optional_add_if_requested!(
description: IppAttribute::PRINTER_INFO.try_into()?,
self.info.info.clone().map(IppValue::TextWithoutLanguage)
);
optional_add_if_requested!(
description: IppAttribute::PRINTER_MAKE_AND_MODEL.try_into()?,
self.info
.make_and_model
.clone()
.map(IppValue::TextWithoutLanguage)
);
optional_add_if_requested!(
description: "printer-dns-sd-name".try_into()?,
self.info.dnssd_name.clone().map(IppValue::NameWithoutLanguage)
);
optional_add_if_requested!(
description: "printer-uuid".try_into()?,
match self.info.uuid {
Some(uuid) => Some(IppValue::Uri(
uuid.urn()
.encode_lower(&mut Uuid::encode_buffer())
.to_string()
.try_into()?
)),
None => None,
}
);
Ok(r)
}
fn uptime(&self) -> Duration {
self.start_time.elapsed()
}
async fn alloc_job(
&self,
init: impl FnOnce(i32) -> anyhow::Result<JobInfo>,
) -> anyhow::Result<RwLock<JobInfo>> {
let id = self.job_id.fetch_add(1, Ordering::Relaxed);
let data = RwLock::new(init(id)?);
self.job_snapshot.insert(id, data.clone()).await;
Ok(data)
}
async fn find_job(&self, r: &IppAttributes) -> anyhow::Result<RwLock<JobInfo>> {
let job_id = get_ipp_attribute(r, DelimiterTag::OperationAttributes, IppAttribute::JOB_ID)
.and_then(|attr| attr.as_integer())
.cloned();
let job = match job_id {
Some(job_id) => self.job_snapshot.get(&job_id).await,
_ => None,
};
match job {
Some(job) => Ok(job),
_ => Err(IppError {
code: StatusCode::ClientErrorNotFound,
msg: StatusCode::ClientErrorNotFound.to_string(),
}
.into()),
}
}
fn take_document_format(&self, r: &mut IppAttributes) -> anyhow::Result<Option<IppMimeMediaType>> {
let format = take_ipp_attribute(r, DelimiterTag::OperationAttributes, "document-format")
.and_then(|attr| attr.into_mime_media_type().ok());
if let Some(ref x) = format {
if !self.info.document_format_supported.contains(x) {
return Err(IppError {
code: StatusCode::ClientErrorDocumentFormatNotSupported,
msg: StatusCode::ClientErrorDocumentFormatNotSupported.to_string(),
}
.into());
}
}
Ok(format)
}
fn lite_job_attributes_for(
&self,
head: &ReqParts,
job: &JobInfo,
) -> anyhow::Result<Vec<IppAttribute>> {
Ok(vec![
IppAttribute::new(
IppAttribute::JOB_URI.try_into()?,
IppValue::Uri(self.make_url(head, format!("job/{}", job.id).as_str())?),
),
IppAttribute::new(IppAttribute::JOB_ID.try_into()?, IppValue::Integer(job.id)),
IppAttribute::new(
IppAttribute::JOB_STATE.try_into()?,
IppValue::Enum(job.state as i32),
),
IppAttribute::new(
"job-state-message".try_into()?,
IppValue::TextWithoutLanguage(job.state_message.clone()),
),
IppAttribute::new(
IppAttribute::JOB_STATE_REASONS.try_into()?,
job.state_reasons.clone(),
),
])
}
fn job_attributes_for(
&self,
head: &ReqParts,
job: &JobInfo,
requested: &HashSet<&str>,
) -> anyhow::Result<Vec<IppAttribute>> {
let mut r = Vec::<IppAttribute>::new();
let requested_all = requested.contains("all");
let requested_job_description = requested_all || requested.contains("job-description");
let requested_job_template = requested_all || requested.contains("job-template");
macro_rules! is_requested {
(description : $name:expr) => {
requested_job_description || requested.contains($name)
};
(template : $name:expr) => {
requested_job_template || requested.contains($name)
};
}
macro_rules! add_if_requested {
($kind:ident : $name:expr, $value:expr) => {
let name: IppName = $name;
if is_requested!($kind : name.as_str()) {
r.push(IppAttribute::new(name, $value));
}
};
}
macro_rules! optional_add_if_requested {
($kind:ident : $name:expr, $value:expr) => {
let name: IppName = $name;
if is_requested!($kind : name.as_str()) {
if let Some(value) = $value {
r.push(IppAttribute::new(name, value));
}
}
};
}
add_if_requested!(
description: IppAttribute::JOB_URI.try_into()?,
IppValue::Uri(self.make_url(head, format!("job/{}", job.id).as_str())?)
);
add_if_requested!(description: IppAttribute::JOB_ID.try_into()?, IppValue::Integer(job.id));
add_if_requested!(
description: "job-uuid".try_into()?,
IppValue::Uri(job.uuid.urn().encode_lower(&mut Uuid::encode_buffer()).to_string().try_into()?)
);
add_if_requested!(description: IppAttribute::JOB_STATE.try_into()?, IppValue::Enum(job.state as i32));
add_if_requested!(description: "job-state-message".try_into()?, IppValue::TextWithoutLanguage(job.state_message.clone()));
add_if_requested!(description: IppAttribute::JOB_STATE_REASONS.try_into()?, job.state_reasons.clone());
add_if_requested!(
description: "job-printer-uri".try_into()?,
IppValue::Uri(self.make_url(head, "")?)
);
add_if_requested!(
description: IppAttribute::JOB_NAME.try_into()?,
IppValue::NameWithoutLanguage(format!("Job #{}", job.id).try_into()?)
);
add_if_requested!(
description: "job-originating-user-name".try_into()?,
IppValue::NameWithoutLanguage(job.attributes.originating_user_name.clone())
);
add_if_requested!(
description: "time-at-creation".try_into()?,
IppValue::Integer(job.created_at.as_secs() as i32)
);
add_if_requested!(
description: "time-at-processing".try_into()?,
job.processing_at
.map_or(IppValue::NoValue, |x| IppValue::Integer(x.as_secs() as i32))
);
add_if_requested!(
description: "time-at-completed".try_into()?,
job.completed_at
.map_or(IppValue::NoValue, |x| IppValue::Integer(x.as_secs() as i32))
);
add_if_requested!(
description: "job-printer-up-time".try_into()?,
IppValue::Integer(self.uptime().as_secs() as i32)
);
add_if_requested!(template: "media".try_into()?, IppValue::Keyword(job.attributes.media.clone()));
add_if_requested!(
template: "orientation-requested".try_into()?,
job.attributes
.orientation
.map_or(IppValue::NoValue, IppValue::from)
);
add_if_requested!(template: "sides".try_into()?, IppValue::Keyword(job.attributes.sides.clone()));
add_if_requested!(
template: "print-color-mode".try_into()?,
IppValue::Keyword(job.attributes.print_color_mode.clone())
);
optional_add_if_requested!(
template: "printer-resolution".try_into()?,
job.attributes.printer_resolution.map(IppValue::from)
);
Ok(r)
}
}
impl<T: SimpleIppServiceHandler> IppService for SimpleIppService<T> {
fn version(&self) -> IppVersion {
IppVersion::v2_0()
}
async fn print_job(&self, head: ReqParts, mut req: IppRequestResponse) -> IppResult {
let mut attributes = std::mem::take(req.attributes_mut());
let req_id = req.header().request_id;
let version = req.header().version;
let requesting_user_name = take_requesting_user_name(&mut attributes);
let job_attributes = SimpleIppJobAttributes::take_ipp_attributes(
&self.info,
requesting_user_name,
&mut attributes,
);
let created_at = self.uptime();
let job = self
.alloc_job(|id| {
Ok(JobInfo {
id,
uuid: Uuid::new_v4(),
state: JobState::Processing,
state_message: "Processing".try_into()?,
state_reasons: IppValue::Keyword("none".try_into()?),
attributes: job_attributes.clone(),
created_at,
processing_at: Some(created_at),
completed_at: None,
})
})
.await?;
let format = self.take_document_format(&mut attributes)?;
let compression = take_ipp_attribute(
&mut attributes,
DelimiterTag::OperationAttributes,
"compression",
)
.and_then(|attr| attr.into_keyword().ok());
let payload = decommpress_payload(req.into_payload(), compression.as_deref())?;
let document_handled = self
.handler
.handle_document(SimpleIppDocument {
format,
job_attributes,
payload,
})
.await;
{
let mut job = job.write().await;
if let Err(ref error) = document_handled {
job.state = JobState::Aborted;
job.state_message = format!("Aborted: {}", error).try_into()?;
} else {
job.state = JobState::Completed;
job.state_message = "Completed".try_into()?;
};
job.completed_at = Some(self.uptime());
}
let mut resp = if let Err(error) = document_handled {
self.build_error_response(version, req_id, error)
} else {
IppRequestResponse::new_response(version, StatusCode::SuccessfulOk, req_id)?
};
self.add_basic_attributes(&mut resp);
let job_attributes = self.lite_job_attributes_for(&head, job.read().await.deref())?;
let mut group = IppAttributeGroup::new(DelimiterTag::JobAttributes);
group
.attributes_mut()
.extend(job_attributes.into_iter().map(|x| (x.name().to_owned(), x)));
resp.attributes_mut().groups_mut().push(group);
Ok(resp)
}
async fn validate_job(&self, _head: ReqParts, req: IppRequestResponse) -> IppResult {
let mut resp = IppRequestResponse::new_response(
req.header().version,
StatusCode::SuccessfulOk,
req.header().request_id,
)?;
self.add_basic_attributes(&mut resp);
Ok(resp)
}
async fn create_job(&self, head: ReqParts, mut req: IppRequestResponse) -> IppResult {
let mut attributes = std::mem::take(req.attributes_mut());
let req_id = req.header().request_id;
let version = req.header().version;
let requesting_user_name = take_requesting_user_name(&mut attributes);
let job_attributes = SimpleIppJobAttributes::take_ipp_attributes(
&self.info,
requesting_user_name,
&mut attributes,
);
let created_at = self.uptime();
let job = self
.alloc_job(|id| {
Ok(JobInfo {
id,
uuid: Uuid::new_v4(),
state: JobState::Pending,
state_message: "Pending".try_into()?,
state_reasons: IppValue::Keyword("none".try_into()?),
attributes: job_attributes.clone(),
created_at,
processing_at: Some(created_at),
completed_at: None,
})
})
.await?;
let mut resp = IppRequestResponse::new_response(version, StatusCode::SuccessfulOk, req_id)?;
self.add_basic_attributes(&mut resp);
let job_attributes = self.lite_job_attributes_for(&head, job.read().await.deref())?;
let mut group = IppAttributeGroup::new(DelimiterTag::JobAttributes);
group
.attributes_mut()
.extend(job_attributes.into_iter().map(|x| (x.name().to_owned(), x)));
resp.attributes_mut().groups_mut().push(group);
Ok(resp)
}
async fn send_document(&self, head: ReqParts, mut req: IppRequestResponse) -> IppResult {
let req_id = req.header().request_id;
let version = req.header().version;
let job = self.find_job(req.attributes()).await?;
let job_attributes;
{
let mut job = job.write().await;
if job.state != JobState::Processing {
if job.state == JobState::Canceled {
return Err(IppError {
code: StatusCode::ClientErrorNotPossible,
msg: "Job is canceled".to_string(),
}
.into());
}
job.state = JobState::Processing;
job.state_message = "Processing".try_into()?;
job.processing_at = Some(self.uptime());
}
job_attributes = job.attributes.clone();
}
let mut attributes = std::mem::take(req.attributes_mut());
let format = self.take_document_format(&mut attributes)?;
let compression = take_ipp_attribute(
&mut attributes,
DelimiterTag::OperationAttributes,
"compression",
)
.and_then(|attr| attr.into_keyword().ok());
let payload = decommpress_payload(req.into_payload(), compression.as_deref())?;
let document_handled = self
.handler
.handle_document(SimpleIppDocument {
format,
job_attributes,
payload,
})
.await;
{
let mut job = job.write().await;
if let Err(ref error) = document_handled {
job.state = JobState::Aborted;
job.state_message = format!("Aborted: {}", error).try_into()?;
} else {
job.state = JobState::Completed;
job.state_message = "Completed".try_into()?;
};
job.completed_at = Some(self.uptime());
}
let mut resp = if let Err(error) = document_handled {
self.build_error_response(version, req_id, error)
} else {
IppRequestResponse::new_response(version, StatusCode::SuccessfulOk, req_id)?
};
self.add_basic_attributes(&mut resp);
let job_attributes = self.lite_job_attributes_for(&head, job.read().await.deref())?;
let mut group = IppAttributeGroup::new(DelimiterTag::JobAttributes);
group
.attributes_mut()
.extend(job_attributes.into_iter().map(|x| (x.name().to_owned(), x)));
resp.attributes_mut().groups_mut().push(group);
Ok(resp)
}
async fn cancel_job(&self, _head: ReqParts, req: IppRequestResponse) -> IppResult {
let job = self.find_job(req.attributes()).await?;
let mut job = job.write().await;
if job.state == JobState::Pending {
job.state = JobState::Canceled;
job.state_message = "Canceled".try_into()?;
let mut resp = IppRequestResponse::new_response(
req.header().version,
StatusCode::SuccessfulOk,
req.header().request_id,
)?;
self.add_basic_attributes(&mut resp);
Ok(resp)
} else {
let mut resp = IppRequestResponse::new_response(
req.header().version,
StatusCode::ClientErrorNotPossible,
req.header().request_id,
)?;
self.add_basic_attributes(&mut resp);
Ok(resp)
}
}
async fn get_job_attributes(&self, head: ReqParts, req: IppRequestResponse) -> IppResult {
let job = self.find_job(req.attributes()).await?;
let requested_attributes = get_requested_attributes(req.attributes());
let mut resp = IppRequestResponse::new_response(
req.header().version,
StatusCode::SuccessfulOk,
req.header().request_id,
)?;
self.add_basic_attributes(&mut resp);
let job_attributes =
self.job_attributes_for(&head, job.read().await.deref(), &requested_attributes)?;
let mut group = IppAttributeGroup::new(DelimiterTag::JobAttributes);
group
.attributes_mut()
.extend(job_attributes.into_iter().map(|x| (x.name().to_owned(), x)));
resp.attributes_mut().groups_mut().push(group);
Ok(resp)
}
async fn get_jobs(&self, head: ReqParts, mut req: IppRequestResponse) -> IppResult {
let mut count = 0;
let limit = take_ipp_attribute(
req.attributes_mut(),
DelimiterTag::OperationAttributes,
"limit",
)
.and_then(|attr| attr.into_integer().ok());
let which_jobs = take_ipp_attribute(
req.attributes_mut(),
DelimiterTag::OperationAttributes,
"which-jobs",
)
.and_then(|attr| attr.into_keyword().ok());
let which_jobs = match which_jobs.as_deref() {
Some("completed") => WhichJob::Completed,
Some("not-completed") | None => WhichJob::NotCompleted,
Some("aborted") => WhichJob::Aborted,
Some("all") => WhichJob::All,
Some("canceled") => WhichJob::Canceled,
Some("pending") => WhichJob::Pending,
Some("pending-held") => WhichJob::PendingHeld,
Some("processing") => WhichJob::Processing,
Some("processing-stopped") => WhichJob::ProcessingStopped,
Some(unknown) => {
let mut resp = IppRequestResponse::new_response(
req.header().version,
StatusCode::ClientErrorAttributesOrValuesNotSupported,
req.header().request_id,
)?;
self.add_basic_attributes(&mut resp);
let mut group = IppAttributeGroup::new(DelimiterTag::UnsupportedAttributes);
group.attributes_mut().insert(
"which-jobs".try_into()?,
IppAttribute::new(
"which-jobs".try_into()?,
IppValue::Keyword(unknown.try_into()?),
),
);
resp.attributes_mut().groups_mut().push(group);
return Ok(resp);
}
};
let requested_attributes = get_requested_attributes(req.attributes());
let mut resp = IppRequestResponse::new_response(
req.header().version,
StatusCode::SuccessfulOk,
req.header().request_id,
)?;
self.add_basic_attributes(&mut resp);
for (_, job) in self.job_snapshot.iter() {
let job = job.read().await;
if which_jobs.match_state(job.state) {
let job_attributes =
self.job_attributes_for(&head, job.deref(), &requested_attributes)?;
let mut group = IppAttributeGroup::new(DelimiterTag::JobAttributes);
group
.attributes_mut()
.extend(job_attributes.into_iter().map(|x| (x.name().to_owned(), x)));
resp.attributes_mut().groups_mut().push(group);
count += 1;
if limit.is_some_and(|x| count >= x) {
break;
}
}
}
Ok(resp)
}
async fn get_printer_attributes(&self, head: ReqParts, req: IppRequestResponse) -> IppResult {
let mut resp = IppRequestResponse::new_response(
req.header().version,
StatusCode::SuccessfulOk,
req.header().request_id,
)?;
self.add_basic_attributes(&mut resp);
let requested_attributes = get_requested_attributes(req.attributes());
let printer_attributes = self.printer_attributes(&head, &requested_attributes)?;
let mut group = IppAttributeGroup::new(DelimiterTag::PrinterAttributes);
group.attributes_mut().extend(
printer_attributes
.into_iter()
.map(|x| (x.name().to_owned(), x)),
);
resp.attributes_mut().groups_mut().push(group);
Ok(resp)
}
}