/*
 * SPDX-FileCopyrightText: 2024-2026 Sébastien Helleu <flashcode@flashtux.org>
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 *
 * This file is part of WeeChat, the extensible chat client.
 *
 * WeeChat 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 3 of the License, or
 * (at your option) any later version.
 *
 * WeeChat 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 WeeChat.  If not, see <https://www.gnu.org/licenses/>.
 */

/* Network functions for relay remote */

#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <gcrypt.h>

#include <gnutls/gnutls.h>
#include <gnutls/x509.h>
#include <cjson/cJSON.h>

#include "../../../weechat-plugin.h"
#include "../../relay.h"
#include "../../relay-auth.h"
#include "../../relay-config.h"
#include "../../relay-http.h"
#include "../../relay-raw.h"
#include "../../relay-remote.h"
#include "../../relay-websocket.h"
#include "../relay-api.h"
#include "relay-remote-event.h"


/*
 * Get URL to an API resource.
 *
 * For example if remote URL is "https://localhost:9000" and the resource is
 * "handshake", it returns: "https://localhost:9000/api/handshake".
 *
 * Note: result must be freed after use.
 */

char *
relay_remote_network_get_url_resource (struct t_relay_remote *remote,
                                       const char *resource)
{
    char *url;
    int colon_in_address;

    if (!remote || !remote->address || !resource || !resource[0])
        return NULL;

    colon_in_address = (strchr (remote->address, ':')) ? 1 : 0;
    weechat_asprintf (&url, "%s://%s%s%s:%d/api/%s",
                      (remote->tls) ? "https" : "http",
                      (colon_in_address) ? "[" : "",
                      remote->address,
                      (colon_in_address) ? "]" : "",
                      remote->port,
                      resource);

    return url;
}

/*
 * Close connection with remote.
 */

void
relay_remote_network_close_connection (struct t_relay_remote *remote)
{
    if (!remote)
        return;

    if (remote->hook_url_handshake)
    {
        weechat_unhook (remote->hook_url_handshake);
        remote->hook_url_handshake = NULL;
    }
    if (remote->hook_connect)
    {
        weechat_unhook (remote->hook_connect);
        remote->hook_connect = NULL;
    }
    if (remote->hook_fd)
    {
        weechat_unhook (remote->hook_fd);
        remote->hook_fd = NULL;
    }
    if (remote->sock != -1)
    {
#ifdef _WIN32
        closesocket (remote->sock);
#else
        close (remote->sock);
#endif /* _WIN32 */
        remote->sock = -1;
    }
    relay_websocket_deflate_reinit (remote->ws_deflate);
    remote->version_ok = 0;
    remote->synced = 0;
    if (remote->partial_ws_frame)
    {
        free (remote->partial_ws_frame);
        remote->partial_ws_frame = NULL;
    }
    remote->partial_ws_frame_size = 0;
}

/*
 * Disconnect from remote.
 */

void
relay_remote_network_disconnect (struct t_relay_remote *remote)
{
    if (!remote)
        return;

    relay_remote_network_close_connection (remote);
    relay_remote_set_status (remote, RELAY_STATUS_DISCONNECTED);
    weechat_printf (NULL, _("remote[%s]: disconnected"), remote->name);
    relay_remote_reconnect_schedule (remote);
}

/*
 * Check if authentication via websocket handshake was successful.
 *
 * Return:
 *   1: authentication successful
 *   0: authentication has failed
 */

int
relay_remote_network_check_auth (struct t_relay_remote *remote,
                                 const char *buffer)
{
    struct t_relay_http_response *http_resp;
    cJSON *json_body, *json_error;
    const char *msg_error, *msg_resp_error, *ptr_ws_accept;
    char *key, hash[160 / 8], sec_websocket_accept[128];
    int accept_ok, hash_size;

    http_resp = NULL;
    json_body = NULL;
    msg_error = NULL;
    msg_resp_error = NULL;
    accept_ok = 0;

    http_resp = relay_http_parse_response (buffer);
    if (!http_resp)
    {
        msg_error = _("invalid response from remote relay");
        goto error;
    }

    if (http_resp->body)
    {
        json_body = cJSON_Parse (http_resp->body);
        if (json_body)
        {
            json_error = cJSON_GetObjectItem (json_body, "error");
            if (json_error && cJSON_IsString (json_error))
                msg_resp_error = cJSON_GetStringValue (json_error);
        }
    }

    if ((http_resp->return_code != 101)
        || (weechat_strcasecmp (http_resp->message, "Switching Protocols") != 0))
    {
        if (http_resp->return_code == 401)
            msg_error = _("authentication failed with remote relay");
        else
            msg_error = _("invalid response from remote relay");
        goto error;
    }

    if (remote->websocket_key)
    {
        ptr_ws_accept = weechat_hashtable_get (http_resp->headers,
                                               "sec-websocket-accept");
        if (ptr_ws_accept)
        {
            if (weechat_asprintf (&key, "%s%s", remote->websocket_key,
                                  WEBSOCKET_GUID) >= 0)
            {
                if (weechat_crypto_hash (key, strlen (key), "sha1",
                                         hash, &hash_size))
                {
                    if (weechat_string_base_encode ("64", hash, hash_size,
                                                    sec_websocket_accept) > 0)
                    {
                        if (strcmp (ptr_ws_accept, sec_websocket_accept) == 0)
                            accept_ok = 1;
                    }
                }
                free (key);
            }
        }
    }

    relay_websocket_parse_extensions (
        weechat_hashtable_get (http_resp->headers, "sec-websocket-extensions"),
        remote->ws_deflate,
        1);  /* ws_deflate_allowed */

    if (!accept_ok)
    {
        msg_error = _("invalid websocket response (handshake error)");
        goto error;
    }

    relay_http_response_free (http_resp);

    if (json_body)
        cJSON_Delete (json_body);

    return 1;

error:
    weechat_printf (
        NULL,
        _("%sremote[%s]: error: %s%s%s%s"),
        weechat_prefix ("error"),
        remote->name,
        msg_error,
        (msg_resp_error) ? " (" : "",
        (msg_resp_error) ? msg_resp_error : "",
        (msg_resp_error) ? ")" : "");
    relay_http_response_free (http_resp);
    if (json_body)
        cJSON_Delete (json_body);
    return 0;
}

/*
 * Send data to the remote.
 *
 * Return the number of bytes sent to the remote.
 */

int
relay_remote_network_send_data (struct t_relay_remote *remote,
                                const char *data, int data_size)
{
    if (!remote)
        return 0;

    if (remote->tls)
    {
        return (remote->sock >= 0) ?
            gnutls_record_send (remote->gnutls_sess, data, data_size) :
            data_size;
    }
    else
    {
        return (remote->sock >= 0) ?
            send (remote->sock, data, data_size, 0) :
            data_size;
    }

}

/*
 * Send data to the remote.
 * If the remote is connected, encapsulate data in a websocket frame.
 *
 * Return the number of bytes sent to the remote.
 */

int
relay_remote_network_send (struct t_relay_remote *remote,
                           enum t_relay_msg_type msg_type,
                           const char *data, int data_size)
{
    const char *ptr_data;
    char *websocket_frame;
    unsigned long long length_frame;
    int opcode, flags, num_sent;

    if (!remote)
        return 0;

    ptr_data = data;
    websocket_frame = NULL;

    if (remote->status == RELAY_STATUS_CONNECTED)
    {
        /* encapsulate data in a websocket frame */
        switch (msg_type)
        {
            case RELAY_MSG_PING:
                opcode = WEBSOCKET_FRAME_OPCODE_PING;
                break;
            case RELAY_MSG_PONG:
                opcode = WEBSOCKET_FRAME_OPCODE_PONG;
                break;
            case RELAY_MSG_CLOSE:
                opcode = WEBSOCKET_FRAME_OPCODE_CLOSE;
                break;
            default:
                opcode = WEBSOCKET_FRAME_OPCODE_TEXT;
                break;
        }
        websocket_frame = relay_websocket_encode_frame (
            remote->ws_deflate, opcode, 1, data, data_size, &length_frame);
        if (websocket_frame)
        {
            ptr_data = websocket_frame;
            data_size = length_frame;
        }
    }

    num_sent = relay_remote_network_send_data (remote, ptr_data, data_size);

    free (websocket_frame);

    if (num_sent >= 0)
    {
        flags = RELAY_RAW_FLAG_SEND;
        if ((msg_type == RELAY_MSG_PING)
            || (msg_type == RELAY_MSG_PONG)
            || (msg_type == RELAY_MSG_CLOSE))
        {
            flags |= RELAY_RAW_FLAG_BINARY;
        }
        relay_raw_print_remote (remote, msg_type, flags, data, data_size);
    }

    return num_sent;
}

/*
 * Send JSON data to the remote.
 *
 * Return the number of bytes sent to the remote.
 */

int
relay_remote_network_send_json (struct t_relay_remote *remote, cJSON *json)
{
    char *string;
    int num_bytes;

    if (!remote || !json)
        return 0;

    num_bytes = 0;

    string = cJSON_PrintUnformatted (json);
    if (string)
    {
        num_bytes = relay_remote_network_send (remote, RELAY_MSG_STANDARD,
                                               string, strlen (string));
        free (string);
    }

    return num_bytes;
}

/*
 * Read text buffer from a remote.
 */

void
relay_remote_network_recv_text (struct t_relay_remote *remote,
                                const char *buffer, int buffer_size)
{
    char request[1024];

    relay_raw_print_remote (remote, RELAY_MSG_STANDARD,
                            RELAY_RAW_FLAG_RECV,
                            buffer, buffer_size);

    if (remote->status == RELAY_STATUS_AUTHENTICATING)
    {
        if (!relay_remote_network_check_auth (remote, buffer))
        {
            relay_remote_network_disconnect (remote);
            return;
        }
        relay_remote_set_status (remote, RELAY_STATUS_CONNECTED);
        remote->reconnect_delay = 0;
        remote->reconnect_start = 0;
        snprintf (request, sizeof (request),
                  "{\"request\": \"GET /api/version\"}");
        relay_remote_network_send (remote, RELAY_MSG_STANDARD,
                                   request, strlen (request));
    }
    else
    {
        relay_remote_event_recv (remote, buffer);
    }
}

/*
 * Read websocket frames.
 */

void
relay_remote_network_read_websocket_frames (struct t_relay_remote *remote,
                                            struct t_relay_websocket_frame *frames,
                                            int num_frames)
{
    int i;

    if (!frames || (num_frames <= 0))
        return;

    for (i = 0; i < num_frames; i++)
    {
        if (frames[i].payload_size == 0)
        {
            /*
             * When decoded length is 0, assume remote sent a PONG frame.
             *
             * RFC 6455 Section 5.5.3:
             *
             *   "A Pong frame MAY be sent unsolicited.  This serves as a
             *   unidirectional heartbeat.  A response to an unsolicited
             *   Pong frame is not expected."
             */
            continue;
        }
        switch (frames[i].opcode)
        {
            case RELAY_MSG_PING:
                /* print message in raw buffer */
                relay_raw_print_remote (remote, RELAY_MSG_PING,
                                        RELAY_RAW_FLAG_RECV | RELAY_RAW_FLAG_BINARY,
                                        frames[i].payload,
                                        frames[i].payload_size);
                /* answer with a PONG */
                relay_remote_network_send (remote,
                                           RELAY_MSG_PONG,
                                           frames[i].payload,
                                           frames[i].payload_size);
                break;
            case RELAY_MSG_CLOSE:
                /* print message in raw buffer */
                relay_raw_print_remote (remote, RELAY_MSG_CLOSE,
                                        RELAY_RAW_FLAG_RECV | RELAY_RAW_FLAG_BINARY,
                                        frames[i].payload,
                                        frames[i].payload_size);
                /* answer with a CLOSE */
                relay_remote_network_send (remote,
                                           RELAY_MSG_CLOSE,
                                           frames[i].payload,
                                           frames[i].payload_size);
                /* close the connection */
                relay_remote_network_disconnect (remote);
                /* ignore any other message after the close */
                return;
            default:
                if (frames[i].payload)
                {
                    relay_remote_network_recv_text (remote,
                                                    frames[i].payload,
                                                    frames[i].payload_size);
                }
                break;
        }
    }
}

/*
 * Read a buffer of bytes from a remote.
 */

void
relay_remote_network_recv_buffer (struct t_relay_remote *remote,
                                  const char *buffer, int buffer_size)
{
    struct t_relay_websocket_frame *frames;
    int rc, i, buffer2_size, num_frames;
    char *buffer2;

    /* if authenticating is in progress, check if it was successful */
    if (remote->status == RELAY_STATUS_AUTHENTICATING)
    {
        relay_remote_network_recv_text (remote, buffer, buffer_size);
    }
    else if (remote->status == RELAY_STATUS_CONNECTED)
    {
        buffer2 = NULL;
        buffer2_size = 0;
        if (remote->partial_ws_frame)
        {
            buffer2_size = buffer_size + remote->partial_ws_frame_size;
            buffer2 = malloc (buffer2_size);
            if (!buffer2)
            {
                weechat_printf (
                    NULL,
                    _("%sremote[%s]: not enough memory"),
                    weechat_prefix ("error"),
                    remote->name);
                return;
            }
            memcpy (buffer2, remote->partial_ws_frame,
                    remote->partial_ws_frame_size);
            memcpy (buffer2 + remote->partial_ws_frame_size,
                    buffer, buffer_size);
        }
        frames = NULL;
        num_frames = 0;
        rc = relay_websocket_decode_frame (
            (buffer2) ? (unsigned char *)buffer2 : (const unsigned char *)buffer,
            (buffer2) ? (unsigned long long)buffer2_size : (unsigned long long)buffer_size,
            0,  /* expect_masked_frame */
            remote->ws_deflate,
            &frames,
            &num_frames,
            &remote->partial_ws_frame,
            &remote->partial_ws_frame_size);
        free (buffer2);
        if (!rc)
        {
            /* fatal error when decoding frame: close connection */
            if (frames)
            {
                for (i = 0; i < num_frames; i++)
                {
                    free (frames[i].payload);
                }
                free (frames);
            }
            weechat_printf (
                NULL,
                _("%sremote[%s]: error decoding websocket frame"),
                weechat_prefix ("error"),
                remote->name);
            relay_remote_network_disconnect (remote);
            return;
        }
        relay_remote_network_read_websocket_frames (remote, frames, num_frames);
        for (i = 0; i < num_frames; i++)
        {
            free (frames[i].payload);
        }
        free (frames);
    }
}

/*
 * Callback for fd hook.
 */

int
relay_remote_network_recv_cb (const void *pointer, void *data, int fd)
{
    struct t_relay_remote *remote;
    static char buffer[4096 + 2];
    int num_read, end_recv;

    /* make C compiler happy */
    (void) data;
    (void) fd;

    remote = (struct t_relay_remote *)pointer;
    if (!remote)
        return WEECHAT_RC_ERROR;

    if (remote->sock < 0)
        return WEECHAT_RC_OK;

    end_recv = 0;
    while (!end_recv)
    {
        end_recv = 1;

        if (remote->tls)
        {
            if (!remote->gnutls_sess)
                return WEECHAT_RC_ERROR;
            num_read = gnutls_record_recv (remote->gnutls_sess, buffer,
                                           sizeof (buffer) - 2);
        }
        else
        {
            num_read = recv (remote->sock, buffer, sizeof (buffer) - 2, 0);
        }

        if (num_read > 0)
        {
            buffer[num_read] = '\0';
            if (remote->tls
                && (gnutls_record_check_pending (remote->gnutls_sess) > 0))
            {
                /*
                 * if there are unread data in the gnutls buffers,
                 * go on with recv
                 */
                end_recv = 0;
            }
            relay_remote_network_recv_buffer (remote, buffer, num_read);
        }
        else
        {
            if (remote->tls)
            {
                if ((num_read == 0)
                    || ((num_read != GNUTLS_E_AGAIN)
                        && (num_read != GNUTLS_E_INTERRUPTED)))
                {
                    weechat_printf (
                        NULL,
                        _("%sremote[%s]: reading data on socket: error %d %s"),
                        weechat_prefix ("error"),
                        remote->name,
                        num_read,
                        (num_read == 0) ? _("(connection closed by peer)") :
                        gnutls_strerror (num_read));
                    relay_remote_network_disconnect (remote);
                }
            }
            else
            {
                if ((num_read == 0)
                    || ((errno != EAGAIN) && (errno != EWOULDBLOCK)))
                {
                    weechat_printf (
                        NULL,
                        _("%sremote[%s]: reading data on socket: error %d %s"),
                        weechat_prefix ("error"),
                        remote->name,
                        errno,
                        (num_read == 0) ? _("(connection closed by peer)") :
                        strerror (errno));
                    relay_remote_network_disconnect (remote);
                }
            }
        }
    }

    return WEECHAT_RC_OK;
}

/*
 * Connect to remote using websocket, with authentication.
 */

void
relay_remote_network_connect_ws_auth (struct t_relay_remote *remote)
{
    char *password, *totp_secret, *totp;
    char *salt_password, salt[64], str_auth[4096], str_auth_base64[4096];
    char str_http[8192], str_totp[128], str_extensions[256];
    char hash[512 / 8], hash_hexa[((512 / 8) * 2) + 1];
    char ws_key[16], ws_key_base64[64];
    int hash_size;
    time_t time_now;

    relay_remote_set_status (remote, RELAY_STATUS_AUTHENTICATING);

    password = NULL;
    totp_secret = NULL;
    str_auth[0] = '\0';
    str_auth_base64[0] = '\0';
    str_totp[0] = '\0';
    str_extensions[0] = '\0';

    password = weechat_string_eval_expression (
        weechat_config_string (remote->options[RELAY_REMOTE_OPTION_PASSWORD]),
        NULL, NULL, NULL);
    if (!password)
        goto end;
    totp_secret = weechat_string_eval_expression (
        weechat_config_string (remote->options[RELAY_REMOTE_OPTION_TOTP_SECRET]),
        NULL, NULL, NULL);
    if (!totp_secret)
        goto end;

    time_now = time (NULL);

    switch (remote->password_hash_algo)
    {
        case RELAY_AUTH_PASSWORD_HASH_PLAIN:
            snprintf (str_auth, sizeof (str_auth), "plain:%s", password);
            break;
        case RELAY_AUTH_PASSWORD_HASH_SHA256:
        case RELAY_AUTH_PASSWORD_HASH_SHA512:
            if (weechat_asprintf (&salt_password, "%ld%s", time_now, password) >= 0)
            {
                if (weechat_crypto_hash (
                        salt_password, strlen (salt_password),
                        relay_auth_password_hash_algo_name[remote->password_hash_algo],
                        hash, &hash_size))
                {
                    weechat_string_base_encode ("16", hash, hash_size, hash_hexa);
                    snprintf (str_auth, sizeof (str_auth),
                              "hash:%s:%ld:%s",
                              relay_auth_password_hash_algo_name[remote->password_hash_algo],
                              time_now,
                              hash_hexa);
                }
                free (salt_password);
            }
            break;
        case RELAY_AUTH_PASSWORD_HASH_PBKDF2_SHA256:
        case RELAY_AUTH_PASSWORD_HASH_PBKDF2_SHA512:
            snprintf (salt, sizeof (salt), "%ld", time_now);
            if (weechat_crypto_hash_pbkdf2 (
                    password,
                    strlen (password),
                    relay_auth_password_hash_algo_name[remote->password_hash_algo] + 7,
                    salt, strlen (salt),
                    remote->password_hash_iterations,
                    hash, &hash_size))
            {
                weechat_string_base_encode ("16", hash, hash_size, hash_hexa);
                snprintf (str_auth, sizeof (str_auth),
                          "hash:%s:%s:%d:%s",
                          relay_auth_password_hash_algo_name[remote->password_hash_algo],
                          salt,
                          remote->password_hash_iterations,
                          hash_hexa);
            }
            break;
    }

    if (password[0] && !str_auth[0])
    {
        weechat_printf (NULL,
                        _("%sremote[%s]: failed to build authentication"),
                        weechat_prefix ("error"), remote->name);
        relay_remote_network_disconnect (remote);
        goto end;
    }

    /* generate random websocket key (16 bytes) */
    gcry_create_nonce (ws_key, sizeof (ws_key));
    weechat_string_base_encode ("64", ws_key, sizeof (ws_key), ws_key_base64);
    free (remote->websocket_key);
    remote->websocket_key = strdup (ws_key_base64);

    if (str_auth[0])
        weechat_string_base_encode ("64", str_auth, strlen (str_auth), str_auth_base64);

    if (totp_secret && totp_secret[0])
    {
        /* generate the TOTP with the secret */
        totp = weechat_info_get ("totp_generate", totp_secret);
        if (totp)
        {
            snprintf (str_totp, sizeof (str_totp),
                      "x-weechat-totp: %s\r\n",
                      totp);
            free (totp);
        }
    }

    /* add supported extensions */
    if (weechat_config_boolean (relay_config_network_websocket_permessage_deflate))
    {
        snprintf (str_extensions, sizeof (str_extensions),
                  "%s",
                  "Sec-WebSocket-Extensions: permessage-deflate; "
                  "client_max_window_bits\r\n");
    }

    snprintf (
        str_http, sizeof (str_http),
        "GET /api HTTP/1.1\r\n"
        "%s%s%s"
        "%s"
        "Sec-WebSocket-Version: 13\r\n"
        "Sec-WebSocket-Key: %s\r\n"
        "Connection: Upgrade\r\n"
        "Upgrade: websocket\r\n"
        "%s"
        "Host: %s:%d\r\n"
        "\r\n",
        (str_auth_base64[0]) ? "Authorization: Basic " : "",
        (str_auth_base64[0]) ? str_auth_base64 : "",
        (str_auth_base64[0]) ? "\r\n" : "",
        str_totp,
        ws_key_base64,
        str_extensions,
        remote->address,
        remote->port);
    relay_remote_network_send (remote, RELAY_MSG_STANDARD,
                               str_http, strlen (str_http));

end:
    free (password);
    free (totp_secret);
}

/*
 * Callback for connect hook.
 */

int
relay_remote_network_connect_cb (const void *pointer, void *data, int status,
                                 int gnutls_rc, int sock, const char *error,
                                 const char *ip_address)
{
    struct t_relay_remote *remote;

    /* make C compiler happy */
    (void) data;
    (void) gnutls_rc;

    remote = (struct t_relay_remote *)pointer;

    remote->hook_connect = NULL;

    remote->sock = sock;

    switch (status)
    {
        case WEECHAT_HOOK_CONNECT_OK:
            weechat_printf (NULL,
                            _("remote[%s]: connected to %s/%d (%s)"),
                            remote->name, remote->address, remote->port,
                            ip_address);
            remote->hook_fd = weechat_hook_fd (remote->sock, 1, 0, 0,
                                               &relay_remote_network_recv_cb,
                                               remote, NULL);
            /* authenticate with remote relay */
            relay_remote_network_connect_ws_auth (remote);
            break;
        case WEECHAT_HOOK_CONNECT_ADDRESS_NOT_FOUND:
            weechat_printf (NULL,
                            _("%sremote[%s]: address \"%s\" not found"),
                            weechat_prefix ("error"), remote->name,
                            remote->address);
            if (error && error[0])
            {
                weechat_printf (NULL,
                                _("%sremote[%s]: error: %s"),
                                weechat_prefix ("error"), remote->name, error);
            }
            relay_remote_network_disconnect (remote);
            break;
        case WEECHAT_HOOK_CONNECT_IP_ADDRESS_NOT_FOUND:
            weechat_printf (NULL,
                            _("%sremote[%s]: IP address not found"),
                            weechat_prefix ("error"), remote->name);
            if (error && error[0])
            {
                weechat_printf (NULL,
                                _("%sremote[%s]: error: %s"),
                                weechat_prefix ("error"), remote->name, error);
            }
            relay_remote_network_disconnect (remote);
            break;
        case WEECHAT_HOOK_CONNECT_CONNECTION_REFUSED:
            weechat_printf (NULL,
                            _("%sremote[%s]: connection refused"),
                            weechat_prefix ("error"), remote->name);
            if (error && error[0])
            {
                weechat_printf (NULL,
                                _("%sremote[%s]: error: %s"),
                                weechat_prefix ("error"), remote->name, error);
            }
            relay_remote_network_disconnect (remote);
            break;
        case WEECHAT_HOOK_CONNECT_PROXY_ERROR:
            weechat_printf (
                NULL,
                _("%sremote[%s]: proxy fails to establish connection to server (check "
                  "username/password if used and if server address/port is "
                  "allowed by proxy)"),
                weechat_prefix ("error"), remote->name);
            if (error && error[0])
            {
                weechat_printf (NULL,
                                _("%sremote[%s]: error: %s"),
                                weechat_prefix ("error"), remote->name, error);
            }
            relay_remote_network_disconnect (remote);
            break;
        case WEECHAT_HOOK_CONNECT_LOCAL_HOSTNAME_ERROR:
            weechat_printf (NULL,
                            _("%sremote[%s]: unable to set local hostname/IP"),
                            weechat_prefix ("error"), remote->name);
            if (error && error[0])
            {
                weechat_printf (NULL,
                                _("%sremote[%s]: error: %s"),
                                weechat_prefix ("error"), remote->name, error);
            }
            relay_remote_network_disconnect (remote);
            break;
        case WEECHAT_HOOK_CONNECT_GNUTLS_INIT_ERROR:
            weechat_printf (NULL,
                            _("%sremote[%s]: TLS init error"),
                            weechat_prefix ("error"), remote->name);
            if (error && error[0])
            {
                weechat_printf (NULL,
                                _("%sremote[%s]: error: %s"),
                                weechat_prefix ("error"), remote->name, error);
            }
            relay_remote_network_disconnect (remote);
            break;
        case WEECHAT_HOOK_CONNECT_GNUTLS_HANDSHAKE_ERROR:
            weechat_printf (NULL,
                            _("%sremote[%s]: TLS handshake failed"),
                            weechat_prefix ("error"), remote->name);
            if (error && error[0])
            {
                weechat_printf (NULL,
                                _("%sremote[%s]: error: %s"),
                                weechat_prefix ("error"), remote->name, error);
            }
            relay_remote_network_disconnect (remote);
            break;
        case WEECHAT_HOOK_CONNECT_MEMORY_ERROR:
            weechat_printf (NULL,
                            _("%sremote[%s]: not enough memory"),
                            weechat_prefix ("error"), remote->name);
            if (error && error[0])
            {
                weechat_printf (NULL,
                                _("%sremote[%s]: error: %s"),
                                weechat_prefix ("error"), remote->name, error);
            }
            relay_remote_network_disconnect (remote);
            break;
        case WEECHAT_HOOK_CONNECT_TIMEOUT:
            weechat_printf (NULL,
                            _("%sremote[%s]: timeout"),
                            weechat_prefix ("error"), remote->name);
            if (error && error[0])
            {
                weechat_printf (NULL,
                                _("%sremote[%s]: error: %s"),
                                weechat_prefix ("error"), remote->name, error);
            }
            relay_remote_network_disconnect (remote);
            break;
        case WEECHAT_HOOK_CONNECT_SOCKET_ERROR:
            weechat_printf (NULL,
                            _("%sremote[%s]: unable to create socket"),
                            weechat_prefix ("error"), remote->name);
            if (error && error[0])
            {
                weechat_printf (NULL,
                                _("%sremote[%s]: error: %s"),
                                weechat_prefix ("error"), remote->name, error);
            }
            relay_remote_network_disconnect (remote);
            break;
    }

    return WEECHAT_RC_OK;
}

/*
 * GnuTLS callback called during handshake.
 *
 * Return:
 *    0: certificate OK
 *   -1: error in certificate
 */

int
relay_remote_network_gnutls_callback (const void *pointer, void *data,
                                      gnutls_session_t tls_session,
                                      const gnutls_datum_t *req_ca, int nreq,
                                      const gnutls_pk_algorithm_t *pk_algos,
                                      int pk_algos_len,
                                      gnutls_retr2_st *answer,
                                      int action)
{
    struct t_relay_remote *remote;
    gnutls_x509_crt_t cert_temp;
    const gnutls_datum_t *cert_list;
    unsigned int i, cert_list_len, status;
    time_t cert_time;
    int rc, hostname_match, cert_temp_init;
    gnutls_datum_t cinfo;
    int rinfo;

    /* make C compiler happy */
    (void) data;
    (void) req_ca;
    (void) nreq;
    (void) pk_algos;
    (void) pk_algos_len;
    (void) answer;

    rc = 0;

    if (!pointer)
        return -1;

    remote = (struct t_relay_remote *) pointer;
    cert_temp_init = 0;
    cert_list = NULL;
    cert_list_len = 0;

    if (action == WEECHAT_HOOK_CONNECT_GNUTLS_CB_VERIFY_CERT)
    {
        /* initialize the certificate structure */
        if (gnutls_x509_crt_init (&cert_temp) != GNUTLS_E_SUCCESS)
        {
            weechat_printf_date_tags (
                NULL, 0, "tls",
                _("%sremote[%s]: gnutls: failed to initialize certificate structure"),
                weechat_prefix ("error"), remote->name);
            rc = -1;
            goto end;
        }

        /* flag to do the "deinit" (at the end of function) */
        cert_temp_init = 1;

        /* set match options */
        hostname_match = 0;

        /* get the peer's raw certificate (chain) as sent by the peer */
        cert_list = gnutls_certificate_get_peers (tls_session, &cert_list_len);
        if (cert_list)
        {
            weechat_printf_date_tags (
                NULL, 0, "tls",
                NG_("remote[%s]: gnutls: receiving %d certificate",
                    "remote[%s]: gnutls: receiving %d certificates",
                    cert_list_len),
                remote->name,
                cert_list_len);

            for (i = 0; i < cert_list_len; i++)
            {
                if (gnutls_x509_crt_import (cert_temp,
                                            &cert_list[i],
                                            GNUTLS_X509_FMT_DER) != GNUTLS_E_SUCCESS)
                {
                    weechat_printf_date_tags (
                        NULL, 0, "tls",
                        _("%sremote[%s]: gnutls: failed to import certificate[%d]"),
                        weechat_prefix ("error"), remote->name, i + 1);
                    rc = -1;
                    goto end;
                }

                /* checks on first certificate received */
                if (i == 0)
                {
                    /* check if hostname matches in the first certificate */
                    if (gnutls_x509_crt_check_hostname (cert_temp,
                                                        remote->address) != 0)
                    {
                        hostname_match = 1;
                    }
                }
                /* display infos about certificate */
                rinfo = gnutls_x509_crt_print (cert_temp,
                                               GNUTLS_CRT_PRINT_ONELINE, &cinfo);
                if (rinfo == 0)
                {
                    weechat_printf_date_tags (
                        NULL, 0, "tls",
                        _("remote[%s] - certificate[%d] info:"),
                        remote->name, i + 1);
                    weechat_printf_date_tags (
                        NULL, 0, "tls",
                        "remote[%s]   - %s",
                        remote->name, cinfo.data);
                    gnutls_free (cinfo.data);
                }
                /* check expiration date */
                cert_time = gnutls_x509_crt_get_expiration_time (cert_temp);
                if (cert_time < time (NULL))
                {
                    weechat_printf_date_tags (
                        NULL, 0, "tls",
                        _("%sremote[%s]: gnutls: certificate has expired"),
                        weechat_prefix ("error"), remote->name);
                    rc = -1;
                }
                /* check activation date */
                cert_time = gnutls_x509_crt_get_activation_time (cert_temp);
                if (cert_time > time (NULL))
                {
                    weechat_printf_date_tags (
                        NULL, 0, "tls",
                        _("%sremote[%s]: gnutls: certificate is not yet activated"),
                        weechat_prefix ("error"), remote->name);
                    rc = -1;
                }
            }

            if (!hostname_match)
            {
                weechat_printf_date_tags (
                    NULL, 0, "tls",
                    _("%sremote[%s]: gnutls: the hostname in the certificate "
                      "does NOT match \"%s\""),
                    weechat_prefix ("error"), remote->name, remote->address);
                rc = -1;
            }
        }

        /* verify the peer’s certificate */
        if (gnutls_certificate_verify_peers2 (tls_session, &status) < 0)
        {
            weechat_printf_date_tags (
                NULL, 0, "tls",
                _("%sremote[%s]: gnutls: error while checking peer's certificate"),
                weechat_prefix ("error"), remote->name);
            rc = -1;
            goto end;
        }

        /* check if certificate is trusted */
        if (status & GNUTLS_CERT_INVALID)
        {
            weechat_printf_date_tags (
                NULL, 0, "tls",
                _("%sremote[%s]: gnutls: peer's certificate is NOT trusted"),
                weechat_prefix ("error"), remote->name);
            rc = -1;
        }
        else
        {
            weechat_printf_date_tags (
                NULL, 0, "tls",
                _("remote[%s]: gnutls: peer's certificate is trusted"),
                remote->name);
        }

        /* check if certificate issuer is known */
        if (status & GNUTLS_CERT_SIGNER_NOT_FOUND)
        {
            weechat_printf_date_tags (
                NULL, 0, "tls",
                _("%sremote[%s]: gnutls: peer's certificate issuer is unknown"),
                weechat_prefix ("error"), remote->name);
            rc = -1;
        }

        /* check that certificate is not revoked */
        if (status & GNUTLS_CERT_REVOKED)
        {
            weechat_printf_date_tags (
                NULL, 0, "tls",
                _("%sremote[%s]: gnutls: the certificate has been revoked"),
                weechat_prefix ("error"), remote->name);
            rc = -1;
        }
    }
    else if (action == WEECHAT_HOOK_CONNECT_GNUTLS_CB_SET_CERT)
    {
        /* nothing here */
    }

end:
    /* an error should stop the handshake unless the user doesn't care */
    if ((rc == -1)
        && !weechat_config_boolean (remote->options[RELAY_REMOTE_OPTION_TLS_VERIFY]))
    {
        rc = 0;
    }

    if (cert_temp_init)
        gnutls_x509_crt_deinit (cert_temp);

    return rc;
}

/*
 * Callback for handshake URL.
 */

int
relay_remote_network_url_handshake_cb (const void *pointer,
                                       void *data,
                                       const char *url,
                                       struct t_hashtable *options,
                                       struct t_hashtable *output)
{
    struct t_relay_remote *remote;
    struct t_config_option *proxy_type, *proxy_ipv6, *proxy_address, *proxy_port;
    const char *ptr_output, *ptr_resp_code, *ptr_error;
    const char *proxy, *str_proxy_type, *str_proxy_address;
    char *option_name;
    cJSON *json_body, *json_hash_algo, *json_hash_iterations, *json_totp;
    int length;

    /* make C compiler happy */
    (void) data;
    (void) url;
    (void) options;

    remote = (struct t_relay_remote *)pointer;

    remote->hook_url_handshake = NULL;

    json_body = NULL;

    ptr_resp_code = weechat_hashtable_get (output, "response_code");
    if (ptr_resp_code && ptr_resp_code[0] && (strcmp (ptr_resp_code, "200") != 0))
    {
        weechat_printf (
            NULL,
            _("%sremote[%s]: handshake failed with URL %s, response code: %s"),
            weechat_prefix ("error"),
            remote->name,
            weechat_config_string (remote->options[RELAY_REMOTE_OPTION_URL]),
            ptr_resp_code);
        relay_remote_network_disconnect (remote);
        goto end;
    }

    ptr_error = weechat_hashtable_get (output, "error");
    if (ptr_error && ptr_error[0])
    {
        weechat_printf (
            NULL,
            _("%sremote[%s]: handshake failed with URL %s, error: %s"),
            weechat_prefix ("error"),
            remote->name,
            weechat_config_string (remote->options[RELAY_REMOTE_OPTION_URL]),
            ptr_error);
        relay_remote_network_disconnect (remote);
        goto end;
    }

    ptr_output = weechat_hashtable_get (output, "output");
    if (ptr_output && ptr_output[0])
    {
        json_body = cJSON_Parse (weechat_hashtable_get (output, "output"));
        if (json_body)
        {
            /* hash algorithm */
            json_hash_algo = cJSON_GetObjectItem (json_body, "password_hash_algo");
            if (json_hash_algo && cJSON_IsString (json_hash_algo))
            {
                remote->password_hash_algo = relay_auth_password_hash_algo_search (
                    cJSON_GetStringValue (json_hash_algo));
            }
            /* hash iterations */
            json_hash_iterations = cJSON_GetObjectItem (json_body, "password_hash_iterations");
            if (json_hash_iterations && cJSON_IsNumber (json_hash_iterations))
                remote->password_hash_iterations = (int)cJSON_GetNumberValue (json_hash_iterations);
            /* TOTP */
            json_totp = cJSON_GetObjectItem (json_body, "totp");
            if (json_totp && cJSON_IsBool (json_totp))
                remote->totp = (cJSON_IsTrue (json_totp)) ? 1 : 0;
        }
    }

    if (remote->password_hash_algo < 0)
    {
        weechat_printf (
            NULL,
            _("%sremote[%s]: handshake failed with URL %s, error: %s"),
            weechat_prefix ("error"),
            remote->name,
            weechat_config_string (remote->options[RELAY_REMOTE_OPTION_URL]),
            _("hash algorithm not found"));
        relay_remote_network_disconnect (remote);
        goto end;
    }

    if (remote->password_hash_iterations < 0)
    {
        weechat_printf (
            NULL,
            _("%sremote[%s]: handshake failed with URL %s, error: %s"),
            weechat_prefix ("error"),
            remote->name,
            weechat_config_string (remote->options[RELAY_REMOTE_OPTION_URL]),
            _("unknown number of hash iterations"));
        relay_remote_network_disconnect (remote);
        goto end;
    }

    if (remote->totp < 0)
    {
        weechat_printf (
            NULL,
            _("%sremote[%s]: handshake failed with URL %s, error: %s"),
            weechat_prefix ("error"),
            remote->name,
            weechat_config_string (remote->options[RELAY_REMOTE_OPTION_URL]),
            _("unknown TOTP status"));
        relay_remote_network_disconnect (remote);
        goto end;
    }

    if (weechat_relay_plugin->debug >= 1)
    {
        weechat_printf (
            NULL,
            _("%sremote[%s]: successful handshake with URL %s: "
              "hash_algo=%s, iterations=%d, totp=%d"),
            RELAY_PLUGIN_NAME,
            remote->name,
            weechat_config_string (remote->options[RELAY_REMOTE_OPTION_URL]),
            relay_auth_password_hash_algo_name[remote->password_hash_algo],
            remote->password_hash_iterations,
            remote->totp);
    }

    proxy_type = NULL;
    proxy_ipv6 = NULL;
    proxy_address = NULL;
    proxy_port = NULL;
    str_proxy_type = NULL;
    str_proxy_address = NULL;

    proxy = weechat_config_string (remote->options[RELAY_REMOTE_OPTION_PROXY]);
    if (proxy && proxy[0])
    {
        length = 32 + strlen (proxy) + 1;
        option_name = malloc (length);
        if (!option_name)
        {
            weechat_printf (
                NULL,
                _("%sremote[%s]: not enough memory"),
                weechat_prefix ("error"),
                remote->name);
            relay_remote_network_disconnect (remote);
            goto end;
        }
        snprintf (option_name, length, "weechat.proxy.%s.type", proxy);
        proxy_type = weechat_config_get (option_name);
        snprintf (option_name, length, "weechat.proxy.%s.ipv6", proxy);
        proxy_ipv6 = weechat_config_get (option_name);
        snprintf (option_name, length, "weechat.proxy.%s.address", proxy);
        proxy_address = weechat_config_get (option_name);
        snprintf (option_name, length, "weechat.proxy.%s.port", proxy);
        proxy_port = weechat_config_get (option_name);
        free (option_name);
        if (!proxy_type || !proxy_address)
        {
            weechat_printf (
                NULL,
                _("%sremote[%s]: proxy \"%s\" not found, cannot connect"),
                weechat_prefix ("error"), remote->name, proxy);
            relay_remote_network_disconnect (remote);
            goto end;
        }
        str_proxy_type = weechat_config_string (proxy_type);
        str_proxy_address = weechat_config_string (proxy_address);
        if (!str_proxy_type[0] || !proxy_ipv6 || !str_proxy_address[0]
            || !proxy_port)
        {
            weechat_printf (
                NULL,
                _("%sremote[%s]: missing proxy settings, check options for "
                  "proxy \"%s\""),
                weechat_prefix ("error"), remote->name, proxy);
            relay_remote_network_disconnect (remote);
            goto end;
        }
    }

    remote->hook_connect = weechat_hook_connect (
        proxy,
        remote->address,
        remote->port,
        (proxy_type) ? weechat_config_integer (proxy_ipv6) : 1,  /* IPv6 */
        0,  /* retry */
        (remote->tls) ? &remote->gnutls_sess : NULL,
        (remote->tls) ? &relay_remote_network_gnutls_callback : NULL,
        2048,  /* gnutls_dhkey_size */
        "NORMAL",  /* gnutls_priorities */
        NULL,  /* local_hostname */
        &relay_remote_network_connect_cb,
        remote,
        NULL);

end:
    if (json_body)
        cJSON_Delete (json_body);
    return WEECHAT_RC_OK;
}

/*
 * Build a string with the API HTTP handshake request.
 *
 * Note: result must be freed after use.
 */

char *
relay_remote_network_get_handshake_request (void)
{
    cJSON *json, *json_algos;
    char *result;
    int i;

    json = cJSON_CreateObject ();
    if (!json)
        return NULL;

    json_algos = cJSON_CreateArray ();
    if (!json_algos)
    {
        cJSON_Delete (json);
        return NULL;
    }

    /* all password hash algorithms are supported */
    for (i = 0; i < RELAY_NUM_PASSWORD_HASH_ALGOS; i++)
    {
        cJSON_AddItemToArray (
            json_algos,
            cJSON_CreateString (relay_auth_password_hash_algo_name[i]));
    }

    cJSON_AddItemToObject (json, "password_hash_algo", json_algos);
    result = cJSON_PrintUnformatted (json);

    cJSON_Delete (json);

    return result;
}

/*
 * Connect to a remote WeeChat relay/api.
 *
 * Return:
 *   1: OK
 *   0: error
 */

int
relay_remote_network_connect (struct t_relay_remote *remote)
{
    char *url, *body;
    struct t_hashtable *options;

    url = NULL;
    body = NULL;
    options = NULL;

    if (!remote)
        return 0;

    if (remote->sock != -1)
    {
        weechat_printf (
            NULL,
            _("%s%s: already connected to remote relay \"%s\"!"),
            weechat_prefix ("error"), RELAY_PLUGIN_NAME, remote->name);
        return 0;
    }

    relay_remote_set_status (remote, RELAY_STATUS_CONNECTING);

    weechat_printf (NULL,
                    _("remote[%s]: connecting to remote relay %s/%d%s..."),
                    remote->name,
                    remote->address,
                    remote->port,
                    (remote->tls) ? " (TLS)" : "");

    url = relay_remote_network_get_url_resource (remote, "handshake");
    if (!url)
        goto error;

    options = weechat_hashtable_new (32,
                                     WEECHAT_HASHTABLE_STRING,
                                     WEECHAT_HASHTABLE_STRING,
                                     NULL, NULL);
    if (!options)
        goto error;

    weechat_hashtable_set (options, "post", "1");
    weechat_hashtable_set (options,
                           "httpheader",
                           "Accept: application/json\n"
                           "Content-Type: application/json; charset=utf-8");
    if (!weechat_config_boolean (remote->options[RELAY_REMOTE_OPTION_TLS_VERIFY]))
    {
        weechat_hashtable_set (options, "ssl_verifypeer", "0");
        weechat_hashtable_set (options, "ssl_verifyhost", "0");
    }
    body = relay_remote_network_get_handshake_request ();
    if (!body)
        goto error;

    weechat_hashtable_set (options, "postfields", body);

    remote->hook_url_handshake = weechat_hook_url (
        url, options, 5 * 1000,
        &relay_remote_network_url_handshake_cb, remote, NULL);

    free (url);
    free (body);
    weechat_hashtable_free (options);

    return 1;

error:
    weechat_printf (NULL,
                    _("remote[%s]: failed to connect, not enough memory"),
                    remote->name);
    free (url);
    free (body);
    weechat_hashtable_free (options);
    return 0;
}
