/*
 *  Copyright (C) 2015 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
 *  Copyright (C) 2009 Sourcefire, Inc.
 *
 *  Authors: Tomasz Kojm, aCaB, Mickey Sola
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  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
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 *  MA 02110-1301, USA.
 */

#if HAVE_CONFIG_H
#include "clamav-config.h"
#endif

#if defined(C_SOLARIS)
#ifndef __EXTENSIONS__
#define __EXTENSIONS__
#endif
#endif

/* must be first because it may define _XOPEN_SOURCE */
#include "shared/fdpassing.h"
#include <stdio.h>
#include <curl/curl.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>
#ifdef HAVE_SYS_SELECT_H
#include <sys/select.h>
#endif
#ifndef _WIN32
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netdb.h>
#endif

#include "libclamav/clamav.h"
#include "shared/actions.h"
#include "shared/output.h"
#include "shared/misc.h"

#include "communication.h"
#include "protocol.h"
#include "client.h"

static const char *scancmd[] = {"CONTSCAN", "MULTISCAN", "INSTREAM", "FILDES", "ALLMATCHSCAN"};

/* Issues an INSTREAM command to clamd and streams the given file
 * Returns >0 on success, 0 soft fail, -1 hard fail */
static int onas_send_stream(CURL *curl, const char *filename, int fd, int64_t timeout, uint64_t maxstream)
{
    uint32_t buf[BUFSIZ / sizeof(uint32_t)];
    uint64_t len;
    uint64_t todo  = maxstream;
    int ret        = 1;
    int close_flag = 0;

    if (0 == fd) {
        if (filename) {
            if ((fd = safe_open(filename, O_RDONLY | O_BINARY)) < 0) {
                logg("~%s: Access denied. ERROR\n", filename);
                return 0;
            }
            //logg("DEBUG: >>>>> fd is %d\n", fd);
            close_flag = 1;
        } else {
            fd = 0;
        }
    }

    if (onas_sendln(curl, "zINSTREAM", 10, timeout)) {
        ret = -1;
        goto strm_out;
    }

    while ((len = read(fd, &buf[1], sizeof(buf) - sizeof(uint32_t))) > 0) {
        if ((uint64_t)len > todo) len = todo;
        buf[0] = htonl(len);
        if (onas_sendln(curl, (const char *)buf, len + sizeof(uint32_t), timeout)) {
            ret = -1;
            goto strm_out;
        }
        todo -= len;
        if (!todo) {
            len = 0;
            break;
        }
    }

    if (len) {
        logg("!Failed to read from %s.\n", filename ? filename : "STDIN");
        ret = 0;
        goto strm_out;
    }
    *buf = 0;
    onas_sendln(curl, (const char *)buf, 4, timeout);

strm_out:
    if (close_flag) {
        //logg("DEBUG: >>>>> closed fd %d\n", fd);
        close(fd);
    }
    return ret;
}

#ifdef HAVE_FD_PASSING
/* Issues a FILDES command and pass a FD to clamd
 * Returns >0 on success, 0 soft fail, -1 hard fail */
static int onas_send_fdpass(CURL *curl, const char *filename, int fd, int64_t timeout)
{
    CURLcode result;
    struct iovec iov[1];
    struct msghdr msg;
    struct cmsghdr *cmsg;
    unsigned char fdbuf[CMSG_SPACE(sizeof(int))];
    char dummy[]   = "";
    int ret        = 1;
    int close_flag = 0;

    if (0 == fd) {
        if (filename) {
            if ((fd = open(filename, O_RDONLY)) < 0) {
                logg("~%s: Access denied. ERROR\n", filename);
                return 0;
            }
            close_flag = 1;
        } else {
            fd = 0;
        }
    }
    if (result = onas_sendln(curl, "zFILDES", 8, timeout)) {
        logg("*ClamProto: error sending w/ curl, %s\n", curl_easy_strerror(result));
        ret = -1;
        goto fd_out;
    }

    iov[0].iov_base = dummy;
    iov[0].iov_len  = 1;
    memset(&msg, 0, sizeof(msg));
    msg.msg_control         = fdbuf;
    msg.msg_iov             = iov;
    msg.msg_iovlen          = 1;
    msg.msg_controllen      = CMSG_LEN(sizeof(int));
    cmsg                    = CMSG_FIRSTHDR(&msg);
    cmsg->cmsg_len          = CMSG_LEN(sizeof(int));
    cmsg->cmsg_level        = SOL_SOCKET;
    cmsg->cmsg_type         = SCM_RIGHTS;
    *(int *)CMSG_DATA(cmsg) = fd;
    if (onas_sendln(curl, &msg, 0, timeout) == -1) {
        logg("!FD send failed: %s\n", strerror(errno));
        ret = -1;
        goto fd_out;
    }
fd_out:
    if (close_flag) {
        close(fd);
    }
    return ret;
}
#endif

/* Sends a proper scan request to clamd and parses its replies
 * This is used only in non IDSESSION mode
 * Returns the number of infected files or -1 on error
 * NOTE: filename may be NULL for STREAM scantype. */
int onas_dsresult(CURL *curl, int scantype, uint64_t maxstream, const char *filename, int fd, int64_t timeout, int *printok, int *errors, cl_error_t *ret_code)
{
    int infected = 0, len = 0, beenthere = 0;
    char *bol, *eol;
    struct RCVLN rcv;
    STATBUF sb;

    onas_recvlninit(&rcv, curl);

    if (ret_code) {
        *ret_code = CL_SUCCESS;
    }

    switch (scantype) {
        case MULTI:
        case CONT:
        case ALLMATCH:
            if (!filename) {
                logg("Filename cannot be NULL for MULTISCAN or CONTSCAN.\n");
                if (ret_code) {
                    *ret_code = CL_ENULLARG;
                }
                return -1;
            }
            len = strlen(filename) + strlen(scancmd[scantype]) + 3;
            if (!(bol = malloc(len))) {
                logg("!Cannot allocate a command buffer: %s\n", strerror(errno));
                if (ret_code) {
                    *ret_code = CL_EMEM;
                }
                return -1;
            }
            sprintf(bol, "z%s %s", scancmd[scantype], filename);
            if (onas_sendln(curl, bol, len, timeout)) {
                if (ret_code) {
                    *ret_code = CL_EWRITE;
                }
                free(bol);
                return -1;
            }
            free(bol);
            break;

        case STREAM:
            /* NULL filename safe in send_stream() */
            len = onas_send_stream(curl, filename, fd, timeout, maxstream);
            break;
#ifdef HAVE_FD_PASSING
        case FILDES:
            /* NULL filename safe in send_fdpass() */
            len = onas_send_fdpass(curl, filename, fd, timeout);
            break;
#endif
    }

    if (len <= 0) {
        *printok = 0;
        if (errors)
            (*errors)++;
        return len;
    }

    while ((len = onas_recvln(&rcv, &bol, &eol, timeout))) {
        if (len == -1) {
            if (ret_code) {
                *ret_code = CL_EREAD;
            }
            return -1;
        }
        beenthere = 1;
        if (!filename) {
            logg("~%s\n", bol);
        }
        if (len > 7) {
            char *colon = strrchr(bol, ':');

            if (colon && colon[1] != ' ') {
                char *br;
                *colon = 0;

                br = strrchr(bol, '(');
                if (br) {
                    *br = 0;
                }
                colon = strrchr(bol, ':');
            }

            if (!colon) {
                char *unkco = "UNKNOWN COMMAND";
                if (!strncmp(bol, unkco, sizeof(unkco) - 1)) {
                    logg("*clamd replied \"UNKNOWN COMMAND\". Command was %s\n",
                         (scantype < 0 || scantype > MAX_SCANTYPE) ? "unidentified" : scancmd[scantype]);
                } else {
                    logg("*Failed to parse reply: \"%s\"\n", bol);
                }

                if (ret_code) {
                    *ret_code = CL_EPARSE;
                }
                return -1;

            } else if (!memcmp(eol - 7, " FOUND", 6)) {
                static char last_filename[PATH_MAX + 1] = {'\0'};
                *(eol - 7)                              = 0;
                *printok                                = 0;

                if (scantype != ALLMATCH) {
                    infected++;
                } else {
                    if (filename != NULL && strcmp(filename, last_filename)) {
                        infected++;
                        strncpy(last_filename, filename, PATH_MAX);
                        last_filename[PATH_MAX] = '\0';
                    }
                }

                if (filename) {
                    if (scantype >= STREAM) {
                        logg("~%s%s FOUND\n", filename, colon);
                        if (action) {
                            action(filename);
                        }
                    } else {
                        logg("~%s FOUND\n", bol);
                        *colon = '\0';
                        if (action) {
                            action(bol);
                        }
                    }
                }

                if (ret_code) {
                    *ret_code = CL_VIRUS;
                }

            } else if (len > 49 && !memcmp(eol - 50, " lstat() failed: No such file or directory. ERROR", 49)) {
                if (errors) {
                    (*errors)++;
                }
                *printok = 0;

                if (filename) {
                    (scantype >= STREAM) ? logg("*%s%s\n", filename, colon) : logg("*%s\n", bol);
                }

                if (ret_code) {
                    *ret_code = CL_ESTAT;
                }
            } else if (len > 41 && !memcmp(eol - 42, " lstat() failed: Permission denied. ERROR", 41)) {
                if (errors) {
                    (*errors)++;
                }
                *printok = 0;

                if (filename) {
                    (scantype >= STREAM) ? logg("*%s%s\n", filename, colon) : logg("*%s\n", bol);
                }

                if (ret_code) {
                    *ret_code = CL_ESTAT;
                }
            } else if (len > 21 && !memcmp(eol - 22, " Access denied. ERROR", 21)) {
                if (errors) {
                    (*errors)++;
                }
                *printok = 0;

                if (filename) {
                    (scantype >= STREAM) ? logg("*%s%s\n", filename, colon) : logg("*%s\n", bol);
                }

                if (ret_code) {
                    *ret_code = CL_EACCES;
                }
            } else if (!memcmp(eol - 7, " ERROR", 6)) {
                if (errors) {
                    (*errors)++;
                }
                *printok = 0;

                if (filename) {
                    (scantype >= STREAM) ? logg("~%s%s\n", filename, colon) : logg("~%s\n", bol);
                }

                if (ret_code) {
                    *ret_code = CL_ESTATE;
                }
            }
        }
    }
    if (!beenthere) {
        if (!filename) {
            logg("STDIN: noreply from clamd\n.");
            if (ret_code) {
                *ret_code = CL_EACCES;
            }
            return -1;
        }
        if (CLAMSTAT(filename, &sb) == -1) {
            logg("~%s: stat() failed with %s, clamd may not be responding\n",
                 filename, strerror(errno));
            if (ret_code) {
                *ret_code = CL_EACCES;
            }
            return -1;
        }
        if (!S_ISDIR(sb.st_mode)) {
            logg("~%s: no reply from clamd\n", filename);
            if (ret_code) {
                *ret_code = CL_EACCES;
            }
            return -1;
        }
    }
    return infected;
}