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
11const 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 pub fn new() -> Result<Clamd> {
52 Clamd::local_connect("/var/run/clamav/clamd.ctl")
53 }
54
55 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 pub fn ping(&mut self) -> Result<String> {
65 self.command("zPING")
66 }
67
68 pub fn version(&mut self) -> Result<String> {
70 self.command("zVERSION")
71 }
72
73 pub fn reload(&mut self) -> Result<String> {
75 self.command("zRELOAD")
76 }
77
78 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 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 pub fn contscan<P: AsRef<Path>>(&mut self, path: P) -> Result<String> {
99 self.command(format!("zCONTSCAN {}", path.as_ref().display()))
100 }
101
102 pub fn multiscan<P: AsRef<Path>>(&mut self, path: P) -> Result<String> {
104 self.command(format!("zMULTISCAN {}", path.as_ref().display()))
105 }
106
107 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])?; 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}