/*
 *  OpenVPN -- An application to securely tunnel IP networks
 *             over a single TCP/UDP port, with support for SSL/TLS-based
 *             session authentication and key exchange,
 *             packet encryption, packet authentication, and
 *             packet compression.
 *
 *  Copyright (C) 2002-2026 OpenVPN Inc <sales@openvpn.net>
 *
 *  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, see <https://www.gnu.org/licenses/>.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "syshead.h"

#include "common.h"
#include "misc.h"
#include "crypto.h"
#include "win32.h"
#include "socket.h"
#include "fdmisc.h"
#include "proxy.h"
#include "base64.h"
#include "httpdigest.h"
#include "memdbg.h"
#include "forward.h"

#define UP_TYPE_PROXY "HTTP Proxy"

struct http_proxy_options *
init_http_proxy_options_once(struct http_proxy_options **hpo, struct gc_arena *gc)
{
    if (!*hpo)
    {
        ALLOC_OBJ_CLEAR_GC(*hpo, struct http_proxy_options, gc);
        /* http proxy defaults */
        (*hpo)->http_version = "1.0";
    }
    return *hpo;
}


/* cached proxy username/password */
static struct user_pass static_proxy_user_pass;

bool
proxy_recv_char(uint8_t *c, const char *name, socket_descriptor_t sd,
                struct timeval *timeout, volatile int *signal_received)
{
    fd_set reads;
    FD_ZERO(&reads);
    openvpn_fd_set(sd, &reads);

    const int status = openvpn_select(sd + 1, &reads, NULL, NULL, timeout);

    get_signal(signal_received);
    if (*signal_received)
    {
        return false;
    }

    /* timeout? */
    if (status == 0)
    {
        msg(D_LINK_ERRORS | M_ERRNO, "%s: TCP port read timeout expired", name);
        return false;
    }

    /* error */
    if (status < 0)
    {
        msg(D_LINK_ERRORS | M_ERRNO, "%s: TCP port read failed on select()", name);
        return false;
    }

    /* read single char */
    const ssize_t size = recv(sd, (void *)c, 1, MSG_NOSIGNAL);

    /* error? */
    if (size != 1)
    {
        msg(D_LINK_ERRORS | M_ERRNO, "%s: TCP port read failed on recv()", name);
        return false;
    }

    return true;
}


static bool
recv_line(socket_descriptor_t sd, char *buf, int len, const int timeout_sec, const bool verbose,
          struct buffer *lookahead, volatile int *signal_received)
{
    struct buffer la;
    int lastc = 0;

    CLEAR(la);
    if (lookahead)
    {
        la = *lookahead;
    }

    while (true)
    {
        struct timeval tv;
        uint8_t c;

        if (buf_defined(&la))
        {
            ASSERT(buf_init(&la, 0));
        }

        tv.tv_sec = timeout_sec;
        tv.tv_usec = 0;

        if (!proxy_recv_char(&c, "recv_line", sd, &tv, signal_received))
        {
            return false;
        }

#if 0
        if (isprint(c))
        {
            msg(M_INFO, "PROXY: read '%c' (%d)", c, (int)c);
        }
        else
        {
            msg(M_INFO, "PROXY: read (%d)", (int)c);
        }
#endif

        /* store char in buffer */
        if (len > 1)
        {
            *buf++ = c;
            --len;
        }

        /* also store char in lookahead buffer */
        if (buf_defined(&la))
        {
            buf_write_u8(&la, c);
            if (!isprint(c) && !isspace(c)) /* not ascii? */
            {
                if (verbose)
                {
                    msg(D_LINK_ERRORS | M_ERRNO,
                        "recv_line: Non-ASCII character (%d) read on recv()", (int)c);
                }
                *lookahead = la;
                return false;
            }
        }

        /* end of line? */
        if (lastc == '\r' && c == '\n')
        {
            break;
        }

        lastc = c;
    }

    /* append trailing null */
    if (len > 0)
    {
        *buf++ = '\0';
    }

    return true;
}

bool
proxy_send(socket_descriptor_t sd, const void *buf, size_t buf_len)
{
    const ssize_t size = openvpn_send(sd, buf, buf_len, MSG_NOSIGNAL);
    if (size != (ssize_t)buf_len)
    {
        msg(D_LINK_ERRORS | M_ERRNO, "proxy_send: TCP port write failed on send()");
        return false;
    }
    return true;
}

static bool
send_line_crlf(socket_descriptor_t sd, const char *src)
{
    bool ret;

    struct buffer buf = alloc_buf(strlen(src) + 2);
    ASSERT(buf_write(&buf, src, strlen(src)));
    ASSERT(buf_write(&buf, "\r\n", 2));
    ret = proxy_send(sd, BSTR(&buf), BLEN(&buf));
    free_buf(&buf);
    return ret;
}

static bool
send_crlf(socket_descriptor_t sd)
{
    return send_line_crlf(sd, "");
}

uint8_t *
make_base64_string2(const uint8_t *str, int src_len, struct gc_arena *gc)
{
    uint8_t *ret = NULL;
    char *b64out = NULL;
    ASSERT(openvpn_base64_encode((const void *)str, src_len, &b64out) >= 0);
    ret = (uint8_t *)string_alloc(b64out, gc);
    free(b64out);
    return ret;
}

uint8_t *
make_base64_string(const uint8_t *str, struct gc_arena *gc)
{
    return make_base64_string2(str, (int)strlen((const char *)str), gc);
}

static const char *
username_password_as_base64(const struct http_proxy_info *p, struct gc_arena *gc)
{
    struct buffer out = alloc_buf_gc(strlen(p->up.username) + strlen(p->up.password) + 2, gc);
    ASSERT(strlen(p->up.username) > 0);
    buf_printf(&out, "%s:%s", p->up.username, p->up.password);
    char *ret = (char *)make_base64_string((const uint8_t *)BSTR(&out), gc);
    secure_memzero(BSTR(&out), out.len);
    return ret;
}

static void
clear_user_pass_http(void)
{
    purge_user_pass(&static_proxy_user_pass, true);
}

static void
get_user_pass_http(struct http_proxy_info *p, const bool force)
{
    /*
     * in case of forced (re)load, make sure the static storage is set as
     * undefined, otherwise get_user_pass() won't try to load any credential
     */
    if (force)
    {
        clear_user_pass_http();
    }

    if (!static_proxy_user_pass.defined)
    {
        unsigned int flags = GET_USER_PASS_MANAGEMENT;
        const char *auth_file = p->options.auth_file;
        if (p->options.auth_file_up)
        {
            auth_file = p->options.auth_file_up;
        }
        if (p->queried_creds && !static_proxy_user_pass.nocache)
        {
            flags |= GET_USER_PASS_PREVIOUS_CREDS_FAILED;
        }
        if (p->options.inline_creds)
        {
            flags |= GET_USER_PASS_INLINE_CREDS;
        }
        get_user_pass(&static_proxy_user_pass, auth_file, UP_TYPE_PROXY, flags);
        static_proxy_user_pass.nocache = p->options.nocache;
        protect_user_pass(&static_proxy_user_pass);
    }

    /*
     * Using cached credentials
     */
    p->queried_creds = true;
    p->up = static_proxy_user_pass; /* this is a copy of protected memory */
}

#if 0
/* function only used in #if 0 debug statement */
static void
dump_residual(socket_descriptor_t sd,
              int timeout,
              volatile int *signal_received)
{
    char buf[256];
    while (true)
    {
        if (!recv_line(sd, buf, sizeof(buf), timeout, true, NULL, signal_received))
        {
            return;
        }
        chomp(buf);
        msg(D_PROXY, "PROXY HEADER: '%s'", buf);
    }
}
#endif

/*
 * Extract the Proxy-Authenticate header from the stream.
 * Consumes all headers.
 */
static int
get_proxy_authenticate(socket_descriptor_t sd, int timeout, char **data,
                       volatile int *signal_received)
{
    char buf[256];
    int ret = HTTP_AUTH_NONE;
    while (true)
    {
        if (!recv_line(sd, buf, sizeof(buf), timeout, true, NULL, signal_received))
        {
            free(*data);
            *data = NULL;
            return HTTP_AUTH_NONE;
        }
        chomp(buf);
        if (!strlen(buf))
        {
            return ret;
        }
        if (ret == HTTP_AUTH_NONE && !strncmp(buf, "Proxy-Authenticate: ", 20))
        {
            if (!strncmp(buf + 20, "Basic ", 6))
            {
                msg(D_PROXY, "PROXY AUTH BASIC: '%s'", buf);
                *data = string_alloc(buf + 26, NULL);
                ret = HTTP_AUTH_BASIC;
            }
#if PROXY_DIGEST_AUTH
            else if (!strncmp(buf + 20, "Digest ", 7))
            {
                msg(D_PROXY, "PROXY AUTH DIGEST: '%s'", buf);
                *data = string_alloc(buf + 27, NULL);
                ret = HTTP_AUTH_DIGEST;
            }
#endif
        }
    }
}

static void
store_proxy_authenticate(struct http_proxy_info *p, char *data)
{
    free(p->proxy_authenticate);
    p->proxy_authenticate = data;
}

/*
 * Parse out key/value pairs from Proxy-Authenticate string.
 * Return true on success, or false on parse failure.
 */
static bool
get_key_value(const char *str,                                         /* source string */
              char *key,                                               /* key stored here */
              char *value,                                             /* value stored here */
              int max_key_len, int max_value_len, const char **endptr) /* next search position */
{
    int c;
    bool starts_with_quote = false;
    bool escape = false;

    for (c = max_key_len - 1; (*str && (*str != '=') && c--);)
    {
        *key++ = *str++;
    }
    *key = '\0';

    if ('=' != *str++)
    {
        /* no key/value found */
        return false;
    }

    if ('\"' == *str)
    {
        /* quoted string */
        str++;
        starts_with_quote = true;
    }

    for (c = max_value_len - 1; *str && c--; str++)
    {
        switch (*str)
        {
            case '\\':
                if (!escape)
                {
                    /* possibly the start of an escaped quote */
                    escape = true;
                    *value++ = '\\'; /* even though this is an escape character, we still
                                      * store it as-is in the target buffer */
                    continue;
                }
                break;

            case ',':
                if (!starts_with_quote)
                {
                    /* this signals the end of the value if we didn't get a starting quote
                     * and then we do "sloppy" parsing */
                    c = 0; /* the end */
                    continue;
                }
                break;

            case '\r':
            case '\n':
                /* end of string */
                c = 0;
                continue;

            case '\"':
                if (!escape && starts_with_quote)
                {
                    /* end of string */
                    c = 0;
                    continue;
                }
                break;
        }
        escape = false;
        *value++ = *str;
    }
    *value = '\0';

    *endptr = str;

    return true; /* success */
}

static char *
get_pa_var(const char *key, const char *pa, struct gc_arena *gc)
{
    char k[64];
    char v[256];
    const char *content = pa;

    while (true)
    {
        const int status = get_key_value(content, k, v, sizeof(k), sizeof(v), &content);
        if (status)
        {
            if (!strcmp(key, k))
            {
                return string_alloc(v, gc);
            }
        }
        else
        {
            return NULL;
        }

        /* advance to start of next key */
        if (*content == ',')
        {
            ++content;
        }
        while (*content && isspace(*content))
        {
            ++content;
        }
    }
}

struct http_proxy_info *
http_proxy_new(const struct http_proxy_options *o)
{
    struct http_proxy_info *p;

    if (!o || !o->server)
    {
        msg(M_FATAL, "HTTP_PROXY: server not specified");
    }

    ASSERT(o->port);

    ALLOC_OBJ_CLEAR(p, struct http_proxy_info);
    p->options = *o;

    /* parse authentication method */
    p->auth_method = HTTP_AUTH_NONE;
    if (o->auth_method_string)
    {
        if (!strcmp(o->auth_method_string, "none"))
        {
            p->auth_method = HTTP_AUTH_NONE;
        }
        else if (!strcmp(o->auth_method_string, "basic"))
        {
            p->auth_method = HTTP_AUTH_BASIC;
        }
        else
        {
            msg(M_FATAL, "ERROR: unknown HTTP authentication method: '%s'", o->auth_method_string);
        }
    }

    /* When basic authentication is requested, get credentials now.
     * In case of "auto" negotiation credentials will be retrieved later once
     * we know whether we need any. */
    if (p->auth_method == HTTP_AUTH_BASIC)
    {
        get_user_pass_http(p, p->options.first_time);
    }

    p->defined = true;
    return p;
}

void
http_proxy_close(struct http_proxy_info *hp)
{
    free(hp);
}

static bool
add_proxy_headers(struct http_proxy_info *p, socket_descriptor_t sd, /* already open to proxy */
                  const char *host                                   /* openvpn server remote */
)
{
    char buf[512];
    int i;
    bool host_header_sent = false;

    /*
     * Send custom headers if provided
     * If content is NULL the whole header is in name
     * Also remember if we already sent a Host: header
     */
    for (i = 0; i < MAX_CUSTOM_HTTP_HEADER && p->options.custom_headers[i].name; i++)
    {
        if (p->options.custom_headers[i].content)
        {
            snprintf(buf, sizeof(buf), "%s: %s", p->options.custom_headers[i].name,
                     p->options.custom_headers[i].content);
            if (!strcasecmp(p->options.custom_headers[i].name, "Host"))
            {
                host_header_sent = true;
            }
        }
        else
        {
            snprintf(buf, sizeof(buf), "%s", p->options.custom_headers[i].name);
            if (!strncasecmp(p->options.custom_headers[i].name, "Host:", 5))
            {
                host_header_sent = true;
            }
        }

        msg(D_PROXY, "Send to HTTP proxy: '%s'", buf);
        if (!send_line_crlf(sd, buf))
        {
            return false;
        }
    }

    if (!host_header_sent)
    {
        snprintf(buf, sizeof(buf), "Host: %s", host);
        msg(D_PROXY, "Send to HTTP proxy: '%s'", buf);
        if (!send_line_crlf(sd, buf))
        {
            return false;
        }
    }

    /* send User-Agent string if provided */
    if (p->options.user_agent)
    {
        snprintf(buf, sizeof(buf), "User-Agent: %s", p->options.user_agent);
        msg(D_PROXY, "Send to HTTP proxy: '%s'", buf);
        if (!send_line_crlf(sd, buf))
        {
            return false;
        }
    }

    return true;
}


bool
establish_http_proxy_passthru(struct http_proxy_info *p,
                              socket_descriptor_t sd, /* already open to proxy */
                              const char *host,       /* openvpn server remote */
                              const char *port,       /* openvpn server port */
                              struct event_timeout *server_poll_timeout, struct buffer *lookahead,
                              struct signal_info *sig_info)
{
    struct gc_arena gc = gc_new();
    char buf[512];
    int status;
    int nparms;
    bool ret = false;
    bool processed = false;
    volatile int *signal_received = &sig_info->signal_received;

    /* get user/pass if not previously given */
    if (p->auth_method == HTTP_AUTH_BASIC || p->auth_method == HTTP_AUTH_DIGEST)
    {
        get_user_pass_http(p, false);

        if (p->up.nocache)
        {
            clear_user_pass_http();
        }
        unprotect_user_pass(&p->up);
    }

    /* are we being called again after getting the digest server nonce in the previous transaction?
     */
    if (p->auth_method == HTTP_AUTH_DIGEST && p->proxy_authenticate)
    {
        nparms = 1;
        status = 407;
    }
    else
    {
        /* format HTTP CONNECT message */
        snprintf(buf, sizeof(buf), "CONNECT %s:%s HTTP/%s", host, port, p->options.http_version);

        msg(D_PROXY, "Send to HTTP proxy: '%s'", buf);

        /* send HTTP CONNECT message to proxy */
        if (!send_line_crlf(sd, buf))
        {
            goto error;
        }

        if (!add_proxy_headers(p, sd, host))
        {
            goto error;
        }

        /* auth specified? */
        switch (p->auth_method)
        {
            case HTTP_AUTH_NONE:
                break;

            case HTTP_AUTH_BASIC:
                snprintf(buf, sizeof(buf), "Proxy-Authorization: Basic %s",
                         username_password_as_base64(p, &gc));
                msg(D_PROXY, "Attempting Basic Proxy-Authorization");
                dmsg(D_SHOW_KEYS, "Send to HTTP proxy: '%s'", buf);
                if (!send_line_crlf(sd, buf))
                {
                    goto error;
                }
                break;

            default:
                ASSERT(0);
        }

        /* clear any sensitive content in buf */
        secure_memzero(buf, sizeof(buf));

        /* send empty CR, LF */
        if (!send_crlf(sd))
        {
            goto error;
        }

        /* receive reply from proxy */
        if (!recv_line(sd, buf, sizeof(buf), get_server_poll_remaining_time(server_poll_timeout),
                       true, NULL, signal_received))
        {
            goto error;
        }

        /* remove trailing CR, LF */
        chomp(buf);

        msg(D_PROXY, "HTTP proxy returned: '%s'", buf);

        /* parse return string */
        nparms = sscanf(buf, "%*s %d", &status);
    }

    /* check for a "407 Proxy Authentication Required" response */
    while (nparms >= 1 && status == 407)
    {
        msg(D_PROXY, "Proxy requires authentication");

        if (p->auth_method == HTTP_AUTH_BASIC && !processed)
        {
            processed = true;
        }
#if PROXY_DIGEST_AUTH
        else if (p->auth_method == HTTP_AUTH_DIGEST && !processed)
        {
            char *pa = p->proxy_authenticate;
            const int method = p->auth_method;
            ASSERT(pa);

            if (method == HTTP_AUTH_DIGEST)
            {
                const char *http_method = "CONNECT";
                const char *nonce_count = "00000001";
                const char *qop = "auth";
                const char *username = p->up.username;
                const char *password = p->up.password;
                char *opaque_kv = "";
                char uri[128];
                uint8_t cnonce_raw[8];
                uint8_t *cnonce;
                HASHHEX session_key;
                HASHHEX response;

                const char *realm = get_pa_var("realm", pa, &gc);
                const char *nonce = get_pa_var("nonce", pa, &gc);
                const char *algor = get_pa_var("algorithm", pa, &gc);
                const char *opaque = get_pa_var("opaque", pa, &gc);

                if (!realm || !nonce)
                {
                    msg(D_LINK_ERRORS, "HTTP proxy: digest auth failed, malformed response "
                                       "from server: realm= or nonce= missing");
                    goto error;
                }

                /* generate a client nonce */
                ASSERT(rand_bytes(cnonce_raw, sizeof(cnonce_raw)));
                cnonce = make_base64_string2(cnonce_raw, sizeof(cnonce_raw), &gc);


                /* build the digest response */
                snprintf(uri, sizeof(uri), "%s:%s", host, port);

                if (opaque)
                {
                    const size_t len = strlen(opaque) + 16;
                    opaque_kv = gc_malloc(len, false, &gc);
                    snprintf(opaque_kv, len, ", opaque=\"%s\"", opaque);
                }

                DigestCalcHA1(algor, username, realm, password, nonce, (char *)cnonce, session_key);
                DigestCalcResponse(session_key, nonce, nonce_count, (char *)cnonce, qop,
                                   http_method, uri, NULL, response);

                /* format HTTP CONNECT message */
                snprintf(buf, sizeof(buf), "%s %s HTTP/%s", http_method, uri,
                         p->options.http_version);

                msg(D_PROXY, "Send to HTTP proxy: '%s'", buf);

                /* send HTTP CONNECT message to proxy */
                if (!send_line_crlf(sd, buf))
                {
                    goto error;
                }

                /* send HOST etc, */
                if (!add_proxy_headers(p, sd, host))
                {
                    goto error;
                }

                /* send digest response */
                if (!checked_snprintf(
                        buf, sizeof(buf),
                        "Proxy-Authorization: Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", qop=%s, nc=%s, cnonce=\"%s\", response=\"%s\"%s",
                        username, realm, nonce, uri, qop, nonce_count, cnonce, response, opaque_kv))
                {
                    goto error;
                }

                msg(D_PROXY, "Send to HTTP proxy: '%s'", buf);
                if (!send_line_crlf(sd, buf))
                {
                    goto error;
                }
                if (!send_crlf(sd))
                {
                    goto error;
                }
                /* clear any sensitive content in buf */
                secure_memzero(buf, sizeof(buf));

                /* receive reply from proxy */
                if (!recv_line(sd, buf, sizeof(buf),
                               get_server_poll_remaining_time(server_poll_timeout), true, NULL,
                               signal_received))
                {
                    goto error;
                }

                /* remove trailing CR, LF */
                chomp(buf);

                msg(D_PROXY, "HTTP proxy returned: '%s'", buf);

                /* parse return string */
                nparms = sscanf(buf, "%*s %d", &status);
                processed = true;
            }
            else
            {
                msg(D_PROXY, "HTTP proxy: digest method not supported");
                goto error;
            }
        }
#endif /* if PROXY_DIGEST_AUTH */
        else if (p->options.auth_retry)
        {
            /* figure out what kind of authentication the proxy needs */
            char *pa = NULL;
            const int method = get_proxy_authenticate(
                sd, get_server_poll_remaining_time(server_poll_timeout), &pa, signal_received);
            if (method != HTTP_AUTH_NONE)
            {
                if (pa)
                {
                    msg(D_PROXY, "HTTP proxy authenticate '%s'", pa);
                }
                if (p->options.auth_retry == PAR_NCT && method == HTTP_AUTH_BASIC)
                {
                    msg(D_PROXY,
                        "HTTP proxy: support for basic auth and other cleartext proxy auth methods is disabled");
                    free(pa);
                    goto error;
                }
                p->auth_method = method;
                store_proxy_authenticate(p, pa);
                ret = true;
                goto done;
            }
            else
            {
                msg(D_PROXY,
                    "HTTP proxy: do not recognize the authentication method required by proxy");
                free(pa);
                goto error;
            }
        }
        else
        {
            if (!processed)
            {
                msg(D_PROXY, "HTTP proxy: no support for proxy authentication method");
            }
            goto error;
        }
    }

    /* check return code, success = 200 */
    if (nparms < 1 || status != 200)
    {
        msg(D_LINK_ERRORS, "HTTP proxy returned bad status");
#if 0
        /* DEBUGGING -- show a multi-line HTTP error response */
        dump_residual(sd, get_server_poll_remaining_time(server_poll_timeout), signal_received);
#endif
        goto error;
    }

    /* SUCCESS */

    /* receive line from proxy and discard */
    if (!recv_line(sd, NULL, 0, get_server_poll_remaining_time(server_poll_timeout), true, NULL,
                   signal_received))
    {
        goto error;
    }

    /*
     * Toss out any extraneous chars, but don't throw away the
     * start of the OpenVPN data stream (put it in lookahead).
     */
    while (recv_line(sd, NULL, 0, 2, false, lookahead, signal_received))
    {
    }

    /* reset queried_creds so that we don't think that the next creds request is due to an auth
     * error */
    p->queried_creds = false;

#if 0
    if (lookahead && BLEN(lookahead))
    {
        msg(M_INFO, "HTTP PROXY: lookahead: %s", format_hex(BPTR(lookahead), BLEN(lookahead), 0));
    }
#endif

done:
    purge_user_pass(&p->up, true);
    gc_free(&gc);
    return ret;

error:
    purge_user_pass(&p->up, true);
    register_signal(sig_info, SIGUSR1, "HTTP proxy error"); /* SOFT-SIGUSR1 -- HTTP proxy error */
    gc_free(&gc);
    return ret;
}
