diff --git a/Cargo.toml b/Cargo.toml index 2c1bdae9b9ab1342ee33d4929f28fd5c303fd796..bab5e187bb47cc821161643b8f67346f9f767a66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ hisysevent = { path = "hisysevent" } members = [ "ozone", "image", + "ipp_proxy", "tests/mod_test", ] diff --git a/ipp_proxy/src/proxy_client/http.rs b/ipp_proxy/src/proxy_client/http.rs new file mode 100644 index 0000000000000000000000000000000000000000..70e81e896be07fe298d3f452c41d9bfc6188b3a1 --- /dev/null +++ b/ipp_proxy/src/proxy_client/http.rs @@ -0,0 +1,119 @@ +// Copyright (c) 2025 Huawei Technologies Co.,Ltd. All rights reserved. +// +// StratoVirt is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan +// PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// See the Mulan PSL v2 for more details. + +const HTTP_MAX_CONTENT_LENGTH: u32 = 2 * 1024 * 1024; // 2M for non-chunked data +pub const HTTP_CONTENT_LENGTH_TOKEN: &str = "Content-Length:"; +pub const HTTP_HOST_TOKEN: &str = "Host:"; +/// HTTP standard constant. +pub const HTTP_STATUS_OK: usize = 200; +pub const HTTP_BAD_REQUEST: usize = 400; + +pub const HTTP_POST_TOKEN: &str = "POST"; +// Content type. +const HTTP_CONTENT_TYPE_TOKEN: &str = "Content-Type:"; +const HTTP_CONTENT_TYPE_IPP: &str = "application/ipp"; +// Transfer encoding. +const HTTP_TRANSFER_ENCODING_TOKEN: &str = "Transfer-Encoding:"; +const HTTP_TRANSFER_ENCODING_CHUNKED: &str = "chunked"; + +const HTTP_PRINT_LINK: &str = "/ipp/print"; +const HTTP_HOSTNAME: &str = "localhost"; + +use log::error; + +#[derive(Debug, Default)] +pub struct ParsedHttp { + pub http_valid: bool, + pub is_chunked: bool, + pub content_len: usize, // valid for non-chunked transfer + pub ipp_valid: bool, +} + +impl ParsedHttp { + pub fn new() -> Self { + Self::default() + } + + pub fn reset(&mut self) { + *self = ParsedHttp::new(); + } + + pub fn parse(&mut self, headers: &Vec) -> bool { + self.reset(); + if headers.is_empty() { + return false; + } + + let first = &headers[0]; + let mut hdr_iter = first.split_whitespace(); + if hdr_iter.next().unwrap_or_default() != HTTP_POST_TOKEN { + return false; + } + + let link = hdr_iter.next().unwrap_or_default().to_string(); + if link != HTTP_PRINT_LINK { + error!("Invalid printer link!"); + return false; + } + + for header in headers { + if header.starts_with(HTTP_CONTENT_TYPE_TOKEN) { + if header.split_whitespace().nth(1).unwrap_or_default() == HTTP_CONTENT_TYPE_IPP { + self.ipp_valid = true; + } + } else if header.starts_with(HTTP_TRANSFER_ENCODING_TOKEN) { + if header.split_whitespace().nth(1).unwrap_or_default() + == HTTP_TRANSFER_ENCODING_CHUNKED + { + self.is_chunked = true; + } + } else if header.starts_with(HTTP_CONTENT_LENGTH_TOKEN) { + // Content-length is in decimal + match header + .split_whitespace() + .nth(1) + .unwrap_or_default() + .parse::() + { + Ok(n) => { + if n > HTTP_MAX_CONTENT_LENGTH { + error!("Invalid IPP-request, content-length is too much: {n}"); + return false; + }; + self.content_len = n as usize; + } + Err(_) => { + return false; + } + }; + } else if header.starts_with(HTTP_HOST_TOKEN) { + let hostname = header.split_whitespace().nth(1).unwrap_or_default(); + if hostname != HTTP_HOSTNAME { + error!("Invalid hostname was provided!"); + return false; + } + } + } + + if !self.ipp_valid { + return false; + } + + if self.is_chunked || self.content_len != 0 { + self.http_valid = true; + true + } else { + self.http_valid = false; + false + } + } +} diff --git a/ipp_proxy/src/proxy_client/ipp_processing.rs b/ipp_proxy/src/proxy_client/ipp_processing.rs index bb6ab50351b3d9f4910947cacbf4317b9368a4e4..25de5a0609341436e1e8af45be4ac37b062015bb 100644 --- a/ipp_proxy/src/proxy_client/ipp_processing.rs +++ b/ipp_proxy/src/proxy_client/ipp_processing.rs @@ -10,17 +10,48 @@ // NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. // See the Mulan PSL v2 for more details. -use std::collections::{HashMap, HashSet}; - +use bytes::Bytes; +use ipp::request::IppRequestResponse; use log::warn; +use std::collections::{HashMap, HashSet}; use uuid::Uuid; use crate::proxy_client::{ + http::HTTP_STATUS_OK, ipp_printer::Printer, print_api::{PrintApi, PrintOps, PrinterId}, VMGT_UUID_PREFIX, }; +pub fn ipp_make_http( + http_status: usize, + ipp_response: Option<&IppRequestResponse>, +) -> (Vec, Option) { + let mut http_headers: Vec = Vec::new(); + let http_status = match http_status { + HTTP_STATUS_OK => "HTTP/1.1 200 OK", + _ => "HTTP/1.1 400 Bad Request", + }; + http_headers.push(http_status.to_string()); + http_headers.push("Connection: close".to_string()); + http_headers.push("Content-Language: en".to_string()); + let (content_len, ipp_data) = match ipp_response { + Some(resp) => { + let ipp_data = resp.to_bytes(); + (ipp_data.len(), Some(ipp_data)) + } + None => (0, None), + }; + http_headers.push(format!("Content-Length: {content_len}")); + http_headers.push("Content-Type: application/ipp".to_string()); + http_headers.push("Server: VMGT/1.0 IPP/2.1".to_string()); + http_headers.push("X-Frame-Options: DENY".to_string()); + http_headers.push("Content-Security-Policy: frame-ancestors 'none'".to_string()); + http_headers.push("".to_string()); + + (http_headers, ipp_data) +} + pub fn ipp_make_uuid(printer_id: &str) -> String { let uuid_in = Uuid::new_v5(&Uuid::NAMESPACE_URL, printer_id.as_bytes()); let mut uuid_bin = uuid_in.into_bytes(); diff --git a/ipp_proxy/src/proxy_client/ipp_utils.rs b/ipp_proxy/src/proxy_client/ipp_utils.rs new file mode 100644 index 0000000000000000000000000000000000000000..ccc3e06edb4fdcdb0d2acdc0d154b829719cedaa --- /dev/null +++ b/ipp_proxy/src/proxy_client/ipp_utils.rs @@ -0,0 +1,587 @@ +// Copyright (c) 2025 Huawei Technologies Co.,Ltd. All rights reserved. +// +// StratoVirt is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan +// PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// See the Mulan PSL v2 for more details. + +use std::{cmp::min, collections::VecDeque, fs::File, io, io::prelude::*, path::Path}; + +use ipp::{ + model::{IppVersion, Operation}, + parser::IppParser, + request::IppRequestResponse, +}; +use log::{error, trace, warn}; + +use crate::proxy_client::http::ParsedHttp; + +pub const CRLF: &str = "\r\n"; + +const MAX_HTTP_HEADERS_COUNT: usize = 64; +const MAX_HTTP_HEADER_SIZE: usize = 4096; +const MAX_IPP_HEADER_SIZE: usize = 8192; +const CR: u8 = b'\r'; +const LF: u8 = b'\n'; + +#[derive(Clone, Copy, PartialEq)] +enum ChunkReaderState { + NonChunked, + ReadSize, + ReadData, + ReadTail, + ReadFinal, + Completed, +} + +struct HttpChunkReader { + state: ChunkReaderState, + size: usize, + size_ready: usize, +} + +impl HttpChunkReader { + pub fn new() -> Self { + Self { + state: ChunkReaderState::NonChunked, + size: 0, + size_ready: 0, + } + } + + pub fn size(&self) -> usize { + self.size + } + + pub fn set_size(&mut self, size: usize) { + self.size = size; + self.size_ready = 0; + } + + pub fn need_read_head(&self) -> bool { + self.state == ChunkReaderState::ReadSize + } + + pub fn need_read_data(&self) -> bool { + self.state == ChunkReaderState::ReadData + } + + pub fn need_read_tail(&self) -> bool { + self.state == ChunkReaderState::ReadTail || self.state == ChunkReaderState::ReadFinal + } + + pub fn is_finished(&self) -> bool { + self.state == ChunkReaderState::Completed + } + + pub fn set_state(&mut self, state: ChunkReaderState) { + self.state = state; + } + + pub fn add_size_ready(&mut self, size: usize) { + self.size_ready += size; + assert!(self.size_ready <= self.size); + if self.size_ready >= self.size { + self.state = ChunkReaderState::ReadTail; + } + } + + pub fn bytes_to_read(&self) -> usize { + self.size - self.size_ready + } + + pub fn parse_size_line(&mut self, line: &str) -> bool { + // Chunk size if hex!! + let size = match u32::from_str_radix(line, 16) { + Ok(n) => n as usize, + Err(_) => { + warn!("failed to read http chunk size"); + return false; + } + }; + + self.size_ready = 0; + self.size = size; + if size == 0 { + self.set_state(ChunkReaderState::ReadFinal); + } else { + self.set_state(ChunkReaderState::ReadData); + } + true + } +} + +pub struct IppReader { + ipp_buf: Vec, + ipp_bytes_ready: usize, + data_deque: VecDeque, + curr_line: String, + cr_seen: bool, + line_overflow: bool, + http_error: bool, + ipp_error: bool, + pub http_headers: Vec, + chunk_reader: HttpChunkReader, + http_hdrs_ready: bool, // true if we received all http headers + ipp_hdr_ready: bool, // true if we received the IPP header + pub http: ParsedHttp, + pub ipp: IppRequestResponse, // parsed ipp + doc_received: bool, // true if it is a printig operation and we fully received the document + doc_receive_started: bool, + spool_file_name: Option, + spool_file: Option, + job_id: Option, // has meaning only for documents + file_error: bool, +} + +impl IppReader { + pub fn new() -> Self { + Self { + ipp_buf: Vec::new(), + ipp_bytes_ready: 0, + data_deque: VecDeque::new(), + curr_line: String::new(), + cr_seen: false, + line_overflow: false, + http_error: false, + ipp_error: false, + http_headers: Vec::new(), + chunk_reader: HttpChunkReader::new(), + http_hdrs_ready: false, + ipp_hdr_ready: false, + http: ParsedHttp::new(), + ipp: IppRequestResponse::new(IppVersion::v2_0(), Operation::PrintJob, None), + doc_received: false, + doc_receive_started: false, + spool_file_name: None, + spool_file: None, + job_id: None, + file_error: false, + } + } + + pub fn is_http_error(&self) -> bool { + self.http_error + } + + pub fn is_file_error(&self) -> bool { + self.file_error + } + + pub fn is_http_ready(&self) -> bool { + self.http_hdrs_ready + } + + pub fn is_ipp_ready(&self) -> bool { + self.ipp_hdr_ready + } + + pub fn is_ipp_error(&self) -> bool { + self.ipp_error + } + + pub fn ipp_has_document(&self) -> bool { + let op = self.ipp.header().operation_or_status; + op == Operation::PrintJob as u16 || op == Operation::SendDocument as u16 + } + + pub fn get_ipp_request(&self) -> &IppRequestResponse { + &self.ipp + } + + pub fn is_doc_received(&self) -> bool { + self.doc_received + } + + pub fn reset(&mut self) { + trace!("http reset"); + *self = Self::new(); + } + + pub fn reset_remove_file(&mut self) { + self.remove_spool_file(); + self.reset(); + } + + pub fn has_unparsed_data(&self) -> bool { + !self.data_deque.is_empty() + } + + pub fn push_data(&mut self, data: Vec) { + self.data_deque = data.into(); + } + + fn read_char(&mut self) -> Option { + self.data_deque.pop_front() + } + + // returns True if line is finished (with "\r\n"). + // line is read to self.curr_line + // Will return empty string if it is just "\r\n" + fn read_line(&mut self) -> bool { + loop { + let ch = if let Some(ch) = self.read_char() { + ch + } else { + return false; + }; + if self.cr_seen { + if ch == LF { + if self.line_overflow { + // ok, we met CRLF. Reset the error. + self.line_overflow = false; + return false; + } + self.cr_seen = false; + return true; // message is ready + } + // we were waiting for LF, but received something else. + // append remembered CR to the line + self.curr_line.push(CR as char); + self.cr_seen = false; + } + if ch == CR { + self.cr_seen = true; + continue; + } + if self.line_overflow { + return true; // note! we return true. Caller must check self.line_overflow + } + + self.curr_line.push(ch as char); + if self.curr_line.len() >= MAX_HTTP_HEADER_SIZE { + error!("http line overflow: {}", self.curr_line); + self.line_overflow = true; + self.curr_line.clear(); + return true; + } + } + } + + // returns false if http is not ready yet + pub fn parse_http(&mut self) -> bool { + loop { + if !self.read_line() { + return false; + }; + if self.line_overflow { + self.http_error = true; + return true; // note! we return true. Caller must check self.http_error + }; + if self.curr_line.is_empty() { + // the http part is finished. + if self.http_error { + error!("Current invalid http is finished"); + return true; + } + + self.http_error = !self.http.parse(&self.http_headers); + if self.http_error { + error!("Failed to parse http request:"); + for header in &self.http_headers { + trace!("{}", header); + } + return true; + } + + trace!("{:?}", self.http); + self.http_hdrs_ready = true; + return true; + }; + if self.http_headers.len() >= MAX_HTTP_HEADERS_COUNT { + error!("too many headers in http"); + self.http_error = true; + return true; // note! we return true. Caller must check self.http_error + }; + self.http_headers.push(std::mem::take(&mut self.curr_line)); + } + } + + fn http_read_chunk_hdr(&mut self) -> bool { + // Chunk starts with size, ends with CRLF. + // Last chunk is CRLF '0' CRLF CRLF + if !self.read_line() { + return false; + }; + + if self.line_overflow { + self.http_error = true; + return true; // note! we return true. Caller must check self.http_error + }; + + if !self.chunk_reader.parse_size_line(&self.curr_line) { + warn!("failed to read http chunk size"); + self.http_error = true; + return true; + }; + + self.curr_line.clear(); + + true + } + + fn http_read_chunk_tail(&mut self) -> bool { + // Chunk starts with size, ends with CRLF. + // Last chunk is CRLF '0' CRLF CRLF + if !self.read_line() { + return false; + }; + if self.line_overflow { + self.http_error = true; + return true; // note! we return true. Caller must check self.http_error + }; + if !self.curr_line.is_empty() { + warn!("Unexpected byte sequence. Expected CRLF, found data."); + self.http_error = true; + self.curr_line.clear(); + return true; // note! we return true. Caller must check self.http_error + }; + + if self.chunk_reader.state == ChunkReaderState::ReadFinal { + self.chunk_reader.set_state(ChunkReaderState::Completed); + return true; + }; + + self.chunk_reader.set_size(0); + self.chunk_reader.set_state(ChunkReaderState::ReadSize); + + true + } + + fn read_ipp_helper(&mut self) -> bool { + assert!(self.http_hdrs_ready); + assert!(!self.ipp_hdr_ready); + + if self.ipp_buf.is_empty() { + // Fix me! we must look on transfer-encoding first. If it is chunked, then we ignore content-len + if self.http.content_len != 0 { + if self.http.content_len > MAX_IPP_HEADER_SIZE { + warn!("Too big ipp-header: {}.", self.http.content_len); + self.http_error = true; + return true; + } + trace!("ipp header: {} bytes", self.http.content_len); + self.chunk_reader.set_state(ChunkReaderState::NonChunked); + self.ipp_buf.resize(self.http.content_len, 0u8); + } else { + // Chunked http. Need read chunk size. + // We checked for chunked-transfer in read_http, This assert is just a recheck + assert!(self.http.is_chunked); + + self.chunk_reader.set_state(ChunkReaderState::ReadSize); + + if !self.http_read_chunk_hdr() { + return false; + } + if self.http_error { + return true; + } + + if self.chunk_reader.size() == 0 { + warn!("Unexpected zero chunk."); + self.http_error = true; + return true; + } + + // state have to be ReadData here + assert!(self.chunk_reader.state == ChunkReaderState::ReadData); + + if self.chunk_reader.size() > MAX_IPP_HEADER_SIZE { + warn!("Too big ipp-header: {}.", self.chunk_reader.size()); + self.http_error = true; + return true; + } + + trace!("ipp header: {} bytes", self.chunk_reader.size()); + self.ipp_buf.resize(self.chunk_reader.size(), 0u8); + } + } + + if self.ipp_bytes_ready != self.ipp_buf.len() { + let need_size = self.ipp_buf.len() - self.ipp_bytes_ready; + let copied = if self.data_deque.len() < need_size { + let copied = self.data_deque.len(); + self.ipp_buf[self.ipp_bytes_ready..self.ipp_bytes_ready + copied] + .copy_from_slice(&self.data_deque.as_slices().0); + self.data_deque.clear(); + copied + } else { + self.ipp_buf[self.ipp_bytes_ready..] + .copy_from_slice(&self.data_deque.as_slices().0[..need_size]); + drop(self.data_deque.drain(..need_size)); + need_size + }; + if copied == 0 { + return false; + } + self.ipp_bytes_ready += copied; + if self.http.is_chunked { + self.chunk_reader.add_size_ready(copied); + } + } + + if self.http.is_chunked { + if self.ipp_bytes_ready == self.ipp_buf.len() && !self.chunk_reader.need_read_tail() { + warn!("ipp header is finished, but chunk isn't.") + } + if self.chunk_reader.need_read_tail() && self.ipp_bytes_ready != self.ipp_buf.len() { + warn!("http-chunk is finished, but ipp header isn't.") + } + } + + if self.chunk_reader.need_read_tail() && !self.http_read_chunk_tail() { + return false; + } + + if self.ipp_bytes_ready != self.ipp_buf.len() { + return false; + } + + true + } + + pub fn parse_ipp(&mut self) -> bool { + loop { + // read_ipp_helper() returns true if it read anything from stream + if !self.read_ipp_helper() { + return false; + } + if self.http_error { + return true; + } + + if self.ipp_bytes_ready == self.ipp_buf.len() { + trace!("ipp header finished: {} bytes", self.ipp_bytes_ready); + self.ipp_hdr_ready = true; + break; + } + } + + let ipp_data = std::mem::take(&mut self.ipp_buf); + let result = IppParser::new(ipp::reader::IppReader::new(io::Cursor::new(ipp_data))).parse(); + self.ipp = match result { + Ok(ipp) => ipp, + Err(e) => { + warn!("failed to parse the ipp message: {:?}", e); + self.ipp_error = true; + return true; + } + }; + true + } + + pub fn is_doc_started(&self) -> bool { + self.doc_receive_started + } + + pub fn start_read_doc(&mut self, job_id: Option, spool_file_name: Option) { + // we can get None here for spool_file_name + // if printer or job doesn't exist + assert!(self.ipp_has_document()); + self.doc_receive_started = true; + self.job_id = job_id; + self.spool_file_name = spool_file_name; + } + + pub fn get_job_id(&self) -> Option { + self.job_id + } + + pub fn get_doc_file_name(&self) -> Option { + if self.spool_file_name.is_some() { + Some(self.spool_file_name.as_ref().unwrap().clone()) + } else { + None + } + } + + fn remove_spool_file(&mut self) { + self.spool_file = None; + if self.spool_file_name.is_some() { + let spool_file_name = self.spool_file_name.as_ref().unwrap(); + if Path::new(spool_file_name).exists() { + let _ = std::fs::remove_file(spool_file_name); + } + } + } + + pub fn read_doc(&mut self) -> bool { + assert!(self.http_hdrs_ready); + assert!(self.ipp_hdr_ready); + assert!(self.doc_receive_started); + + loop { + if !self.http.is_chunked { + warn!("Operation with document, but non-chunked http."); + self.remove_spool_file(); + self.http_error = true; + return true; + } + + if self.chunk_reader.need_read_head() { + if !self.http_read_chunk_hdr() { + return false; + } + if self.http_error { + self.remove_spool_file(); + return true; + } + } + + // Read to file + if self.chunk_reader.need_read_data() { + if self.chunk_reader.bytes_to_read() == 0 { + warn!("Unexpected zero chunk."); + self.http_error = true; + self.remove_spool_file(); + return true; + } + let size = min(self.data_deque.len(), self.chunk_reader.bytes_to_read()); + if size == 0 { + return false; + } + let file_write_result = if self.spool_file.is_some() { + self.spool_file + .as_mut() + .unwrap() + .write(&self.data_deque.as_slices().0[..size]) + } else { + Ok(0) + }; + + if file_write_result.is_err() { + error!( + "Failed to write to spool file: {}", + file_write_result.err().unwrap() + ); + self.file_error = true; + self.remove_spool_file(); + } + drop(self.data_deque.drain(..size)); + self.chunk_reader.add_size_ready(size); + } + + if self.chunk_reader.need_read_tail() { + if !self.http_read_chunk_tail() { + return false; + } + if self.http_error { + self.remove_spool_file(); + return true; + } + } + + if self.chunk_reader.is_finished() { + // Close the file and return true + self.spool_file = None; + self.doc_received = true; + return true; + } + } + } +} diff --git a/ipp_proxy/src/proxy_client/mod.rs b/ipp_proxy/src/proxy_client/mod.rs index 65f598ec4fdd30eb90368b8be2c48515397e04a3..11150d4eb32152af01beb794f42f3ddf3544a718 100644 --- a/ipp_proxy/src/proxy_client/mod.rs +++ b/ipp_proxy/src/proxy_client/mod.rs @@ -10,12 +10,15 @@ // NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. // See the Mulan PSL v2 for more details. +pub mod http; pub mod ipp_printer; pub mod ipp_processing; +mod ipp_utils; pub mod print_api; pub mod state_channel; use std::{ + collections::HashMap, io, io::{prelude::*, ErrorKind}, os::fd::AsRawFd, @@ -26,7 +29,8 @@ use std::{ use anyhow::{bail, Result}; use byteorder::{ByteOrder, LittleEndian}; -use log::{error, info, trace}; +use bytes::Bytes; +use log::{error, info, trace, warn}; use strum::EnumCount; use strum_macros::EnumCount; use vmm_sys_util::{ @@ -35,7 +39,10 @@ use vmm_sys_util::{ }; use crate::proxy_client::{ - ipp_processing::IppProcessing, + http::HTTP_BAD_REQUEST, + ipp_processing::{ipp_make_http, IppProcessing}, + ipp_utils::IppReader, + ipp_utils::CRLF, print_api::{PrintApi, PrintOps}, state_channel::*, }; @@ -44,6 +51,8 @@ pub type StateThreadHandle = Option>; pub const VMGT_UUID_PREFIX: &str = "VMGT31dc"; +const MAX_IPP_READERS: usize = 16; + #[derive(Debug, EnumCount)] enum ProxyClientEvent { Stop = 0, @@ -73,6 +82,7 @@ struct StreamData { } pub struct ProxyClient { + ipp_readers: HashMap, _ipp_process: Arc>, exit_evt: Arc, need_reset: bool, @@ -114,6 +124,7 @@ impl ProxyClient { info!("ProxyClient is initialized"); Ok(Self { + ipp_readers: HashMap::new(), _ipp_process: ipp_process, exit_evt, need_reset: false, @@ -153,6 +164,18 @@ impl ProxyClient { } } + fn reset_current_stream(&mut self) { + trace!("reset stream {}", self.curr_stream_id); + self.ipp_readers.remove(&self.curr_stream_id); + } + + fn reset_stream_remove_file(&mut self, stream_id: u32) { + trace!("reset and remove file, stream {}", stream_id); + if let Some(mut reader) = self.ipp_readers.remove(&stream_id) { + reader.reset_remove_file(); + } + } + // Disable all transmits until guest explicitly doesn't reset error. fn set_data_error(&mut self) -> Result<()> { let _ = self.epoll.ctl( @@ -164,6 +187,122 @@ impl ProxyClient { Ok(()) } + // returns TRUE if reader has unparsed data + fn reader_parse_data(&mut self) -> bool { + let curr_printer_num = self.get_curr_printer_num(); + // ipp_reader is guaranteed to be some, since we got here + let ipp_reader = self.ipp_readers.get_mut(&self.curr_stream_id).unwrap(); + + // Check HTTP part + if !ipp_reader.is_http_ready() { + if !ipp_reader.parse_http() { + return ipp_reader.has_unparsed_data(); + } + + trace!("ipp_proxy: received http on {}", self.curr_stream_id); + for header in &ipp_reader.http_headers { + trace!("{}", header); + } + + if ipp_reader.is_http_error() { + self.send_ipp_response_error(); + self.reset_current_stream(); + return false; + } + } + + // Read IPP part + if !ipp_reader.is_ipp_ready() { + if !ipp_reader.parse_ipp() { + return ipp_reader.has_unparsed_data(); + } + if ipp_reader.is_http_error() { + self.reset_current_stream(); + return false; + } + if ipp_reader.is_ipp_error() { + self.send_ipp_response_error(); + return false; + } + trace!("received ipp request on {}", self.curr_stream_id); + } + + if ipp_reader.ipp_has_document() && !ipp_reader.is_doc_received() { + if !ipp_reader.is_doc_started() { + trace!("ipp has document"); + // So far there is a temporary stub that never writes data to file, just reads all data. + let job_id = None; + let file_name = None; + + ipp_reader.start_read_doc(job_id, file_name); + } + + // We read all the data anyway, even if some error occurred. + // Error will be sent later, in handle_ipp_request() + if !ipp_reader.read_doc() { + return ipp_reader.has_unparsed_data(); + } + if ipp_reader.is_http_error() { + self.reset_stream_remove_file(self.curr_stream_id); + return false; + } + + trace!("document was received on {}", self.curr_stream_id); + } + + let ipp_reader = self.ipp_readers.remove(&self.curr_stream_id).unwrap(); + self.handle_ipp_request(curr_printer_num, ipp_reader); + false + } + + fn send_ipp_response_error(&mut self) { + warn!("send_ipp_response_error"); + let (http, _) = ipp_make_http(HTTP_BAD_REQUEST, None); + let _ = self.send_ipp_response(http, None); + } + + fn send_ipp_response(&mut self, http: Vec, resp_bytes: Option) -> Result { + trace!("send_ipp_response for {}", self.curr_stream_id); + + // Compute total len + let mut total: usize = 0; + for line in &http { + total += line.as_bytes().len() + 2; // 2 bytes for CRLF + } + if resp_bytes.is_some() { + total += resp_bytes.as_ref().unwrap().len(); + } + + // send hdr: {stream_id: u32, length: u32} + let mut hdr = [0u8; VPRINT_DATA_HDR_SIZE]; + LittleEndian::write_u32(&mut hdr[0..], self.curr_stream_id as u32); + LittleEndian::write_u32(&mut hdr[4..], total as u32); + self.data_stream.write_all(&hdr[0..8])?; + + for line in &http { + self.data_stream.write_all(line.as_bytes())?; + self.data_stream.write_all(CRLF.as_bytes())?; + } + if resp_bytes.is_some() { + self.data_stream.write_all(&resp_bytes.unwrap())?; + } + Ok(true) + } + + // returns TRUE if command was handled + fn handle_ipp_request(&mut self, printer_num: u32, ipp_reader: IppReader) { + trace!("proxy_client::handle_ipp_request"); + // Just return error so far. Real handling will be implemented later. + trace!( + "printer: {printer_num}, job_id: {:?}, op: {}, file_name: {:?}, file_err: {}", + ipp_reader.get_job_id(), + ipp_reader.get_ipp_request().header().operation_or_status, + ipp_reader.get_doc_file_name(), + ipp_reader.is_file_error() + ); + self.send_ipp_response_error(); + } + fn check_stream_hdr_ready(&mut self) -> Result { if self.stream.is_some() { return Ok(true); @@ -210,6 +349,10 @@ impl ProxyClient { Ok(true) } + fn get_curr_printer_num(&self) -> u32 { + self.curr_stream_id / 2 + } + fn handle_data_event(&mut self) -> Result<()> { if !self.check_stream_hdr_ready()? { return Ok(()); @@ -227,13 +370,33 @@ impl ProxyClient { if stream.data_ready != stream.size { return Ok(()); } - let _data = std::mem::take(&mut stream.data); + let data = std::mem::take(&mut stream.data); self.curr_stream_id = stream.id; self.stream = None; self.hdr_bytes_ready = 0; - // just discard data at the moment - Ok(()) + // The limitation about "no more than 16 readers" is artificial. + // For USB-based, we know exactly to which printer this ID relates, + // so we can check reliably and allow exactly 2 readers per printer. + // This will be corrected later. + if let Some(reader) = self.ipp_readers.get_mut(&self.curr_stream_id) { + reader.push_data(data); + } else { + if self.ipp_readers.len() > MAX_IPP_READERS { + error!("Max number of IPP readers is reached. Ignoring request."); + return Ok(()); + } + let mut reader = IppReader::new(); + reader.push_data(data); + self.ipp_readers.insert(self.curr_stream_id, reader); + } + + loop { + // while there are unparsed data on reader, do parsing + if !self.reader_parse_data() { + return Ok(()); + } + } } pub fn run(&mut self) -> Result<()> {