clamd_client/
clamd.rs

1use anyhow::{Context, Result};
2use std::ffi::CString;
3use std::fs::File;
4use std::io::prelude::*;
5use std::net::{Shutdown, SocketAddr};
6#[cfg(target_family = "unix")]
7use std::os::unix::net::UnixStream;
8use std::path::{Path, PathBuf};
9use thiserror::Error;
10
11/// Default value for chunk size used in instream scan ref: man clamd
12const DEFAULT_CHUNK_SIZE: u32 = 4096;
13
14#[derive(Debug, Error)]
15pub enum ClamdError {
16    #[error("Can't create String from utf8 vec")]
17    StringifyError(Vec<u8>),
18    #[error("The path is not absolute")]
19    PathIsNotAbsolute,
20}
21
22#[derive(Debug)]
23pub enum StreamType {
24    Tcp(SocketAddr),
25    #[cfg(target_family = "unix")]
26    Unix(PathBuf),
27}
28
29impl std::string::ToString for StreamType {
30    fn to_string(&self) -> String {
31        match self {
32            Self::Tcp(addr) => addr.to_string(),
33            Self::Unix(path) => String::from(path.to_string_lossy()),
34        }
35    }
36}
37
38#[derive(Debug)]
39pub struct Clamd {
40    stream: UnixStream,
41}
42
43impl Drop for Clamd {
44    fn drop(&mut self) {
45        _ = self.stream.shutdown(Shutdown::Both);
46    }
47}
48
49impl Clamd {
50    /// Connect to clamd with default socket
51    pub fn new() -> Result<Clamd> {
52        Clamd::local_connect("/var/run/clamav/clamd.ctl")
53    }
54
55    /// Connect to clamd with specificated socket
56    pub fn local_connect<P: AsRef<Path>>(sock: P) -> Result<Clamd> {
57        Ok(Clamd {
58            stream: UnixStream::connect(sock.as_ref())
59                .with_context(|| "Can't connect unix stream")?,
60        })
61    }
62
63    /// Check the daemon's state. It should reply with "PONG\0".
64    pub fn ping(&mut self) -> Result<String> {
65        self.command("zPING")
66    }
67
68    /// Check the clamav and database versions.
69    pub fn version(&mut self) -> Result<String> {
70        self.command("zVERSION")
71    }
72
73    /// Reload the database.
74    pub fn reload(&mut self) -> Result<String> {
75        self.command("zRELOAD")
76    }
77
78    /// Shutdown the clamd service.
79    pub fn shutdown(&mut self) -> Result<()> {
80        self.stream
81            .write_all(
82                CString::new("SHUTDOWN")
83                    .with_context(|| "Can't create CString")?
84                    .as_bytes_with_nul(),
85            )
86            .with_context(|| "Can't write to unix stream")
87    }
88
89    /// Scan a file or directory (recursively).
90    pub fn scan<P: AsRef<Path>>(&mut self, path: P) -> Result<String> {
91        if !path.as_ref().is_absolute() {
92            return Err(ClamdError::PathIsNotAbsolute.into());
93        }
94        self.command(format!("zSCAN {}", path.as_ref().display()))
95    }
96
97    /// Scan a file or directory (recursively) and don't stop the scanning when a malware found.
98    pub fn contscan<P: AsRef<Path>>(&mut self, path: P) -> Result<String> {
99        self.command(format!("zCONTSCAN {}", path.as_ref().display()))
100    }
101
102    /// Scan a file or directory (recursively) using multi thread.
103    pub fn multiscan<P: AsRef<Path>>(&mut self, path: P) -> Result<String> {
104        self.command(format!("zMULTISCAN {}", path.as_ref().display()))
105    }
106
107    /// Instream Scan.
108    pub fn instream_scan<P: AsRef<Path>>(
109        &mut self,
110        path: P,
111        chunk_size: Option<u32>,
112    ) -> Result<String> {
113        self.sendmsg("zINSTREAM")?;
114        let mut file = File::open(path).with_context(|| "Can't open file")?;
115        let mut buf = vec![0; chunk_size.unwrap_or(DEFAULT_CHUNK_SIZE) as usize];
116        loop {
117            let size = file
118                .read(&mut buf)
119                .with_context(|| "Can't read from file")?;
120            if size != 0 {
121                self.send((size as u32).to_be_bytes())?;
122                self.send(&buf[0..size])?
123            } else {
124                self.send([0; 4])?; // zero sized chunk
125                break;
126            }
127        }
128
129        self.recv()
130    }
131
132    fn send<S: AsRef<[u8]>>(&mut self, data: S) -> Result<()> {
133        self.stream.write_all(data.as_ref()).with_context(|| "Can't write to unix stream")
134    }
135
136    fn sendmsg<S: AsRef<[u8]>>(&mut self, msg: S) -> Result<()> {
137        let req = CString::new(msg.as_ref()).with_context(|| "Can't create CString for send from {:?}")?;
138        self.stream
139            .write_all(req.as_bytes_with_nul())
140            .with_context(|| "Can't write to unix stream")?;
141        Ok(())
142    }
143
144    fn recv(&mut self) -> Result<String> {
145        let mut resp = Vec::new();
146        self.stream
147            .read_to_end(&mut resp)
148            .with_context(|| "Can't read from unix stream")?;
149
150        Ok(String::from_utf8(resp)
151            .map_err(|e| ClamdError::StringifyError(e.as_bytes().to_vec()))?)
152    }
153
154    fn command<S: AsRef<[u8]>>(&mut self, msg: S) -> Result<String> {
155        self.sendmsg(msg)?;
156        self.recv()
157    }
158}
159
160#[cfg(test)]
161mod clamd {
162    use super::*;
163
164    #[test]
165    fn clamd_local_connection() {
166        assert!(Clamd::new().is_ok());
167        assert!(Clamd::local_connect("/var/run/clamav/clamd.ctl").is_ok());
168    }
169
170    #[test]
171    fn ping() {
172        let mut clamd = Clamd::new().unwrap();
173
174        let resp = clamd.ping();
175        assert!(resp.is_ok());
176        assert_eq!("PONG\0", &resp.unwrap()[..])
177    }
178    #[test]
179    fn version() {
180        let mut clamd = Clamd::new().unwrap();
181
182        let resp = clamd.version();
183        assert!(resp.is_ok());
184        assert!(resp.unwrap().contains("ClamAV"));
185    }
186
187    #[test]
188    fn reload() {
189        let mut clamd = Clamd::new().unwrap();
190
191        let resp = clamd.reload();
192        assert!(resp.is_ok());
193        assert_eq!("RELOADING\0", &resp.unwrap()[..]);
194    }
195
196    #[test]
197    fn scan() {
198        let mut clamd = Clamd::new().unwrap();
199
200        let resp = clamd.scan("/proc/self/exe");
201        assert!(resp.is_ok());
202        assert_eq!("/proc/self/exe: OK\0", &resp.unwrap()[..]);
203    }
204
205    #[test]
206    fn contscan() {
207        let mut clamd = Clamd::new().unwrap();
208
209        let resp = clamd.contscan("/proc/self/exe");
210        assert!(resp.is_ok());
211        assert_eq!("/proc/self/exe: OK\0", &resp.unwrap()[..]);
212    }
213
214    #[test]
215    fn multiscan() {
216        let mut clamd = Clamd::new().unwrap();
217
218        let resp = clamd.multiscan("/proc/self/exe");
219        assert!(resp.is_ok());
220        assert_eq!("/proc/self/exe: OK\0", &resp.unwrap()[..]);
221    }
222
223    #[test]
224    fn scan_failure_iff_path_is_not_absolute() {
225        let mut clamd = Clamd::new().unwrap();
226
227        let resp = clamd.scan("./d0a353461bc77cb023d730e527c5160c7eb8b303");
228        assert!(resp.is_err());
229    }
230
231    #[test]
232    fn instream_scan() {
233        let mut clamd = Clamd::new().unwrap();
234
235        let resp = clamd.instream_scan("/bin/ls", None);
236        assert!(resp.is_ok());
237    }
238}