clamd/session.c
81131381
 /*
086eab5c
  *  Copyright (C) 2007-2009 Sourcefire, Inc.
  *
  *  Authors: Tomasz Kojm, Török Edvin
81131381
  *
  *  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.
81131381
  *
  *  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.
81131381
  */
 
67118e92
 #ifdef	_MSC_VER
 #include <winsock.h>
 #endif
 
98ac8d19
 #if HAVE_CONFIG_H
 #include "clamav-config.h"
 #endif
 
81131381
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
67118e92
 #ifdef	HAVE_UNISTD_H
81131381
 #include <unistd.h>
67118e92
 #endif
81131381
 #include <sys/types.h>
67118e92
 #ifndef	C_WINDOWS
8765287e
 #include <dirent.h>
3dc90d4a
 #ifdef HAVE_SYS_SELECT_H
 #include <sys/select.h>
 #endif
81131381
 #include <sys/socket.h>
725a2969
 #ifdef HAVE_FD_PASSING
cb4e478c
 #ifdef HAVE_SYS_UIO_H
725a2969
 #include <sys/uio.h>
 #endif
cb4e478c
 #endif
725a2969
 
31e6c6fb
 #include <sys/time.h>
67118e92
 #endif
81131381
 #include <pthread.h>
 #include <time.h>
c238ac42
 #include <errno.h>
67118e92
 #include <stddef.h>
9ac109f3
 #include <limits.h>
81131381
 
bd8603aa
 #include "libclamav/clamav.h"
 #include "libclamav/str.h"
9e751804
 #include "libclamav/others.h"
bd8603aa
 
064b4a0c
 #include "shared/optparser.h"
bd8603aa
 #include "shared/output.h"
53725d8c
 #include "shared/misc.h"
bd8603aa
 
81131381
 #include "others.h"
 #include "scanner.h"
 #include "server.h"
 #include "session.h"
b9b47784
 #include "thrmgr.h"
81131381
 
39b84970
 #ifndef HAVE_FDPASSING
 #define FEATURE_FDPASSING 0
 #else
 #define FEATURE_FDPASSING 1
 #endif
 
949c6fe5
 static struct {
     const char *cmd;
     const size_t len;
     enum commands cmdtype;
     int need_arg;
537292a7
     int support_old;
39b84970
     int enabled;
949c6fe5
 } commands[] = {
39b84970
     {CMD1,  sizeof(CMD1)-1,	COMMAND_SCAN,	    1,	1, 0},
     {CMD3,  sizeof(CMD3)-1,	COMMAND_SHUTDOWN,   0,	1, 0},
     {CMD4,  sizeof(CMD4)-1,	COMMAND_RELOAD,	    0,	1, 0},
     {CMD5,  sizeof(CMD5)-1,	COMMAND_PING,	    0,	1, 0},
     {CMD6,  sizeof(CMD6)-1,	COMMAND_CONTSCAN,   1,	1, 0},
     /* must be before VERSION, because they share common prefix! */
     {CMD18, sizeof(CMD18)-1,	COMMAND_COMMANDS,   0,	0, 1},
     {CMD7,  sizeof(CMD7)-1,	COMMAND_VERSION,    0,	1, 1},
     {CMD8,  sizeof(CMD8)-1,	COMMAND_STREAM,	    0,	1, 1},
     {CMD10, sizeof(CMD10)-1,	COMMAND_END,	    0,	0, 1},
     {CMD11, sizeof(CMD11)-1,	COMMAND_SHUTDOWN,   0,	1, 1},
     {CMD13, sizeof(CMD13)-1,	COMMAND_MULTISCAN,  1,	1, 1},
     {CMD14, sizeof(CMD14)-1,	COMMAND_FILDES,	    0,	1, FEATURE_FDPASSING},
     {CMD15, sizeof(CMD15)-1,	COMMAND_STATS,	    0,	0, 1},
     {CMD16, sizeof(CMD16)-1,	COMMAND_IDSESSION,  0,	0, 1},
     {CMD17, sizeof(CMD17)-1,	COMMAND_INSTREAM,   0,	0, 1}
949c6fe5
 };
 
 
537292a7
 enum commands parse_command(const char *cmd, const char **argument, int oldstyle)
725a2969
 {
949c6fe5
     size_t i;
     *argument = NULL;
     for (i=0; i < sizeof(commands)/sizeof(commands[0]); i++) {
 	const size_t len = commands[i].len;
 	if (!strncmp(cmd, commands[i].cmd, len)) {
 	    const char *arg = cmd + len;
 	    if (commands[i].need_arg) {
 		if (!*arg) {/* missing argument */
fb6fe4f5
 		    logg("$Command %s missing argument!\n", commands[i].cmd);
949c6fe5
 		    return COMMAND_UNKNOWN;
 		}
 		*argument = arg+1;
 	    } else {
 		if (*arg) {/* extra stuff after command */
fb6fe4f5
 		    logg("$Command %s has trailing garbage!\n", commands[i].cmd);
949c6fe5
 		    return COMMAND_UNKNOWN;
725a2969
 		}
949c6fe5
 		*argument = NULL;
 	    }
537292a7
 	    if (oldstyle && !commands[i].support_old) {
 		logg("$Command sent as old-style when not supported: %s\n", commands[i].cmd);
 		return COMMAND_UNKNOWN;
 	    }
949c6fe5
 	    return commands[i].cmdtype;
725a2969
 	}
949c6fe5
     }
     return COMMAND_UNKNOWN;
725a2969
 }
 
949c6fe5
 int conn_reply_single(const client_conn_t *conn, const char *path, const char *status)
725a2969
 {
949c6fe5
     if (conn->id) {
 	if (path)
 	    return mdprintf(conn->sd, "%u: %s: %s%c", conn->id, path, status, conn->term);
 	return mdprintf(conn->sd, "%u: %s%c", conn->id, status, conn->term);
     }
     if (path)
 	return mdprintf(conn->sd, "%s: %s%c", path, status, conn->term);
     return mdprintf(conn->sd, "%s%c", status, conn->term);
725a2969
 }
949c6fe5
 
 int conn_reply(const client_conn_t *conn, const char *path,
 	       const char *msg, const char *status)
 {
     if (conn->id) {
 	if (path)
 	    return mdprintf(conn->sd, "%u: %s: %s %s%c", conn->id, path, msg,
 			    status, conn->term);
 	return mdprintf(conn->sd, "%u: %s %s%c", conn->id, msg, status,
 			conn->term);
     }
     if (path)
 	return mdprintf(conn->sd, "%s: %s %s%c", path, msg, status, conn->term);
     return mdprintf(conn->sd, "%s %s%c", msg, status, conn->term);
 }
 
 int conn_reply_error(const client_conn_t *conn, const char *msg)
 {
     return conn_reply(conn, NULL, msg, "ERROR");
 }
 
 #define BUFFSIZE 1024
 int conn_reply_errno(const client_conn_t *conn, const char *path,
 		     const char *msg)
 {
e68d70e7
     char err[BUFFSIZE + sizeof(". ERROR")];
     cli_strerror(errno, err, BUFFSIZE-1);
     strcat(err, ". ERROR");
     return conn_reply(conn, path, msg, err);
949c6fe5
 }
725a2969
 
949c6fe5
 /* returns
  *  -1 on fatal error (shutdown)
  *  0 on ok
  *  >0 errors encountered
  */
 int command(client_conn_t *conn, int *virus)
81131381
 {
949c6fe5
     int desc = conn->sd;
     struct cl_engine *engine = conn->engine;
     unsigned int options = conn->options;
     const struct optstruct *opts = conn->opts;
     int type = -1; /* TODO: make this enum */
     int maxdirrec;
     int ret = 0;
     int flags = CLI_FTW_STD;
31e6c6fb
 
949c6fe5
     struct scan_cb_data scandata;
     struct cli_ftw_cbdata data;
     unsigned ok, error, total;
     jobgroup_t *group = NULL;
81131381
 
949c6fe5
     if (thrmgr_group_need_terminate(conn->group)) {
fb6fe4f5
 	logg("$Client disconnected while command was active\n");
949c6fe5
 	if (conn->scanfd != -1)
 	    close(conn->scanfd);
 	return 1;
     }
deb30312
     thrmgr_setactiveengine(engine);
81131381
 
949c6fe5
     data.data = &scandata;
     memset(&scandata, 0, sizeof(scandata));
     scandata.id = conn->id;
     scandata.group = conn->group;
     scandata.odesc = desc;
     scandata.conn = conn;
     scandata.options = options;
     scandata.engine = engine;
     scandata.opts = opts;
     scandata.thr_pool = conn->thrpool;
     scandata.toplevel_path = conn->filename;
7c225c08
 
949c6fe5
     switch (conn->cmdtype) {
 	case COMMAND_SCAN:
 	    thrmgr_setactivetask(NULL, "SCAN");
 	    type = TYPE_SCAN;
 	    break;
 	case COMMAND_CONTSCAN:
 	    thrmgr_setactivetask(NULL, "CONTSCAN");
 	    type = TYPE_CONTSCAN;
 	    break;
 	case COMMAND_MULTISCAN:
 	    flags &= ~CLI_FTW_NEED_STAT;
 	    thrmgr_setactivetask(NULL, "MULTISCAN");
 	    type = TYPE_MULTISCAN;
 	    scandata.group = group = thrmgr_group_new();
7660b7cb
 	    if (!group)
 		return CL_EMEM;
949c6fe5
 	    break;
 	case COMMAND_MULTISCANFILE:
 	    thrmgr_setactivetask(NULL, "MULTISCANFILE");
 	    scandata.group = NULL;
 	    scandata.type = TYPE_SCAN;
 	    scandata.thr_pool = NULL;
 	    /* TODO: check ret value */
36e4bc14
 	    ret = scan_callback(NULL, conn->filename, conn->filename, visit_file, &data);	    /* callback freed it */
949c6fe5
 	    conn->filename = NULL;
 	    *virus = scandata.infected;
36e4bc14
 	    if (ret == CL_BREAK) {
 		thrmgr_group_terminate(conn->group);
 		return CL_BREAK;
 	    }
4e24a361
 	    return scandata.errors > 0 ? scandata.errors : 0;
949c6fe5
 	case COMMAND_FILDES:
 	    thrmgr_setactivetask(NULL, "FILDES");
 #ifdef HAVE_FD_PASSING
 	    if (conn->scanfd == -1)
 		conn_reply_error(conn, "FILDES: didn't receive file descriptor.");
 	    else {
 		ret = scanfd(conn->scanfd, conn, NULL, engine, options, opts, desc, 0);
0378a9ab
 		if (ret == CL_VIRUS) {
949c6fe5
 		    *virus = 1;
0378a9ab
 		} else if (ret == CL_EMEM) {
949c6fe5
 		    if(optget(opts, "ExitOnOOM")->enabled)
 			ret = -1;
0378a9ab
 		} else if (ret == CL_ETIMEOUT) {
 			thrmgr_group_terminate(conn->group);
 			ret = 1;
949c6fe5
 		} else
 		    ret = 0;
 	    }
fb6fe4f5
 	    logg("$Closed fd %d\n", conn->scanfd);
949c6fe5
 	    close(conn->scanfd);
 	    return ret;
 #else
 	    conn_reply_error(conn, "FILDES support not compiled in.");
 	    close(conn->scanfd);
 	    return 0;
 #endif
 	case COMMAND_STATS:
 	    thrmgr_setactivetask(NULL, "STATS");
b3b8212f
 	    if (conn->group)
 		mdprintf(desc, "%u: ", conn->id);
aa22174b
 	    thrmgr_printstats(desc);
949c6fe5
 	    return 0;
 	case COMMAND_STREAM:
 	    thrmgr_setactivetask(NULL, "STREAM");
 	    ret = scanstream(desc, NULL, engine, options, opts, conn->term);
 	    if (ret == CL_VIRUS)
 		*virus = 1;
 	    if (ret == CL_EMEM) {
 		if(optget(opts, "ExitOnOOM")->enabled)
 		    return -1;
 	    }
 	    return 0;
 	case COMMAND_INSTREAMSCAN:
52cecde4
 	    thrmgr_setactivetask(NULL, "INSTREAM");
949c6fe5
 	    ret = scanfd(conn->scanfd, conn, NULL, engine, options, opts, desc, 1);
0378a9ab
 	    if (ret == CL_VIRUS) {
949c6fe5
 		*virus = 1;
0378a9ab
 	    } else if (ret == CL_EMEM) {
949c6fe5
 		if(optget(opts, "ExitOnOOM")->enabled)
 		    ret = -1;
0378a9ab
 	    } else if (ret == CL_ETIMEOUT) {
 		thrmgr_group_terminate(conn->group);
 		ret = 1;
949c6fe5
 	    } else
 		ret = 0;
 	    if (ftruncate(conn->scanfd, 0) == -1) {
 		/* not serious, we're going to close it and unlink it anyway */
 		logg("*ftruncate failed: %d\n", errno);
 	    }
 	    close(conn->scanfd);
 	    conn->scanfd = -1;
 	    cli_unlink(conn->filename);
 	    return ret;
     }
 
     scandata.type = type;
     maxdirrec = optget(opts, "MaxDirectoryRecursion")->numarg;
     if (optget(opts, "FollowDirectorySymlinks")->enabled)
 	flags |= CLI_FTW_FOLLOW_DIR_SYMLINK;
     if (optget(opts, "FollowFileSymlinks")->enabled)
 	flags |= CLI_FTW_FOLLOW_FILE_SYMLINK;
0378a9ab
     ret = cli_ftw(conn->filename, flags,  maxdirrec ? maxdirrec : INT_MAX, scan_callback, &data);
     if (ret == CL_EMEM)
949c6fe5
 	if(optget(opts, "ExitOnOOM")->enabled)
 	    return -1;
     if (scandata.group && conn->cmdtype == COMMAND_MULTISCAN) {
 	thrmgr_group_waitforall(group, &ok, &error, &total);
81131381
     } else {
949c6fe5
 	error = scandata.errors;
 	total = scandata.total;
 	ok = total - error - scandata.infected;
81131381
     }
 
949c6fe5
     if (ok + error == total && (error != total)) {
0378a9ab
 	if (conn_reply_single(conn, conn->filename, "OK") == -1)
 	    ret = CL_ETIMEOUT;
949c6fe5
     }
     *virus = total - (ok + error);
0378a9ab
 
     if (ret == CL_ETIMEOUT)
 	thrmgr_group_terminate(conn->group);
949c6fe5
     return error;
 }
 
 static int dispatch_command(client_conn_t *conn, enum commands cmd, const char *argument)
 {
     int ret = 0;
     client_conn_t *dup_conn = (client_conn_t *) malloc(sizeof(struct client_conn_tag));
 
     if(!dup_conn) {
 	logg("!Can't allocate memory for client_conn\n");
 	return -1;
     }
     memcpy(dup_conn, conn, sizeof(*conn));
     dup_conn->cmdtype = cmd;
     if(cl_engine_addref(dup_conn->engine)) {
 	logg("!cl_engine_addref() failed\n");
 	free(dup_conn);
 	return -1;
     }
     dup_conn->scanfd = -1;
     switch (cmd) {
 	case COMMAND_FILDES:
 	    if (conn->scanfd == -1) {
 		conn_reply_error(dup_conn, "No file descriptor received.");
 		ret = 1;
 	    }
 	    dup_conn->scanfd = conn->scanfd;
 	    /* consume FD */
 	    conn->scanfd = -1;
 	    break;
 	case COMMAND_SCAN:
 	case COMMAND_CONTSCAN:
 	case COMMAND_MULTISCAN:
 	    dup_conn->filename = strdup(argument);
 	    if (!dup_conn->filename) {
 		logg("!Failed to allocate memory for filename\n");
 		ret = -1;
 	    }
 	    break;
 	case COMMAND_INSTREAMSCAN:
 	    dup_conn->scanfd = conn->scanfd;
 	    conn->scanfd = -1;
 	    break;
 	case COMMAND_STREAM:
 	case COMMAND_STATS:
 	    /* just dispatch the command */
 	    break;
     }
     if(!ret && !thrmgr_group_dispatch(dup_conn->thrpool, dup_conn->group, dup_conn)) {
 	logg("!thread dispatch failed\n");
 	ret = -2;
     }
     if (ret) {
 	cl_engine_free(dup_conn->engine);
 	free(dup_conn);
     }
     return ret;
 }
 
39b84970
 static int print_ver(int desc, char term, const struct cl_engine *engine)
 {
     uint32_t ver;
 
2accc66f
     ver = cl_engine_get_num(engine, CL_ENGINE_DB_VERSION, NULL);
39b84970
     if(ver) {
 	char timestr[32];
 	const char *tstr;
 	time_t t;
2accc66f
 	t = cl_engine_get_num(engine, CL_ENGINE_DB_TIME, NULL);
39b84970
 	tstr = cli_ctime(&t, timestr, sizeof(timestr));
 	/* cut trailing \n */
 	timestr[strlen(tstr)-1] = '\0';
 	return mdprintf(desc, "ClamAV %s/%u/%s%c", get_version(), (unsigned int) ver, tstr, term);
     }
     return mdprintf(desc, "ClamAV %s%c", get_version(), term);
 }
 
 static void print_commands(int desc, char term, const struct cl_engine *engine)
 {
     unsigned i, n;
     const char *engine_ver = cl_retver();
     const char *clamd_ver = get_version();
     if (strcmp(engine_ver, clamd_ver)) {
 	mdprintf(desc, "ENGINE VERSION MISMATCH: %s != %s. ERROR%c",
 		 engine_ver, clamd_ver, term);
 	return;
     }
     print_ver(desc, '|', engine);
     mdprintf(desc, " COMMANDS:");
     n = sizeof(commands)/sizeof(commands[0]);
     for (i=0;i<n;i++) {
 	mdprintf(desc, " %s", commands[i].cmd);
     }
     mdprintf(desc, "%c", term);
 }
 
949c6fe5
 /* returns:
  *  <0 for error
  *     -1 out of memory
  *     -2 other
  *   0 for async dispatched
  *   1 for command completed (connection can be closed)
  */
 int execute_or_dispatch_command(client_conn_t *conn, enum commands cmd, const char *argument)
 {
     int desc = conn->sd;
     char term = conn->term;
     const struct cl_engine *engine = conn->engine;
     /* execute commands that can be executed quickly on the recvloop thread,
      * these must:
      *  - not involve any operation that can block for a long time, such as disk
      *  I/O
      *  - send of atomic message is allowed.
      * Dispatch other commands */
     if (conn->group) {
 	switch (cmd) {
 	    case COMMAND_FILDES:
 	    case COMMAND_SCAN:
 	    case COMMAND_END:
 	    case COMMAND_INSTREAM:
 	    case COMMAND_INSTREAMSCAN:
b3b8212f
 	    case COMMAND_VERSION:
39b84970
 	    case COMMAND_PING:
b3b8212f
 	    case COMMAND_STATS:
39b84970
 	    case COMMAND_COMMANDS:
949c6fe5
 		/* These commands are accepted inside IDSESSION */
 		break;
 	    default:
 		/* these commands are not recognized inside an IDSESSION */
 		conn_reply_error(conn, "Command invalid inside IDSESSION.");
fb6fe4f5
 		logg("$SESSION: command is not valid inside IDSESSION: %d\n", cmd);
949c6fe5
 		conn->group = NULL;
 		return 1;
 	}
     }
 
     switch (cmd) {
 	case COMMAND_SHUTDOWN:
 	    pthread_mutex_lock(&exit_mutex);
 	    progexit = 1;
 	    pthread_mutex_unlock(&exit_mutex);
 	    return 1;
 	case COMMAND_RELOAD:
 	    pthread_mutex_lock(&reload_mutex);
 	    reload = 1;
 	    pthread_mutex_unlock(&reload_mutex);
 	    mdprintf(desc, "RELOADING%c", term);
 	    /* we set reload flag, and we'll reload before closing the
 	     * connection */
 	    return 1;
 	case COMMAND_PING:
39b84970
 	    if (conn->group)
 		mdprintf(desc, "%u: PONG%c", conn->id, term);
 	    else
 		mdprintf(desc, "PONG%c", term);
 	    return conn->group ? 0 : 1;
949c6fe5
 	case COMMAND_VERSION:
 	    {
b3b8212f
 		if (conn->group)
 		    mdprintf(desc, "%u: ", conn->id);
39b84970
 		print_ver(desc, conn->term, engine);
 		return conn->group ? 0 : 1;
 	    }
 	case COMMAND_COMMANDS:
 	    {
 		if (conn->group)
 		    mdprintf(desc, "%u: ", conn->id);
 		print_commands(desc, conn->term, engine);
b3b8212f
 		return conn->group ? 0 : 1;
949c6fe5
 	    }
 	case COMMAND_INSTREAM:
 	    {
0dcb7037
 		int rc = cli_gentempfd(optget(conn->opts, "TemporaryDirectory")->strarg, &conn->filename, &conn->scanfd);
949c6fe5
 		if (rc != CL_SUCCESS)
 		    return rc;
 		conn->quota = optget(conn->opts, "StreamMaxLength")->numarg;
 		conn->mode = MODE_STREAM;
 		return 0;
 	    }
 	case COMMAND_STREAM:
 	case COMMAND_MULTISCAN:
 	case COMMAND_CONTSCAN:
 	case COMMAND_STATS:
 	case COMMAND_FILDES:
 	case COMMAND_SCAN:
 	case COMMAND_INSTREAMSCAN:
 	    return dispatch_command(conn, cmd, argument);
 	case COMMAND_IDSESSION:
 	    conn->group = thrmgr_group_new();
 	    if (!conn->group)
 		return CL_EMEM;
 	    return 0;
 	case COMMAND_END:
 	    if (!conn->group) {
 		/* end without idsession? */
 		conn_reply_single(conn, NULL, "UNKNOWN COMMAND");
 		return 1;
 	    }
 	    /* need to close connection  if we were last in group */
 	    return 1;
 	/*case COMMAND_UNKNOWN:*/
 	default:
 	    conn_reply_single(conn, NULL, "UNKNOWN COMMAND");
 	    return 1;
     }
81131381
 }