Index: proftpd-mod-clamav-0.10/mod_clamav.c =================================================================== --- proftpd-mod-clamav-0.10.orig/mod_clamav.c +++ proftpd-mod-clamav-0.10/mod_clamav.c @@ -1,6 +1,7 @@ /* * mod_clamav - ClamAV virus scanning module for ProFTPD - * Copyright (c) 2005-2008, Joseph Benden + * Copyright (c) 2005-2016, Joseph Benden + * Copyright (c) 2012-2013, TJ Saunders * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,7 +22,7 @@ * source code for ClamAV in the source distribution. * * ClamAV is available at http://www.clamav.net/ - * + * * Thanks to TJ Saunders for his helpful comments and suggestions! * * DO NOT EDIT THE LINE BELOW @@ -29,12 +30,13 @@ #include "conf.h" #include "privs.h" #include +#include +#include "mod_clamav.h" /** * Module version and declaration */ -#define MOD_CLAMAV_VERSION "mod_clamav/0.10" - +#define MOD_CLAMAV_VERSION "mod_clamav/0.14rc2" module clamav_module; /** @@ -43,641 +45,839 @@ module clamav_module; static int clamd_sockd = 0, is_remote = 0; static char *clamd_host = NULL; static int clamd_port = 0; -static unsigned long clamd_minsize = 0, clamd_maxsize = 0; +static unsigned long long clamd_minsize = 0, clamd_maxsize = 0; static int clam_errno; +static int remove_on_failure = 0; +static const char *trace_channel = "clamav"; /** * Local declarations */ static unsigned long parse_nbytes(char *nbytes_str, char *units_str); +static int clamavd_result(int sockd, const char *abs_filename, const char *rel_filename); +static int clamavd_connect_check(int sockd); +static int clamavd_scan(int sockd, const char *abs_filename, const char *rel_filename); static int clamavd_connect(void); -static int clamavd_scan(int, const char *, const char *); /** * Read the returned information from Clamavd. */ static int clamavd_result(int sockd, const char *abs_filename, const char *rel_filename) { - int infected = 0, waserror = 0, ret; - char buff[4096], *pt, *pt1; - FILE *fd = 0; - - if ((fd=fdopen(dup(sockd), "r")) == NULL) { - pr_log_pri(PR_LOG_ERR, - MOD_CLAMAV_VERSION ": error: Cant open descriptor for reading: %d", - errno); - return -1; - } - - memset(buff, '\0', sizeof(buff)); - if (fgets(buff, sizeof(buff)-1, fd)) { - if (strstr(buff, "FOUND\n")) { - ++infected; - - /* Advance past the portion of the response, - * and the path name, and the colon and space that - * follow the path name. - */ - pt = strchr(buff, ':'); - pt++; - pt += strlen(abs_filename); - pt += 3; - - pt1 = strchr(pt, '('); - if (pt1 != NULL) { - *pt1 = '\0'; - } - - /* Delete the infected upload */ - if ((ret=pr_fsio_unlink(rel_filename))!=0) { - pr_log_pri(PR_LOG_ERR, - MOD_CLAMAV_VERSION ": notice: unlink() failed (%d): %s", - errno, strerror(errno)); - } - - /* Inform the client the file contained a virus */ - pr_response_add_err(R_550, "Virus Detected and Removed: %s", pt); - - /* Log the fact */ - pr_log_pri(PR_LOG_ERR, - MOD_CLAMAV_VERSION ": Virus '%s' found in '%s'", pt, abs_filename); - } else if (strstr(buff, "ERROR\n") != NULL || - strstr(buff, "UNKNOWN COMMAND") != NULL) { - pr_log_pri(PR_LOG_ERR, MOD_CLAMAV_VERSION ": Clamd Error: %s", buff); - waserror = 1; - } - } - fclose(fd); - return infected ? infected : (waserror ? -1 : 0); -} - -/** - * Start a session with Clamavd. - */ -static int clamavd_session_start(int sockd) { - if (sockd != -1 && write(sockd, "nIDSESSION\n", 11) <= 0) { - pr_log_pri(PR_LOG_ERR, - MOD_CLAMAV_VERSION ": error: Clamd didn't accept the session request."); - return -1; - } - return 0; -} - -/** - * End session. - */ -static int clamavd_session_stop(int sockd) { - if (sockd != -1 && write(sockd, "nEND\n", 5) <= 0) { - pr_log_pri(PR_LOG_INFO, - MOD_CLAMAV_VERSION ": info: Clamd didn't accept the session end request."); - return -1; - } - return 0; + int infected = 0, waserror = 0, ret; + char buff[4096], *pt, *pt1; + FILE *fd = 0; + + (void) pr_trace_msg("clamav", 1, "clamavd_result (sockd %d, abs_filename '%s', rel_filename '%s')", sockd, abs_filename, rel_filename); + + if ((fd=fdopen(dup(sockd), "r")) == NULL) { + pr_log_pri(PR_LOG_ERR, + MOD_CLAMAV_VERSION ": error: Cant open descriptor for reading: %d", + errno); + return -1; + } + + memset(buff, '\0', sizeof(buff)); + if (fgets(buff, sizeof(buff) - 1, fd)) { + if (strstr(buff, "FOUND\n")) { + const char *proto; + + ++infected; + + pt = strrchr(buff, ':'); + if (pt) + *pt = 0; + + /* Delete the infected upload */ + /* TODO: delete only the hiddenstore file, not + the actual file. + */ + if ((ret=pr_fsio_unlink(rel_filename))!=0) { + pr_log_pri(PR_LOG_ERR, + MOD_CLAMAV_VERSION ": notice: unlink() failed (%d): %s", + errno, strerror(errno)); + } + + /* clean up the response */ + pt += 2; + pt1 = strstr(pt, " FOUND"); + if (pt1) { + *pt1 = 0; + } + + /* Generate a custom event for any listeners + * (e.g. mod_ban) which might be listening. Pass + * in the string containing the virus + * information. + */ + pr_event_generate("mod_clamav.virus-found", pt); + + /* Inform the client the file contained a + * virus (only for FTP/FTPS connections.) + */ + proto = pr_session_get_protocol(0); + if (strncmp(proto, "ftp", 3) == 0 || + strncmp(proto, "ftps", 4) == 0) { + pr_response_send(R_550, "Virus Detected and Removed: %s", pt); + } + + /* Log the fact */ + pr_log_pri(PR_LOG_ERR, + MOD_CLAMAV_VERSION ": Virus '%s' found in '%s'", pt, abs_filename); + } else if (strstr(buff, "ERROR\n") != NULL || + strstr(buff, "UNKNOWN COMMAND") != NULL) { + char *err = buff, *errend; + + errend = strstr(err, " ERROR"); + if (errend) { + *errend = 0; + } + errend = strstr(err, " UNKNOWN COMMAND"); + if (errend) { + *errend = 0; + } + + pr_log_pri(PR_LOG_ERR, MOD_CLAMAV_VERSION ": Clamd Error: %s", err); + waserror = 1; + + pr_trace_msg("clamav", 1, "Clamd scanner was not able to function; please check that Clamd is functioning before filing a bug report."); + } + } + fclose(fd); + return infected ? infected : (waserror ? -1 : 0); } /** * Test the connection with Clamd. */ static int clamavd_connect_check(int sockd) { - FILE *fd = NULL; - char buff[32]; + FILE *fd = NULL; + char buff[32]; + + (void) pr_trace_msg("clamav", 6, "clamavd_connect_check (sockd %d)", + sockd); + + if (sockd == -1) + return 0; - if (sockd == -1) - return 0; - - if (write(sockd, "PING\n", 5) <= 0) { - pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION - ": Clamd did not accept PING (%d): %s", errno, strerror(errno)); - close(sockd); - clamd_sockd = -1; - clam_errno = errno; - return 0; - } - - if ((fd = fdopen(dup(sockd), "r")) == NULL) { - pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION ": Clamd can not open descriptor for reading (%d): %s", - errno, strerror(errno)); - close(sockd); - clamd_sockd = -1; - clam_errno = errno; - return 0; - } - - if (fgets(buff, sizeof(buff), fd)) { - if (strstr(buff, "PONG")) { - fclose(fd); - return 1; - } - pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION ": Clamd return unknown response to PING: '%s'", buff); - } - - pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION ": Clamd did not respond to fgets (%d): %s", errno, strerror(errno)); - fclose(fd); - close(sockd); - clamd_sockd = -1; - clam_errno = errno; - return 0; + if (write(sockd, "PING\n", 5) <= 0) { + pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION ": Clamd did not accept PING (%d): %s", + errno, strerror(errno)); + close(sockd); + clamd_sockd = -1; + clam_errno = errno; + return 0; + } + + if ((fd = fdopen(dup(sockd), "r")) == NULL) { + pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION ": Clamd can not open descriptor for reading (%d): %s", + errno, strerror(errno)); + close(sockd); + clamd_sockd = -1; + clam_errno = errno; + return 0; + } + + if (fgets(buff, sizeof(buff), fd)) { + if (strstr(buff, "PONG")) { + fclose(fd); + return 1; + } + pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION ": Clamd return unknown response to PING: '%s'", buff); + } + + pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION ": Clamd did not respond to fgets (%d): %s", + errno, strerror(errno)); + fclose(fd); + close(sockd); + clamd_sockd = -1; + clam_errno = errno; + return 0; +} + +/** + * Request Clamavd to perform a scan of the file contents. + */ +static int clamavd_scan_stream(int sockd, const char *abs_filename, + const char *rel_filename) { + u_int32_t len = 0; + char *buf; + size_t bufsz = 4096; + long res; + FILE *fd; + + if (!clamavd_connect_check(sockd)) { + if ((clamd_sockd = clamavd_connect()) < 0) { + pr_log_pri(PR_LOG_ERR, + MOD_CLAMAV_VERSION ": error: Cannot re-connect to Clamd (%d): %s", + errno, strerror(errno)); + clam_errno = errno; + return -1; + } + sockd = clamd_sockd; + pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION ": Successfully reconnected to Clamd."); + clam_errno = 0; + } + + if (write(sockd, "nINSTREAM\n", 10) <= 0) { + pr_log_pri(PR_LOG_ERR, + MOD_CLAMAV_VERSION ": Cannot write to the Clamd socket: %d", errno); + clam_errno = errno; + return -1; + } + + fd = fopen(rel_filename, "r"); + if (!fd) { + pr_log_pri(PR_LOG_ERR, + MOD_CLAMAV_VERSION ": Cannot open file '%s' errno=%d.", rel_filename, errno); + clam_errno = errno; + return -1; + } + + /* rewind file descriptor */ + fseek(fd, 0, SEEK_SET); + + buf = malloc(bufsz); + if (!buf) { + pr_log_pri(PR_LOG_CRIT, "Out of memory!"); + end_login(1); + } + + /* send file contents using protocol defined by Clamd */ + while ((res = fread(buf, 1, bufsz, fd)) > 0) { + len = htonl(res); + pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION ": Streaming %" PR_LU " bytes (%d, %u) to Clamd.", res, len, sizeof(len)); + if (write(sockd, (void *) &len, sizeof(len)) <= 0) { + pr_log_pri(PR_LOG_ERR, + MOD_CLAMAV_VERSION ": Cannot write byte count to Clamd socket: %d", errno); + clam_errno = errno; + fclose(fd); + free(buf); + return -1; + } + if (write(sockd, buf, res) != res) { + pr_log_pri(PR_LOG_ERR, + MOD_CLAMAV_VERSION ": Cannot stream file to Clamd socket: %d", errno); + clam_errno = errno; + fclose(fd); + free(buf); + return -1; + } + if (feof(fd)) break; + } + fclose(fd); + free(buf); + + /* send null length byte, to terminate stream */ + len = 0; + if (write(sockd, (void *) &len, sizeof(len)) <= 0) { + pr_log_pri(PR_LOG_ERR, + MOD_CLAMAV_VERSION ": Cannot write termination byte to Clamd socket: %d", errno); + clam_errno = errno; + return -1; + } + if (write(sockd, "\n", 1) <= 0) { + pr_log_pri(PR_LOG_ERR, + MOD_CLAMAV_VERSION ": Cannot write terminating return. %d", errno); + } + + /* interpret results */ + return clamavd_result(sockd, abs_filename, rel_filename); } /** * Request Clamavd to perform a scan. */ -static int clamavd_scan(int sockd, const char *abs_filename, const char *rel_filename) { - char *scancmd = NULL; - - scancmd = calloc(strlen(abs_filename) + 20, sizeof(char)); - if (!scancmd) { - pr_log_pri(PR_LOG_ERR, MOD_CLAMAV_VERSION ": error: Cannot allocate memory."); - return -1; - } - - sprintf(scancmd, "nSCAN %s\n", abs_filename); - - if (!clamavd_connect_check(sockd)) { - if ((clamd_sockd = clamavd_connect()) < 0) { - pr_log_pri(PR_LOG_ERR, - MOD_CLAMAV_VERSION ": error: Cannot re-connect to Clamd (%d): %s", - errno, strerror(errno)); - free(scancmd); - scancmd = NULL; - clam_errno = errno; - return -1; - } - clamavd_session_start(clamd_sockd); - sockd = clamd_sockd; - pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION ": Successfully reconnected to Clamd"); - clam_errno = 0; - } - - if (write(sockd, scancmd, strlen(scancmd)) <= 0) { - pr_log_pri(PR_LOG_ERR, - MOD_CLAMAV_VERSION ": error: Cannot write to the Clamd socket: %d", errno); - free(scancmd); - scancmd = NULL; - clam_errno = errno; - return -1; - } - - free(scancmd); - scancmd = NULL; - return clamavd_result(sockd, abs_filename, rel_filename); -} +static int clamavd_scan(int sockd, const char *abs_filename, + const char *rel_filename) { + char *scancmd = NULL; + + scancmd = calloc(strlen(abs_filename) + 20, sizeof(char)); + if (!scancmd) { + pr_log_pri(PR_LOG_ERR, MOD_CLAMAV_VERSION ": error: Cannot allocate memory."); + return -1; + } + + (void) pr_trace_msg("clamav", 6, "abs_filename '%s' being scanned.", + abs_filename); + + sprintf(scancmd, "SCAN %s\n", abs_filename); + + if (!clamavd_connect_check(sockd)) { + if ((clamd_sockd = clamavd_connect()) < 0) { + pr_log_pri(PR_LOG_ERR, + MOD_CLAMAV_VERSION ": error: Cannot re-connect to Clamd (%d): %s", + errno, strerror(errno)); + clam_errno = errno; + free(scancmd); + scancmd = NULL; + return -1; + } + sockd = clamd_sockd; + + (void) pr_trace_msg("clamav", 4, "Successfully reconnected to the ClamAV scanner."); + + clam_errno = 0; + } + + if (write(sockd, scancmd, strlen(scancmd)) <= 0) { + pr_log_pri(PR_LOG_ERR, + MOD_CLAMAV_VERSION ": error: Cannot write to the Clamd socket: %d", errno); + free(scancmd); + scancmd = NULL; + clam_errno = errno; + return -1; + } + + free(scancmd); + scancmd = NULL; + return clamavd_result(sockd, abs_filename, rel_filename); +} /** * Connect a socket to ClamAVd. */ static int clamavd_connect(void) { - struct sockaddr_un server; - struct sockaddr_in server2; - struct hostent *he; - int sockd, *port; - - /** - * We will set the global socket to non-connected, just in-case. - */ - clamd_sockd = -1; - - memset((char*)&server, 0, sizeof(server)); - memset((char*)&server2, 0, sizeof(server2)); - - clamd_host = (char *) get_param_ptr(CURRENT_CONF, "ClamLocalSocket", TRUE); - if (!clamd_host) { - clamd_host = (char *) get_param_ptr(CURRENT_CONF, "ClamServer", TRUE); - if (!clamd_host) { - pr_log_pri(PR_LOG_INFO, - MOD_CLAMAV_VERSION ": warning: No local socket or server was specified."); - return -1; - } - is_remote = 1; - if ((port = (int *) get_param_ptr(CURRENT_CONF, "ClamPort", TRUE)) <= 0) - clamd_port = 3310; - else - clamd_port = *port; - pr_log_debug(DEBUG4, - MOD_CLAMAV_VERSION ": Connecting to remote Clamd host '%s' on port %d", clamd_host, clamd_port); - } else { - pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION ": Connecting to local Clamd socket '%s'", clamd_host); - } - - PRIVS_ROOT; - - if (is_remote == 0) { - /* Local Socket */ - server.sun_family = AF_UNIX; - strncpy(server.sun_path, clamd_host, sizeof(server.sun_path)); - - if ((sockd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) { - PRIVS_RELINQUISH; - pr_log_pri(PR_LOG_ERR, - MOD_CLAMAV_VERSION ": error: Cannot create socket connection to Clamd (%d): %s", - errno, strerror(errno)); - clam_errno = errno; - return -1; - } - - if (connect(sockd, (struct sockaddr *)&server, sizeof(struct sockaddr_un)) < 0) { - close(sockd); - PRIVS_RELINQUISH; - pr_log_pri(PR_LOG_ERR, - MOD_CLAMAV_VERSION ": error: Cannot connect to Clamd (%d): %s", errno, strerror(errno)); - clam_errno = errno; - return -1; - } - } else { - /* Remote Socket */ - server2.sin_family = AF_INET; - server2.sin_port = htons(clamd_port); - - if ((sockd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { - PRIVS_RELINQUISH; - pr_log_pri(PR_LOG_ERR, - MOD_CLAMAV_VERSION ": error: Cannot create socket connection Clamd (%d): %s", - errno, strerror(errno)); - clam_errno = errno; - return -1; - } - - if ((he = gethostbyname(clamd_host)) == 0) { - close(sockd); - PRIVS_RELINQUISH; - pr_log_pri(PR_LOG_ERR, MOD_CLAMAV_VERSION ": error: Cannot resolve hostname '%s'", clamd_host); - clam_errno = errno; - return -1; - } - server2.sin_addr = *(struct in_addr *) he->h_addr_list[0]; - - if (connect(sockd, (struct sockaddr *)&server2, sizeof(struct sockaddr_in)) < 0) { - close(sockd); - PRIVS_RELINQUISH; - pr_log_pri(PR_LOG_ERR, - MOD_CLAMAV_VERSION ": error: Cannot connect to Clamd (%d): %s", - errno, strerror(errno)); - clam_errno = errno; - return -1; - } - } - - PRIVS_RELINQUISH; - - clam_errno = 0; - - return sockd; -} + struct sockaddr_un server; + struct sockaddr_in server2; + struct hostent *he; + int sockd, *port; + + /** + * We will set the global socket to non-connected, just in-case. + */ + clamd_sockd = -1; + + memset((char*)&server, 0, sizeof(server)); + memset((char*)&server2, 0, sizeof(server2)); + + clamd_host = (char *) get_param_ptr(CURRENT_CONF, "ClamLocalSocket", TRUE); + if (!clamd_host) { + clamd_host = (char *) get_param_ptr(CURRENT_CONF, "ClamServer", TRUE); + if (!clamd_host) { + pr_log_pri(PR_LOG_INFO, + MOD_CLAMAV_VERSION ": warning: No local socket or server was specified."); + return -1; + } + is_remote = 1; + if ((port = (int *) get_param_ptr(CURRENT_CONF, "ClamPort", TRUE)) <= 0) + clamd_port = 3310; + else + clamd_port = *port; + + (void) pr_trace_msg("clamav", 4, "Connecting to remote ClamAV scanner on host '%s' and port %d.", clamd_host, clamd_port); + } else { + (void) pr_trace_msg("clamav", 4, "Connecting to local ClamAV scanner on unix socket '%s'.", clamd_host); + } + + PRIVS_ROOT; + + if (is_remote == 0) { + /* Local Socket */ + server.sun_family = AF_UNIX; + strncpy(server.sun_path, clamd_host, sizeof(server.sun_path)); + + if ((sockd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) { + PRIVS_RELINQUISH; + pr_log_pri(PR_LOG_ERR, + MOD_CLAMAV_VERSION ": error: Cannot create socket connection to Clamd (%d): %s", + errno, strerror(errno)); + clam_errno = errno; + return -1; + } + + if (connect(sockd, (struct sockaddr *)&server, sizeof(struct sockaddr_un)) < 0) { + close(sockd); + PRIVS_RELINQUISH; + pr_log_pri(PR_LOG_ERR, + MOD_CLAMAV_VERSION ": error: Cannot connect to Clamd (%d): %s", errno, strerror(errno)); + clam_errno = errno; + return -1; + } + } else { + /* Remote Socket */ + server2.sin_family = AF_INET; + server2.sin_port = htons(clamd_port); + + if ((sockd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { + PRIVS_RELINQUISH; + pr_log_pri(PR_LOG_ERR, + MOD_CLAMAV_VERSION ": error: Cannot create socket connection Clamd (%d): %s", + errno, strerror(errno)); + clam_errno = errno; + return -1; + } + + if ((he = gethostbyname(clamd_host)) == 0) { + close(sockd); + PRIVS_RELINQUISH; + pr_log_pri(PR_LOG_ERR, MOD_CLAMAV_VERSION ": error: Cannot resolve hostname '%s'", clamd_host); + clam_errno = errno; + return -1; + } + server2.sin_addr = *(struct in_addr *) he->h_addr_list[0]; + + if (connect(sockd, (struct sockaddr *)&server2, sizeof(struct sockaddr_in)) < 0) { + close(sockd); + PRIVS_RELINQUISH; + pr_log_pri(PR_LOG_ERR, + MOD_CLAMAV_VERSION ": error: Cannot connect to Clamd (%d): %s", + errno, strerror(errno)); + clam_errno = errno; + return -1; + } + } + + PRIVS_RELINQUISH; + clam_errno = 0; + + return sockd; +} + +/** + * Entry point of mod_xfer during an upload + */ static int clamav_fsio_close(pr_fh_t *fh, int fd) { - char *abs_path, *rel_path; - struct stat st; - int do_scan = FALSE; - config_rec *c = NULL; - unsigned long *minsize, *maxsize; - - /* We're only interested in STOR, APPE, and maybe STOU commands. */ - if (session.curr_cmd) { - if (strcmp(session.curr_cmd, C_STOR) == 0 || - strcmp(session.curr_cmd, C_APPE) == 0 || - strcmp(session.curr_cmd, C_STOU) == 0) { - do_scan = TRUE; - } - } - - if (!do_scan) { - return close(fd); - } - - /* Make sure the data is written to disk, so that the fstat(2) picks - * up the size properly. - */ - if (fsync(fd) < 0) { - return -1; - } - - pr_fs_clear_cache(); - if (pr_fsio_fstat(fh, &st) < 0) { - return -1; - } - - if (close(fd) < 0) { - return -1; - } - - c = find_config(CURRENT_CONF, CONF_PARAM, "ClamAV", FALSE); - if (!c || !*(int*)(c->argv[0])) - return 0; - - /** - * Figure out the absolute path of our directory - */ - - if (session.chroot_path) { - abs_path = pdircat(fh->fh_pool, session.chroot_path, fh->fh_path, NULL); - } else { - abs_path = pstrdup(fh->fh_pool, fh->fh_path); - } - - rel_path = pstrdup(fh->fh_pool, fh->fh_path); - - pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION ": absolute path is '%s', relative path is '%s'", abs_path, rel_path); - - /** - * Handle min/max settings - */ - if ((minsize = (unsigned long *) get_param_ptr(CURRENT_CONF, "ClamMinSize", TRUE)) == 0UL) - clamd_minsize = 0; - else - clamd_minsize = *minsize; - - if ((maxsize = (unsigned long *) get_param_ptr(CURRENT_CONF, "ClamMaxSize", TRUE)) == 0UL) - clamd_maxsize = 0; - else - clamd_maxsize = *maxsize; - - if (clamd_minsize > 0 || clamd_maxsize > 0) { - /* Stat the file to acquire the size */ - pr_fs_clear_cache(); - if (pr_fsio_fstat(fh, &st) == -1) { - pr_log_pri(PR_LOG_ERR, MOD_CLAMAV_VERSION ": error: Can not stat file (%d): %s", errno, - strerror(errno)); - return -1; - } - pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION ": ClamMinSize=%lu ClamMaxSize=%lu Filesize=%" PR_LU, clamd_minsize, clamd_maxsize, (pr_off_t) st.st_size); - } - - if (clamd_minsize > 0) { - /* test the minimum size */ - if (st.st_size < clamd_minsize) { - pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION ": File is too small, skipping virus scan. min = %lu size = %" PR_LU, - clamd_minsize, (pr_off_t) st.st_size); - return 0; - } - } - - if (clamd_maxsize > 0) { - /* test the maximum size */ - if (st.st_size > clamd_maxsize) { - pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION ": File is too large, skipping virus scan. max = %lu size = %" PR_LU, - clamd_maxsize, (pr_off_t) st.st_size); - return 0; - } - } - - pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION - ": Going to virus scan absolute filename = '%s' with relative filename = '%s'.", abs_path, rel_path); - - clam_errno = 0; - if (clamavd_scan(clamd_sockd, abs_path, rel_path) > 0) { - errno = EPERM; - return -1; - } - - if (clam_errno == 0) - pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION ": No virus detected in filename = '%s'.", abs_path); - else - pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION ": Skipped virus scan due to errno = %d", clam_errno); - - return 0; + char *abs_path = NULL, *rel_path = NULL; + struct stat st; + int do_scan = FALSE, res; + config_rec *c = NULL; + unsigned long *minsize, *maxsize; + + /* We're only interested in STOR, APPE, and maybe STOU commands. */ + if (session.curr_cmd) { + if (strcmp(session.curr_cmd, C_STOR) == 0 || + strcmp(session.curr_cmd, C_APPE) == 0 || + strcmp(session.curr_cmd, C_STOU) == 0) { + do_scan = TRUE; + } + } + + if (!do_scan) { + return close(fd); + } + + /* Make sure the data is written to disk, so that the fstat(2) picks + * up the size properly. + */ + if (fsync(fd) < 0) { + int xerrno = errno; + + pr_trace_msg(trace_channel, 9, "fsync(2) error on fd %d (path '%s'): %s", + fd, fh->fh_path, strerror(xerrno)); + + errno = xerrno; + return -1; + } + + pr_fs_clear_cache(); + if (pr_fsio_fstat(fh, &st) < 0) { + int xerrno = errno; + + pr_trace_msg(trace_channel, 9, "pr_fsio_fstat() error on fd %d (path '%s'): %s", + fd, fh->fh_path, strerror(xerrno)); + + errno = xerrno; + return -1; + } + + if (close(fd) < 0) { + return -1; + } + + c = find_config(CURRENT_CONF, CONF_PARAM, "ClamAV", FALSE); + if (!c || !*(int *)(c->argv[0])) { + (void) pr_trace_msg("clamav", 8, "skipping ClamAV virus scan."); + + return 0; + } + + c = find_config(CURRENT_CONF, CONF_PARAM, "ClamFailsafe", FALSE); + if (!c || *(int *)(c->argv[0])) + remove_on_failure = 1; + else + remove_on_failure = 0; + + (void) pr_trace_msg("clamav", 8, "fail-safe mode is %s.", + (remove_on_failure ? "ON" : "OFF")); + + /** + * Figure out the absolute path of our directory. + */ + char buf[PR_TUNABLE_PATH_MAX + 1]; + getcwd(buf, PR_TUNABLE_PATH_MAX); + abs_path = fh->fh_path; + if (abs_path) { + (void) pr_trace_msg("clamav", 8, "vwd=%s fh_path=%s chroot=%s cwd=%s buf=%s", + pr_fs_getvwd(), abs_path, session.chroot_path, pr_fs_getcwd(), + buf); + if (strcmp(buf, pr_fs_getcwd()) != 0) { + if (strcmp(pr_fs_getcwd(), "/") != 0) { + char *pos = strstr(buf, pr_fs_getcwd()); + if (pos) { + *pos = 0; + } + } + + abs_path = pdircat(fh->fh_pool, buf, abs_path, NULL); + } else if (strcmp(buf, pr_fs_getcwd()) == 0 && session.chroot_path) + abs_path = pdircat(fh->fh_pool, session.chroot_path, abs_path, NULL); + } + rel_path = pstrdup(fh->fh_pool, fh->fh_path); + + (void) pr_trace_msg("clamav", 6, "absolute path is '%s' and relative path is '%s'.", abs_path, rel_path); + + /** + * Handle min/max settings + */ + if ((minsize = (unsigned long *) get_param_ptr(CURRENT_CONF, "ClamMinSize", TRUE)) == 0UL) + clamd_minsize = 0; + else + clamd_minsize = *minsize; + + if ((maxsize = (unsigned long *) get_param_ptr(CURRENT_CONF, "ClamMaxSize", TRUE)) == 0UL) + clamd_maxsize = 0; + else + clamd_maxsize = *maxsize; + + (void) pr_trace_msg("clamav", 6, "ClamMinSize=%" PR_LU " ClamMaxSize=%" PR_LU " Filesize=%" PR_LU, clamd_minsize, clamd_maxsize, (pr_off_t) st.st_size); + + if (clamd_minsize > 0) { + /* test the minimum size */ + if (st.st_size < clamd_minsize) { + pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION ": File is too small, skipping virus scan. min = %" PR_LU " size = %" PR_LU, + clamd_minsize, (pr_off_t) st.st_size); + return 0; + } + } + + if (clamd_maxsize > 0) { + /* test the maximum size */ + if (st.st_size > clamd_maxsize) { + pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION ": File is too large, skipping virus scan. max = %" PR_LU " size = %" PR_LU, + clamd_maxsize, (pr_off_t) st.st_size); + return 0; + } + } + + (void) pr_trace_msg("clamav", 1, "Going to virus scan absolute filename of '%s' and a relative filename of '%s'.", abs_path, rel_path); + + clam_errno = 0; + c = find_config(CURRENT_CONF, CONF_PARAM, "ClamStream", FALSE); + if (c && *(int *)(c->argv[0])) { + res = clamavd_scan_stream(clamd_sockd, abs_path, rel_path); + } else { + res = clamavd_scan(clamd_sockd, abs_path, rel_path); + } + if (res < 0) { + if (remove_on_failure) { + pr_log_debug(DEBUG4, + MOD_CLAMAV_VERSION ": removing failed upload of filename = '%s' with relative filename = '%s'.", abs_path, rel_path); + if (clam_errno && pr_fsio_unlink(rel_path)!=0) { + pr_log_pri(PR_LOG_ERR, + MOD_CLAMAV_VERSION ": notice : unlink() failed (%d): %s", + errno, strerror(errno)); + } + } + errno = EPERM; + return -1; + } + + if (clam_errno == 0) + pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION ": No virus detected in filename = '%s'.", abs_path); + else + pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION ": Skipped virus scan due to errno = %d", clam_errno); + + return 0; } /** * Parse string size description and return value. */ static unsigned long parse_nbytes(char *nbytes_str, char *units_str) { - long res; - unsigned long nbytes; - char *endp = NULL; - float units_factor = 0.0; - - /* clear any previous local errors */ - clam_errno = 0; - - /* first, check the given units to determine the correct multiplier - */ - if (!strcasecmp("Gb", units_str)) { - units_factor = 1024.0 * 1024.0 * 1024.0; - - } else if (!strcasecmp("Mb", units_str)) { - units_factor = 1024.0 * 1024.0; - - } else if (!strcasecmp("Kb", units_str)) { - units_factor = 1024.0; - - } else if (!strcasecmp("b", units_str)) { - units_factor = 1.0; - - } else { - clam_errno = EINVAL; - return 0; - } - - /* make sure a number was given */ - if (!isdigit((int) *nbytes_str)) { - clam_errno = EINVAL; - return 0; - } - - /* knowing the factor, now convert the given number string to a real - * number - */ - res = strtol(nbytes_str, &endp, 10); - - if (errno == ERANGE) { - clam_errno = ERANGE; - return 0; - } - - if (endp && *endp) { - clam_errno = EINVAL; - return 0; - } - - /* don't bother to apply the factor if that will cause the number to - * overflow - */ - if (res > (ULONG_MAX / units_factor)) { - clam_errno = ERANGE; - return 0; - } + long res; + unsigned long nbytes; + char *endp = NULL; + float units_factor = 0.0; + + /* clear any previous local errors */ + clam_errno = 0; + + /* first, check the given units to determine the correct multiplier + */ + if (!strcasecmp("Gb", units_str)) { + units_factor = 1024.0 * 1024.0 * 1024.0; + + } else if (!strcasecmp("Mb", units_str)) { + units_factor = 1024.0 * 1024.0; + + } else if (!strcasecmp("Kb", units_str)) { + units_factor = 1024.0; + + } else if (!strcasecmp("b", units_str)) { + units_factor = 1.0; + + } else { + clam_errno = EINVAL; + return 0; + } + + /* make sure a number was given */ + if (!isdigit((int) *nbytes_str)) { + clam_errno = EINVAL; + return 0; + } + + /* knowing the factor, now convert the given number string to a real + * number + */ + res = strtol(nbytes_str, &endp, 10); + + if (errno == ERANGE) { + clam_errno = ERANGE; + return 0; + } + + if (endp && *endp) { + clam_errno = EINVAL; + return 0; + } + + /* don't bother to apply the factor if that will cause the number to + * overflow + */ + if (res > (ULONG_MAX / units_factor)) { + clam_errno = ERANGE; + return 0; + } - nbytes = (unsigned long) res * units_factor; - return nbytes; + nbytes = (unsigned long) res * units_factor; + return nbytes; } /** * Configuration setter: ClamAV */ MODRET set_clamav(cmd_rec *cmd) { - int bool = -1; - config_rec *c = NULL; - - CHECK_ARGS(cmd, 1); - CHECK_CONF(cmd, CONF_ROOT|CONF_LIMIT|CONF_VIRTUAL|CONF_GLOBAL|CONF_DIR); - - if ((bool = get_boolean(cmd,1)) == -1) - CONF_ERROR(cmd, "expected Boolean parameter"); - - c = add_config_param(cmd->argv[0], 1, NULL); - c->argv[0] = pcalloc(c->pool, sizeof(unsigned char)); - *((unsigned char *) c->argv[0]) = bool; - c->flags |= CF_MERGEDOWN; - - return PR_HANDLED(cmd); + int bool = -1; + config_rec *c = NULL; + + CHECK_ARGS(cmd, 1); + CHECK_CONF(cmd, CONF_ROOT|CONF_LIMIT|CONF_VIRTUAL|CONF_GLOBAL|CONF_DIR); + + if ((bool = get_boolean(cmd,1)) == -1) + CONF_ERROR(cmd, "expected Boolean parameter"); + + c = add_config_param(cmd->argv[0], 1, NULL); + c->argv[0] = pcalloc(c->pool, sizeof(unsigned char)); + *((unsigned char *) c->argv[0]) = bool; + c->flags |= CF_MERGEDOWN; + + return PR_HANDLED(cmd); +} + +/** + * Configuration setter: ClamStream + */ +MODRET set_clamstream(cmd_rec *cmd) { + int bool = -1; + config_rec *c = NULL; + + CHECK_ARGS(cmd, 1); + CHECK_CONF(cmd, CONF_ROOT|CONF_LIMIT|CONF_VIRTUAL|CONF_GLOBAL|CONF_DIR); + + if ((bool = get_boolean(cmd,1)) == -1) + CONF_ERROR(cmd, "expected Boolean parameter"); + + c = add_config_param(cmd->argv[0], 1, NULL); + c->argv[0] = pcalloc(c->pool, sizeof(unsigned char)); + *((unsigned char *) c->argv[0]) = bool; + c->flags |= CF_MERGEDOWN; + + return PR_HANDLED(cmd); +} + +/** + * Configuration setter: ClamFailsafe + */ +MODRET set_clamfailsafe(cmd_rec *cmd) { + int bool = -1; + config_rec *c = NULL; + + CHECK_ARGS(cmd, 1); + CHECK_CONF(cmd, CONF_ROOT|CONF_LIMIT|CONF_VIRTUAL|CONF_GLOBAL|CONF_DIR); + + if ((bool = get_boolean(cmd, 1)) == -1) + CONF_ERROR(cmd, "expected Boolean parameter"); + + c = add_config_param(cmd->argv[0], 1, NULL); + c->argv[0] = pcalloc(c->pool, sizeof(unsigned char)); + *((unsigned char *) c->argv[0]) = bool; + c->flags |= CF_MERGEDOWN; + + return PR_HANDLED(cmd); } /** * Configuration setter: ClamLocalSocket */ MODRET set_clamavd_local_socket(cmd_rec *cmd) { - config_rec *c = NULL; - - CHECK_ARGS(cmd, 1); - CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_DIR); - - c = add_config_param_str("ClamLocalSocket", 1, (void *) cmd->argv[1]); - c->flags |= CF_MERGEDOWN; - - return PR_HANDLED(cmd); + config_rec *c = NULL; + + CHECK_ARGS(cmd, 1); + CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_DIR); + + c = add_config_param_str("ClamLocalSocket", 1, (void *) cmd->argv[1]); + c->flags |= CF_MERGEDOWN; + + return PR_HANDLED(cmd); } /** * Configuration setter: ClamServer */ MODRET set_clamavd_server(cmd_rec *cmd) { - config_rec *c = NULL; - - CHECK_ARGS(cmd, 1); - CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_DIR); - - c = add_config_param_str("ClamServer", 1, (void *) cmd->argv[1]); - c->flags |= CF_MERGEDOWN; - - return PR_HANDLED(cmd); + config_rec *c = NULL; + + CHECK_ARGS(cmd, 1); + CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_DIR); + + c = add_config_param_str("ClamServer", 1, (void *) cmd->argv[1]); + c->flags |= CF_MERGEDOWN; + + return PR_HANDLED(cmd); } /** * Configuration setter: ClamPort */ MODRET set_clamavd_port(cmd_rec *cmd) { - config_rec *c = NULL; - - CHECK_ARGS(cmd, 1); - CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_DIR); - - c = add_config_param(cmd->argv[0], 1, NULL); - c->argv[0] = pcalloc(c->pool, sizeof(int)); - *((int *) c->argv[0]) = (int) atol(cmd->argv[1]); - c->flags |= CF_MERGEDOWN; - - return PR_HANDLED(cmd); + config_rec *c = NULL; + + CHECK_ARGS(cmd, 1); + CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_DIR); + + c = add_config_param(cmd->argv[0], 1, NULL); + c->argv[0] = pcalloc(c->pool, sizeof(int)); + *((int *) c->argv[0]) = (int) atol(cmd->argv[1]); + c->flags |= CF_MERGEDOWN; + + return PR_HANDLED(cmd); } /** * Configuration setter: ClamMinSize */ MODRET set_clamavd_minsize(cmd_rec *cmd) { - config_rec *c = NULL; - unsigned long nbytes = 0; - - CHECK_ARGS(cmd, 2); - CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_DIR); - - if ((nbytes = parse_nbytes(cmd->argv[1], cmd->argv[2])) == 0) { - char ulong_max[80] = {'\0'}; - sprintf(ulong_max, "%lu", (unsigned long) ULONG_MAX); - - if (clam_errno == EINVAL) - CONF_ERROR(cmd, "invalid parameters"); - - if (clam_errno == ERANGE) - CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, - "number of bytes must be between 0 and ", ulong_max, NULL)); - } - - c = add_config_param(cmd->argv[0], 1, NULL); - c->argv[0] = pcalloc(c->pool, sizeof(unsigned long)); - *((unsigned long *) c->argv[0]) = nbytes; - c->flags |= CF_MERGEDOWN; - - return PR_HANDLED(cmd); + config_rec *c = NULL; + unsigned long nbytes = 0; + + CHECK_ARGS(cmd, 2); + CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_DIR); + + if ((nbytes = parse_nbytes(cmd->argv[1], cmd->argv[2])) == 0) { + char ulong_max[80] = {'\0'}; + sprintf(ulong_max, "%lu", (unsigned long) ULONG_MAX); + + if (clam_errno == EINVAL) + CONF_ERROR(cmd, "invalid parameters"); + + if (clam_errno == ERANGE) + CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, + "number of bytes must be between 0 and ", + ulong_max, NULL)); + } + + c = add_config_param(cmd->argv[0], 1, NULL); + c->argv[0] = pcalloc(c->pool, sizeof(unsigned long)); + *((unsigned long *) c->argv[0]) = nbytes; + c->flags |= CF_MERGEDOWN; + + return PR_HANDLED(cmd); } /** * Configuration setter: ClamMaxSize */ MODRET set_clamavd_maxsize(cmd_rec *cmd) { - config_rec *c = NULL; - unsigned long nbytes = 0; - - CHECK_ARGS(cmd, 2); - CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_DIR); - - if ((nbytes = parse_nbytes(cmd->argv[1], cmd->argv[2])) == 0) { - char ulong_max[80] = {'\0'}; - sprintf(ulong_max, "%lu", (unsigned long) ULONG_MAX); - - if (clam_errno == EINVAL) - CONF_ERROR(cmd, "invalid parameters"); - - if (clam_errno == ERANGE) - CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, - "number of bytes must be between 0 and ", ulong_max, NULL)); - } - - c = add_config_param(cmd->argv[0], 1, NULL); - c->argv[0] = pcalloc(c->pool, sizeof(unsigned long)); - *((unsigned long *) c->argv[0]) = nbytes; - c->flags |= CF_MERGEDOWN; - - return PR_HANDLED(cmd); + config_rec *c = NULL; + unsigned long nbytes = 0; + + CHECK_ARGS(cmd, 2); + CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_DIR); + + if ((nbytes = parse_nbytes(cmd->argv[1], cmd->argv[2])) == 0) { + char ulong_max[80] = {'\0'}; + sprintf(ulong_max, "%lu", (unsigned long) ULONG_MAX); + + if (clam_errno == EINVAL) + CONF_ERROR(cmd, "invalid parameters"); + + if (clam_errno == ERANGE) + CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, + "number of bytes must be between 0 and ", + ulong_max, NULL)); + } + + c = add_config_param(cmd->argv[0], 1, NULL); + c->argv[0] = pcalloc(c->pool, sizeof(unsigned long)); + *((unsigned long *) c->argv[0]) = nbytes; + c->flags |= CF_MERGEDOWN; + + return PR_HANDLED(cmd); } /** * End FTP Session */ static void clamav_shutdown(const void *event_data, void *user_data) { - if (clamd_sockd != -1) { - clamavd_session_stop(clamd_sockd); - close(clamd_sockd); - clamd_sockd = -1; - pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION ": debug: disconnected from Clamd"); - } + if (clamd_sockd != -1) { + close(clamd_sockd); + clamd_sockd = -1; + pr_log_debug(DEBUG4, MOD_CLAMAV_VERSION ": debug: disconnected from Clamd"); + } } /** * Start FTP Session */ static int clamav_sess_init(void) { - pr_fs_t *fs; + pr_fs_t *fs; + + is_remote = 0; clamd_sockd = -1; + + pr_event_register(&clamav_module, "core.exit", clamav_shutdown, NULL); - is_remote = 0; clamd_sockd = -1; - - pr_event_register(&clamav_module, "core.exit", clamav_shutdown, NULL); - - fs = pr_register_fs(main_server->pool, "clamav", "/"); - if (fs) { - fs->close = clamav_fsio_close; - } + fs = pr_register_fs(session.pool, "clamav", "/"); + if (fs) { + fs->close = clamav_fsio_close; + } - return 0; + return 0; } static conftable clamav_conftab[] = { - { "ClamAV", set_clamav, NULL }, - { "ClamLocalSocket", set_clamavd_local_socket, NULL }, - { "ClamServer", set_clamavd_server, NULL }, - { "ClamPort", set_clamavd_port, NULL }, - { "ClamMinSize", set_clamavd_minsize, NULL }, - { "ClamMaxSize", set_clamavd_maxsize, NULL }, - { NULL } + { "ClamAV", set_clamav, NULL }, + { "ClamFailsafe", set_clamfailsafe, NULL }, + { "ClamLocalSocket", set_clamavd_local_socket, NULL }, + { "ClamServer", set_clamavd_server, NULL }, + { "ClamPort", set_clamavd_port, NULL }, + { "ClamMinSize", set_clamavd_minsize, NULL }, + { "ClamMaxSize", set_clamavd_maxsize, NULL }, + { "ClamStream", set_clamstream, NULL }, + { NULL } }; module clamav_module = { - NULL, - NULL, - 0x20, /* api ver */ - "clamav", - clamav_conftab, - NULL, - NULL, /* auth function table */ - NULL, /* init function */ - clamav_sess_init, /* session init function */ - MOD_CLAMAV_VERSION + NULL, /* Always NULL */ + NULL, /* Always NULL */ + 0x20, /* module api version */ + "clamav", /* module name */ + clamav_conftab, /* module configuration handler table */ + NULL, /* module command handler table */ + NULL, /* module authentication handler table */ + NULL, /* module initialization */ + clamav_sess_init, /* session initialization */ + MOD_CLAMAV_VERSION /* module version */ }; Index: proftpd-mod-clamav-0.10/mod_clamav.h =================================================================== --- proftpd-mod-clamav-0.10.orig/mod_clamav.h +++ proftpd-mod-clamav-0.10/mod_clamav.h @@ -1,6 +1,6 @@ /* * mod_clamav - ClamAV virus scanning module for ProFTPD - * Copyright (c) 2005-2008, Joseph Benden + * Copyright (c) 2005-2013, Joseph Benden * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,24 +21,22 @@ * source code for ClamAV in the source distribution. * * ClamAV is available at http://www.clamav.net/ - * + * * Thanks to TJ Saunders for his helpful comments and suggestions! * * DO NOT EDIT THE LINE BELOW */ - + #if !defined(MOD_CLAMAV_H) #define MOD_CLAMAV_H /** * Function declarations */ -int clamavd_result(int sockd, const char *abs_filename, const char *rel_filename); -int clamavd_session_start(int sockd); -int clamavd_session_stop(int sockd); -int clamavd_connect_check(int sockd); -int clamavd_scan(int sockd, const char *abs_filename, const char *rel_filename); -int clamavd_connect(void); +static int clamavd_result(int sockd, const char *abs_filename, const char *rel_filename); +static int clamavd_connect_check(int sockd); +static int clamavd_scan(int sockd, const char *abs_filename, const char *rel_filename); +static int clamavd_connect(void); int clamav_scan(cmd_rec *cmd); #endif