+ case M_CLAMD: /* "clamd" scanner type ----------------------------------- */
+ {
+ /* This code was originally contributed by David Saez */
+ /* There are three scanning methods available to us:
+ * (1) Use the SCAN command, pointing to a file in the filesystem
+ * (2) Use the STREAM command, send the data on a separate port
+ * (3) Use the zINSTREAM command, send the data inline
+ * The zINSTREAM command was introduced with ClamAV 0.95, which marked
+ * STREAM deprecated; see: http://wiki.clamav.net/bin/view/Main/UpgradeNotes095
+ * In Exim, we use SCAN if using a Unix-domain socket or explicitly told that
+ * the TCP-connected daemon is actually local; otherwise we use zINSTREAM unless
+ * WITH_OLD_CLAMAV_STREAM is defined.
+ * See Exim bug 926 for details. */
+
+ uschar *p, *vname, *result_tag, *response_end;
+ int bread=0;
+ unsigned int port;
+ uschar * file_name;
+ uschar av_buffer[1024];
+ uschar *hostname = US"";
+ host_item connhost;
+ uschar *clamav_fbuf;
+ int clam_fd, result;
+ unsigned int fsize;
+ BOOL use_scan_command = FALSE;
+ clamd_address_container * clamd_address_vector[MAX_CLAMD_SERVERS];
+ int current_server;
+ int num_servers = 0;
+ #ifdef WITH_OLD_CLAMAV_STREAM
+ uschar av_buffer2[1024];
+ int sockData;
+ #else
+ uint32_t send_size, send_final_zeroblock;
+ #endif
+
+ if (*scanner_options == '/')
+ /* Local file; so we def want to use_scan_command and don't want to try
+ * passing IP/port combinations */
+ use_scan_command = TRUE;
+ else {
+ const uschar *address = scanner_options;
+ uschar address_buffer[MAX_CLAMD_ADDRESS_LENGTH + 20];
+
+ /* Go through the rest of the list of host/port and construct an array
+ * of servers to try. The first one is the bit we just passed from
+ * scanner_options so process that first and then scan the remainder of
+ * the address buffer */
+ do {
+ clamd_address_container *this_clamd;
+
+ /* The 'local' option means use the SCAN command over the network
+ * socket (ie common file storage in use) */
+ if (strcmpic(address,US"local") == 0) {
+ use_scan_command = TRUE;
+ continue;
+ }
+
+ /* XXX: If unsuccessful we should free this memory */
+ this_clamd =
+ (clamd_address_container *)store_get(sizeof(clamd_address_container));
+
+ /* extract host and port part */
+ if( sscanf(CS address, "%" MAX_CLAMD_ADDRESS_LENGTH_S "s %u",
+ this_clamd->tcp_addr, &(this_clamd->tcp_port)) != 2 ) {
+ (void) m_errlog_defer(scanent,
+ string_sprintf("invalid address '%s'", address));
+ continue;
+ }
+
+ clamd_address_vector[num_servers] = this_clamd;
+ num_servers++;
+ if (num_servers >= MAX_CLAMD_SERVERS) {
+ (void) m_errlog_defer(scanent,
+ US"More than " MAX_CLAMD_SERVERS_S " clamd servers "
+ "specified; only using the first " MAX_CLAMD_SERVERS_S );
+ break;
+ }
+ } while ((address = string_nextinlist(&av_scanner_work, &sep,
+ address_buffer,
+ sizeof(address_buffer))) != NULL);
+
+ /* check if we have at least one server */
+ if (!num_servers)
+ return m_errlog_defer(scanent,
+ US"no useable server addresses in malware configuration option.");
+ }
+
+ /* See the discussion of response formats below to see why we really don't
+ like colons in filenames when passing filenames to ClamAV. */
+ if (use_scan_command && Ustrchr(eml_filename, ':'))
+ return m_errlog_defer(scanent,
+ string_sprintf("local/SCAN mode incompatible with" \
+ " : in path to email filename [%s]", eml_filename));
+
+ /* We have some network servers specified */
+ if (num_servers) {
+
+ /* Confirmed in ClamAV source (0.95.3) that the TCPAddr option of clamd
+ * only supports AF_INET, but we should probably be looking to the
+ * future and rewriting this to be protocol-independent anyway. */
+
+ while ( num_servers > 0 ) {
+ /* Randomly pick a server to start with */
+ current_server = random_number( num_servers );
+
+ debug_printf("trying server name %s, port %u\n",
+ clamd_address_vector[current_server]->tcp_addr,
+ clamd_address_vector[current_server]->tcp_port);
+
+ /* Lookup the host. This is to ensure that we connect to the same IP
+ * on both connections (as one host could resolve to multiple ips) */
+ sock= m_tcpsocket(clamd_address_vector[current_server]->tcp_addr,
+ clamd_address_vector[current_server]->tcp_port,
+ &connhost, &errstr);
+ if (sock >= 0) {
+ /* Connection successfully established with a server */
+ hostname = clamd_address_vector[current_server]->tcp_addr;
+ break;
+ }
+
+ (void) m_errlog_defer(scanent, errstr);
+
+ /* Remove the server from the list. XXX We should free the memory */
+ num_servers--;
+ int i;
+ for( i = current_server; i < num_servers; i++ )
+ clamd_address_vector[i] = clamd_address_vector[i+1];
+ }
+
+ if ( num_servers == 0 )
+ return m_errlog_defer(scanent, US"all servers failed");
+
+ } else {
+ if ((sock = m_unixsocket(scanner_options, &errstr)) < 0)
+ return m_errlog_defer(scanent, errstr);
+ }
+
+ /* have socket in variable "sock"; command to use is semi-independent of
+ * the socket protocol. We use SCAN if is local (either Unix/local
+ * domain socket, or explicitly told local) else we stream the data.
+ * How we stream the data depends upon how we were built. */
+
+ if (!use_scan_command) {
+
+ #ifdef WITH_OLD_CLAMAV_STREAM
+ /* "STREAM\n" command, get back a "PORT <N>\n" response, send data to
+ * that port on a second connection; then in the scan-method-neutral
+ * part, read the response back on the original connection. */
+
+ DEBUG(D_acl) debug_printf("Malware scan: issuing %s old-style remote scan (PORT)\n",
+ scanner_name);
+
+ /* Pass the string to ClamAV (7 = "STREAM\n") */
+ if (m_sock_send(sock, US"STREAM\n", 7, &errstr) < 0)
+ return m_errlog_defer(scanent, errstr);
+
+ memset(av_buffer2, 0, sizeof(av_buffer2));
+ bread = ip_recv(sock, av_buffer2, sizeof(av_buffer2), MALWARE_TIMEOUT);
+
+ if (bread < 0)
+ return m_errlog_defer_3(scanent,
+ string_sprintf("unable to read PORT from socket (%s)",
+ strerror(errno)),
+ sock);
+
+ if (bread == sizeof(av_buffer2))
+ return m_errlog_defer_3(scanent, "buffer too small", sock);
+
+ if (!(*av_buffer2))
+ return m_errlog_defer_3(scanent, "ClamAV returned null", sock);
+
+ av_buffer2[bread] = '\0';
+ if( sscanf(CS av_buffer2, "PORT %u\n", &port) != 1 )
+ return m_errlog_defer_3(scanent,
+ string_sprintf("Expected port information from clamd, got '%s'",
+ av_buffer2),
+ sock);
+
+ sockData = m_tcpsocket(connhost.address, port, NULL, &errstr);
+ if (sockData < 0)
+ return m_errlog_defer_3(scanent, errstr, sock);
+
+ #define CLOSE_SOCKDATA (void)close(sockData)
+ #else /* WITH_OLD_CLAMAV_STREAM not defined */
+ /* New protocol: "zINSTREAM\n" followed by a sequence of <length><data>
+ chunks, <n> a 4-byte number (network order), terminated by a zero-length
+ chunk. */
+
+ DEBUG(D_acl) debug_printf("Malware scan: issuing %s new-style remote scan (zINSTREAM)\n",
+ scanner_name);
+
+ /* Pass the string to ClamAV (10 = "zINSTREAM\0") */
+ if (send(sock, "zINSTREAM", 10, 0) < 0)
+ return m_errlog_defer_3(scanent,
+ string_sprintf("unable to send zINSTREAM to socket (%s)",
+ strerror(errno)),
+ sock);
+
+ #define CLOSE_SOCKDATA /**/
+ #endif
+
+ /* calc file size */
+ if ((clam_fd = open(CS eml_filename, O_RDONLY)) < 0) {
+ int err = errno;
+ CLOSE_SOCKDATA;
+ return m_errlog_defer_3(scanent,
+ string_sprintf("can't open spool file %s: %s",
+ eml_filename, strerror(err)),
+ sock);
+ }
+ if ((fsize = lseek(clam_fd, 0, SEEK_END)) < 0) {
+ int err = errno;
+ CLOSE_SOCKDATA; (void)close(clam_fd);
+ return m_errlog_defer_3(scanent,
+ string_sprintf("can't seek spool file %s: %s",
+ eml_filename, strerror(err)),
+ sock);
+ }
+ lseek(clam_fd, 0, SEEK_SET);
+
+ if (!(clamav_fbuf = (uschar *) malloc (fsize))) {
+ CLOSE_SOCKDATA; (void)close(clam_fd);
+ return m_errlog_defer_3(scanent,
+ string_sprintf("unable to allocate memory %u for file (%s)",
+ fsize, eml_filename),
+ sock);
+ }
+
+ if ((result = read(clam_fd, clamav_fbuf, fsize)) < 0) {
+ int err = errno;
+ free(clamav_fbuf); CLOSE_SOCKDATA; (void)close(clam_fd);
+ return m_errlog_defer_3(scanent,
+ string_sprintf("can't read spool file %s: %s",
+ eml_filename, strerror(err)),
+ sock);
+ }
+ (void)close(clam_fd);
+
+ /* send file body to socket */
+ #ifdef WITH_OLD_CLAMAV_STREAM
+ if (send(sockData, clamav_fbuf, fsize, 0) < 0) {
+ free(clamav_fbuf); CLOSE_SOCKDATA;
+ return m_errlog_defer_3(scanent,
+ string_sprintf("unable to send file body to socket (%s:%u)",
+ hostname, port),
+ sock);
+ }
+ #else
+ send_size = htonl(fsize);
+ send_final_zeroblock = 0;
+ if ((send(sock, &send_size, sizeof(send_size), 0) < 0) ||
+ (send(sock, clamav_fbuf, fsize, 0) < 0) ||
+ (send(sock, &send_final_zeroblock, sizeof(send_final_zeroblock), 0) < 0))
+ {
+ free(clamav_fbuf);
+ return m_errlog_defer_3(scanent,
+ string_sprintf("unable to send file body to socket (%s:%u)",
+ hostname, port),
+ sock);
+ }
+ #endif
+
+ free(clamav_fbuf);
+
+ CLOSE_SOCKDATA;
+ #undef CLOSE_SOCKDATA
+
+ } else { /* use scan command */
+ /* Send a SCAN command pointing to a filename; then in the then in the
+ * scan-method-neutral part, read the response back */
+
+ /* ================================================================= */
+
+ /* Prior to the reworking post-Exim-4.72, this scanned a directory,
+ which dates to when ClamAV needed us to break apart the email into the
+ MIME parts (eg, with the now deprecated demime condition coming first).
+ Some time back, ClamAV gained the ability to deconstruct the emails, so
+ doing this would actually have resulted in the mail attachments being
+ scanned twice, in the broken out files and from the original .eml.
+ Since ClamAV now handles emails (and has for quite some time) we can
+ just use the email file itself. */
+ /* Pass the string to ClamAV (7 = "SCAN \n" + \0) */
+ file_name = string_sprintf("SCAN %s\n", eml_filename);
+
+ DEBUG(D_acl) debug_printf("Malware scan: issuing %s local-path scan [%s]\n",
+ scanner_name, scanner_options);
+
+ if (send(sock, file_name, Ustrlen(file_name), 0) < 0)
+ return m_errlog_defer_3(scanent,
+ string_sprintf("unable to write to socket (%s)", strerror(errno)),
+ sock);
+
+ /* Do not shut down the socket for writing; a user report noted that
+ * clamd 0.70 does not react well to this. */
+ }
+ /* Commands have been sent, no matter which scan method or connection
+ * type we're using; now just read the result, independent of method. */
+
+ /* Read the result */
+ memset(av_buffer, 0, sizeof(av_buffer));
+ bread = ip_recv(sock, av_buffer, sizeof(av_buffer), MALWARE_TIMEOUT);
+ (void)close(sock);
+ sock = -1;
+
+ if (!(bread > 0))
+ return m_errlog_defer(scanent,
+ string_sprintf("unable to read from socket (%s)", strerror(errno)));
+
+ if (bread == sizeof(av_buffer))
+ return m_errlog_defer(scanent, US"buffer too small");
+ /* We're now assured of a NULL at the end of av_buffer */
+
+ /* Check the result. ClamAV returns one of two result formats.
+ In the basic mode, the response is of the form:
+ infected: -> "<filename>: <virusname> FOUND"
+ not-infected: -> "<filename>: OK"
+ error: -> "<filename>: <errcode> ERROR
+ If the ExtendedDetectionInfo option has been turned on, then we get:
+ "<filename>: <virusname>(<virushash>:<virussize>) FOUND"
+ for the infected case. Compare:
+ /tmp/eicar.com: Eicar-Test-Signature FOUND
+ /tmp/eicar.com: Eicar-Test-Signature(44d88612fea8a8f36de82e1278abb02f:68) FOUND
+
+ In the streaming case, clamd uses the filename "stream" which you should
+ be able to verify with { ktrace clamdscan --stream /tmp/eicar.com }. (The
+ client app will replace "stream" with the original filename before returning
+ results to stdout, but the trace shows the data).
+
+ We will assume that the pathname passed to clamd from Exim does not contain
+ a colon. We will have whined loudly above if the eml_filename does (and we're
+ passing a filename to clamd). */
+
+ if (!(*av_buffer))
+ return m_errlog_defer(scanent, US"ClamAV returned null");
+
+ /* strip newline at the end (won't be present for zINSTREAM)
+ (also any trailing whitespace, which shouldn't exist, but we depend upon
+ this below, so double-check) */
+ p = av_buffer + Ustrlen(av_buffer) - 1;
+ if (*p == '\n') *p = '\0';
+
+ DEBUG(D_acl) debug_printf("Malware response: %s\n", av_buffer);
+
+ while (isspace(*--p) && (p > av_buffer))
+ *p = '\0';
+ if (*p) ++p;
+ response_end = p;
+
+ /* colon in returned output? */
+ if((p = Ustrchr(av_buffer,':')) == NULL)
+ return m_errlog_defer(scanent,
+ string_sprintf("ClamAV returned malformed result (missing colon): %s",
+ av_buffer));
+
+ /* strip filename */
+ while (*p && isspace(*++p)) /**/;
+ vname = p;
+
+ /* It would be bad to encounter a virus with "FOUND" in part of the name,
+ but we should at least be resistant to it. */
+ p = Ustrrchr(vname, ' ');
+ result_tag = p ? p+1 : vname;
+
+ if (Ustrcmp(result_tag, "FOUND") == 0) {
+ /* p should still be the whitespace before the result_tag */
+ while (isspace(*p)) --p;
+ *++p = '\0';
+ /* Strip off the extended information too, which will be in parens
+ after the virus name, with no intervening whitespace. */
+ if (*--p == ')') {
+ /* "(hash:size)", so previous '(' will do; if not found, we have
+ a curious virus name, but not an error. */
+ p = Ustrrchr(vname, '(');
+ if (p)
+ *p = '\0';
+ }
+ malware_name = string_copy(vname);
+ DEBUG(D_acl) debug_printf("Malware found, name \"%s\"\n", malware_name);
+
+ } else if (Ustrcmp(result_tag, "ERROR") == 0)
+ return m_errlog_defer(scanent,
+ string_sprintf("ClamAV returned: %s", av_buffer));
+
+ else if (Ustrcmp(result_tag, "OK") == 0) {
+ /* Everything should be OK */
+ malware_name = NULL;
+ DEBUG(D_acl) debug_printf("Malware not found\n");
+
+ } else
+ return m_errlog_defer(scanent,
+ string_sprintf("unparseable response from ClamAV: {%s}", av_buffer));
+
+ break;
+ } /* clamd */
+
+ case M_SOCK: /* "sock" scanner type ------------------------------------- */
+ /* This code was derived by Martin Poole from the clamd code contributed
+ by David Saez and the cmdline code
+ */
+ {
+ int bread;
+ uschar * commandline;
+ uschar av_buffer[1024];
+ uschar * linebuffer;
+ uschar * sockline_scanner;
+ uschar sockline_scanner_default[] = "%s\n";
+ const pcre *sockline_trig_re;
+ const pcre *sockline_name_re;
+
+ /* find scanner command line */
+ if ((sockline_scanner = string_nextinlist(&av_scanner_work, &sep,
+ NULL, 0)))
+ { /* check for no expansions apart from one %s */
+ char * s = index(CS sockline_scanner, '%');
+ if (s++)
+ if ((*s != 's' && *s != '%') || index(s+1, '%'))
+ return m_errlog_defer_3(scanent,
+ US"unsafe sock scanner call spec", sock);
+ }
+ else
+ sockline_scanner = sockline_scanner_default;
+
+ /* find scanner output trigger */
+ sockline_trig_re = m_pcre_nextinlist(&av_scanner_work, &sep,
+ "missing trigger specification", &errstr);
+ if (!sockline_trig_re)
+ return m_errlog_defer_3(scanent, errstr, sock);
+
+ /* find virus name regex */
+ sockline_name_re = m_pcre_nextinlist(&av_scanner_work, &sep,
+ "missing virus name regex specification", &errstr);
+ if (!sockline_name_re)
+ return m_errlog_defer_3(scanent, errstr, sock);
+
+ /* prepare scanner call - security depends on expansions check above */
+ commandline = string_sprintf("%s/scan/%s/%s.eml", spool_directory, message_id, message_id);
+ commandline = string_sprintf( CS sockline_scanner, CS commandline);
+
+
+ /* Pass the command string to the socket */
+ if (m_sock_send(sock, commandline, Ustrlen(commandline), &errstr) < 0)
+ return m_errlog_defer(scanent, errstr);
+
+ /* Read the result */
+ memset(av_buffer, 0, sizeof(av_buffer));
+ bread = read(sock, av_buffer, sizeof(av_buffer));
+
+ if (!(bread > 0))
+ return m_errlog_defer_3(scanent,
+ string_sprintf("unable to read from socket (%s)", strerror(errno)),
+ sock);
+
+ if (bread == sizeof(av_buffer))
+ return m_errlog_defer_3(scanent, US"buffer too small", sock);
+ linebuffer = string_copy(av_buffer);
+
+ /* try trigger match */
+ if (regex_match_and_setup(sockline_trig_re, linebuffer, 0, -1)) {
+ if (!(malware_name = m_pcre_exec(sockline_name_re, av_buffer)))
+ malware_name = US "unknown";
+ }
+ else /* no virus found */
+ malware_name = NULL;
+ break;