/*
 *  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-2018 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, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#elif defined(_MSC_VER)
#include "config-msvc.h"
#endif

#include "syshead.h"

#include "push.h"
#include "options.h"
#include "ssl.h"
#include "ssl_verify.h"
#include "manage.h"

#include "memdbg.h"

#if P2MP

static char push_reply_cmd[] = "PUSH_REPLY";

/*
 * Auth username/password
 *
 * Client received an authentication failed message from server.
 * Runs on client.
 */
void
receive_auth_failed(struct context *c, const struct buffer *buffer)
{
    msg(M_VERB0, "AUTH: Received control message: %s", BSTR(buffer));
    c->options.no_advance = true;

    if (c->options.pull)
    {
        /* Before checking how to react on AUTH_FAILED, first check if the
         * failed auth might be the result of an expired auth-token.
         * Note that a server restart will trigger a generic AUTH_FAILED
         * instead an AUTH_FAILED,SESSION so handle all AUTH_FAILED message
         * identical for this scenario */
        if (ssl_clean_auth_token())
        {
            c->sig->signal_received = SIGUSR1; /* SOFT-SIGUSR1 -- Auth failure error */
            c->sig->signal_text = "auth-failure (auth-token)";
        }
        else
        {
            switch (auth_retry_get())
            {
            case AR_NONE:
                c->sig->signal_received = SIGTERM; /* SOFT-SIGTERM -- Auth failure error */
                break;

            case AR_INTERACT:
                ssl_purge_auth(false);

            case AR_NOINTERACT:
                c->sig->signal_received = SIGUSR1; /* SOFT-SIGUSR1 -- Auth failure error */
                break;

            default:
                ASSERT(0);
            }
            c->sig->signal_text = "auth-failure";
        }
#ifdef ENABLE_MANAGEMENT
        if (management)
        {
            const char *reason = NULL;
            struct buffer buf = *buffer;
            if (buf_string_compare_advance(&buf, "AUTH_FAILED,") && BLEN(&buf))
            {
                reason = BSTR(&buf);
            }
            management_auth_failure(management, UP_TYPE_AUTH, reason);
        }
#endif
        /*
         * Save the dynamic-challenge text even when management is defined
         */
        {
#ifdef ENABLE_CLIENT_CR
            struct buffer buf = *buffer;
            if (buf_string_match_head_str(&buf, "AUTH_FAILED,CRV1:") && BLEN(&buf))
            {
                buf_advance(&buf, 12); /* Length of "AUTH_FAILED," substring */
                ssl_put_auth_challenge(BSTR(&buf));
            }
#endif
        }
    }
}

/*
 * Act on received restart message from server
 */
void
server_pushed_signal(struct context *c, const struct buffer *buffer, const bool restart, const int adv)
{
    if (c->options.pull)
    {
        struct buffer buf = *buffer;
        const char *m = "";
        if (buf_advance(&buf, adv) && buf_read_u8(&buf) == ',' && BLEN(&buf))
        {
            m = BSTR(&buf);
        }

        /* preserve cached passwords? */
        /* advance to next server? */
        {
            bool purge = true;

            if (m[0] == '[')
            {
                int i;
                for (i = 1; m[i] != '\0' && m[i] != ']'; ++i)
                {
                    if (m[i] == 'P')
                    {
                        purge = false;
                    }
                    else if (m[i] == 'N')
                    {
                        /* next server? */
                        c->options.no_advance = false;
                    }
                }
            }
            if (purge)
            {
                ssl_purge_auth(true);
            }
        }

        if (restart)
        {
            msg(D_STREAM_ERRORS, "Connection reset command was pushed by server ('%s')", m);
            c->sig->signal_received = SIGUSR1; /* SOFT-SIGUSR1 -- server-pushed connection reset */
            c->sig->signal_text = "server-pushed-connection-reset";
        }
        else
        {
            msg(D_STREAM_ERRORS, "Halt command was pushed by server ('%s')", m);
            c->sig->signal_received = SIGTERM; /* SOFT-SIGTERM -- server-pushed halt */
            c->sig->signal_text = "server-pushed-halt";
        }
#ifdef ENABLE_MANAGEMENT
        if (management)
        {
            management_notify(management, "info", c->sig->signal_text, m);
        }
#endif
    }
}

#if P2MP_SERVER
/**
 * Add an option to the given push list by providing a format string.
 *
 * The string added to the push options is allocated in o->gc, so the caller
 * does not have to preserve anything.
 *
 * @param gc        GC arena where options are allocated
 * @param push_list Push list containing options
 * @param msglevel  The message level to use when printing errors
 * @param fmt       Format string for the option
 * @param ...       Format string arguments
 *
 * @return true on success, false on failure.
 */
static bool push_option_fmt(struct gc_arena *gc, struct push_list *push_list,
                            int msglevel, const char *fmt, ...)
#ifdef __GNUC__
#if __USE_MINGW_ANSI_STDIO
__attribute__ ((format(gnu_printf, 4, 5)))
#else
__attribute__ ((format(__printf__, 4, 5)))
#endif
#endif
;

/*
 * Send auth failed message from server to client.
 */
void
send_auth_failed(struct context *c, const char *client_reason)
{
    struct gc_arena gc = gc_new();
    static const char auth_failed[] = "AUTH_FAILED";
    size_t len;

    schedule_exit(c, c->options.scheduled_exit_interval, SIGTERM);

    len = (client_reason ? strlen(client_reason)+1 : 0) + sizeof(auth_failed);
    if (len > PUSH_BUNDLE_SIZE)
    {
        len = PUSH_BUNDLE_SIZE;
    }

    {
        struct buffer buf = alloc_buf_gc(len, &gc);
        buf_printf(&buf, auth_failed);
        if (client_reason)
        {
            buf_printf(&buf, ",%s", client_reason);
        }
        send_control_channel_string(c, BSTR(&buf), D_PUSH);
    }

    gc_free(&gc);
}

/*
 * Send restart message from server to client.
 */
void
send_restart(struct context *c, const char *kill_msg)
{
    schedule_exit(c, c->options.scheduled_exit_interval, SIGTERM);
    send_control_channel_string(c, kill_msg ? kill_msg : "RESTART", D_PUSH);
}

#endif /* if P2MP_SERVER */

/*
 * Push/Pull
 */

void
incoming_push_message(struct context *c, const struct buffer *buffer)
{
    struct gc_arena gc = gc_new();
    unsigned int option_types_found = 0;
    int status;

    msg(D_PUSH, "PUSH: Received control message: '%s'", sanitize_control_message(BSTR(buffer), &gc));

    status = process_incoming_push_msg(c,
                                       buffer,
                                       c->options.pull,
                                       pull_permission_mask(c),
                                       &option_types_found);

    if (status == PUSH_MSG_ERROR)
    {
        msg(D_PUSH_ERRORS, "WARNING: Received bad push/pull message: %s", sanitize_control_message(BSTR(buffer), &gc));
    }
    else if (status == PUSH_MSG_REPLY || status == PUSH_MSG_CONTINUATION)
    {
        c->options.push_option_types_found |= option_types_found;

        /* delay bringing tun/tap up until --push parms received from remote */
        if (status == PUSH_MSG_REPLY)
        {
            if (!do_up(c, true, c->options.push_option_types_found))
            {
                msg(D_PUSH_ERRORS, "Failed to open tun/tap interface");
                goto error;
            }
        }
        event_timeout_clear(&c->c2.push_request_interval);
    }
    else if (status == PUSH_MSG_REQUEST)
    {
        if (c->options.mode == MODE_SERVER)
        {
            struct tls_session *session = &c->c2.tls_multi->session[TM_ACTIVE];
            /* Do not regenerate keys if client send a second push request */
            if (!session->key[KS_PRIMARY].crypto_options.key_ctx_bi.initialized
                && !tls_session_update_crypto_params(session, &c->options,
                                                     &c->c2.frame))
            {
                msg(D_TLS_ERRORS, "TLS Error: initializing data channel failed");
                goto error;
            }
        }
    }

    goto cleanup;
error:
    register_signal(c, SIGUSR1, "process-push-msg-failed");
cleanup:
    gc_free(&gc);
}

bool
send_push_request(struct context *c)
{
    const int max_push_requests = c->options.handshake_window / PUSH_REQUEST_INTERVAL;
    if (++c->c2.n_sent_push_requests <= max_push_requests)
    {
        return send_control_channel_string(c, "PUSH_REQUEST", D_PUSH);
    }
    else
    {
        msg(D_STREAM_ERRORS, "No reply from server after sending %d push requests", max_push_requests);
        c->sig->signal_received = SIGUSR1; /* SOFT-SIGUSR1 -- server-pushed connection reset */
        c->sig->signal_text = "no-push-reply";
        return false;
    }
}

#if P2MP_SERVER

/**
 * Prepare push options, based on local options and available peer info.
 *
 * @param context       context structure storing data for VPN tunnel
 * @param gc            gc arena for allocating push options
 * @param push_list     push list to where options are added
 *
 * @return true on success, false on failure.
 */
static bool
prepare_push_reply(struct context *c, struct gc_arena *gc,
                   struct push_list *push_list)
{
    const char *optstr = NULL;
    struct tls_multi *tls_multi = c->c2.tls_multi;
    const char *const peer_info = tls_multi->peer_info;
    struct options *o = &c->options;

    /* ipv6 */
    if (c->c2.push_ifconfig_ipv6_defined && !o->push_ifconfig_ipv6_blocked)
    {
        push_option_fmt(gc, push_list, M_USAGE, "ifconfig-ipv6 %s/%d %s",
                        print_in6_addr(c->c2.push_ifconfig_ipv6_local, 0, gc),
                        c->c2.push_ifconfig_ipv6_netbits,
                        print_in6_addr(c->c2.push_ifconfig_ipv6_remote,
                                       0, gc));
    }

    /* ipv4 */
    if (c->c2.push_ifconfig_defined && c->c2.push_ifconfig_local
        && c->c2.push_ifconfig_remote_netmask)
    {
        in_addr_t ifconfig_local = c->c2.push_ifconfig_local;
        if (c->c2.push_ifconfig_local_alias)
        {
            ifconfig_local = c->c2.push_ifconfig_local_alias;
        }
        push_option_fmt(gc, push_list, M_USAGE, "ifconfig %s %s",
                        print_in_addr_t(ifconfig_local, 0, gc),
                        print_in_addr_t(c->c2.push_ifconfig_remote_netmask,
                                        0, gc));
    }

    /* Send peer-id if client supports it */
    optstr = peer_info ? strstr(peer_info, "IV_PROTO=") : NULL;
    if (optstr)
    {
        int proto = 0;
        int r = sscanf(optstr, "IV_PROTO=%d", &proto);
        if ((r == 1) && (proto >= 2))
        {
            push_option_fmt(gc, push_list, M_USAGE, "peer-id %d",
                            tls_multi->peer_id);
            tls_multi->use_peer_id = true;
        }
    }

    /* Push cipher if client supports Negotiable Crypto Parameters */
    if (tls_peer_info_ncp_ver(peer_info) >= 2 && o->ncp_enabled)
    {
        /* if we have already created our key, we cannot *change* our own
         * cipher -> so log the fact and push the "what we have now" cipher
         * (so the client is always told what we expect it to use)
         */
        const struct tls_session *session = &tls_multi->session[TM_ACTIVE];
        if (session->key[KS_PRIMARY].crypto_options.key_ctx_bi.initialized)
        {
            msg( M_INFO, "PUSH: client wants to negotiate cipher (NCP), but "
                 "server has already generated data channel keys, "
                 "re-sending previously negotiated cipher '%s'",
                 o->ciphername );
        }
        else
        {
            /* Push the first cipher from --ncp-ciphers to the client.
             * TODO: actual negotiation, instead of server dictatorship. */
            char *push_cipher = string_alloc(o->ncp_ciphers, &o->gc);
            o->ciphername = strtok(push_cipher, ":");
        }
        push_option_fmt(gc, push_list, M_USAGE, "cipher %s", o->ciphername);
    }
    else if (o->ncp_enabled)
    {
        tls_poor_mans_ncp(o, tls_multi->remote_ciphername);
    }

    /* If server uses --auth-gen-token and we have an auth token
     * to send to the client
     */
    if (false == tls_multi->auth_token_sent && NULL != tls_multi->auth_token)
    {
        push_option_fmt(gc, push_list, M_USAGE,
                        "auth-token %s", tls_multi->auth_token);
        tls_multi->auth_token_sent = true;
    }
    return true;
}

static bool
send_push_options(struct context *c, struct buffer *buf,
                  struct push_list *push_list, int safe_cap,
                  bool *push_sent, bool *multi_push)
{
    struct push_entry *e = push_list->head;

    while (e)
    {
        if (e->enable)
        {
            const int l = strlen(e->option);
            if (BLEN(buf) + l >= safe_cap)
            {
                buf_printf(buf, ",push-continuation 2");
                {
                    const bool status = send_control_channel_string(c, BSTR(buf), D_PUSH);
                    if (!status)
                    {
                        return false;
                    }
                    *push_sent = true;
                    *multi_push = true;
                    buf_reset_len(buf);
                    buf_printf(buf, "%s", push_reply_cmd);
                }
            }
            if (BLEN(buf) + l >= safe_cap)
            {
                msg(M_WARN, "--push option is too long");
                return false;
            }
            buf_printf(buf, ",%s", e->option);
        }
        e = e->next;
    }
    return true;
}

static bool
send_push_reply(struct context *c, struct push_list *per_client_push_list)
{
    struct gc_arena gc = gc_new();
    struct buffer buf = alloc_buf_gc(PUSH_BUNDLE_SIZE, &gc);
    bool multi_push = false;
    const int extra = 84; /* extra space for possible trailing ifconfig and push-continuation */
    const int safe_cap = BCAP(&buf) - extra;
    bool push_sent = false;

    buf_printf(&buf, "%s", push_reply_cmd);

    /* send options which are common to all clients */
    if (!send_push_options(c, &buf, &c->options.push_list, safe_cap,
                           &push_sent, &multi_push))
    {
        goto fail;
    }

    /* send client-specific options */
    if (!send_push_options(c, &buf, per_client_push_list, safe_cap,
                           &push_sent, &multi_push))
    {
        goto fail;
    }

    if (multi_push)
    {
        buf_printf(&buf, ",push-continuation 1");
    }

    if (BLEN(&buf) > sizeof(push_reply_cmd)-1)
    {
        const bool status = send_control_channel_string(c, BSTR(&buf), D_PUSH);
        if (!status)
        {
            goto fail;
        }
        push_sent = true;
    }

    /* If nothing have been pushed, send an empty push,
     * as the client is expecting a response
     */
    if (!push_sent)
    {
        bool status = false;

        buf_reset_len(&buf);
        buf_printf(&buf, "%s", push_reply_cmd);
        status = send_control_channel_string(c, BSTR(&buf), D_PUSH);
        if (!status)
        {
            goto fail;
        }
    }

    gc_free(&gc);
    return true;

fail:
    gc_free(&gc);
    return false;
}

static void
push_option_ex(struct gc_arena *gc, struct push_list *push_list,
               const char *opt, bool enable, int msglevel)
{
    if (!string_class(opt, CC_ANY, CC_COMMA))
    {
        msg(msglevel, "PUSH OPTION FAILED (illegal comma (',') in string): '%s'", opt);
    }
    else
    {
        struct push_entry *e;
        ALLOC_OBJ_CLEAR_GC(e, struct push_entry, gc);
        e->enable = true;
        e->option = opt;
        if (push_list->head)
        {
            ASSERT(push_list->tail);
            push_list->tail->next = e;
            push_list->tail = e;
        }
        else
        {
            ASSERT(!push_list->tail);
            push_list->head = e;
            push_list->tail = e;
        }
    }
}

void
push_option(struct options *o, const char *opt, int msglevel)
{
    push_option_ex(&o->gc, &o->push_list, opt, true, msglevel);
}

void
clone_push_list(struct options *o)
{
    if (o->push_list.head)
    {
        const struct push_entry *e = o->push_list.head;
        push_reset(o);
        while (e)
        {
            push_option_ex(&o->gc, &o->push_list,
                           string_alloc(e->option, &o->gc), true, M_FATAL);
            e = e->next;
        }
    }
}

void
push_options(struct options *o, char **p, int msglevel, struct gc_arena *gc)
{
    const char **argv = make_extended_arg_array(p, gc);
    char *opt = print_argv(argv, gc, 0);
    push_option(o, opt, msglevel);
}

static bool
push_option_fmt(struct gc_arena *gc, struct push_list *push_list,
                int msglevel, const char *format, ...)
{
    va_list arglist;
    char tmp[256] = {0};
    int len;
    va_start(arglist, format);
    len = vsnprintf(tmp, sizeof(tmp), format, arglist);
    va_end(arglist);
    if (len > sizeof(tmp)-1)
    {
        return false;
    }
    push_option_ex(gc, push_list, string_alloc(tmp, gc), true, msglevel);
    return true;
}

void
push_reset(struct options *o)
{
    CLEAR(o->push_list);
}

void
push_remove_option(struct options *o, const char *p)
{
    msg(D_PUSH_DEBUG, "PUSH_REMOVE searching for: '%s'", p);

    /* ifconfig-ipv6 is special, as not part of the push list */
    if (streq( p, "ifconfig-ipv6" ))
    {
        o->push_ifconfig_ipv6_blocked = true;
        return;
    }

    if (o && o->push_list.head)
    {
        struct push_entry *e = o->push_list.head;

        /* cycle through the push list */
        while (e)
        {
            if (e->enable
                && strncmp( e->option, p, strlen(p) ) == 0)
            {
                msg(D_PUSH_DEBUG, "PUSH_REMOVE removing: '%s'", e->option);
                e->enable = false;
            }

            e = e->next;
        }
    }
}
#endif /* if P2MP_SERVER */

#if P2MP_SERVER
int
process_incoming_push_request(struct context *c)
{
    int ret = PUSH_MSG_ERROR;

#ifdef ENABLE_ASYNC_PUSH
    c->c2.push_request_received = true;
#endif
    if (tls_authentication_status(c->c2.tls_multi, 0) == TLS_AUTHENTICATION_FAILED || c->c2.context_auth == CAS_FAILED)
    {
        const char *client_reason = tls_client_reason(c->c2.tls_multi);
        send_auth_failed(c, client_reason);
        ret = PUSH_MSG_AUTH_FAILURE;
    }
    else if (!c->c2.push_reply_deferred && c->c2.context_auth == CAS_SUCCEEDED)
    {
        time_t now;

        openvpn_time(&now);
        if (c->c2.sent_push_reply_expiry > now)
        {
            ret = PUSH_MSG_ALREADY_REPLIED;
        }
        else
        {
            /* per-client push options - peer-id, cipher, ifconfig, ipv6-ifconfig */
            struct push_list push_list;
            struct gc_arena gc = gc_new();

            CLEAR(push_list);
            if (prepare_push_reply(c, &gc, &push_list)
                && send_push_reply(c, &push_list))
            {
                ret = PUSH_MSG_REQUEST;
                c->c2.sent_push_reply_expiry = now + 30;
            }
            gc_free(&gc);
        }
    }
    else
    {
        ret = PUSH_MSG_REQUEST_DEFERRED;
    }

    return ret;
}
#endif /* if P2MP_SERVER */

static void
push_update_digest(md_ctx_t *ctx, struct buffer *buf, const struct options *opt)
{
    char line[OPTION_PARM_SIZE];
    while (buf_parse(buf, ',', line, sizeof(line)))
    {
        /* peer-id might change on restart and this should not trigger reopening tun */
        if (strprefix(line, "peer-id "))
        {
            continue;
        }
        /* tun reopen only needed if cipher change can change tun MTU */
        if (strprefix(line, "cipher ") && !opt->ce.tun_mtu_defined)
        {
            continue;
        }
        md_ctx_update(ctx, (const uint8_t *) line, strlen(line)+1);
    }
}

int
process_incoming_push_msg(struct context *c,
                          const struct buffer *buffer,
                          bool honor_received_options,
                          unsigned int permission_mask,
                          unsigned int *option_types_found)
{
    int ret = PUSH_MSG_ERROR;
    struct buffer buf = *buffer;

#if P2MP_SERVER
    if (buf_string_compare_advance(&buf, "PUSH_REQUEST"))
    {
        ret = process_incoming_push_request(c);
    }
    else
#endif

    if (honor_received_options && buf_string_compare_advance(&buf, "PUSH_REPLY"))
    {
        const uint8_t ch = buf_read_u8(&buf);
        if (ch == ',')
        {
            struct buffer buf_orig = buf;
            if (!c->c2.pulled_options_digest_init_done)
            {
                c->c2.pulled_options_state = md_ctx_new();
                md_ctx_init(c->c2.pulled_options_state, md_kt_get("SHA256"));
                c->c2.pulled_options_digest_init_done = true;
            }
            if (!c->c2.did_pre_pull_restore)
            {
                pre_pull_restore(&c->options, &c->c2.gc);
                c->c2.did_pre_pull_restore = true;
            }
            if (apply_push_options(&c->options,
                                   &buf,
                                   permission_mask,
                                   option_types_found,
                                   c->c2.es))
            {
                push_update_digest(c->c2.pulled_options_state, &buf_orig,
                                   &c->options);
                switch (c->options.push_continuation)
                {
                    case 0:
                    case 1:
                        md_ctx_final(c->c2.pulled_options_state, c->c2.pulled_options_digest.digest);
                        md_ctx_cleanup(c->c2.pulled_options_state);
                        md_ctx_free(c->c2.pulled_options_state);
                        c->c2.pulled_options_state = NULL;
                        c->c2.pulled_options_digest_init_done = false;
                        ret = PUSH_MSG_REPLY;
                        break;

                    case 2:
                        ret = PUSH_MSG_CONTINUATION;
                        break;
                }
            }
        }
        else if (ch == '\0')
        {
            ret = PUSH_MSG_REPLY;
        }
        /* show_settings (&c->options); */
    }
    return ret;
}

#if P2MP_SERVER

/*
 * Remove iroutes from the push_list.
 */
void
remove_iroutes_from_push_route_list(struct options *o)
{
    if (o && o->push_list.head && o->iroutes)
    {
        struct gc_arena gc = gc_new();
        struct push_entry *e = o->push_list.head;

        /* cycle through the push list */
        while (e)
        {
            char *p[MAX_PARMS];
            bool enable = true;

            /* parse the push item */
            CLEAR(p);
            if (e->enable
                && parse_line(e->option, p, SIZE(p), "[PUSH_ROUTE_REMOVE]", 1, D_ROUTE_DEBUG, &gc))
            {
                /* is the push item a route directive? */
                if (p[0] && !strcmp(p[0], "route") && !p[3])
                {
                    /* get route parameters */
                    bool status1, status2;
                    const in_addr_t network = getaddr(GETADDR_HOST_ORDER, p[1], 0, &status1, NULL);
                    const in_addr_t netmask = getaddr(GETADDR_HOST_ORDER, p[2] ? p[2] : "255.255.255.255", 0, &status2, NULL);

                    /* did route parameters parse correctly? */
                    if (status1 && status2)
                    {
                        const struct iroute *ir;

                        /* does route match an iroute? */
                        for (ir = o->iroutes; ir != NULL; ir = ir->next)
                        {
                            if (network == ir->network && netmask == netbits_to_netmask(ir->netbits >= 0 ? ir->netbits : 32))
                            {
                                enable = false;
                                break;
                            }
                        }
                    }
                }

                /* should we copy the push item? */
                e->enable = enable;
                if (!enable)
                {
                    msg(D_PUSH, "REMOVE PUSH ROUTE: '%s'", e->option);
                }
            }

            e = e->next;
        }

        gc_free(&gc);
    }
}

#endif /* if P2MP_SERVER */

#endif /* if P2MP */
