clam_client/client.rs
1//! `ClamClient` provides the bridge between the Rust code and the ClamD socket, and implements
2//! most Clam commands in a Rust idiomatic interface.
3
4use byteorder::{BigEndian, ByteOrder};
5use error::ClamError;
6use response::{ClamStats, ClamVersion, ClamScanResult};
7use std::io::{BufReader, Read, Write};
8use std::net::IpAddr;
9use std::net::SocketAddr;
10use std::net::TcpStream;
11use std::time::Duration;
12
13/// `ClamResult` is a simple wrapper used for all operations, this makes it simple to handle
14/// from the callers side.
15pub type ClamResult<T> = ::std::result::Result<T, ClamError>;
16
17/// `ClamClient` is the crux of the crate, it retains information about what socket to connect
18/// to, thus that it can reconnect, and what timeout (if any) to use when connecting.
19///
20/// *Note:* Future versions may move `timeout` to be use in command operations as well as
21/// when connecting. However since the latter is so variable, this may require a different - or even
22/// per call - timeout value.
23pub struct ClamClient {
24 socket: SocketAddr,
25 timeout: Option<Duration>,
26}
27
28impl ClamClient {
29 /// Creates a new instance of `ClamClient` with no connect timeout, commands issued from this
30 /// client will indefinitely block if ClamD becomes unavailable.
31 ///
32 /// *Arguments*
33 ///
34 /// - `ip`: The IP address to connect to
35 /// - `port`: The port to connect to
36 ///
37 /// *Example*
38 ///
39 /// ```rust
40 /// extern crate clam_client;
41 ///
42 /// use clam_client::client::ClamClient;
43 ///
44 /// fn main() {
45 /// if let Ok(client) = ClamClient::new("127.0.0.1", 3310) {
46 /// println!("{:?}", client.version());
47 /// }
48 /// }
49 /// ```
50 pub fn new(ip: &str, port: u16) -> ClamResult<ClamClient> {
51 build(ip, port, None)
52 }
53
54 /// Creates a new instance of `ClamClient` with a connection timeout (in seconds). Any command
55 /// issued from this client will error after `timeout_secs` if ClamD is unavailable.
56 ///
57 /// *Arguments*
58 ///
59 /// - `ip`: The IP address to connect to
60 /// - `port`: The port to connect to
61 /// - `timeout_secs`: The number of seconds to wait before aborting the connection
62 ///
63 /// *Example*
64 ///
65 /// ```rust
66 /// extern crate clam_client;
67 ///
68 /// use clam_client::client::ClamClient;
69 ///
70 /// fn main() {
71 /// if let Ok(client) = ClamClient::new_with_timeout("127.0.0.1", 3310, 10) {
72 /// println!("{:?}", client.version());
73 /// }
74 /// }
75 /// ```
76 pub fn new_with_timeout(ip: &str, port: u16, timeout_secs: u64) -> ClamResult<ClamClient> {
77 build(ip, port, Some(Duration::from_secs(timeout_secs)))
78 }
79
80 /// Implements the ClamD `PING` command, returns true if ClamD responds with `PONG`, or false if
81 /// there was an error, or ClamD did not respond with `PONG`.
82 pub fn ping(&self) -> bool {
83 match self.send_command(b"zPING\0") {
84 Ok(resp) => resp == "PONG",
85 Err(_) => false,
86 }
87 }
88
89 /// Implements the ClamD `VERSION` conmand, returns a struct of `ClamVersion` if successful,
90 /// or an error if processing the respnose failed, or if there was an issue talking to ClamD.
91 pub fn version(&self) -> ClamResult<ClamVersion> {
92 let resp = self.send_command(b"zVERSION\0")?;
93 ClamVersion::parse(resp)
94 }
95
96
97 /// Implements the ClamD `RELOAD` command, returns the state of the request as a `String` from
98 /// ClamD, or a network error if the command failed.
99 pub fn reload(&self) -> ClamResult<String> {
100 self.send_command(b"zRELOAD\0")
101 }
102
103 /// Implements the ClamD `SCAN` and `CONTSCAN` commands, returns a `Vec<ClamScanResult>` if the command
104 /// was successful, or a network error if the command failed.
105 ///
106 /// *Arguments:*
107 ///
108 /// - `path`: The path to scan, this is a path that is on the ClamD server, or that it has access to.
109 /// - `continue_on_virus`: If true, instructs ClamD to continue scanning even after it detects a virus.
110 ///
111 /// *Example*
112 ///
113 /// ```rust
114 /// extern crate clam_client;
115 ///
116 /// use clam_client::client::ClamClient;
117 /// use clam_client::response::ClamScanResult;
118 ///
119 /// fn main() {
120 /// let client = ClamClient::new("127.0.0.1", 3310).unwrap();
121 ///
122 /// if let Ok(scan_results) = client.scan_path("/tmp/", true){
123 /// for result in scan_results.iter() {
124 /// match result {
125 /// ClamScanResult::Found(location, virus) => {
126 /// println!("Found virus: '{}' in {}", virus, location)
127 /// },
128 /// _ => {},
129 /// }
130 /// }
131 /// }
132 /// }
133 /// ```
134 pub fn scan_path(&self, path: &str, continue_on_virus: bool) -> ClamResult<Vec<ClamScanResult>> {
135 let result = if continue_on_virus {
136 self.send_command(&format!("zCONTSCAN {}\0", path).into_bytes())?
137 } else {
138 self.send_command(&format!("zSCAN {}\0", path).into_bytes())?
139 };
140
141 Ok(ClamScanResult::parse(result))
142 }
143
144 /// Implements the ClamD `MULTISCAN` command which allows the ClamD instance to perform
145 /// multi-threaded scanning. Returns a `Vec<ClamScanResult>` if the command wassuccessful,
146 /// or a network error if the command failed.
147 pub fn multiscan_path(&self, path: &str) -> ClamResult<Vec<ClamScanResult>> {
148 let result = self.send_command(&format!("zSCAN {}\0", path).into_bytes())?;
149 Ok(ClamScanResult::parse(result))
150 }
151
152 /// Implements the ClamD `INSTREAM` command, which allows the caller to stream a file to the ClamD
153 /// instance. Retuns a `ClamScanResult` if the command was successful.
154 ///
155 /// *Arguments*:
156 ///
157 /// - `stream`: The object to be scanned, this must implement `Read`, it will be read into a buffer
158 /// of 4096 bytes and then written to the ClamD instance. This object must not exceed the ClamD
159 /// max stream size, else the socket will be forcibly closed - in which case an error will be reutned
160 /// from this function.
161 ///
162 /// *Example*
163 ///
164 /// ```rust
165 /// extern crate clam_client;
166 ///
167 /// use clam_client::client::ClamClient;
168 /// use clam_client::response::ClamScanResult;
169 /// use std::fs::File;
170 ///
171 /// fn main() {
172 /// let client = ClamClient::new("127.0.0.1", 3310).unwrap();
173 /// let file = File::open("/etc/hosts").unwrap();
174 ///
175 /// match client.scan_stream(file) {
176 /// Ok(result) => match result {
177 /// ClamScanResult::Ok => println!("File /etc/hostname is OK!"),
178 /// ClamScanResult::Found(location, virus) => {
179 /// println!("Found virus: '{}' in {}", virus, location)
180 /// },
181 /// ClamScanResult::Error(err) => println!("Received error from ClamAV: {}", err),
182 /// },
183 /// Err(e) => println!("A network error occurred whilst talking to ClamAV:\n{}", e),
184 /// }
185 /// }
186 /// ```
187 pub fn scan_stream<T: Read>(&self, stream: T) -> ClamResult<ClamScanResult> {
188 let mut reader = BufReader::new(stream);
189 let mut buffer = [0; 4096];
190 let mut length_buffer = [0; 4];
191 let mut connection = self.connect()?;
192
193 self.connection_write(&connection, b"zINSTREAM\0")?;
194
195 while let Ok(bytes_read) = reader.read(&mut buffer) {
196 if bytes_read > ::std::u32::MAX as usize {
197 return Err(ClamError::InvalidDataLengthError(bytes_read))
198 }
199
200 BigEndian::write_u32(&mut length_buffer, bytes_read as u32);
201
202 self.connection_write(&connection, &length_buffer)?;
203 self.connection_write(&connection, &buffer)?;
204
205 if bytes_read < 4096 {
206 break;
207 }
208 }
209
210 self.connection_write(&connection, &[0, 0, 0, 0])?;
211
212 let mut result = String::new();
213 match connection.read_to_string(&mut result) {
214 Ok(_) => {
215 let scan_result = ClamScanResult::parse(&result);
216
217 if let Some(singular) = scan_result.first() {
218 Ok(singular.clone())
219 } else {
220 Err(ClamError::InvalidData(result))
221 }
222 }
223 Err(e) => Err(ClamError::ConnectionError(e))
224 }
225 }
226
227 /// Implements the ClamD `STATS` command, and returns a struct of `ClamStats`.
228 pub fn stats(&self) -> ClamResult<ClamStats> {
229 let resp: String = self.send_command(b"zSTATS\0")?;
230 ClamStats::parse(&resp)
231 }
232
233 /// Implements the ClamD `SHUTDOWN` command, and returns the status message - if any -
234 /// from ClamD.
235 ///
236 /// *Note*: Since this shuts down the ClamD instance, it will ensure all future calls to
237 /// this or any other `ClamClient` return errors, as such, thus function consumes the calling client.
238 pub fn shutdown(self) -> ClamResult<String> {
239 self.send_command(b"zSHUTDOWN\0")
240 }
241
242 /// Simple reusable wrapper function to send a basic command to the ClamD instance and obtain
243 /// a `ClamResult` that can propogate up the error chain. This is responsible for creating,
244 /// writing to, and managing the connection in all 'one-shot' operations.
245 ///
246 /// *Arguments*:
247 ///
248 /// - `command`: The command to issue in byte form.
249 fn send_command(&self, command: &[u8]) -> ClamResult<String> {
250 let mut connection = self.connect()?;
251
252 match connection.write_all(command) {
253 Ok(_) => {
254 let mut result = String::new();
255 match connection.read_to_string(&mut result) {
256 Ok(_) => Ok(result),
257 Err(e) => Err(ClamError::CommandError(e)),
258 }
259 }
260 Err(e) => Err(ClamError::CommandError(e)),
261 }
262 }
263
264 /// Simple reusable wrapper function for writing a byte stream to an established connection,
265 /// returns the lengh of the data written if successful. This is especially useful for writing
266 /// file streams.
267 ///
268 /// *Arguments*:
269 ///
270 /// - `connection`: The established connection to write to.
271 /// - `data`: The byte stream to send.
272 fn connection_write(&self, mut connection: &TcpStream, data: &[u8]) -> ClamResult<usize> {
273 match connection.write(data) {
274 Ok(v) => Ok(v),
275 Err(e) => Err(ClamError::CommandError(e)),
276 }
277 }
278
279 /// Simple helper function to create a new connection to the ClamD socket.
280 fn connect(&self) -> ClamResult<TcpStream> {
281 let connection = if let Some(t) = self.timeout {
282 TcpStream::connect_timeout(&self.socket, t)
283 } else {
284 TcpStream::connect(&self.socket)
285 };
286
287 match connection {
288 Ok(handle) => Ok(handle),
289 Err(e) => Err(ClamError::ConnectionError(e)),
290 }
291 }
292}
293
294/// Creates a new instance of `ClamClient`.
295fn build(ip: &str, port: u16, timeout: Option<Duration>) -> ClamResult<ClamClient> {
296 let addr: IpAddr = match ip.parse() {
297 Ok(v) => v,
298 Err(e) => return Err(ClamError::InvalidIpAddress(e)),
299 };
300
301 let socket = SocketAddr::new(addr, port);
302
303 Ok(ClamClient { timeout, socket })
304}
305
306#[cfg(test)]
307mod test {
308 use client::ClamClient;
309
310 #[test]
311 fn test_client_no_timeout() {
312 let cclient = ClamClient::new("127.0.0.1", 3310).unwrap();
313 let socket_addr = ::std::net::SocketAddr::new(::std::net::IpAddr::from([127, 0, 0, 1]), 3310);
314 assert_eq!(cclient.socket, socket_addr);
315 assert_eq!(cclient.timeout, None);
316 }
317
318
319 #[test]
320 fn test_client_with_timeout() {
321 let cclient = ClamClient::new_with_timeout("127.0.0.1", 3310, 60).unwrap();
322 let socket_addr = ::std::net::SocketAddr::new(::std::net::IpAddr::from([127, 0, 0, 1]), 3310);
323 assert_eq!(cclient.socket, socket_addr);
324 assert_eq!(cclient.timeout, Some(::std::time::Duration::from_secs(60)));
325 }
326}