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}