// Copyright (c) 2020, Redis Labs
// SPDX-FileCopyrightText: 2024 Redict Contributors
// SPDX-FileCopyrightText: 2024 Salvatore Sanfilippo <antirez at gmail dot com>
//
// SPDX-License-Identifier: BSD-3-Clause
// SPDX-License-Identifier: LGPL-3.0-only

#include "fmacros.h"
#include "cli_common.h"
#include "version.h"

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <hiredict.h>
#include <sds.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#ifdef USE_OPENSSL
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <hiredict_ssl.h>
#endif

#define UNUSED(V) ((void) V)

char *redictGitSHA1(void);
char *redictGitDirty(void);

/* Wrapper around redictSecureConnection to avoid hiredict_ssl dependencies if
 * not building with TLS support.
 */
int cliSecureConnection(redictContext *c, cliSSLconfig config, const char **err) {
#ifdef USE_OPENSSL
    static SSL_CTX *ssl_ctx = NULL;

    if (!ssl_ctx) {
        ssl_ctx = SSL_CTX_new(SSLv23_client_method());
        if (!ssl_ctx) {
            *err = "Failed to create SSL_CTX";
            goto error;
        }
        SSL_CTX_set_options(ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
        SSL_CTX_set_verify(ssl_ctx, config.skip_cert_verify ? SSL_VERIFY_NONE : SSL_VERIFY_PEER, NULL);

        if (config.cacert || config.cacertdir) {
            if (!SSL_CTX_load_verify_locations(ssl_ctx, config.cacert, config.cacertdir)) {
                *err = "Invalid CA Certificate File/Directory";
                goto error;
            }
        } else {
            if (!SSL_CTX_set_default_verify_paths(ssl_ctx)) {
                *err = "Failed to use default CA paths";
                goto error;
            }
        }

        if (config.cert && !SSL_CTX_use_certificate_chain_file(ssl_ctx, config.cert)) {
            *err = "Invalid client certificate";
            goto error;
        }

        if (config.key && !SSL_CTX_use_PrivateKey_file(ssl_ctx, config.key, SSL_FILETYPE_PEM)) {
            *err = "Invalid private key";
            goto error;
        }
        if (config.ciphers && !SSL_CTX_set_cipher_list(ssl_ctx, config.ciphers)) {
            *err = "Error while configuring ciphers";
            goto error;
        }
#ifdef TLS1_3_VERSION
        if (config.ciphersuites && !SSL_CTX_set_ciphersuites(ssl_ctx, config.ciphersuites)) {
            *err = "Error while setting cypher suites";
            goto error;
        }
#endif
    }

    SSL *ssl = SSL_new(ssl_ctx);
    if (!ssl) {
        *err = "Failed to create SSL object";
        return REDICT_ERR;
    }

    if (config.sni && !SSL_set_tlsext_host_name(ssl, config.sni)) {
        *err = "Failed to configure SNI";
        SSL_free(ssl);
        return REDICT_ERR;
    }

    return redictInitiateSSL(c, ssl);

error:
    SSL_CTX_free(ssl_ctx);
    ssl_ctx = NULL;
    return REDICT_ERR;
#else
    (void) config;
    (void) c;
    (void) err;
    return REDICT_OK;
#endif
}

/* Wrapper around hiredict to allow arbitrary reads and writes.
 *
 * We piggybacks on top of hiredict to achieve transparent TLS support,
 * and use its internal buffers so it can co-exist with commands
 * previously/later issued on the connection.
 *
 * Interface is close to enough to read()/write() so things should mostly
 * work transparently.
 */

/* Write a raw buffer through a redictContext. If we already have something
 * in the buffer (leftovers from hiredict operations) it will be written
 * as well.
 */
ssize_t cliWriteConn(redictContext *c, const char *buf, size_t buf_len)
{
    int done = 0;

    /* Append data to buffer which is *usually* expected to be empty
     * but we don't assume that, and write.
     */
    c->obuf = sdscatlen(c->obuf, buf, buf_len);
    if (redictBufferWrite(c, &done) == REDICT_ERR) {
        if (!(c->flags & REDICT_BLOCK))
            errno = EAGAIN;

        /* On error, we assume nothing was written and we roll back the
         * buffer to its original state.
         */
        if (sdslen(c->obuf) > buf_len)
            sdsrange(c->obuf, 0, -(buf_len+1));
        else
            sdsclear(c->obuf);

        return -1;
    }

    /* If we're done, free up everything. We may have written more than
     * buf_len (if c->obuf was not initially empty) but we don't have to
     * tell.
     */
    if (done) {
        sdsclear(c->obuf);
        return buf_len;
    }

    /* Write was successful but we have some leftovers which we should
     * remove from the buffer.
     *
     * Do we still have data that was there prior to our buf? If so,
     * restore buffer to it's original state and report no new data was
     * written.
     */
    if (sdslen(c->obuf) > buf_len) {
        sdsrange(c->obuf, 0, -(buf_len+1));
        return 0;
    }

    /* At this point we're sure no prior data is left. We flush the buffer
     * and report how much we've written.
     */
    size_t left = sdslen(c->obuf);
    sdsclear(c->obuf);
    return buf_len - left;
}

/* Wrapper around OpenSSL (libssl and libcrypto) initialisation
 */
int cliSecureInit(void)
{
#ifdef USE_OPENSSL
    ERR_load_crypto_strings();
    SSL_load_error_strings();
    SSL_library_init();
#endif
    return REDICT_OK;
}

/* Create an sds from stdin */
sds readArgFromStdin(void) {
    char buf[1024];
    sds arg = sdsempty();

    while(1) {
        int nread = read(fileno(stdin),buf,1024);

        if (nread == 0) break;
        else if (nread == -1) {
            perror("Reading from standard input");
            exit(1);
        }
        arg = sdscatlen(arg,buf,nread);
    }
    return arg;
}

/* Create an sds array from argv, either as-is or by dequoting every
 * element. When quoted is non-zero, may return a NULL to indicate an
 * invalid quoted string.
 *
 * The caller should free the resulting array of sds strings with
 * sdsfreesplitres().
 */
sds *getSdsArrayFromArgv(int argc,char **argv, int quoted) {
    sds *res = sds_malloc(sizeof(sds) * argc);

    for (int j = 0; j < argc; j++) {
        if (quoted) {
            sds unquoted = unquoteCString(argv[j]);
            if (!unquoted) {
                while (--j >= 0) sdsfree(res[j]);
                sds_free(res);
                return NULL;
            }
            res[j] = unquoted;
        } else {
            res[j] = sdsnew(argv[j]);
        }
    }

    return res;
}

/* Unquote a null-terminated string and return it as a binary-safe sds. */
sds unquoteCString(char *str) {
    int count;
    sds *unquoted = sdssplitargs(str, &count);
    sds res = NULL;

    if (unquoted && count == 1) {
        res = unquoted[0];
        unquoted[0] = NULL;
    }

    if (unquoted)
        sdsfreesplitres(unquoted, count);

    return res;
}


/* URL-style percent decoding. */
#define isHexChar(c) (isdigit(c) || ((c) >= 'a' && (c) <= 'f'))
#define decodeHexChar(c) (isdigit(c) ? (c) - '0' : (c) - 'a' + 10)
#define decodeHex(h, l) ((decodeHexChar(h) << 4) + decodeHexChar(l))

static sds percentDecode(const char *pe, size_t len) {
    const char *end = pe + len;
    sds ret = sdsempty();
    const char *curr = pe;

    while (curr < end) {
        if (*curr == '%') {
            if ((end - curr) < 2) {
                fprintf(stderr, "Incomplete URI encoding\n");
                exit(1);
            }

            char h = tolower(*(++curr));
            char l = tolower(*(++curr));
            if (!isHexChar(h) || !isHexChar(l)) {
                fprintf(stderr, "Illegal character in URI encoding\n");
                exit(1);
            }
            char c = decodeHex(h, l);
            ret = sdscatlen(ret, &c, 1);
            curr++;
        } else {
            ret = sdscatlen(ret, curr++, 1);
        }
    }

    return ret;
}

/* Parse a URI and extract the server connection information.
 * URI scheme is based on the provisional specification[1] excluding support
 * for query parameters. Valid URIs are:
 *   scheme:    "redict://" or "redis://"
 *   authority: [[<username> ":"] <password> "@"] [<hostname> [":" <port>]]
 *   path:      ["/" [<db>]]
 *
 *  [1]: https://www.iana.org/assignments/uri-schemes/prov/redis */
void parseRedictUri(const char *uri, const char* tool_name, cliConnInfo *connInfo, int *tls_flag) {
#ifdef USE_OPENSSL
    UNUSED(tool_name);
#else
    UNUSED(tls_flag);
#endif

    const char *scheme = "redict://";
    const char *tlsscheme = "redicts://";
    const char *schemecompat = "redis://";
    const char *tlsschemecompat = "rediss://";
    const char *curr = uri;
    const char *end = uri + strlen(uri);
    const char *userinfo, *username, *port, *host, *path;

    /* URI must start with a valid scheme. */
    if (!strncasecmp(tlsscheme, curr, strlen(tlsscheme))) {
#ifdef USE_OPENSSL
        *tls_flag = 1;
        curr += strlen(tlsscheme);
#else
        fprintf(stderr,"redicts:// is only supported when %s is compiled with OpenSSL\n", tool_name);
        exit(1);
#endif
    } else if (!strncasecmp(scheme, curr, strlen(scheme))) {
        curr += strlen(scheme);
    } else if (!strncasecmp(tlsschemecompat, curr, strlen(tlsschemecompat))) {
#ifdef USE_OPENSSL
        *tls_flag = 1;
        curr += strlen(tlsschemecompat);
#else
        fprintf(stderr,"rediss:// is only supported when %s is compiled with OpenSSL\n", tool_name);
        exit(1);
#endif
    } else if (!strncasecmp(schemecompat, curr, strlen(schemecompat))) {
        curr += strlen(schemecompat);
    } else {
        fprintf(stderr,"Invalid URI scheme\n");
        exit(1);
    }
    if (curr == end) return;

    /* Extract user info. */
    if ((userinfo = strchr(curr,'@'))) {
        if ((username = strchr(curr, ':')) && username < userinfo) {
            connInfo->user = percentDecode(curr, username - curr);
            curr = username + 1;
        }

        connInfo->auth = percentDecode(curr, userinfo - curr);
        curr = userinfo + 1;
    }
    if (curr == end) return;

    /* Extract host and port. */
    path = strchr(curr, '/');
    if (*curr != '/') {
        host = path ? path - 1 : end;
        if (*curr == '[') {
            curr += 1;
            if ((port = strchr(curr, ']'))) {
                if (*(port+1) == ':') {
                    connInfo->hostport = atoi(port + 2);
                }
                host = port - 1;
            }
        } else {
            if ((port = strchr(curr, ':'))) {
                connInfo->hostport = atoi(port + 1);
                host = port - 1;
            }
        }
        sdsfree(connInfo->hostip);
        connInfo->hostip = sdsnewlen(curr, host - curr + 1);
    }
    curr = path ? path + 1 : end;
    if (curr == end) return;

    /* Extract database number. */
    connInfo->input_dbnum = atoi(curr);
}

void freeCliConnInfo(cliConnInfo connInfo){
    if (connInfo.hostip) sdsfree(connInfo.hostip);
    if (connInfo.auth) sdsfree(connInfo.auth);
    if (connInfo.user) sdsfree(connInfo.user);
}

/*
 * Escape a Unicode string for JSON output (--json), following RFC 7159:
 * https://datatracker.ietf.org/doc/html/rfc7159#section-7
*/
sds escapeJsonString(sds s, const char *p, size_t len) {
    s = sdscatlen(s,"\"",1);
    while(len--) {
        switch(*p) {
        case '\\':
        case '"':
            s = sdscatprintf(s,"\\%c",*p);
            break;
        case '\n': s = sdscatlen(s,"\\n",2); break;
        case '\f': s = sdscatlen(s,"\\f",2); break;
        case '\r': s = sdscatlen(s,"\\r",2); break;
        case '\t': s = sdscatlen(s,"\\t",2); break;
        case '\b': s = sdscatlen(s,"\\b",2); break;
        default:
            s = sdscatprintf(s,*(unsigned char *)p <= 0x1f ? "\\u%04x" : "%c",*p);
        }
        p++;
    }
    return sdscatlen(s,"\"",1);
}

sds cliVersion(void) {
    sds version = sdscatprintf(sdsempty(), "%s", REDICT_VERSION);

    /* Add git commit and working tree status when available. */
    if (strtoll(redictGitSHA1(),NULL,16)) {
        version = sdscatprintf(version, " (git:%s", redictGitSHA1());
        if (strtoll(redictGitDirty(),NULL,10))
            version = sdscatprintf(version, "-dirty");
        version = sdscat(version, ")");
    }
    return version;
}

/* This is a wrapper to call redictConnect or redictConnectWithTimeout. */
redictContext *redictConnectWrapper(const char *ip, int port, const struct timeval tv) {
    if (tv.tv_sec == 0 && tv.tv_usec == 0) {
        return redictConnect(ip, port);
    } else {
        return redictConnectWithTimeout(ip, port, tv);
    }
}

/* This is a wrapper to call redictConnectUnix or redictConnectUnixWithTimeout. */
redictContext *redictConnectUnixWrapper(const char *path, const struct timeval tv) {
    if (tv.tv_sec == 0 && tv.tv_usec == 0) {
        return redictConnectUnix(path);
    } else {
        return redictConnectUnixWithTimeout(path, tv);
    }
}
