#include <config.h>

#ifdef WITH_PUREDB

#include "ftpd.h"
#include "messages.h"
#include "log_puredb.h"
#include "pure-pw.h"
#include "../puredb/src/puredb_read.h"
#include "utils.h"

#ifdef HAVE_LIBSODIUM
# include <sodium.h>
#endif

#ifdef WITH_DMALLOC
# include <dmalloc.h>
#endif

static char *pdb_filename;

void pw_puredb_parse(const char * const file)
{
    if (file == NULL || *file == 0) {
        die(421, LOG_ERR, MSG_NO_VIRTUAL_FILE);
    }
    if ((pdb_filename = strdup(file)) == NULL) {
        die_mem();
    }
}

void pw_puredb_exit(void)
{
    free(pdb_filename);
}

/*
 * The difference between this strtok() and the libc's one is that
 * this one doesn't skip empty fields, and takes a char instead of a
 * string as a delimiter.
 * This strtok2() variant leaves zeroes.
 */

static char *my_strtok2(char *str, const char delim)
{
    static char *s;
    static char save;

    if (str != NULL) {
        if (*str == 0) {
            return NULL;
        }
        s = str;
        scan:
        while (*s != 0 && *s != delim) {
            s++;
        }
        save = *s;
        *s = 0;

        return str;
    }
    if (s == NULL || save == 0) {
        return NULL;
    }
    s++;
    str = s;

    goto scan;
}

/* Check whether an IP address matches a pattern. 1 = match 0 = nomatch */

static int access_ip_match(const struct sockaddr_storage * const sa,
                           char * pattern)
{
    unsigned int ip0, ip1, ip2, ip3;
    unsigned int netbits;
    unsigned long ip;
    unsigned long mask;
    unsigned long saip;
    const unsigned char *saip_raw;
    char *comapoint;

    if (*pattern == 0) {
        return 1;
    }
    if (STORAGE_FAMILY(*sa) != AF_INET) {
        return 0;                      /* TODO: IPv6 */
    }
    do {
        if ((comapoint = strchr(pattern, ',')) != NULL) {
            *comapoint = 0;
        }
        if (sscanf(pattern, "%u.%u.%u.%u/%u",    /* IPv4 */
                   &ip0, &ip1, &ip2, &ip3, &netbits) == 5) {
            ipcheck:
            if (STORAGE_FAMILY(*sa) != AF_INET || netbits == 0U) {
                return -1;
            }
            ip =
                ((unsigned long) ip0 << 24) |
                ((unsigned long) ip1 << 16) |
                ((unsigned long) ip2 << 8) |
                (unsigned long) ip3;
            ipcheck_ipdone:
            mask = ~((0x80000000 >> (netbits - 1U)) - 1U);
            saip_raw = (const unsigned char *) &(STORAGE_SIN_ADDR_CONST(*sa));
            saip =
                ((unsigned long) saip_raw[0] << 24) |
                ((unsigned long) saip_raw[1] << 16) |
                ((unsigned long) saip_raw[2] << 8) |
                (unsigned long) saip_raw[3];
            if ((ip & mask) == (saip & mask)) {
                return 1;
            }
        } else if (sscanf(pattern, "%u.%u.%u.%u",
                          &ip0, &ip1, &ip2, &ip3) == 4) {
            netbits = 32U;
            goto ipcheck;
        } else {
            struct addrinfo hints, *res;
            int on;

            memset(&hints, 0, sizeof hints);
            hints.ai_family = AF_INET;
            hints.ai_addr = NULL;
            if ((on = getaddrinfo(pattern, NULL, &hints, &res)) != 0) {
                logfile(LOG_WARNING, "puredb: [%s] => [%d]", pattern, on);
            } else if (res->ai_family != AF_INET) {
                freeaddrinfo(res);
            } else {
                const unsigned char * const ip_raw =
                    (const unsigned char *) &
                    (((const struct sockaddr_in *) (void *)
                      (res->ai_addr))->sin_addr.s_addr);

                ip =
                    ((unsigned long) ip_raw[0] << 24) |
                    ((unsigned long) ip_raw[1] << 16) |
                    ((unsigned long) ip_raw[2] << 8) |
                    (unsigned long) ip_raw[3];
                netbits = 32U;
                freeaddrinfo(res);
                goto ipcheck_ipdone;
            }
        }
        if (comapoint == NULL) {
            break;
        }
        *comapoint = ',';
        pattern = comapoint + 1;
    } while (*pattern != 0);

    return 0;
}

/* IP check. 0 = ok, -1 = denied */

static int access_ip_check(const struct sockaddr_storage * const sa,
                           char * const allow, char * const deny)
{
    if (sa == NULL) {
        return 0;
    }
    if (*allow == 0) {
        if (*deny == 0) {
            return 0;
        }
        if (access_ip_match(sa, deny) != 0) {
            return -1;
        }
        return 0;
    }
    if (*deny == 0) {
        if (access_ip_match(sa, allow) != 0) {
            return 0;
        }
        return -1;
    }
    if (access_ip_match(sa, allow) != 0 && access_ip_match(sa, deny) == 0) {
        return 0;
    }
    return -1;
}

static int time_restrictions_check(const char * const restrictions)
{
    const struct tm *tm;
    time_t now_t;
    unsigned int time_begin, time_end;
    unsigned int now;

    if (*restrictions == 0) {
        return 0;
    }
    if (sscanf(restrictions, "%u-%u", &time_begin, &time_end) != 2 ||
        (now_t = time(NULL)) == (time_t) -1 ||
        (tm = localtime(&now_t)) == NULL) {
        return 0;
    }
    now = (unsigned int) tm->tm_hour * 100U + (unsigned int) tm->tm_min;
    if (time_begin <= time_end) {
        if (time_begin <= now && now <= time_end) {
            return 0;
        }
        return -1;
    }
    if (now >= time_begin || now <= time_end) {
        return 0;
    }
    return -1;
}

static int pw_puredb_parseline(char *line, const char * const pwd,
                               const struct sockaddr_storage * const sa,
                               const struct sockaddr_storage * const peer,
                               AuthResult * const result)
{
    char *allow_local_ip, *deny_local_ip;
    char *allow_remote_ip, *deny_remote_ip;
    const char *time_restrictions;

    if ((line = my_strtok2(line, *PW_LINE_SEP)) == NULL || *line == 0) {   /* pwd */
        return -1;
    }
    {
        const char *crypted;
        int         ret = -1;

#ifdef crypto_pwhash_STRPREFIX
        if (crypto_pwhash_str_verify(line, pwd, strlen(pwd)) == 0) {
            /* pass */
        } else
#endif
#ifdef crypto_pwhash_scryptsalsa208sha256_STRPREFIX
        if (crypto_pwhash_scryptsalsa208sha256_str_verify
            (line, pwd, strlen(pwd)) == 0) {
            /* pass */
        } else
#endif
        {
            ret = - ((crypted = (const char *) crypt(pwd, line)) == NULL ||
                     pure_strcmp(line, crypted) != 0);
            if (ret != 0) {
                return -1;
            }
        }
    }
    if ((line = my_strtok2(NULL, *PW_LINE_SEP)) == NULL || *line == 0) {   /* uid */
        return -1;
    }
    result->uid = (uid_t) strtoul(line, NULL, 10);
    if ((line = my_strtok2(NULL, *PW_LINE_SEP)) == NULL || *line == 0) {   /* gid */
        return -1;
    }
    result->gid = (gid_t) strtoul(line, NULL, 10);
#ifndef ACCEPT_ROOT_VIRTUAL_USERS
    if (result->uid <= (uid_t) 0 || result->gid <= (gid_t) 0) {
        return -1;
    }
#endif
    if (my_strtok2(NULL, *PW_LINE_SEP) == NULL) {   /* gecos */
        return -1;
    }
    if ((line = my_strtok2(NULL, *PW_LINE_SEP)) == NULL || *line == 0) {   /* home */
        return -1;
    }
    if ((result->dir = strdup(line)) == NULL || *result->dir != '/') {
        return -1;
    }
    if ((line = my_strtok2(NULL, *PW_LINE_SEP)) == NULL) {   /* bw_ul */
        return 0;
    }
#ifdef THROTTLING
    if (*line != 0) {
        result->throttling_ul_changed = 1;
        result->throttling_bandwidth_ul = strtoul(line, NULL, 10);
    }
#endif
    if ((line = my_strtok2(NULL, *PW_LINE_SEP)) == NULL) {   /* bw_dl */
        return 0;
    }
#ifdef THROTTLING
    if (*line != 0) {
        result->throttling_dl_changed = 1;
        result->throttling_bandwidth_dl = strtoul(line, NULL, 10);
    }
#endif
    if ((line = my_strtok2(NULL, *PW_LINE_SEP)) == NULL) {   /* ratio up */
        return 0;
    }
#ifdef RATIOS
    if (*line != 0) {
        result->ratio_upload = (unsigned int) strtoul(line, NULL, 10);
        if (result->ratio_upload > 0U) {
            result->ratio_ul_changed = 1;
        }
    }
#endif
    if ((line = my_strtok2(NULL, *PW_LINE_SEP)) == NULL) {   /* ratio down */
        return 0;
    }
#ifdef RATIOS
    if (*line != 0) {
        result->ratio_download = (unsigned int) strtoul(line, NULL, 10);
        if (result->ratio_download > 0U) {
            result->ratio_dl_changed = 1;
        }
    }
#endif
    if ((line = my_strtok2(NULL, *PW_LINE_SEP)) == NULL) {   /* max cnx */
        return 0;
    }
#ifdef PER_USER_LIMITS
    if (*line != 0) {
        result->per_user_max = (unsigned int) strtoull(line, NULL, 10);
    }
#endif
    if ((line = my_strtok2(NULL, *PW_LINE_SEP)) == NULL) {   /* files quota */
        return 0;
    }
#ifdef QUOTAS
    if (*line != 0) {
        result->quota_files_changed = 1;
        result->user_quota_files = strtoull(line, NULL, 10);
    }
#endif
    if ((line = my_strtok2(NULL, *PW_LINE_SEP)) == NULL) {   /* size quota */
        return 0;
    }
#ifdef QUOTAS
    if (*line != 0) {
        result->quota_size_changed = 1;
        result->user_quota_size = strtoull(line, NULL, 10);
    }
#endif
    if ((line = my_strtok2(NULL, *PW_LINE_SEP)) == NULL) {   /* allowed local ip */
        return 0;
    }
    allow_local_ip = line;
    if ((line = my_strtok2(NULL, *PW_LINE_SEP)) == NULL) {   /* denied local ip */
        return 0;
    }
    deny_local_ip = line;
    if ((line = my_strtok2(NULL, *PW_LINE_SEP)) == NULL) {   /* allowed remote ip */
        return 0;
    }
    allow_remote_ip = line;
    if ((line = my_strtok2(NULL, *PW_LINE_SEP)) == NULL) {   /* denied remote ip */
        return 0;
    }
    deny_remote_ip = line;
    if (access_ip_check(sa, allow_local_ip, deny_local_ip) != 0 ||
        access_ip_check(peer, allow_remote_ip, deny_remote_ip) != 0) {
        return -1;
    }
    if ((line = my_strtok2(NULL, *PW_LINE_SEP)) == NULL) {   /* time restrictions */
        return 0;
    }
    time_restrictions = line;
    if (time_restrictions_check(time_restrictions) != 0) {
        return -1;
    }

    return 0;
}

void pw_puredb_check(AuthResult * const result,
                     const char *account, const char *password,
                     const struct sockaddr_storage * const sa,
                     const struct sockaddr_storage * const peer)
{
    char *line = NULL;
    PureDB db;
    off_t retpos;
    size_t retlen;

    result->auth_ok = 0;
    (void) sa;
    (void) peer;
    if (puredb_open(&db, pdb_filename) != 0) {
        die(421, LOG_ERR, MSG_PDB_BROKEN);
    }
    if (puredb_find_s(&db, account, &retpos, &retlen) != 0) {
        goto bye;
    }
    if ((line = puredb_read(&db, retpos, retlen)) == NULL) {
        goto bye;
    }
    result->auth_ok--;
    if (pw_puredb_parseline(line, password, sa, peer, result) != 0) {
        goto bye;
    }
    result->slow_tilde_expansion = 1;
    result->auth_ok = -result->auth_ok;
    bye:
    puredb_read_free(line);
    puredb_close(&db);
}

#endif
