/*
 *  OpenVPN-GUI -- A Windows GUI for OpenVPN.
 *
 *  Copyright (C) 2004 Mathias Sundman <mathias@nilings.se>
 *                2010 Heiko Hund <heikoh@users.sf.net>
 *                2016 Selva Nair <selva.nair@gmail.com>
 *
 *  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
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  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 (see the file COPYING included with this
 *  distribution); if not, write to the Free Software Foundation, Inc.,
 *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <windows.h>

#include "main.h"
#include "openvpn-gui-res.h"
#include "options.h"
#include "localization.h"
#include "save_pass.h"
#include "misc.h"

typedef enum
{
    match_false,
    match_file,
    match_dir
} match_t;

extern options_t o;

static match_t
match(const WIN32_FIND_DATA *find, const TCHAR *ext)
{
    size_t ext_len = _tcslen(ext);
    int i;

    if (find->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
    {
        return match_dir;
    }

    if (ext_len == 0)
    {
        return match_file;
    }

    i = _tcslen(find->cFileName) - ext_len - 1;

    if (i > 0 && find->cFileName[i] == '.'
        && _tcsicmp(find->cFileName + i + 1, ext) == 0)
    {
        return match_file;
    }

    return match_false;
}

static bool
CheckReadAccess(const TCHAR *dir, const TCHAR *file)
{
    TCHAR path[MAX_PATH];

    _sntprintf_0(path, _T("%ls\\%ls"), dir, file);

    return CheckFileAccess(path, GENERIC_READ);
}

static int
ConfigAlreadyExists(TCHAR *newconfig)
{
    for (connection_t *c = o.chead; c; c = c->next)
    {
        if (_tcsicmp(c->config_file, newconfig) == 0)
        {
            return true;
        }
    }
    return false;
}

static void
AddConfigFileToList(int group, const TCHAR *filename, const TCHAR *config_dir)
{
    connection_t *c = calloc(1, sizeof(connection_t));

    if (!c)
    {
        ErrorExit(1, L"Out of memory in AddConfigFileToList");
    }

    if (o.ctail)
    {
        o.ctail->next = c;
        o.ctail = c;
    }
    else
    {
        o.chead = o.ctail = c;
    }

    c->id = o.num_configs++;
    c->group = group;

    _tcsncpy(c->config_file, filename, _countof(c->config_file) - 1);
    _tcsncpy(c->config_dir, config_dir, _countof(c->config_dir) - 1);
    _tcsncpy(c->config_name, c->config_file, _countof(c->config_name) - 1);
    c->config_name[_tcslen(c->config_name) - _tcslen(o.ext_string) - 1] = _T('\0');
    _sntprintf_0(c->log_path, _T("%ls\\%ls.log"), o.log_dir, c->config_name);

    c->manage.sk = INVALID_SOCKET;
    c->manage.skaddr.sin_family = AF_INET;
    c->manage.skaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    c->manage.skaddr.sin_port = htons(o.mgmt_port_offset + c->id);

    if (wcsstr(config_dir, o.config_auto_dir))
    {
        c->flags |= FLAG_DAEMON_PERSISTENT;
        _sntprintf_0(c->log_path, _T("%ls\\%ls.log"), o.global_log_dir, c->config_name);
        /* set to auto-connect -- this attempts to attach to them on startup */
        if (o.enable_persistent == 2)
        {
            c->auto_connect = true;
        }
    }

    /* Check if connection should be autostarted */
    for (int i = 0; i < o.num_auto_connect; ++i)
    {
        if (_tcsicmp(c->config_file, o.auto_connect[i]) == 0
            || _tcsicmp(c->config_name, o.auto_connect[i]) == 0)
        {
            c->auto_connect = true;
            break;
        }
    }
    /* check whether passwords are saved */
    if (o.disable_save_passwords)
    {
        DisableSavePasswords(c);
    }
    else
    {
        if (IsAuthPassSaved(c->config_name))
        {
            c->flags |= FLAG_SAVE_AUTH_PASS;
        }
        if (IsKeyPassSaved(c->config_name))
        {
            c->flags |= FLAG_SAVE_KEY_PASS;
        }
    }
    if (o.disable_popup_messages)
    {
        DisablePopupMessages(c);
    }
}

#define FLAG_WARN_DUPLICATES        (0x1)
#define FLAG_WARN_MAX_CONFIGS       (0x2)
#define FLAG_ADD_CONFIG_GROUPS      (0x4)

/*
 * Create a new group with the given name as a child of the
 * specified parent group id and returns the id of the new group.
 * If FLAG_ADD_CONFIG_GROUPS is not enabled, returns the
 * parent itself.
 */
static int
NewConfigGroup(const wchar_t *name, int parent, int flags)
{
    if (!(flags & FLAG_ADD_CONFIG_GROUPS))
    {
        return parent;
    }

    if (!o.groups || o.num_groups == o.max_groups)
    {
        o.max_groups += 10;
        void *tmp = realloc(o.groups, sizeof(*o.groups)*o.max_groups);
        if (!tmp)
        {
            o.max_groups -= 10;
            ErrorExit(1, L"Out of memory while grouping configs");
        }
        o.groups = tmp;
    }

    config_group_t *cg = &o.groups[o.num_groups];
    memset(cg, 0, sizeof(*cg));

    _sntprintf_0(cg->name, L"%ls", name);
    cg->id = o.num_groups++;
    cg->parent = parent;
    cg->active = false; /* activated later if not empty */

    return cg->id;
}

/*
 * All groups that link at least one config to the root are
 * enabled. Dangling entries with no terminal configs will stay
 * disabled and are not displayed in the menu tree.
 * Also groups with single configs are squashed if the group
 * and config names match --- this improves the display.
 */
static void
ActivateConfigGroups(void)
{
    /* the root group is always active */
    o.groups[0].active = true;

    /* children is a counter re-used for activation, menu indexing etc. -- reset before use */
    for (int i = 0; i < o.num_groups; i++)
    {
        o.groups[i].children = 0;
    }

    /* count children of each group -- this includes groups
     * and configs which have it as parent
     */
    for (connection_t *c = o.chead; c; c = c->next)
    {
        CONFIG_GROUP(c)->children++;
    }

    for (int i = 1; i < o.num_groups; i++)
    {
        config_group_t *this = &o.groups[i];
        config_group_t *parent = PARENT_GROUP(this);
        if (parent) /* should be true as i = 0 is omitted */
        {
            parent->children++;
        }

        /* unless activated below the group stays inactive */
        this->active = false;
    }

    /* Squash single config directories with name matching the config
     * one depth up. This is done so that automatically imported configs
     * which are added as a single config per directory are handled
     * as if its in the parent directory. This encourages the
     * practice of keeping each config and its  dependencies (certs,
     * script etc.) in a separate directory, without making the menu structure
     * too deeply nested.
     */
    for (connection_t *c = o.chead; c; c = c->next)
    {
        config_group_t *cg = CONFIG_GROUP(c);

        /* if not root and has only this config as child -- squash it */
        if (PARENT_GROUP(cg) && cg->children == 1
            && !wcscmp(cg->name, c->config_name))
        {
            cg->children--;
            c->group = cg->parent;
        }
    }

    /* activate all groups that connect a config to the root */
    for (connection_t *c = o.chead; c; c = c->next)
    {
        config_group_t *cg = CONFIG_GROUP(c);

        while (cg)
        {
            cg->active = true;
            cg = PARENT_GROUP(cg);
        }
    }
}

/* Scan for configs in config_dir recursing down up to recurse_depth.
 * Input: config_dir -- root of the directory to scan from
 *        group      -- the group into which add the configs to
 *        flags      -- enable warnings, use directory based
 *                      grouping of configs etc.
 * Currently configs in a directory are grouped together and group is
 * the id of the current group in the global group array |o.groups|
 * This may be recursively called until depth becomes 1 and each time
 * the group is changed to that of the directory being recursed into.
 */
static void
BuildFileList0(const TCHAR *config_dir, int recurse_depth, int group, int flags)
{
    WIN32_FIND_DATA find_obj;
    HANDLE find_handle;
    TCHAR find_string[MAX_PATH];
    TCHAR subdir_name[MAX_PATH];

    _sntprintf_0(find_string, _T("%ls\\*"), config_dir);
    find_handle = FindFirstFile(find_string, &find_obj);
    if (find_handle == INVALID_HANDLE_VALUE)
    {
        return;
    }

    /* Loop over each config file in config dir */
    do
    {
        match_t match_type = match(&find_obj, o.ext_string);
        if (match_type == match_file)
        {
            if (ConfigAlreadyExists(find_obj.cFileName))
            {
                if (flags & FLAG_WARN_DUPLICATES)
                {
                    ShowLocalizedMsg(IDS_ERR_CONFIG_EXIST, find_obj.cFileName);
                }
                continue;
            }

            if (CheckReadAccess(config_dir, find_obj.cFileName))
            {
                AddConfigFileToList(group, find_obj.cFileName, config_dir);
            }
        }
    } while (FindNextFile(find_handle, &find_obj));

    FindClose(find_handle);

    /* optionally loop over each subdir */
    if (recurse_depth < 1)
    {
        return;
    }

    find_handle = FindFirstFile(find_string, &find_obj);
    if (find_handle == INVALID_HANDLE_VALUE)
    {
        return;
    }

    do
    {
        match_t match_type = match(&find_obj, o.ext_string);
        if (match_type == match_dir)
        {
            if (wcscmp(find_obj.cFileName, _T("."))
                &&  wcscmp(find_obj.cFileName, _T("..")))
            {
                /* recurse into subdirectory */
                _sntprintf_0(subdir_name, _T("%ls\\%ls"), config_dir, find_obj.cFileName);
                int sub_group = NewConfigGroup(find_obj.cFileName, group, flags);

                BuildFileList0(subdir_name, recurse_depth - 1, sub_group, flags);
            }
        }
    } while (FindNextFile(find_handle, &find_obj));

    FindClose(find_handle);
}

/*
 * Open a path and get its file information structure.
 * Returns true on success, false on error.
 */
static bool
GetFileInfo(const wchar_t *path, BY_HANDLE_FILE_INFORMATION *info)
{
    bool ret = false;

    /* FILE_FLAG_BACKUP_SEMANTICS required to open directories */
    HANDLE fd = CreateFileW(path, 0, FILE_SHARE_READ|FILE_SHARE_WRITE,
                            NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);

    if (fd == INVALID_HANDLE_VALUE)
    {
        MsgToEventLog(EVENTLOG_ERROR_TYPE, L"GetFileInfo: Error opening path <%ls> (status = %lu)", path, GetLastError());
        return ret;
    }

    ret = GetFileInformationByHandle(fd, info);
    if (!ret)
    {
        MsgToEventLog(EVENTLOG_ERROR_TYPE, L"GetFileInfo: Error accessing file information for path <%ls> (status = %lu)", path, GetLastError());
    }
    else
    {
        PrintDebug(L"path = <%ls> volumeid = %lu file index = (%lu,%lu)", path, info->dwVolumeSerialNumber, info->nFileIndexLow, info->nFileIndexHigh);
    }
    CloseHandle(fd);

    return ret;
}

/*
 * Compare two paths by checking whether they point to the
 * same object in the file system. Returns true if the paths
 * are same, false otherwise.
 * If the two paths are identical strings return true early.
 * If any of the paths do not exist, are not accessible or
 * fail to provide file information, we return false.
 */
static bool
IsSamePath(const wchar_t *path1, const wchar_t *path2)
{
    BOOL ret = false;
    BY_HANDLE_FILE_INFORMATION info1, info2;

    if (_wcsicmp(path1, path2) == 0)
    {
        return true;
    }

    if (GetFileInfo(path1, &info1) && GetFileInfo(path2, &info2))
    {
        ret = (info1.dwVolumeSerialNumber == info2.dwVolumeSerialNumber
               && info1.nFileIndexLow == info2.nFileIndexLow
               && info1.nFileIndexHigh == info2.nFileIndexHigh);
    }

    return ret;
}

void
BuildFileList()
{
    static bool issue_warnings = true;
    int recurse_depth = 20; /* maximum number of levels below config_dir to recurse into */
    int flags = 0;
    static int root_gp, system_gp, persistent_gp;

    if (o.silent_connection)
    {
        issue_warnings = false;
    }

    /*
     * If first time or no entries in the connection list reset groups and rescan
     * to make a new list. Else we keep all current configs and
     * rescan to add any new one's found.
     */
    if (!o.num_configs)
    {
        o.num_groups = 0;
        flags |= FLAG_ADD_CONFIG_GROUPS;
        root_gp = NewConfigGroup(L"ROOT", -1, flags); /* -1 indicates no parent */
        persistent_gp = NewConfigGroup(L"Persistent Profiles", root_gp, flags);
        system_gp = NewConfigGroup(L"System Profiles", root_gp, flags);
    }
    /* else these parent groups use their saved values */

    if (&o.groups[persistent_gp] != PERSISTENT_ROOT_GROUP)
    {
        MsgToEventLog(EVENTLOG_ERROR_TYPE, L"%hs:%d Logic error", __func__, __LINE__);
        return;
    }

    if (issue_warnings)
    {
        flags |= FLAG_WARN_DUPLICATES | FLAG_WARN_MAX_CONFIGS;
    }

    BuildFileList0(o.config_dir, recurse_depth, root_gp, flags);

    if (!IsSamePath(o.global_config_dir, o.config_dir))
    {
        BuildFileList0(o.global_config_dir, recurse_depth, system_gp, flags);
    }

    if (o.service_state == service_connected
        && o.enable_persistent)
    {
        if (!IsSamePath(o.config_auto_dir, o.config_dir))
        {
            BuildFileList0(o.config_auto_dir, recurse_depth, persistent_gp, flags);
        }
    }

    if (o.num_configs == 0 && issue_warnings)
    {
        ShowLocalizedMsg(IDS_NFO_NO_CONFIGS, o.config_dir, o.global_config_dir);
    }

    ActivateConfigGroups();

    issue_warnings = false;
}

void
FreeConfigList(options_t *o)
{
    connection_t *next = NULL;
    for (connection_t *c = o->chead; c; c = next)
    {
        next = c->next;
        free(c);
    }
    free(o->groups);
    o->groups = NULL;
    o->num_configs = 0;
    o->num_groups = 0;
}