/* Copyright (C) 2007-2012 Open Information Security Foundation
 *
 * You can copy, redistribute or modify this Program 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
 * version 2 along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

/**
 * \file
 *
 * \author Victor Julien <victor@inliniac.net>
 * \author Pablo Rincon <pablo.rincon.crespo@gmail.com>
 *
 */

#include "suricata-common.h"
#include "suricata.h"
#include "debug.h"
#include "flow.h"
#include "stream.h"
#include "util-hash.h"
#include "util-debug.h"
#include "util-memcmp.h"
#include "util-print.h"
#include "app-layer-parser.h"
#include "util-validate.h"

/** \brief switch to force magic checks on all files
 *         regardless of the rules.
 */
static int g_file_force_magic = 0;

/** \brief switch to force md5 calculation on all files
 *         regardless of the rules.
 */
static int g_file_force_md5 = 0;

/** \brief switch to force tracking off all files
 *         regardless of the rules.
 */
static int g_file_force_tracking = 0;

/* prototypes */
static void FileFree(File *);
static void FileDataFree(FileData *);

void FileForceMagicEnable(void)
{
    g_file_force_magic = 1;
}

void FileForceMd5Enable(void)
{
    g_file_force_md5 = 1;
}

int FileForceMagic(void)
{
    return g_file_force_magic;
}

int FileForceMd5(void)
{
    return g_file_force_md5;
}

void FileForceTrackingEnable(void)
{
    g_file_force_tracking = 1;
}

int FileMagicSize(void)
{
    /** \todo make this size configurable */
    return 512;
}

static int FileAppendFileDataFilePtr(File *ff, FileData *ffd)
{
    SCEnter();

    if (ff == NULL) {
        SCReturnInt(-1);
    }

    if (ff->chunks_tail == NULL) {
        ff->chunks_head = ffd;
        ff->chunks_tail = ffd;
    } else {
        ff->chunks_tail->next = ffd;
        ff->chunks_tail = ffd;
    }

#ifdef DEBUG
    ff->chunks_cnt++;
    if (ff->chunks_cnt > ff->chunks_cnt_max)
        ff->chunks_cnt_max = ff->chunks_cnt;
#endif

#ifdef HAVE_NSS
    if (ff->md5_ctx)
        HASH_Update(ff->md5_ctx, ffd->data, ffd->len);
#endif
    SCReturnInt(0);
}

static int FileAppendFileData(FileContainer *ffc, FileData *ffd)
{
    SCEnter();

    if (ffc == NULL) {
        SCReturnInt(-1);
    }

    if (FileAppendFileDataFilePtr(ffc->tail, ffd) == -1)
    {
        SCReturnInt(-1);
    }

    SCReturnInt(0);
}



static void FilePruneFile(File *file)
{
    SCEnter();

    SCLogDebug("file %p, file->chunks_cnt %"PRIu64, file, file->chunks_cnt);

    if (!(file->flags & FILE_NOMAGIC)) {
        /* need magic but haven't set it yet, bail out */
        if (file->magic == NULL)
            SCReturn;
        else
            SCLogDebug("file->magic %s", file->magic);
    } else {
        SCLogDebug("file->flags & FILE_NOMAGIC == true");
    }

    /* okay, we now know we can prune */
    FileData *fd = file->chunks_head;

    while (fd != NULL) {
        SCLogDebug("fd %p", fd);

        if (file->flags & FILE_NOSTORE || fd->stored == 1) {
            file->chunks_head = fd->next;
            if (file->chunks_tail == fd)
                file->chunks_tail = fd->next;

            FileDataFree(fd);

            fd = file->chunks_head;
#ifdef DEBUG
            file->chunks_cnt--;
            SCLogDebug("file->chunks_cnt %"PRIu64, file->chunks_cnt);
#endif
        } else if (fd->stored == 0) {
            fd = NULL;
            break;
        }
    }

    SCReturn;
}

void FilePrune(FileContainer *ffc)
{
    File *file;

    for (file = ffc->head; file != NULL; file = file->next) {
        FilePruneFile(file);
    }
}

/**
 *  \brief allocate a FileContainer
 *
 *  \retval new newly allocated FileContainer
 *  \retval NULL error
 */
FileContainer *FileContainerAlloc(void)
{
    FileContainer *new = SCMalloc(sizeof(FileContainer));
    if (unlikely(new == NULL)) {
        SCLogError(SC_ERR_MEM_ALLOC, "Error allocating mem");
        return NULL;
    }
    memset(new, 0, sizeof(FileContainer));
    new->head = new->tail = NULL;
    return new;
}

/**
 *  \brief Recycle a FileContainer
 *
 *  \param ffc FileContainer
 */
void FileContainerRecycle(FileContainer *ffc)
{
    if (ffc == NULL)
        return;

    File *cur = ffc->head;
    File *next = NULL;
    for (;cur != NULL; cur = next) {
        next = cur->next;
        FileFree(cur);
    }
    ffc->head = ffc->tail = NULL;
}

/**
 *  \brief Free a FileContainer
 *
 *  \param ffc FileContainer
 */
void FileContainerFree(FileContainer *ffc)
{
    if (ffc == NULL)
        return;

    File *ptr = ffc->head;
    File *next = NULL;
    for (;ptr != NULL; ptr = next) {
        next = ptr->next;
        FileFree(ptr);
    }
    ffc->head = ffc->tail = NULL;
    SCFree(ffc);
}

/**
 *  \internal
 *
 *  \brief allocate a FileData chunk and set it up
 *
 *  \param data data chunk to store in the FileData
 *  \param data_len lenght of the data
 *
 *  \retval new FileData object
 */
static FileData *FileDataAlloc(uint8_t *data, uint32_t data_len)
{
    FileData *new = SCMalloc(sizeof(FileData));
    if (unlikely(new == NULL)) {
        return NULL;
    }
    memset(new, 0, sizeof(FileData));

    new->data = SCMalloc(data_len);
    if (new->data == NULL) {
        SCFree(new);
        return NULL;
    }

    new->len = data_len;
    memcpy(new->data, data, data_len);

    new->next = NULL;
    return new;
}

/**
 *  \internal
 *
 *  \brief free a FileData object
 *
 *  \param ffd the flow file data object to free
 */
static void FileDataFree(FileData *ffd)
{
    if (ffd == NULL)
        return;

    if (ffd->data != NULL) {
        SCFree(ffd->data);
    }

    SCFree(ffd);
}

/**
 *  \brief Alloc a new File
 *
 *  \param name character array containing the name (not a string)
 *  \param name_len length in bytes of the name
 *
 *  \retval new File object or NULL on error
 */
static File *FileAlloc(uint8_t *name, uint16_t name_len)
{
    File *new = SCMalloc(sizeof(File));
    if (unlikely(new == NULL)) {
        SCLogError(SC_ERR_MEM_ALLOC, "Error allocating mem");
        return NULL;
    }
    memset(new, 0, sizeof(File));

    new->name = SCMalloc(name_len);
    if (new->name == NULL) {
        SCFree(new);
        return NULL;
    }

    new->name_len = name_len;
    memcpy(new->name, name, name_len);

    return new;
}

static void FileFree(File *ff)
{
    if (ff == NULL)
        return;

    if (ff->name != NULL)
        SCFree(ff->name);

    /* magic returned by libmagic is strdup'd by MagicLookup. */
    if (ff->magic != NULL)
        SCFree(ff->magic);

    if (ff->chunks_head != NULL) {
        FileData *ffd = ff->chunks_head;

        while (ffd != NULL) {
            FileData *next_ffd = ffd->next;
            FileDataFree(ffd);
            ffd = next_ffd;
        }
    }

#ifdef HAVE_NSS
    if (ff->md5_ctx)
        HASH_Destroy(ff->md5_ctx);
#endif
    SCLogDebug("ff chunks_cnt %"PRIu64", chunks_cnt_max %"PRIu64,
            ff->chunks_cnt, ff->chunks_cnt_max);
    SCFree(ff);
}

void FileContainerAdd(FileContainer *ffc, File *ff)
{
    if (ffc->head == NULL || ffc->tail == NULL) {
        ffc->head = ffc->tail = ff;
    } else {
        ffc->tail->next = ff;
        ffc->tail = ff;
    }
}

/**
 *  \brief Tag a file for storing
 *
 *  \param ff The file to store
 */
int FileStore(File *ff)
{
    ff->flags |= FILE_STORE;
    SCReturnInt(0);
}

/**
 *  \brief Set the TX id for a file
 *
 *  \param ff The file to store
 *  \param txid the tx id
 */
int FileSetTx(File *ff, uint64_t txid)
{
    SCLogDebug("ff %p txid %"PRIu64, ff, txid);
    if (ff != NULL)
        ff->txid = txid;
    SCReturnInt(0);
}

/**
 *  \brief check if we have stored enough
 *
 *  \param ff file
 *
 *  \retval 0 limit not reached yet
 *  \retval 1 limit reached
 */
static int FileStoreNoStoreCheck(File *ff)
{
    SCEnter();

    if (ff == NULL) {
        SCReturnInt(0);
    }

    if (ff->flags & FILE_NOSTORE) {
        if (ff->state == FILE_STATE_OPENED &&
                ff->size >= (uint64_t)FileMagicSize())
        {
            SCReturnInt(1);
        }
    }

    SCReturnInt(0);
}

/**
 *  \brief Store a chunk of file data in the flow. The open "flowfile"
 *         will be used.
 *
 *  \param ffc the container
 *  \param data data chunk
 *  \param data_len data chunk len
 *
 *  \retval  0 ok
 *  \retval -1 error
 *  \retval -2 no store for this file
 */
int FileAppendData(FileContainer *ffc, uint8_t *data, uint32_t data_len)
{
    SCEnter();

    if (ffc == NULL || ffc->tail == NULL || data == NULL || data_len == 0) {
        SCReturnInt(-1);
    }

    if (ffc->tail->state != FILE_STATE_OPENED) {
        if (ffc->tail->flags & FILE_NOSTORE) {
            SCReturnInt(-2);
        }
        SCReturnInt(-1);
    }

    ffc->tail->size += data_len;
    SCLogDebug("file size is now %"PRIu64, ffc->tail->size);

    if (FileStoreNoStoreCheck(ffc->tail) == 1) {
#ifdef HAVE_NSS
        /* no storage but forced md5 */
        if (ffc->tail->md5_ctx) {
            if (ffc->tail->md5_ctx)
                HASH_Update(ffc->tail->md5_ctx, data, data_len);

            SCReturnInt(0);
        }
#endif
        if (g_file_force_tracking || (!(ffc->tail->flags & FILE_NOTRACK)))
            SCReturnInt(0);

        ffc->tail->state = FILE_STATE_TRUNCATED;
        SCLogDebug("flowfile state transitioned to FILE_STATE_TRUNCATED");
        SCReturnInt(-2);
    }

    SCLogDebug("appending %"PRIu32" bytes", data_len);

    FileData *ffd = FileDataAlloc(data, data_len);
    if (ffd == NULL) {
        ffc->tail->state = FILE_STATE_ERROR;
        SCReturnInt(-1);
    }

    /* append the data */
    if (FileAppendFileData(ffc, ffd) < 0) {
        ffc->tail->state = FILE_STATE_ERROR;
        FileDataFree(ffd);
        SCReturnInt(-1);
    }
    SCReturnInt(0);
}

/**
 *  \brief Open a new File
 *
 *  \param ffc flow container
 *  \param name filename character array
 *  \param name_len filename len
 *  \param data initial data
 *  \param data_len initial data len
 *  \param flags open flags
 *
 *  \retval ff flowfile object
 *
 *  \note filename is not a string, so it's not nul terminated.
 */
File *FileOpenFile(FileContainer *ffc, uint8_t *name,
        uint16_t name_len, uint8_t *data, uint32_t data_len, uint8_t flags)
{
    SCEnter();

    //PrintRawDataFp(stdout, name, name_len);

    File *ff = FileAlloc(name, name_len);
    if (ff == NULL) {
        SCReturnPtr(NULL, "File");
    }

    if (flags & FILE_STORE) {
        ff->flags |= FILE_STORE;
    } else if (flags & FILE_NOSTORE) {
        SCLogDebug("not storing this file");
        ff->flags |= FILE_NOSTORE;
    }
    if (flags & FILE_NOMAGIC) {
        SCLogDebug("not doing magic for this file");
        ff->flags |= FILE_NOMAGIC;
    }
    if (flags & FILE_NOMD5) {
        SCLogDebug("not doing md5 for this file");
        ff->flags |= FILE_NOMD5;
    }

#ifdef HAVE_NSS
    if (!(ff->flags & FILE_NOMD5) || g_file_force_md5) {
        ff->md5_ctx = HASH_Create(HASH_AlgMD5);
        if (ff->md5_ctx != NULL) {
            HASH_Begin(ff->md5_ctx);
        }
    }
#endif

    ff->state = FILE_STATE_OPENED;
    SCLogDebug("flowfile state transitioned to FILE_STATE_OPENED");

    FileContainerAdd(ffc, ff);

    if (data != NULL) {
        //PrintRawDataFp(stdout, data, data_len);
        ff->size += data_len;
        SCLogDebug("file size is now %"PRIu64, ff->size);

        FileData *ffd = FileDataAlloc(data, data_len);
        if (ffd == NULL) {
            ff->state = FILE_STATE_ERROR;
            SCReturnPtr(NULL, "File");
        }

        /* append the data */
        if (FileAppendFileData(ffc, ffd) < 0) {
            ff->state = FILE_STATE_ERROR;
            FileDataFree(ffd);
            SCReturnPtr(NULL, "File");
        }
    }

    SCReturnPtr(ff, "File");
}

static int FileCloseFilePtr(File *ff, uint8_t *data,
        uint32_t data_len, uint8_t flags)
{
    SCEnter();

    if (ff == NULL) {
        SCReturnInt(-1);
    }

    if (ff->state != FILE_STATE_OPENED) {
        SCReturnInt(-1);
    }

    ff->size += data_len;
    SCLogDebug("file size is now %"PRIu64, ff->size);

    if (data != NULL) {
        //PrintRawDataFp(stdout, data, data_len);

        if (ff->flags & FILE_NOSTORE) {
#ifdef HAVE_NSS
            /* no storage but md5 */
            if (ff->md5_ctx)
                HASH_Update(ff->md5_ctx, data, data_len);
#endif
        } else {
            FileData *ffd = FileDataAlloc(data, data_len);
            if (ffd == NULL) {
                ff->state = FILE_STATE_ERROR;
                SCReturnInt(-1);
            }

            /* append the data */
            if (FileAppendFileDataFilePtr(ff, ffd) < 0) {
                ff->state = FILE_STATE_ERROR;
                FileDataFree(ffd);
                SCReturnInt(-1);
            }
        }
    }

    if (flags & FILE_TRUNCATED) {
        ff->state = FILE_STATE_TRUNCATED;
        SCLogDebug("flowfile state transitioned to FILE_STATE_TRUNCATED");

        if (flags & FILE_NOSTORE) {
            SCLogDebug("not storing this file");
            ff->flags |= FILE_NOSTORE;
        }
    } else {
        ff->state = FILE_STATE_CLOSED;
        SCLogDebug("flowfile state transitioned to FILE_STATE_CLOSED");

#ifdef HAVE_NSS
        if (ff->md5_ctx) {
            unsigned int len = 0;
            HASH_End(ff->md5_ctx, ff->md5, &len, sizeof(ff->md5));
            ff->flags |= FILE_MD5;
        }
#endif
    }

    SCReturnInt(0);
}

/**
 *  \brief Close a File
 *
 *  \param ffc the container
 *  \param data final data if any
 *  \param data_len data len if any
 *  \param flags flags
 *
 *  \retval 0 ok
 *  \retval -1 error
 */
int FileCloseFile(FileContainer *ffc, uint8_t *data,
        uint32_t data_len, uint8_t flags)
{
    SCEnter();

    if (ffc == NULL || ffc->tail == NULL) {
        SCReturnInt(-1);
    }

    if (FileCloseFilePtr(ffc->tail, data, data_len, flags) == -1) {
        SCReturnInt(-1);
    }

    SCReturnInt(0);
}

/**
 *  \brief disable file storage for a flow
 *
 *  \param f *LOCKED* flow
 *  \param direction flow direction
 */
void FileDisableStoring(Flow *f, uint8_t direction)
{
    File *ptr = NULL;

    SCEnter();

    DEBUG_ASSERT_FLOW_LOCKED(f);

    if (direction == STREAM_TOSERVER)
        f->flags |= FLOW_FILE_NO_STORE_TS;
    else
        f->flags |= FLOW_FILE_NO_STORE_TC;

    FileContainer *ffc = AppLayerParserGetFiles(f->proto, f->alproto, f->alstate, direction);
    if (ffc != NULL) {
        for (ptr = ffc->head; ptr != NULL; ptr = ptr->next) {
            /* if we're already storing, we'll continue */
            if (!(ptr->flags & FILE_STORE)) {
                SCLogDebug("not storing this file");
                ptr->flags |= FILE_NOSTORE;
            }
        }
    }
    SCReturn;
}

/**
 *  \brief disable file magic lookups for this flow
 *
 *  \param f *LOCKED* flow
 *  \param direction flow direction
 */
void FileDisableMagic(Flow *f, uint8_t direction)
{
    File *ptr = NULL;

    SCEnter();

    DEBUG_ASSERT_FLOW_LOCKED(f);

    if (direction == STREAM_TOSERVER)
        f->flags |= FLOW_FILE_NO_MAGIC_TS;
    else
        f->flags |= FLOW_FILE_NO_MAGIC_TC;

    FileContainer *ffc = AppLayerParserGetFiles(f->proto, f->alproto, f->alstate, direction);
    if (ffc != NULL) {
        for (ptr = ffc->head; ptr != NULL; ptr = ptr->next) {
            SCLogDebug("disabling magic for file %p from direction %s",
                    ptr, direction == STREAM_TOSERVER ? "toserver":"toclient");
            ptr->flags |= FILE_NOMAGIC;
        }
    }

    SCReturn;
}

/**
 *  \brief disable file md5 calc for this flow
 *
 *  \param f *LOCKED* flow
 *  \param direction flow direction
 */
void FileDisableMd5(Flow *f, uint8_t direction)
{
    File *ptr = NULL;

    SCEnter();

    DEBUG_ASSERT_FLOW_LOCKED(f);

    if (direction == STREAM_TOSERVER)
        f->flags |= FLOW_FILE_NO_MD5_TS;
    else
        f->flags |= FLOW_FILE_NO_MD5_TC;

    FileContainer *ffc = AppLayerParserGetFiles(f->proto, f->alproto, f->alstate, direction);
    if (ffc != NULL) {
        for (ptr = ffc->head; ptr != NULL; ptr = ptr->next) {
            SCLogDebug("disabling md5 for file %p from direction %s",
                    ptr, direction == STREAM_TOSERVER ? "toserver":"toclient");
            ptr->flags |= FILE_NOMD5;

#ifdef HAVE_NSS
            /* destroy any ctx we may have so far */
            if (ptr->md5_ctx != NULL) {
                HASH_Destroy(ptr->md5_ctx);
                ptr->md5_ctx = NULL;
            }
#endif
        }
    }

    SCReturn;
}

/**
 *  \brief disable file size tracking for this flow
 *
 *  \param f *LOCKED* flow
 *  \param direction flow direction
 */
void FileDisableFilesize(Flow *f, uint8_t direction)
{
    File *ptr = NULL;

    SCEnter();

    DEBUG_ASSERT_FLOW_LOCKED(f);

    if (direction == STREAM_TOSERVER)
        f->flags |= FLOW_FILE_NO_SIZE_TS;
    else
        f->flags |= FLOW_FILE_NO_SIZE_TC;

    FileContainer *ffc = AppLayerParserGetFiles(f->proto, f->alproto, f->alstate, direction);
    if (ffc != NULL) {
        for (ptr = ffc->head; ptr != NULL; ptr = ptr->next) {
            SCLogDebug("disabling size tracking for file %p from direction %s",
                    ptr, direction == STREAM_TOSERVER ? "toserver":"toclient");
            ptr->flags |= FILE_NOTRACK;
        }
    }

    SCReturn;
}


/**
 *  \brief set no store flag, close file if needed
 *
 *  \param ff file
 */
void FileDisableStoringForFile(File *ff)
{
    SCEnter();

    if (ff == NULL) {
        SCReturn;
    }

    SCLogDebug("not storing this file");
    ff->flags |= FILE_NOSTORE;

    if (ff->state == FILE_STATE_OPENED && ff->size >= (uint64_t)FileMagicSize()) {
        (void)FileCloseFilePtr(ff, NULL, 0,
                (FILE_TRUNCATED|FILE_NOSTORE));
    }
}

/**
 *  \brief disable file storing for files in a transaction
 *
 *  \param f *LOCKED* flow
 *  \param direction flow direction
 *  \param tx_id transaction id
 */
void FileDisableStoringForTransaction(Flow *f, uint8_t direction, uint64_t tx_id)
{
    File *ptr = NULL;

    DEBUG_ASSERT_FLOW_LOCKED(f);

    SCEnter();

    FileContainer *ffc = AppLayerParserGetFiles(f->proto, f->alproto, f->alstate, direction);
    if (ffc != NULL) {
        for (ptr = ffc->head; ptr != NULL; ptr = ptr->next) {
            if (ptr->txid == tx_id) {
                if (ptr->flags & FILE_STORE) {
                    /* weird, already storing -- let it continue*/
                    SCLogDebug("file is already being stored");
                } else {
                    FileDisableStoringForFile(ptr);
                }
            }
        }
    }

    SCReturn;
}

/**
 *  \brief flag a file with id "file_id" to be stored.
 *
 *  \param fc file store
 *  \param file_id the file's id
 */
void FileStoreFileById(FileContainer *fc, uint16_t file_id)
{
    File *ptr = NULL;

    SCEnter();

    if (fc != NULL) {
        for (ptr = fc->head; ptr != NULL; ptr = ptr->next) {
            if (ptr->file_id == file_id) {
                ptr->flags |= FILE_STORE;
            }
        }
    }
}

void FileStoreAllFilesForTx(FileContainer *fc, uint16_t tx_id)
{
    File *ptr = NULL;

    SCEnter();

    if (fc != NULL) {
        for (ptr = fc->head; ptr != NULL; ptr = ptr->next) {
            if (ptr->txid == tx_id) {
                ptr->flags |= FILE_STORE;
            }
        }
    }
}

void FileStoreAllFiles(FileContainer *fc)
{
    File *ptr = NULL;

    SCEnter();

    if (fc != NULL) {
        for (ptr = fc->head; ptr != NULL; ptr = ptr->next) {
            ptr->flags |= FILE_STORE;
        }
    }
}

void FileTruncateAllOpenFiles(FileContainer *fc)
{
    File *ptr = NULL;

    SCEnter();

    if (fc != NULL) {
        for (ptr = fc->head; ptr != NULL; ptr = ptr->next) {
            if (ptr->state == FILE_STATE_OPENED) {
                FileCloseFilePtr(ptr, NULL, 0, FILE_TRUNCATED);
            }
        }
    }
}
