1 /*
2  * Copyright (c) 2007-2012, Vsevolod Stakhov
3  * All rights reserved.
4 
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are met:
7  * Redistributions of source code must retain the above copyright notice, this
8  * list of conditions and the following disclaimer. Redistributions in binary form
9  * must reproduce the above copyright notice, this list of conditions and the
10  * following disclaimer in the documentation and/or other materials provided with
11  * the distribution. Neither the name of the author nor the names of its
12  * contributors may be used to endorse or promote products derived from this
13  * software without specific prior written permission.
14 
15  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18  * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25  */
26 
27 /******************************************************************************
28 
29  Clamav clamd client library
30 
31  Scanning file with tcp-based clamd servers. Library for general use.
32  Thread-safe if compiled with _THREAD_SAFE defined.
33 
34  Warnings logged via syslog (must be opended).
35 
36  Random generator used (random() must be initialized before use, e.g.
37  by srandomdev() call).
38 
39  Written by Maxim Dounin, mdounin@rambler-co.ru
40 
41  $Id$
42 
43  ******************************************************************************/
44 
45 #include "config.h"
46 
47 #include "cfg_file.h"
48 #include "rmilter.h"
49 #include "libclamc.h"
50 #include "sds.h"
51 
52 /* Maximum time in seconds during which clamav server is marked inactive after scan error */
53 #define INACTIVE_INTERVAL 60.0
54 /* Maximum number of failed attempts before marking server as inactive */
55 #define MAX_FAILED 5
56 /* Maximum inactive timeout (20 min) */
57 #define MAX_TIMEOUT 1200.0
58 
59 /* Global mutexes */
60 
61 #ifdef _THREAD_SAFE
62 pthread_mutex_t mx_clamav_write = PTHREAD_MUTEX_INITIALIZER;
63 #endif
64 
65 /*****************************************************************************/
66 
67 /*
68  * clamscan_socket() - send file to specified host. See clamscan() for
69  * load-balanced wrapper.
70  *
71  * returns 0 when checked, -1 on some error during scan (try another server), -2
72  * on unexpected error (probably clamd died on our file, fallback to another
73  * host not recommended)
74  */
75 
clamscan_socket(const char * file,const struct clamav_server * srv,char * strres,size_t strres_len,struct config_file * cfg,struct mlfi_priv * priv)76 static int clamscan_socket(const char *file, const struct clamav_server *srv,
77 		char *strres, size_t strres_len, struct config_file *cfg,
78 		struct mlfi_priv *priv)
79 {
80 	char *c;
81 	sds readbuf;
82 	char buf[2048];
83 	struct sockaddr_un server_un;
84 	struct sockaddr_in server_in, server_w;
85 	int s, r, fd, port = 0, ofl;
86 	uint32_t sz;
87 	size_t size;
88 	struct stat sb;
89 
90 	*strres = '\0';
91 
92 	/* somebody doesn't need reply... */
93 	if (!srv)
94 		return 0;
95 
96 	s = rmilter_connect_addr (srv->name, srv->port, cfg->clamav_connect_timeout, priv);
97 
98 	if (s == -1) {
99 		return -1;
100 	}
101 
102 	fd = open (file, O_RDONLY);
103 
104 	if (fstat (fd, &sb) == -1) {
105 		msg_warn("<%s>; clamav: stat failed for %s: %s", priv->mlfi_id,
106 				file, strerror (errno));
107 		close (s);
108 		return -1;
109 	}
110 
111 	sz = sb.st_size;
112 	sz = htonl (sz);
113 	r = rmilter_strlcpy (buf, "nINSTREAM\n", sizeof (buf) - sizeof (sz));
114 	memcpy (&buf[r], &sz, sizeof (sz));
115 	r += sizeof (sz);
116 
117 	if (write (s, buf, r) != r) {
118 		msg_warn("<%s>; clamav: write %s: %s", priv->mlfi_id,
119 				srv->name, strerror (errno));
120 		close (s);
121 		return -1;
122 	}
123 
124 	/*
125 	 * send data stream
126 	 */
127 	/* Set blocking again */
128 	ofl = fcntl (s, F_GETFL, 0);
129 	fcntl (s, F_SETFL, ofl & (~O_NONBLOCK));
130 
131 #if defined(FREEBSD) && defined(HAVE_SENDFILE)
132 	if (sendfile(fd, s, 0, 0, 0, 0, 0) != 0) {
133 		msg_warn("<%s>; clamav: sendfile %s (%s): %s", priv->mlfi_id,
134 				file, srv->name, strerror (errno));
135 		close(fd);
136 		close(s);
137 		return -1;
138 	}
139 #elif defined(LINUX) && defined(HAVE_SENDFILE)
140 	off_t off = 0;
141 	if (sendfile(s, fd, &off, sb.st_size) == -1) {
142 		msg_warn("<%s>; clamav: sendfile %s (%s): %s", priv->mlfi_id,
143 				file, srv->name, strerror (errno));
144 		close(fd);
145 		close(s);
146 		return -1;
147 	}
148 #else
149 	void *map;
150 
151 	map = mmap (NULL, sb.st_size, PROT_READ, MAP_SHARED, fd, 0);
152 
153 	if (map == MAP_FAILED) {
154 		msg_warn ("<%s>; rspamd: mmap (%s), %s", priv->mlfi_id, srv->name,
155 				strerror (errno));
156 		close(fd);
157 		close(s);
158 		return -1;
159 	}
160 
161 	if (rmilter_atomic_write (s, map, sb.st_size) == -1) {
162 		msg_warn ("<%s>; rspamd: write (%s), %s", priv->mlfi_id, srv->name,
163 				strerror (errno));
164 		close(fd);
165 		close(s);
166 		munmap (map, sb.st_size);
167 		return -1;
168 	}
169 
170 	munmap (map, sb.st_size);
171 #endif
172 
173 	close (fd);
174 
175 	/* Send zero chunk */
176 	sz = 0;
177 	if (write (s, &sz, sizeof (sz)) <= 0) {
178 		msg_warn("<%s>; clamav: write (%s): %s", priv->mlfi_id,
179 				srv->name, strerror (errno));
180 		close(fd);
181 		close(s);
182 		return -1;
183 	}
184 
185 	fcntl (s, F_SETFL, ofl | O_NONBLOCK);
186 
187 	/* wait for reply */
188 	if (rmilter_poll_fd (s, cfg->clamav_results_timeout, POLLIN) < 1) {
189 		msg_warn("<%s>; clamav: timeout waiting results %s", priv->mlfi_id,
190 				srv->name);
191 		close (s);
192 		return -1;
193 	}
194 
195 	/*
196 	 * read results
197 	 */
198 	readbuf = sdsempty();
199 
200 	for (;;) {
201 		if (rmilter_poll_fd (s, cfg->spamd_results_timeout, POLLIN) < 1) {
202 			msg_warn("<%s>; clamav: timeout waiting results %s", priv->mlfi_id,
203 					srv->name);
204 			close (s);
205 			return -1;
206 		}
207 
208 		r = read (s, buf, sizeof (buf));
209 
210 		if (r == -1) {
211 			if (errno == EAGAIN || errno == EINTR) {
212 				continue;
213 			}
214 			else {
215 				msg_warn("<%s>; clamav: read, %s, %s", priv->mlfi_id,
216 						srv->name, strerror (errno));
217 				close (s);
218 				return -1;
219 			}
220 		}
221 		else if (r == 0) {
222 			break;
223 		}
224 		else {
225 			readbuf = sdscatlen (readbuf, buf, r);
226 		}
227 	}
228 
229 	size = sdslen (readbuf);
230 
231 	close (s);
232 
233 	/*
234 	 * ok, we got result; test what we got
235 	 */
236 
237 	/* msg_warn("clamav: %s", buf); */
238 	if ((c = strstr (readbuf, "OK\n")) != NULL) {
239 		/* <file> ": OK\n" */
240 		sdsfree (readbuf);
241 		return 0;
242 
243 	}
244 	else if ((c = strstr (readbuf, "FOUND\n")) != NULL) {
245 		/* <file> ": " <virusname> " FOUND\n" */
246 
247 		if (strncmp (readbuf, "stream", sizeof ("stream") - 1) != 0) {
248 			msg_warn ("<%s>; clamav: paths differ: '%s' instead of 'stream'",
249 					priv->mlfi_id, readbuf);
250 			snprintf (strres, strres_len, "%.*s", (int)(c - readbuf), readbuf);
251 		}
252 		else {
253 			*(--c) = 0;
254 			c = readbuf + sizeof ("stream") + 1;
255 			snprintf (strres, strres_len, "%s", c);
256 		}
257 
258 		sdsfree (readbuf);
259 
260 		return 0;
261 
262 	}
263 	else if ((c = strstr (readbuf, "ERROR\n")) != NULL) {
264 		*(--c) = 0;
265 		msg_warn("<%s>; clamav: error (%s) %s", priv->mlfi_id, srv->name, readbuf);
266 		sdsfree (readbuf);
267 		return -1;
268 	}
269 
270 	/*
271 	 * Most common reason is clamd died while processing our request. Try to
272 	 * save file for further investigation and fail.
273 	 */
274 	sdsfree (readbuf);
275 	msg_warn("<%s>; clamav: unexpected result on file (%s) %s, %s",
276 			priv->mlfi_id, srv->name, file,
277 			buf);
278 
279 	return -2;
280 }
281 
282 /*
283  * clamscan() - send file to one of remote clamd, with pseudo load-balancing
284  * (select one random server, fallback to others in case of errors).
285  *
286  * returns 0 if file scanned (or not scanned due to filesize limit), -1 when
287  * retry limit exceeded, -2 on unexpected error, e.g. unexpected reply from
288  * server (suppose scanned message killed clamd...)
289  */
290 
clamscan(void * ctx,struct mlfi_priv * priv,struct config_file * cfg,char * strres,size_t strres_len)291 int clamscan (void *ctx, struct mlfi_priv *priv, struct config_file *cfg,
292 		char *strres, size_t strres_len)
293 {
294 	int retry = 5, r = -2;
295 	/* struct stat sb; */
296 	struct timeval t;
297 	double ts, tf;
298 	struct clamav_server *selected = NULL;
299 	struct timespec sleep_ts;
300 	const char *file = priv->file;
301 
302 	*strres = '\0';
303 	/*
304 	 * Parse sockets to use in balancing.
305 	 */
306 	/* msg_warn("(clamscan) defined %d server sockets...", sockets_n); */
307 
308 	/*
309 	 * save scanning start time
310 	 */
311 	gettimeofday (&t, NULL);
312 	ts = t.tv_sec + t.tv_usec / 1000000.0;
313 	retry = cfg->spamd_retry_count;
314 	sleep_ts.tv_sec = cfg->spamd_retry_timeout / 1000;
315 	sleep_ts.tv_nsec = (cfg->spamd_retry_timeout % 1000) * 1000000ULL;
316 
317 	/* try to scan with available servers */
318 	while (1) {
319 		if (cfg->weighted_clamav) {
320 			selected = (struct clamav_server *) get_upstream_master_slave (
321 					(void *) cfg->clamav_servers, cfg->clamav_servers_num,
322 					sizeof(struct clamav_server), t.tv_sec,
323 					cfg->clamav_error_time, cfg->clamav_dead_time,
324 					cfg->clamav_maxerrors, priv);
325 		}
326 		else {
327 			selected = (struct clamav_server *) get_random_upstream (
328 					(void *) cfg->clamav_servers, cfg->clamav_servers_num,
329 					sizeof(struct clamav_server), t.tv_sec,
330 					cfg->clamav_error_time, cfg->clamav_dead_time,
331 					cfg->clamav_maxerrors, priv);
332 		}
333 		if (selected == NULL) {
334 			msg_err("<%s>; clamscan: upstream get error, %s", priv->mlfi_id,
335 					file);
336 			return -1;
337 		}
338 
339 		msg_info ("<%s>; clamscan: start scanning message on %s", priv->mlfi_id,
340 						selected->name);
341 		r = clamscan_socket (file, selected, strres, strres_len, cfg, priv);
342 		msg_info ("<%s>; clamscan: finish scanning message on %s", priv->mlfi_id,
343 						selected->name);
344 
345 		if (r == 0) {
346 			upstream_ok (&selected->up, t.tv_sec);
347 			break;
348 		}
349 		upstream_fail (&selected->up, t.tv_sec);
350 		if (r == -2) {
351 			msg_warn("<%s>; clamscan: unexpected problem, %s, %s", priv->mlfi_id,
352 					selected->name,
353 					file);
354 			break;
355 		}
356 		if (--retry < 1) {
357 			msg_warn("<%s>; clamscan: retry limit exceeded, %s, %s",
358 					priv->mlfi_id, selected->name,
359 					file);
360 			break;
361 		}
362 		msg_warn("<%s>; clamscan: failed to scan, retry, %s, %s", priv->mlfi_id,
363 				selected->name,
364 				file);
365 		nanosleep (&sleep_ts, NULL);
366 	}
367 
368 	/*
369 	 * print scanning time, server and result
370 	 */
371 	gettimeofday (&t, NULL);
372 	tf = t.tv_sec + t.tv_usec / 1000000.0;
373 
374 	if (*strres) {
375 		msg_info("<%s>; clamscan: scan %f, %s, found %s, %s", priv->mlfi_id,
376 				tf - ts, selected->name,
377 				strres, file);
378 	}
379 	else {
380 		msg_info("<%s>; clamscan: scan %f, %s, %s", priv->mlfi_id,
381 				tf - ts, selected->name, file);
382 	}
383 
384 	return r;
385 }
386