clamd/scanner.c
e3aaff8e
 /*
206dbaef
  *  Copyright (C) 2013-2020 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
52cddcbc
  *  Copyright (C) 2007-2013 Sourcefire, Inc.
086eab5c
  *
  *  Authors: Tomasz Kojm, Török Edvin
e3aaff8e
  *
  *  This program is free software; you can redistribute it and/or modify
bb34cb31
  *  it under the terms of the GNU General Public License version 2 as
  *  published by the Free Software Foundation.
e3aaff8e
  *
  *  This program is distributed in the hope that it will be useful,
  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  *  GNU General Public License for more details.
  *
  *  You should have received a copy of the GNU General Public License
  *  along with this program; if not, write to the Free Software
48b7b4a7
  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
  *  MA 02110-1301, USA.
e3aaff8e
  */
 
98ac8d19
 #if HAVE_CONFIG_H
 #include "clamav-config.h"
 #endif
 
e3aaff8e
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
72fd33c8
 #ifdef HAVE_UNISTD_H
e3aaff8e
 #include <unistd.h>
67118e92
 #endif
a7282c2f
 #include <errno.h>
e3aaff8e
 #include <sys/stat.h>
 #include <sys/types.h>
e0bb54d7
 #include <dirent.h>
72fd33c8
 #ifndef _WIN32
31e6c6fb
 #include <sys/time.h>
e3aaff8e
 #include <sys/wait.h>
15edd45f
 #include <sys/param.h>
1a4994e3
 #include <signal.h>
e3aaff8e
 #include <sys/socket.h>
 #include <netinet/in.h>
a0283d44
 #include <arpa/inet.h>
 #include <netdb.h>
67118e92
 #endif
d9855a95
 #include <pthread.h>
31e6c6fb
 
9e20cdf6
 // libclamav
 #include "clamav.h"
 #include "others.h"
 #include "scanners.h"
bd8603aa
 
9e20cdf6
 // shared
 #include "idmef_logging.h"
 #include "optparser.h"
 #include "output.h"
 #include "misc.h"
bd8603aa
 
9e20cdf6
 #include "clamd_others.h"
e3aaff8e
 #include "scanner.h"
afb48b28
 #include "shared.h"
a617b5ea
 #include "thrmgr.h"
949c6fe5
 #include "server.h"
e3aaff8e
 
c695dab4
 #ifdef C_LINUX
 dev_t procdev; /* /proc device */
 #endif
 
949c6fe5
 extern int progexit;
 extern time_t reloaded_time;
 extern pthread_mutex_t reload_mutex;
a617b5ea
 
769f37a6
 void msg_callback(enum cl_msg severity, const char *fullmsg, const char *msg, void *ctx)
 {
     struct cb_context *c = ctx;
     const char *filename = (c && c->filename) ? c->filename : "";
 
6df13d04
     UNUSEDPARAM(fullmsg);
 
769f37a6
     switch (severity) {
72fd33c8
         case CL_MSG_ERROR:
             logg("^[LibClamAV] %s: %s", filename, msg);
             break;
         case CL_MSG_WARN:
             logg("~[LibClamAV] %s: %s", filename, msg);
             break;
         case CL_MSG_INFO_VERBOSE:
             logg("*[LibClamAV] %s: %s", filename, msg);
             break;
         default:
             logg("$[LibClamAV] %s: %s", filename, msg);
             break;
769f37a6
     }
 }
 
 void hash_callback(int fd, unsigned long long size, const unsigned char *md5, const char *virname, void *ctx)
 {
     struct cb_context *c = ctx;
6df13d04
     UNUSEDPARAM(fd);
     UNUSEDPARAM(virname);
 
769f37a6
     if (!c)
72fd33c8
         return;
769f37a6
     c->virsize = size;
6df13d04
     strncpy(c->virhash, (const char *)md5, 32);
769f37a6
     c->virhash[32] = '\0';
 }
 
1f1bf36b
 void clamd_virus_found_cb(int fd, const char *virname, void *ctx)
 {
72fd33c8
     struct cb_context *c   = ctx;
1f1bf36b
     struct scan_cb_data *d = c->scandata;
     const char *fname;
72fd33c8
 
5f4f6910
     UNUSEDPARAM(fd);
 
1f1bf36b
     if (d == NULL)
         return;
d7979d4f
     if (!(d->options->general & CL_SCAN_GENERAL_ALLMATCHES) && !(d->options->general & CL_SCAN_GENERAL_HEURISTIC_PRECEDENCE))
1f1bf36b
         return;
     if (virname == NULL)
         return;
 
     fname = (c && c->filename) ? c->filename : "(filename not set)";
 
     if (virname) {
cbf5017a
         d->infected++;
1f1bf36b
         conn_reply_virus(d->conn, fname, virname);
72fd33c8
         if (c->virsize > 0 && optget(d->opts, "ExtendedDetectionInfo")->enabled)
1f1bf36b
             logg("~%s: %s(%s:%llu) FOUND\n", fname, virname, c->virhash, c->virsize);
         logg("~%s: %s FOUND\n", fname, virname);
     }
 
     return;
 }
 
949c6fe5
 #define BUFFSIZE 1024
4f211336
 cl_error_t scan_callback(STATBUF *sb, char *filename, const char *msg, enum cli_ftw_reason reason, struct cli_ftw_cbdata *data)
e3aaff8e
 {
949c6fe5
     struct scan_cb_data *scandata = data->data;
72fd33c8
     const char *virname           = NULL;
949c6fe5
     int ret;
     int type = scandata->type;
769f37a6
     struct cb_context context;
4f211336
     char *real_filename = NULL;
 
     if (NULL != filename) {
         if (CL_SUCCESS != cli_realpath((const char *)filename, &real_filename)) {
             conn_reply_errno(scandata->conn, msg, "real-path check failed:");
             logg("^Failed to determine real path for: %s\n", filename);
             scandata->errors++;
             return CL_SUCCESS;
         }
         free(filename);
         filename = real_filename;
     }
949c6fe5
 
1c996e88
     /* detect disconnected socket,
7ff6ec03
      * this should NOT detect half-shutdown sockets (SHUT_WR) */
     if (send(scandata->conn->sd, &ret, 0, 0) == -1 && errno != EINTR) {
72fd33c8
         logg("$Client disconnected while command was active!\n");
         thrmgr_group_terminate(scandata->conn->group);
         if (reason == visit_file)
             free(filename);
         return CL_BREAK;
36e4bc14
     }
 
949c6fe5
     if (thrmgr_group_need_terminate(scandata->conn->group)) {
72fd33c8
         logg("^Client disconnected while scanjob was active\n");
         if (reason == visit_file)
             free(filename);
         return CL_BREAK;
949c6fe5
     }
     scandata->total++;
     switch (reason) {
72fd33c8
         case error_mem:
             if (msg)
                 logg("!Memory allocation failed during cli_ftw() on %s\n",
                      msg);
             else
                 logg("!Memory allocation failed during cli_ftw()\n");
             scandata->errors++;
             return CL_EMEM;
         case error_stat:
             conn_reply_errno(scandata->conn, msg, "lstat() failed:");
             logg("^lstat() failed on: %s\n", msg);
             scandata->errors++;
             return CL_SUCCESS;
         case warning_skipped_dir:
             logg("^Directory recursion limit reached, skipping %s\n",
                  msg);
             return CL_SUCCESS;
         case warning_skipped_link:
             logg("$Skipping symlink: %s\n", msg);
             return CL_SUCCESS;
         case warning_skipped_special:
             if (msg == scandata->toplevel_path)
                 conn_reply(scandata->conn, msg, "Not supported file type", "ERROR");
             logg("*Not supported file type: %s\n", msg);
             return CL_SUCCESS;
         case visit_directory_toplev:
             return CL_SUCCESS;
         case visit_file:
             break;
949c6fe5
     }
e3aaff8e
 
949c6fe5
 #ifdef C_LINUX
4f211336
     /* check whether the file is excluded */
72fd33c8
     if (procdev && sb && (sb->st_dev == procdev)) {
         free(filename);
         return CL_SUCCESS;
949c6fe5
     }
2bb229f6
 #endif
f45d19ac
 
72fd33c8
     if (sb && sb->st_size == 0) { /* empty file */
         if (msg == scandata->toplevel_path)
             conn_reply_single(scandata->conn, filename, "Empty file");
         free(filename);
         return CL_SUCCESS;
e3aaff8e
     }
 
949c6fe5
     if (type == TYPE_MULTISCAN) {
72fd33c8
         client_conn_t *client_conn = (client_conn_t *)calloc(1, sizeof(struct client_conn_tag));
         if (client_conn) {
             client_conn->scanfd   = -1;
             client_conn->sd       = scandata->odesc;
             client_conn->filename = filename;
             client_conn->cmdtype  = COMMAND_MULTISCANFILE;
             client_conn->term     = scandata->conn->term;
             client_conn->options  = scandata->options;
             client_conn->opts     = scandata->opts;
             client_conn->group    = scandata->group;
             if (cl_engine_addref(scandata->engine)) {
                 logg("!cl_engine_addref() failed\n");
                 free(filename);
                 free(client_conn);
                 return CL_EMEM;
             } else {
                 client_conn->engine = scandata->engine;
                 pthread_mutex_lock(&reload_mutex);
                 client_conn->engine_timestamp = reloaded_time;
                 pthread_mutex_unlock(&reload_mutex);
                 if (!thrmgr_group_dispatch(scandata->thr_pool, scandata->group, client_conn, 1)) {
                     logg("!thread dispatch failed\n");
                     cl_engine_free(scandata->engine);
                     free(filename);
                     free(client_conn);
                     return CL_EMEM;
                 }
             }
         } else {
             logg("!Can't allocate memory for client_conn\n");
             scandata->errors++;
             free(filename);
             return CL_EMEM;
         }
         return CL_SUCCESS;
e3aaff8e
     }
 
949c6fe5
     if (access(filename, R_OK)) {
72fd33c8
         if (conn_reply(scandata->conn, filename, "Access denied.", "ERROR") == -1) {
             free(filename);
             return CL_ETIMEOUT;
         }
         logg("*Access denied: %s\n", filename);
         scandata->errors++;
         free(filename);
         return CL_SUCCESS;
a617b5ea
     }
 
6174db3a
     thrmgr_setactivetask(filename, NULL);
769f37a6
     context.filename = filename;
72fd33c8
     context.virsize  = 0;
1f1bf36b
     context.scandata = scandata;
72fd33c8
     ret              = cl_scanfile_callback(filename, &virname, &scandata->scanned, scandata->engine, scandata->options, &context);
949c6fe5
     thrmgr_setactivetask(NULL, NULL);
e3aaff8e
 
0378a9ab
     if (thrmgr_group_need_terminate(scandata->conn->group)) {
72fd33c8
         free(filename);
         logg("*Client disconnected while scanjob was active\n");
         return ret == CL_ETIMEOUT ? ret : CL_BREAK;
0378a9ab
     }
 
fab9317e
     if ((ret == CL_VIRUS) && (virname == NULL)) {
aec1e3be
         logg("*%s: reported CL_VIRUS but no virname returned!\n", filename);
fab9317e
         ret = CL_EMEM;
     }
 
949c6fe5
     if (ret == CL_VIRUS) {
97930c24
 
72fd33c8
         if (scandata->options->general & CL_SCAN_GENERAL_ALLMATCHES || (scandata->infected && scandata->options->general & CL_SCAN_GENERAL_HEURISTIC_PRECEDENCE)) {
             if (optget(scandata->opts, "PreludeEnable")->enabled) {
97930c24
                 prelude_logging(filename, virname, context.virhash, context.virsize);
             }
             virusaction(filename, virname, scandata->opts);
         } else {
72fd33c8
             scandata->infected++;
97930c24
             if (conn_reply_virus(scandata->conn, filename, virname) == -1) {
                 free(filename);
                 return CL_ETIMEOUT;
             }
72fd33c8
             if (optget(scandata->opts, "PreludeEnable")->enabled) {
97930c24
                 prelude_logging(filename, virname, context.virhash, context.virsize);
             }
 
72fd33c8
             if (context.virsize && optget(scandata->opts, "ExtendedDetectionInfo")->enabled)
97930c24
                 logg("~%s: %s(%s:%llu) FOUND\n", filename, virname, context.virhash, context.virsize);
             else
                 logg("~%s: %s FOUND\n", filename, virname);
             virusaction(filename, virname, scandata->opts);
         }
949c6fe5
     } else if (ret != CL_CLEAN) {
72fd33c8
         scandata->errors++;
         if (conn_reply(scandata->conn, filename, cl_strerror(ret), "ERROR") == -1) {
             free(filename);
             return CL_ETIMEOUT;
         }
         logg("~%s: %s ERROR\n", filename, cl_strerror(ret));
949c6fe5
     } else if (logok) {
72fd33c8
         logg("~%s: OK\n", filename);
e3aaff8e
     }
 
949c6fe5
     free(filename);
1f1bf36b
 
72fd33c8
     if (ret == CL_EMEM) /* stop scanning */
         return ret;
e0591f05
 
949c6fe5
     if (type == TYPE_SCAN) {
72fd33c8
         /* virus -> break */
         return ret;
e3aaff8e
     }
 
949c6fe5
     /* keep scanning always */
     return CL_SUCCESS;
e3aaff8e
 }
 
51bbedb1
 int scan_pathchk(const char *path, struct cli_ftw_cbdata *data)
 {
72fd33c8
     struct scan_cb_data *scandata = data->data;
     const struct optstruct *opt;
     STATBUF statbuf;
 
     if ((opt = optget(scandata->opts, "ExcludePath"))->enabled) {
         while (opt) {
             if (match_regex(path, opt->strarg) == 1) {
                 if (scandata->type != TYPE_MULTISCAN)
                     conn_reply_single(scandata->conn, path, "Excluded");
                 return 1;
             }
             opt = (const struct optstruct *)opt->nextarg;
         }
51bbedb1
     }
2086dc5c
 
72fd33c8
     if (!optget(scandata->opts, "CrossFilesystems")->enabled) {
         if (CLAMSTAT(path, &statbuf) == 0) {
             if (statbuf.st_dev != scandata->dev) {
                 if (scandata->type != TYPE_MULTISCAN)
                     conn_reply_single(scandata->conn, path, "Excluded (another filesystem)");
                 return 1;
             }
         }
2086dc5c
     }
 
51bbedb1
     return 0;
 }
 
d7979d4f
 int scanfd(
72fd33c8
     const client_conn_t *conn,
     unsigned long int *scanned,
     const struct cl_engine *engine,
     struct cl_scan_options *options,
     const struct optstruct *opts,
     int odesc,
     int stream)
725a2969
 {
0f4639ec
     int ret, fd = conn->scanfd;
72fd33c8
     const char *virname = NULL;
     STATBUF statbuf;
     struct cb_context context;
     char fdstr[32];
     const char *reply_fdstr;
725a2969
 
6df13d04
     UNUSEDPARAM(odesc);
 
72fd33c8
     if (stream) {
         struct sockaddr_in sa;
         socklen_t salen = sizeof(sa);
         if (getpeername(conn->sd, (struct sockaddr *)&sa, &salen) || salen > sizeof(sa) || sa.sin_family != AF_INET)
             strncpy(fdstr, "instream(local)", sizeof(fdstr));
         else
             snprintf(fdstr, sizeof(fdstr), "instream(%s@%u)", inet_ntoa(sa.sin_addr), ntohs(sa.sin_port));
         reply_fdstr = "stream";
     } else {
         snprintf(fdstr, sizeof(fdstr), "fd[%d]", fd);
         reply_fdstr = fdstr;
     }
     if (FSTAT(fd, &statbuf) == -1 || !S_ISREG(statbuf.st_mode)) {
         logg("%s: Not a regular file. ERROR\n", fdstr);
         if (conn_reply(conn, reply_fdstr, "Not a regular file", "ERROR") == -1)
             return CL_ETIMEOUT;
         return -1;
     }
 
     thrmgr_setactivetask(fdstr, NULL);
     context.filename = fdstr;
     context.virsize  = 0;
     context.scandata = NULL;
25ed70fb
     ret              = cl_scandesc_callback(fd, conn->filename, &virname, scanned, engine, options, &context);
72fd33c8
     thrmgr_setactivetask(NULL, NULL);
 
     if (thrmgr_group_need_terminate(conn->group)) {
         logg("*Client disconnected while scanjob was active\n");
         return ret == CL_ETIMEOUT ? ret : CL_BREAK;
     }
 
     if (ret == CL_VIRUS) {
         if (conn_reply_virus(conn, reply_fdstr, virname) == -1)
             ret = CL_ETIMEOUT;
         if (context.virsize && optget(opts, "ExtendedDetectionInfo")->enabled)
             logg("%s: %s(%s:%llu) FOUND\n", fdstr, virname, context.virhash, context.virsize);
         else
             logg("%s: %s FOUND\n", fdstr, virname);
         virusaction(reply_fdstr, virname, opts);
     } else if (ret != CL_CLEAN) {
         if (conn_reply(conn, reply_fdstr, cl_strerror(ret), "ERROR") == -1)
             ret = CL_ETIMEOUT;
         logg("%s: %s ERROR\n", fdstr, cl_strerror(ret));
     } else {
         if (conn_reply_single(conn, reply_fdstr, "OK") == CL_ETIMEOUT)
             ret = CL_ETIMEOUT;
         if (logok)
             logg("%s: OK\n", fdstr);
     }
     return ret;
725a2969
 }
 
d7979d4f
 int scanstream(
72fd33c8
     int odesc,
     unsigned long int *scanned,
     const struct cl_engine *engine,
     struct cl_scan_options *options,
     const struct optstruct *opts,
     char term)
e3aaff8e
 {
72fd33c8
     int ret, sockfd, acceptd;
     int tmpd, bread, retval, firsttimeout, timeout, btread;
     unsigned int port       = 0, portscan, min_port, max_port;
     unsigned long int quota = 0, maxsize = 0;
     short bound         = 0;
     const char *virname = NULL;
     char buff[FILEBUFF];
     char peer_addr[32];
     struct cb_context context;
     struct sockaddr_in server;
     struct sockaddr_in peer;
     socklen_t addrlen;
     char *tmpname;
32c970f8
 
064b4a0c
     min_port = optget(opts, "StreamMinPort")->numarg;
     max_port = optget(opts, "StreamMaxPort")->numarg;
949c6fe5
 
     /* search for a free port to bind to */
72fd33c8
     port  = cli_rndnum(max_port - min_port);
949c6fe5
     bound = 0;
     for (portscan = 0; portscan < 1000; portscan++) {
72fd33c8
         port = (port - 1) % (max_port - min_port + 1);
 
         memset((char *)&server, 0, sizeof(server));
         server.sin_family      = AF_INET;
         server.sin_port        = htons(min_port + port);
         server.sin_addr.s_addr = htonl(INADDR_ANY);
 
         if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
             continue;
 
         if (bind(sockfd, (struct sockaddr *)&server, (socklen_t)sizeof(struct sockaddr_in)) == -1)
             closesocket(sockfd);
         else {
             bound = 1;
             break;
         }
e3aaff8e
     }
949c6fe5
     port += min_port;
32c970f8
 
72fd33c8
     timeout      = optget(opts, "ReadTimeout")->numarg;
5f6edb22
     firsttimeout = optget(opts, "CommandReadTimeout")->numarg;
e3aaff8e
 
72fd33c8
     if (!bound) {
         logg("!ScanStream: Can't find any free port.\n");
         mdprintf(odesc, "Can't find any free port. ERROR%c", term);
138b5aba
         return -1;
72fd33c8
     } else {
         if (listen(sockfd, 1) == -1) {
             logg("!ScanStream: listen() error on socket. Error returned is %s.\n", strerror(errno));
             closesocket(sockfd);
             return -1;
         }
         if (mdprintf(odesc, "PORT %u%c", port, term) <= 0) {
             logg("!ScanStream: error transmitting port.\n");
             closesocket(sockfd);
             return -1;
         }
e3aaff8e
     }
 
5f6edb22
     retval = poll_fd(sockfd, firsttimeout, 0);
949c6fe5
     if (!retval || retval == -1) {
72fd33c8
         const char *reason = !retval ? "timeout" : "poll";
         mdprintf(odesc, "Accept %s. ERROR%c", reason, term);
         logg("!ScanStream %u: accept %s.\n", port, reason);
         closesocket(sockfd);
         return -1;
c238ac42
     }
 
47b68859
     addrlen = sizeof(peer);
72fd33c8
     if ((acceptd = accept(sockfd, (struct sockaddr *)&peer, (socklen_t *)&addrlen)) == -1) {
         closesocket(sockfd);
         mdprintf(odesc, "accept() ERROR%c", term);
         logg("!ScanStream %u: accept() failed.\n", port);
         return -1;
e3aaff8e
     }
 
3b074c78
     *peer_addr = '\0';
49e5f658
     inet_ntop(peer.sin_family, &peer.sin_addr, peer_addr, sizeof(peer_addr));
47b68859
     logg("*Accepted connection from %s on port %u, fd %d\n", peer_addr, port, acceptd);
8139fd99
 
72fd33c8
     if (cli_gentempfd(optget(opts, "TemporaryDirectory")->strarg, &tmpname, &tmpd)) {
         shutdown(sockfd, 2);
         closesocket(sockfd);
         closesocket(acceptd);
         mdprintf(odesc, "cli_gentempfd() failed. ERROR%c", term);
         logg("!ScanStream(%s@%u): Can't create temporary file.\n", peer_addr, port);
         return -1;
32c970f8
     }
 
949c6fe5
     quota = maxsize = optget(opts, "StreamMaxLength")->numarg;
32c970f8
 
72fd33c8
     while ((retval = poll_fd(acceptd, timeout, 0)) == 1) {
         /* only read up to max */
         btread = (maxsize && (quota < sizeof(buff))) ? quota : sizeof(buff);
         if (!btread) {
             logg("^ScanStream(%s@%u): Size limit reached (max: %lu)\n", peer_addr, port, maxsize);
             break; /* Scan what we have */
         }
         bread = recv(acceptd, buff, btread, 0);
         if (bread <= 0)
             break;
 
         quota -= bread;
 
         if (writen(tmpd, buff, bread) != bread) {
             shutdown(sockfd, 2);
             closesocket(sockfd);
             closesocket(acceptd);
             mdprintf(odesc, "Temporary file -> write ERROR%c", term);
             logg("!ScanStream(%s@%u): Can't write to temporary file.\n", peer_addr, port);
             close(tmpd);
             if (!optget(opts, "LeaveTemporaryFiles")->enabled)
                 unlink(tmpname);
             free(tmpname);
             return -1;
         }
32c970f8
     }
e3aaff8e
 
72fd33c8
     switch (retval) {
         case 0: /* timeout */
             mdprintf(odesc, "read timeout ERROR%c", term);
             logg("!ScanStream(%s@%u): read timeout.\n", peer_addr, port);
             break;
         case -1:
             mdprintf(odesc, "read poll ERROR%c", term);
             logg("!ScanStream(%s@%u): read poll failed.\n", peer_addr, port);
             break;
32c970f8
     }
e3aaff8e
 
72fd33c8
     if (retval == 1) {
         lseek(tmpd, 0, SEEK_SET);
         thrmgr_setactivetask(peer_addr, NULL);
         context.filename = peer_addr;
         context.virsize  = 0;
         context.scandata = NULL;
25ed70fb
         ret              = cl_scandesc_callback(tmpd, tmpname, &virname, scanned, engine, options, &context);
72fd33c8
         thrmgr_setactivetask(NULL, NULL);
6d5c43a1
     } else {
72fd33c8
         ret = -1;
6d5c43a1
     }
0e621e7d
     close(tmpd);
72fd33c8
     if (!optget(opts, "LeaveTemporaryFiles")->enabled)
         unlink(tmpname);
0e621e7d
     free(tmpname);
e3aaff8e
 
67118e92
     closesocket(acceptd);
     closesocket(sockfd);
e3aaff8e
 
72fd33c8
     if (ret == CL_VIRUS) {
         if (context.virsize && optget(opts, "ExtendedDetectionInfo")->enabled) {
             mdprintf(odesc, "stream: %s(%s:%llu) FOUND%c", virname, context.virhash, context.virsize, term);
             logg("stream(%s@%u): %s(%s:%llu) FOUND\n", peer_addr, port, virname, context.virhash, context.virsize);
         } else {
             mdprintf(odesc, "stream: %s FOUND%c", virname, term);
             logg("stream(%s@%u): %s FOUND\n", peer_addr, port, virname);
         }
         virusaction("stream", virname, opts);
     } else if (ret != CL_CLEAN) {
         if (retval == 1) {
             mdprintf(odesc, "stream: %s ERROR%c", cl_strerror(ret), term);
             logg("stream(%s@%u): %s ERROR\n", peer_addr, port, cl_strerror(ret));
         }
ee039e40
     } else {
72fd33c8
         mdprintf(odesc, "stream: OK%c", term);
         if (logok)
             logg("stream(%s@%u): OK\n", peer_addr, port);
ee039e40
     }
e3aaff8e
 
     return ret;
 }