+ int clam_fd, result;
+ off_t fsize;
+ unsigned int fsize_uint;
+ BOOL use_scan_command = FALSE;
+ clamd_address * cv[MAX_CLAMD_SERVERS];
+ int num_servers = 0;
+ uint32_t send_size, send_final_zeroblock;
+ blob cmd_str;
+
+ /*XXX if unixdomain socket, only one server supported. Needs fixing;
+ there's no reason we should not mix local and remote servers */
+
+ if (*scanner_options == '/')
+ {
+ clamd_address * cd;
+ const uschar * sublist;
+ int subsep = ' ';
+
+ /* Local file; so we def want to use_scan_command and don't want to try
+ * passing IP/port combinations */
+ use_scan_command = TRUE;
+ cd = (clamd_address *) store_get(sizeof(clamd_address), FALSE);
+
+ /* extract socket-path part */
+ sublist = scanner_options;
+ cd->hostspec = string_nextinlist(&sublist, &subsep, NULL, 0);
+
+ /* parse options */
+ if (clamd_option(cd, sublist, &subsep) != OK)
+ return m_panic_defer(scanent, NULL,
+ string_sprintf("bad option '%s'", scanner_options));
+ cv[0] = cd;
+ }
+ else
+ {
+ /* 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 * cd;
+ const uschar * sublist;
+ int subsep = ' ';
+ uschar * s;
+
+ /* The 'local' option means use the SCAN command over the network
+ * socket (ie common file storage in use) */
+ /*XXX we could accept this also as a local option? */
+ if (strcmpic(scanner_options, US"local") == 0)
+ {
+ use_scan_command = TRUE;
+ continue;
+ }
+
+ cd = (clamd_address *) store_get(sizeof(clamd_address), FALSE);
+
+ /* extract host and port part */
+ sublist = scanner_options;
+ if (!(cd->hostspec = string_nextinlist(&sublist, &subsep, NULL, 0)))
+ {
+ (void) m_panic_defer(scanent, NULL,
+ string_sprintf("missing address: '%s'", scanner_options));
+ continue;
+ }
+ if (!(s = string_nextinlist(&sublist, &subsep, NULL, 0)))
+ {
+ (void) m_panic_defer(scanent, NULL,
+ string_sprintf("missing port: '%s'", scanner_options));
+ continue;
+ }
+ cd->tcp_port = atoi(CS s);
+
+ /* parse options */
+ /*XXX should these options be common over scanner types? */
+ if (clamd_option(cd, sublist, &subsep) != OK)
+ return m_panic_defer(scanent, NULL,
+ string_sprintf("bad option '%s'", scanner_options));
+
+ cv[num_servers++] = cd;
+ if (num_servers >= MAX_CLAMD_SERVERS)
+ {
+ (void) m_panic_defer(scanent, NULL,
+ US"More than " MAX_CLAMD_SERVERS_S " clamd servers "
+ "specified; only using the first " MAX_CLAMD_SERVERS_S );
+ break;
+ }
+ } while ((scanner_options = string_nextinlist(&av_scanner_work, &sep,
+ NULL, 0)));
+
+ /* check if we have at least one server */
+ if (!num_servers)
+ return m_panic_defer(scanent, NULL,
+ 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_panic_defer(scanent, NULL,
+ string_sprintf("local/SCAN mode incompatible with" \
+ " : in path to email filename [%s]", eml_filename));
+
+ /* Set up the very first data we will be sending */
+ if (!use_scan_command)
+ { cmd_str.data = US"zINSTREAM"; cmd_str.len = 10; }
+ else
+ {
+ cmd_str.data = string_sprintf("SCAN %s\n", eml_filename);
+ cmd_str.len = Ustrlen(cmd_str.data);
+ }
+
+ /* 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)
+ {
+ int i = random_number(num_servers);
+ clamd_address * cd = cv[i];
+
+ DEBUG(D_acl) debug_printf_indent("trying server name %s, port %u\n",
+ cd->hostspec, cd->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) */
+ for (;;)
+ {
+ /*XXX we trust that the cmd_str is ideempotent */
+ if ((malware_daemon_ctx.sock = m_tcpsocket(cd->hostspec, cd->tcp_port,
+ &connhost, &errstr, &cmd_str)) >= 0)
+ {
+ /* Connection successfully established with a server */
+ hostname = cd->hostspec;
+ cmd_str.len = 0;
+ break;
+ }
+ if (cd->retry <= 0) break;
+ while (cd->retry > 0) cd->retry = sleep(cd->retry);
+ }
+ if (malware_daemon_ctx.sock >= 0)
+ break;
+
+ (void) m_panic_defer(scanent, CUS callout_address, errstr);
+
+ /* Remove the server from the list. XXX We should free the memory */
+ num_servers--;
+ for (; i < num_servers; i++)
+ cv[i] = cv[i+1];
+ }
+
+ if (num_servers == 0)
+ return m_panic_defer(scanent, NULL, US"all servers failed");
+ }
+ else
+ for (;;)
+ {
+ if ((malware_daemon_ctx.sock = ip_unixsocket(cv[0]->hostspec, &errstr)) >= 0)
+ {
+ hostname = cv[0]->hostspec;
+ break;
+ }
+ if (cv[0]->retry <= 0)
+ return m_panic_defer(scanent, CUS callout_address, errstr);
+ while (cv[0]->retry > 0) cv[0]->retry = sleep(cv[0]->retry);
+ }
+
+ /* 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)
+ {
+ /* 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_indent(
+ "Malware scan: issuing %s new-style remote scan (zINSTREAM)\n",
+ scanner_name);
+
+ /* Pass the string to ClamAV (10 = "zINSTREAM\0"), if not already sent */
+ if (cmd_str.len)
+ if (send(malware_daemon_ctx.sock, cmd_str.data, cmd_str.len, 0) < 0)
+ return m_panic_defer_3(scanent, CUS hostname,
+ string_sprintf("unable to send zINSTREAM to socket (%s)",
+ strerror(errno)),
+ malware_daemon_ctx.sock);
+
+ /* calc file size */
+ if ((clam_fd = exim_open2(CS eml_filename, O_RDONLY)) < 0)
+ {
+ int err = errno;
+ return m_panic_defer_3(scanent, NULL,
+ string_sprintf("can't open spool file %s: %s",
+ eml_filename, strerror(err)),
+ malware_daemon_ctx.sock);
+ }
+ if ((fsize = lseek(clam_fd, 0, SEEK_END)) < 0)
+ {
+ int err;
+b_seek: err = errno;
+ (void)close(clam_fd);
+ return m_panic_defer_3(scanent, NULL,
+ string_sprintf("can't seek spool file %s: %s",
+ eml_filename, strerror(err)),
+ malware_daemon_ctx.sock);
+ }
+ fsize_uint = (unsigned int) fsize;
+ if ((off_t)fsize_uint != fsize)
+ {
+ (void)close(clam_fd);
+ return m_panic_defer_3(scanent, NULL,
+ string_sprintf("seeking spool file %s, size overflow",
+ eml_filename),
+ malware_daemon_ctx.sock);
+ }
+ if (lseek(clam_fd, 0, SEEK_SET) < 0)
+ goto b_seek;
+
+ if (!(clamav_fbuf = store_malloc(fsize_uint)))
+ {
+ (void)close(clam_fd);
+ return m_panic_defer_3(scanent, NULL,
+ string_sprintf("unable to allocate memory %u for file (%s)",
+ fsize_uint, eml_filename),
+ malware_daemon_ctx.sock);
+ }
+
+ if ((result = read(clam_fd, clamav_fbuf, fsize_uint)) < 0)
+ {
+ int err = errno;
+ store_free(clamav_fbuf); (void)close(clam_fd);
+ return m_panic_defer_3(scanent, NULL,
+ string_sprintf("can't read spool file %s: %s",
+ eml_filename, strerror(err)),
+ malware_daemon_ctx.sock);
+ }
+ (void)close(clam_fd);
+
+ /* send file body to socket */
+ send_size = htonl(fsize_uint);
+ send_final_zeroblock = 0;
+ if ((send(malware_daemon_ctx.sock, &send_size, sizeof(send_size), 0) < 0) ||
+ (send(malware_daemon_ctx.sock, clamav_fbuf, fsize_uint, 0) < 0) ||
+ (send(malware_daemon_ctx.sock, &send_final_zeroblock, sizeof(send_final_zeroblock), 0) < 0))
+ {
+ store_free(clamav_fbuf);
+ return m_panic_defer_3(scanent, NULL,
+ string_sprintf("unable to send file body to socket (%s)", hostname),
+ malware_daemon_ctx.sock);
+ }
+ store_free(clamav_fbuf);
+ }
+ 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), if not already sent */
+
+ DEBUG(D_acl) debug_printf_indent(
+ "Malware scan: issuing %s local-path scan [%s]\n",
+ scanner_name, scanner_options);
+
+ if (cmd_str.len)
+ if (send(malware_daemon_ctx.sock, cmd_str.data, cmd_str.len, 0) < 0)
+ return m_panic_defer_3(scanent, CUS callout_address,
+ string_sprintf("unable to write to socket (%s)", strerror(errno)),
+ malware_daemon_ctx.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. */