/*
 * SPDX-FileCopyrightText: 2013-2026 Sébastien Helleu <flashcode@flashtux.org>
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

/* Websocket server functions for relay plugin (RFC 6455) */

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <gcrypt.h>
#include <zlib.h>

#include "../weechat-plugin.h"
#include "relay.h"
#include "relay-client.h"
#include "relay-config.h"
#include "relay-http.h"
#include "relay-websocket.h"


/*
 * Allocate a t_relay_websocket_deflate structure.
 */

struct t_relay_websocket_deflate *
relay_websocket_deflate_alloc (void)
{
    struct t_relay_websocket_deflate *new_ws_deflate;

    new_ws_deflate = (struct t_relay_websocket_deflate *)malloc (sizeof (*new_ws_deflate));
    if (!new_ws_deflate)
        return NULL;

    new_ws_deflate->enabled = 0;
    new_ws_deflate->server_context_takeover = 0;
    new_ws_deflate->client_context_takeover = 0;
    new_ws_deflate->window_bits_deflate = 0;
    new_ws_deflate->window_bits_inflate = 0;
    new_ws_deflate->server_max_window_bits_recv = 0;
    new_ws_deflate->client_max_window_bits_recv = 0;
    new_ws_deflate->strm_deflate = NULL;
    new_ws_deflate->strm_inflate = NULL;

    return new_ws_deflate;
}

/*
 * Initialize stream for deflate (compression).
 *
 * Return:
 *   1: OK
 *   0: error
 */

int
relay_websocket_deflate_init_stream_deflate (struct t_relay_websocket_deflate *ws_deflate)
{
    int rc, compression, compression_level;

    compression = weechat_config_integer (relay_config_network_compression);

    /* convert % to zlib compression level (1-9) */
    compression_level = (((compression - 1) * 9) / 100) + 1;

    rc = deflateInit2 (
        ws_deflate->strm_deflate,
        compression_level,
        Z_DEFLATED,                  /* method */
        -1 * ws_deflate->window_bits_deflate,
        8,                           /* memLevel */
        Z_DEFAULT_STRATEGY);         /* strategy */

    return (rc == Z_OK) ? 1 : 0;
}

/*
 * Free a deflate stream in a deflate structure.
 */

void
relay_websocket_deflate_free_stream_deflate (struct t_relay_websocket_deflate *ws_deflate)
{
    if (ws_deflate->strm_deflate)
    {
        deflateEnd (ws_deflate->strm_deflate);
        free (ws_deflate->strm_deflate);
        ws_deflate->strm_deflate = NULL;
    }
}

/*
 * Initialize stream for inflate (decompression).
 *
 * Return:
 *   1: OK
 *   0: error
 */

int
relay_websocket_deflate_init_stream_inflate (struct t_relay_websocket_deflate *ws_deflate)
{
    int rc;

    rc = inflateInit2 (ws_deflate->strm_inflate,
                       -1 * ws_deflate->window_bits_inflate);

    return (rc == Z_OK) ? 1 : 0;
}

/*
 * Free an inflate stream in a deflate structure.
 */

void
relay_websocket_deflate_free_stream_inflate (struct t_relay_websocket_deflate *ws_deflate)
{
    if (ws_deflate->strm_inflate)
    {
        inflateEnd (ws_deflate->strm_inflate);
        free (ws_deflate->strm_inflate);
        ws_deflate->strm_inflate = NULL;
    }
}

/*
 * Reinitialize a t_relay_websocket_deflate structure.
 */

void
relay_websocket_deflate_reinit (struct t_relay_websocket_deflate *ws_deflate)
{
    ws_deflate->enabled = 0;
    ws_deflate->server_context_takeover = 0;
    ws_deflate->client_context_takeover = 0;
    ws_deflate->window_bits_deflate = 0;
    ws_deflate->window_bits_inflate = 0;
    ws_deflate->server_max_window_bits_recv = 0;
    ws_deflate->client_max_window_bits_recv = 0;
    relay_websocket_deflate_free_stream_deflate (ws_deflate);
    relay_websocket_deflate_free_stream_inflate (ws_deflate);
}

/*
 * Free a websocket deflate structure.
 */

void
relay_websocket_deflate_free (struct t_relay_websocket_deflate *ws_deflate)
{
    if (ws_deflate)
    {
        relay_websocket_deflate_free_stream_deflate (ws_deflate);
        relay_websocket_deflate_free_stream_inflate (ws_deflate);
        free (ws_deflate);
    }
}

/*
 * Check if a message is a HTTP GET with resource "/weechat" (for weechat
 * protocol) or "/api" (for api protocol).
 *
 * Return:
 *   1: message is a HTTP GET with appropriate resource
 *   0: message is NOT a HTTP GET with appropriate resource
 */

int
relay_websocket_is_valid_http_get (enum t_relay_protocol protocol,
                                   const char *message)
{
    char string[128];
    int length;

    if (!message)
        return 0;

    /* the message must start with "GET /weechat" or "GET /api" */
    snprintf (string, sizeof (string),
              "GET /%s", relay_protocol_string[protocol]);
    length = strlen (string);

    if (strncmp (message, string, length) != 0)
        return 0;

    /* after "GET /weechat" or "GET /api", only a new line or " HTTP" is allowed */
    if ((message[length] != '\r') && (message[length] != '\n')
        && (strncmp (message + length, " HTTP", 5) != 0))
    {
        return 0;
    }

    /* valid HTTP GET */
    return 1;
}

/*
 * Check if a client handshake is valid.
 *
 * A websocket query looks like (weechat protocol):
 *   GET /weechat HTTP/1.1
 *   Upgrade: websocket
 *   Connection: Upgrade
 *   Host: myhost:5000
 *   Origin: https://example.org
 *   Pragma: no-cache
 *   Cache-Control: no-cache
 *   Sec-WebSocket-Key: fo1J9uHSsrfDP3BkwUylzQ==
 *   Sec-WebSocket-Version: 13
 *   Sec-WebSocket-Extensions: x-webkit-deflate-frame
 *   Cookie: csrftoken=acb65377798f32dc377ebb50316a12b5
 *
 * A websocket query looks like (api protocol):
 *   GET /api HTTP/1.1
 *   Upgrade: websocket
 *   Connection: Upgrade
 *   Host: myhost:5000
 *   Origin: https://example.org
 *   Pragma: no-cache
 *   Cache-Control: no-cache
 *   Sec-WebSocket-Key: fo1J9uHSsrfDP3BkwUylzQ==
 *   Sec-WebSocket-Version: 13
 *   Sec-WebSocket-Extensions: x-webkit-deflate-frame
 *   Cookie: csrftoken=acb65377798f32dc377ebb50316a12b5
 *
 *
 * Expected HTTP headers with values are:
 *
 *   header              | value
 *   --------------------+----------------
 *   "Upgrade"           | "websocket"
 *   "Sec-WebSocket-Key" | non-empty value
 *
 * If option relay.network.websocket_allowed_origins is set, the HTTP header
 * "Origin" is checked against this regex. If header "Origin" is not set or does
 * not match regex, the handshake is considered as invalid.
 *
 * Return:
 *    0: handshake is valid
 *   -1: handshake is invalid (headers missing or with bad value)
 *   -2: origin is not allowed (option relay.network.websocket_allowed_origins)
 */

int
relay_websocket_client_handshake_valid (struct t_relay_http_request *request)
{
    const char *value;

    if (!request || !request->headers)
        return -1;

    /* check if we have header "Upgrade" with value "websocket" */
    value = weechat_hashtable_get (request->headers, "upgrade");
    if (!value)
        return -1;
    if (weechat_strcasecmp (value, "websocket") != 0)
        return -1;

    /* check if we have header "Sec-WebSocket-Key" with non-empty value */
    value = weechat_hashtable_get (request->headers, "sec-websocket-key");
    if (!value || !value[0])
        return -1;

    if (relay_config_regex_websocket_allowed_origins)
    {
        value = weechat_hashtable_get (request->headers, "origin");
        if (!value || !value[0])
            return -2;
        if (regexec (relay_config_regex_websocket_allowed_origins, value, 0,
                     NULL, 0) != 0)
        {
            return -2;
        }
    }

    /* client handshake is valid */
    return 0;
}

/*
 * Parse websocket extensions (header "Sec-WebSocket-Extensions").
 *
 * Header is for example:
 *   Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
 */

void
relay_websocket_parse_extensions (const char *extensions,
                                  struct t_relay_websocket_deflate *ws_deflate,
                                  int ws_deflate_allowed)
{
    char **exts, **params, **items;
    int i, j, number, num_exts, num_params, num_items;

    if (!extensions || !ws_deflate)
        return;

    exts = weechat_string_split (extensions, ",", " ", 0, 0, &num_exts);
    if (!exts)
        return;

    for (i = 0; i < num_exts; i++)
    {
        params = weechat_string_split (exts[i], ";", " ", 0, 0, &num_params);
        if (params && (num_params >= 1)
            && (strcmp (params[0], "permessage-deflate") == 0)
            && ws_deflate_allowed
            && (weechat_config_boolean (relay_config_network_websocket_permessage_deflate)))
        {
            ws_deflate->enabled = 1;
            ws_deflate->server_context_takeover = 1;
            ws_deflate->client_context_takeover = 1;
            ws_deflate->window_bits_deflate = 15;
            ws_deflate->window_bits_inflate = 15;
            ws_deflate->server_max_window_bits_recv = 0;
            ws_deflate->client_max_window_bits_recv = 0;
            for (j = 1; j < num_params; j++)
            {
                items = weechat_string_split (params[j], "=", " ", 0, 0, &num_items);
                if (items && (num_items >= 1))
                {
                    if (strcmp (items[0], "server_no_context_takeover") == 0)
                    {
                        ws_deflate->server_context_takeover = 0;
                    }
                    else if (strcmp (items[0], "client_no_context_takeover") == 0)
                    {
                        ws_deflate->client_context_takeover = 0;
                    }
                    else if ((strcmp (items[0], "server_max_window_bits") == 0)
                             || (strcmp (items[0], "client_max_window_bits") == 0))
                    {
                        number = 15;
                        if (num_items >= 2)
                        {
                            if (weechat_util_parse_int (items[1], 10, &number))
                            {
                                if (number < 8)
                                    number = 8;
                                else if (number > 15)
                                    number = 15;
                            }
                            else
                            {
                                number = 15;
                            }
                        }
                        if (strcmp (items[0], "server_max_window_bits") == 0)
                        {
                            ws_deflate->server_max_window_bits_recv = 1;
                            ws_deflate->window_bits_deflate = number;
                        }
                        else
                        {
                            ws_deflate->client_max_window_bits_recv = 1;
                            ws_deflate->window_bits_inflate = number;
                        }
                    }
                }
                weechat_string_free_split (items);
            }
        }
        weechat_string_free_split (params);
    }

    weechat_string_free_split (exts);
}

/*
 * Build the handshake that will be returned to client, to initialize and use
 * the websocket.
 *
 * Return a string with content of handshake to send to client, it looks like:
 *   HTTP/1.1 101 Switching Protocols
 *   Upgrade: websocket
 *   Connection: Upgrade
 *   Sec-WebSocket-Accept: 73OzoF/IyV9znm7Tsb4EtlEEmn4=
 *
 * Note: result must be freed after use.
 */

char *
relay_websocket_build_handshake (struct t_relay_http_request *request)
{
    const char *sec_websocket_key, *sec_websocket_protocol_request;
    char *key, sec_websocket_accept[128], handshake[4096], hash[160 / 8];
    char **extensions, **protocol_array, str_window_bits[128];
    char sec_websocket_extensions[1024];
    char sec_websocket_protocol[1024];
    int i, hash_size, protocol_count;

    if (!request)
        return NULL;

    sec_websocket_key = weechat_hashtable_get (request->headers,
                                               "sec-websocket-key");
    if (!sec_websocket_key || !sec_websocket_key[0])
        return NULL;

    /*
     * concatenate header "Sec-WebSocket-Key" with the GUID
     * (globally unique identifier)
     */
    if (weechat_asprintf (&key, "%s%s", sec_websocket_key, WEBSOCKET_GUID) < 0)
        return NULL;

    /* compute 160-bit SHA1 on the key and encode it with base64 */
    if (!weechat_crypto_hash (key, strlen (key), "sha1", hash, &hash_size))
    {
        free (key);
        return NULL;
    }
    if (weechat_string_base_encode ("64", hash, hash_size,
                                    sec_websocket_accept) < 0)
    {
        sec_websocket_accept[0] = '\0';
    }

    free (key);

    if (request->ws_deflate->enabled)
    {
        extensions = weechat_string_dyn_alloc (128);
        if (!extensions)
            return NULL;
        weechat_string_dyn_concat (extensions, "permessage-deflate", -1);
        if (!request->ws_deflate->server_context_takeover)
        {
            weechat_string_dyn_concat (extensions, "; ", -1);
            weechat_string_dyn_concat (extensions, "server_no_context_takeover", -1);
        }
        if (!request->ws_deflate->client_context_takeover)
        {
            weechat_string_dyn_concat (extensions, "; ", -1);
            weechat_string_dyn_concat (extensions, "client_no_context_takeover", -1);
        }
        if (request->ws_deflate->server_max_window_bits_recv)
        {
            weechat_string_dyn_concat (extensions, "; ", -1);
            snprintf (str_window_bits, sizeof (str_window_bits),
                      "server_max_window_bits=%d",
                      request->ws_deflate->window_bits_deflate);
            weechat_string_dyn_concat (extensions, str_window_bits, -1);
        }
        if (request->ws_deflate->client_max_window_bits_recv)
        {
            weechat_string_dyn_concat (extensions, "; ", -1);
            snprintf (str_window_bits, sizeof (str_window_bits),
                      "client_max_window_bits=%d",
                      request->ws_deflate->window_bits_inflate);
            weechat_string_dyn_concat (extensions, str_window_bits, -1);
        }
        snprintf (
            sec_websocket_extensions, sizeof (sec_websocket_extensions),
            "Sec-WebSocket-Extensions: %s\r\n",
            *extensions);
        weechat_string_dyn_free (extensions, 1);
    }
    else
    {
        sec_websocket_extensions[0] = '\0';
    }

    sec_websocket_protocol_request = weechat_hashtable_get (
        request->headers, "sec-websocket-protocol");
    protocol_array = weechat_string_split (sec_websocket_protocol_request,
                                           ",", " ", 0, 0, &protocol_count);

    sec_websocket_protocol[0] = '\0';
    for (i = 0; i < protocol_count; i++)
    {
        if (strcmp (protocol_array[i], WEBSOCKET_SUB_PROTOCOL_API_WEECHAT) == 0)
        {
            snprintf (
                sec_websocket_protocol, sizeof (sec_websocket_protocol),
                "Sec-WebSocket-Protocol: %s\r\n",
                WEBSOCKET_SUB_PROTOCOL_API_WEECHAT);
            break;
        }
    }

    /* build the handshake (it will be sent as-is to client) */
    snprintf (handshake, sizeof (handshake),
              "HTTP/1.1 101 Switching Protocols\r\n"
              "Upgrade: websocket\r\n"
              "Connection: Upgrade\r\n"
              "Sec-WebSocket-Accept: %s\r\n"
              "%s"
              "%s"
              "\r\n",
              sec_websocket_accept,
              sec_websocket_extensions,
              sec_websocket_protocol);

    return strdup (handshake);
}

/*
 * Decompress a decoded and compressed websocket frame compressed with
 * "deflate" (when websocket extension "permessage-deflate" is enabled).
 *
 * A final '\0' is added after the decompressed data (the size_decompressed
 * does not count this final '\0').
 *
 * Return pointer to decompressed data, NULL if error.
 */

char *
relay_websocket_inflate (const void *data, size_t size, z_stream *strm,
                         size_t *size_decompressed)
{
    int rc;
    unsigned char append_bytes[4] = { 0x00, 0x00, 0xFF, 0xFF };
    Bytef *data2, *dest, *dest2;
    uLongf size2, dest_size, new_size;

    if (!data || (size == 0) || !strm || !size_decompressed)
        return NULL;

    dest = NULL;

    *size_decompressed = 0;

    /* append "0x00 0x00 0xFF 0xFF" to data */
    size2 = size + sizeof (append_bytes);
    data2 = malloc (size2);
    if (!data2)
        goto error;
    memcpy (data2, data, size);
    memcpy (data2 + size, append_bytes, sizeof (append_bytes));

    /*
     * estimate the decompressed size, by default 10 * size, capped to the
     * maximum allowed size (also prevents an integer overflow on the
     * multiplication)
     */
    dest_size = (size2 > WEBSOCKET_INFLATE_MAX_SIZE / 10) ?
        WEBSOCKET_INFLATE_MAX_SIZE : 10 * size2;
    dest = malloc (dest_size);
    if (!dest)
        goto error;

    strm->avail_in = (uInt)size2;
    strm->next_in = (Bytef *)data2;
    strm->total_in = 0;

    strm->avail_out = (uInt)dest_size;
    strm->next_out = (Bytef *)dest;
    strm->total_out = 0;

    /* loop until we manage to decompress whole data in dest */
    while (1)
    {
        rc = inflate (strm, Z_SYNC_FLUSH);
        if (((rc == Z_STREAM_END) || (rc == Z_OK))
            && (strm->avail_in == 0))
        {
            /* data successfully decompressed */
            *size_decompressed = strm->total_out;
            break;
        }
        if ((rc == Z_BUF_ERROR)
            || (((rc == Z_STREAM_END) || (rc == Z_OK))
                && (strm->avail_in > 0)))
        {
            /* output buffer is not large enough */
            if (dest_size >= WEBSOCKET_INFLATE_MAX_SIZE)
            {
                /*
                 * decompressed data is too large: reject the frame
                 * (protection against a "deflate bomb")
                 */
                goto error;
            }
            /* double the buffer, capped to the maximum allowed size */
            new_size = (dest_size > WEBSOCKET_INFLATE_MAX_SIZE / 2) ?
                WEBSOCKET_INFLATE_MAX_SIZE : dest_size * 2;
            strm->avail_out += (uInt)(new_size - dest_size);
            dest_size = new_size;
            dest2 = realloc (dest, dest_size);
            if (!dest2)
                goto error;
            dest = dest2;
            strm->next_out = dest + strm->total_out;
        }
        else
        {
            /* any other error is fatal */
            goto error;
        }
    }

    dest2 = realloc (dest, *size_decompressed + 1);
    if (!dest2)
        goto error;
    dest = dest2;
    dest[*size_decompressed] = '\0';
    free (data2);
    return (char *)dest;

error:
    free (data2);
    free (dest);
    return NULL;
}

/*
 * Decode a websocket frame and return a list of frames in "*frames" (each
 * frame is first decompressed if "permessage-deflate" websocket extension
 * is used).
 *
 * Return an error if argument "expect_masked_frame" is 1 and a frame is not
 * masked.
 *
 * Return:
 *   1: frame(s) decoded successfully
 *   0: error decoding frame (connection must be closed if it happens)
 */

int
relay_websocket_decode_frame (const unsigned char *buffer,
                              unsigned long long buffer_length,
                              int expect_masked_frame,
                              struct t_relay_websocket_deflate *ws_deflate,
                              struct t_relay_websocket_frame **frames,
                              int *num_frames,
                              char **partial_ws_frame,
                              int *partial_ws_frame_size)
{
    unsigned long long i, index_buffer, index_buffer_start_frame;
    unsigned long long length_frame_size, length_frame;
    unsigned char opcode;
    size_t size_decompressed;
    char *payload_decompressed;
    struct t_relay_websocket_frame *frames2, *ptr_frame;
    int size, compressed, masked_frame, mask[4];

    if (!buffer || !frames || !num_frames)
        return 0;

    *frames = NULL;
    *num_frames = 0;

    index_buffer = 0;
    index_buffer_start_frame = 0;

    /* loop to decode all frames in message */
    while (index_buffer < buffer_length)
    {
        index_buffer_start_frame = index_buffer;

        if (index_buffer + 1 >= buffer_length)
            goto missing_data;

        opcode = buffer[index_buffer] & 15;

        /* RSV1 indicates whether this message is compressed */
        compressed = (buffer[index_buffer] & 64) ? 1 : 0;

        /* check if frame is masked */
        masked_frame = (buffer[index_buffer + 1] & 128) ? 1 : 0;

        /*
         * error if the frame is not masked and we expect it to be masked,
         * in this case we must reject it and close the connection
         * (see RFC 6455)
         */
        if (!masked_frame && expect_masked_frame)
            return 0;

        /* decode frame length */
        length_frame = buffer[index_buffer + 1] & 127;
        index_buffer += 2;
        if (index_buffer >= buffer_length)
            goto missing_data;
        if ((length_frame == 126) || (length_frame == 127))
        {
            length_frame_size = (length_frame == 126) ? 2 : 8;
            if (index_buffer + length_frame_size > buffer_length)
                goto missing_data;
            length_frame = 0;
            for (i = 0; i < length_frame_size; i++)
            {
                length_frame += (unsigned long long)buffer[index_buffer + i] << ((length_frame_size - i - 1) * 8);
            }
            index_buffer += length_frame_size;
        }

        /*
         * reject the frame if its announced length is too big: this prevents
         * a client from forcing an unbounded allocation (and unbounded
         * accumulation of partial frames) by announcing a huge frame
         */
        if (length_frame > WEBSOCKET_FRAME_MAX_LENGTH)
            return 0;

        if (masked_frame)
        {
            /* read mask (4 bytes) */
            if (index_buffer + 4 > buffer_length)
                goto missing_data;
            for (i = 0; i < 4; i++)
            {
                mask[i] = (int)((unsigned char)buffer[index_buffer + i]);
            }
            index_buffer += 4;
        }

        /* check if we have enough data */
        if ((length_frame > buffer_length)
            || (index_buffer + length_frame > buffer_length))
        {
            goto missing_data;
        }

        /* add a new frame in array */
        (*num_frames)++;

        frames2 = realloc (*frames, sizeof (**frames) * (*num_frames));
        if (!frames2)
            return 0;
        *frames = frames2;

        ptr_frame = &((*frames)[*num_frames - 1]);

        ptr_frame->opcode = 0;
        ptr_frame->payload_size = 0;
        ptr_frame->payload = NULL;

        /* save opcode */
        switch (opcode)
        {
            case WEBSOCKET_FRAME_OPCODE_PING:
                ptr_frame->opcode = RELAY_MSG_PING;
                break;
            case WEBSOCKET_FRAME_OPCODE_CLOSE:
                ptr_frame->opcode = RELAY_MSG_CLOSE;
                break;
            default:
                ptr_frame->opcode = RELAY_MSG_STANDARD;
                break;
        }

        /* allocate payload */
        ptr_frame->payload = malloc (length_frame + 1);
        if (!ptr_frame->payload)
            return 0;
        ptr_frame->payload_size = length_frame;

        /* fill payload */
        if (masked_frame)
        {
            for (i = 0; i < length_frame; i++)
            {
                ptr_frame->payload[i] = (int)((unsigned char)buffer[index_buffer + i]) ^ mask[i % 4];
            }
        }
        else
        {
            memcpy (ptr_frame->payload, buffer + index_buffer, length_frame);
        }
        ptr_frame->payload[length_frame] = '\0';

        /*
         * decompress data if frame is not empty and if "permessage-deflate"
         * is enabled and the message is compressed
         */
        if ((length_frame > 0) && ws_deflate && ws_deflate->enabled && compressed)
        {
            if (!ws_deflate->strm_inflate)
            {
                ws_deflate->strm_inflate = calloc (
                    1, sizeof (*ws_deflate->strm_inflate));
                if (!ws_deflate->strm_inflate)
                    return 0;
                if (!relay_websocket_deflate_init_stream_inflate (ws_deflate))
                    return 0;
            }
            payload_decompressed = relay_websocket_inflate (
                ptr_frame->payload,
                ptr_frame->payload_size,
                ws_deflate->strm_inflate,
                &size_decompressed);
            if (!payload_decompressed)
                return 0;
            free (ptr_frame->payload);
            ptr_frame->payload = payload_decompressed;
            ptr_frame->payload_size = size_decompressed;
            if (!ws_deflate->client_context_takeover)
                relay_websocket_deflate_free_stream_inflate (ws_deflate);
        }

        index_buffer += length_frame;
    }

    if (*partial_ws_frame)
    {
        free (*partial_ws_frame);
        *partial_ws_frame = NULL;
        *partial_ws_frame_size = 0;
    }

    return 1;

missing_data:
    if (*partial_ws_frame)
    {
        free (*partial_ws_frame);
        *partial_ws_frame = NULL;
        *partial_ws_frame_size = 0;
    }
    size = buffer_length - index_buffer_start_frame;
    if (size >= 0)
    {
        *partial_ws_frame = malloc (size);
        if (!*partial_ws_frame)
            return 0;
        memcpy (*partial_ws_frame, buffer + index_buffer_start_frame, size);
        *partial_ws_frame_size = size;
    }
    return 1;
}

/*
 * Compress data to send in a websocket frame (when websocket extension
 * "permessage-deflate" is enabled).
 *
 * Return pointer to compressed data, NULL if error.
 */

char *
relay_websocket_deflate (const void *data, size_t size, z_stream *strm,
                         size_t *size_compressed)
{
    int rc;
    uLongf dest_size;
    Bytef *dest;

    if (!data || (size == 0) || !strm || !size_compressed)
        return NULL;

    *size_compressed = 0;

    dest_size = compressBound (size);
    dest = malloc (dest_size);
    if (!dest)
        return NULL;

    strm->avail_in = (uInt)size;
    strm->next_in = (Bytef *)data;
    strm->total_in = 0;

    strm->avail_out = (uInt)dest_size;
    strm->next_out = (Bytef *)dest;
    strm->total_out = 0;

    rc = deflate (strm, Z_SYNC_FLUSH);
    if ((rc == Z_STREAM_END) || (rc == Z_OK))
    {
        *size_compressed = strm->total_out;
        return (char *)dest;
    }

    free (dest);
    return NULL;
}

/*
 * Encode data in a websocket frame.
 *
 * Return websocket frame, NULL if error.
 * Argument "length_frame" is set with the length of frame built.
 *
 * Argument "mask_frame" must be 1 when sending to server (remote) and 0 when
 * sending to a client.
 *
 * Note: result must be freed after use.
 */

char *
relay_websocket_encode_frame (struct t_relay_websocket_deflate *ws_deflate,
                              int opcode,
                              int mask_frame,
                              const char *payload,
                              unsigned long long payload_size,
                              unsigned long long *length_frame)
{
    const char *ptr_data;
    char *payload_compressed;
    size_t size_compressed;
    unsigned char *frame, *ptr_mask;
    unsigned long long i, index, data_size;

    *length_frame = 0;

    ptr_data = payload;
    data_size = payload_size;

    payload_compressed = NULL;
    size_compressed = 0;

    /*
     * compress data if payload is not empty and if "permessage-deflate"
     * is enabled
     */
    if (((opcode == WEBSOCKET_FRAME_OPCODE_TEXT)
         || (opcode == WEBSOCKET_FRAME_OPCODE_BINARY))
        && (payload_size > 0)
        && ws_deflate
        && ws_deflate->enabled)
    {
        if (!ws_deflate->strm_deflate)
        {
            ws_deflate->strm_deflate = calloc (
                1, sizeof (*ws_deflate->strm_deflate));
            if (!ws_deflate->strm_deflate)
                return NULL;
            if (!relay_websocket_deflate_init_stream_deflate (ws_deflate))
                return NULL;
        }
        payload_compressed = relay_websocket_deflate (
            payload,
            payload_size,
            ws_deflate->strm_deflate,
            &size_compressed);
        if (!payload_compressed)
            return NULL;
        ptr_data = payload_compressed;
        data_size = size_compressed;
        if ((data_size > 4)
            && ((unsigned char)ptr_data[data_size - 4] == 0x00)
            && ((unsigned char)ptr_data[data_size - 3] == 0x00)
            && ((unsigned char)ptr_data[data_size - 2] == 0xFF)
            && ((unsigned char)ptr_data[data_size - 1] == 0xFF))
        {
            data_size -= 4;
        }
        if (!ws_deflate->server_context_takeover)
            relay_websocket_deflate_free_stream_deflate (ws_deflate);
        /* set bit RSV1: indicate permessage-deflate compressed data */
        opcode |= 0x40;
    }

    frame = malloc (data_size + 14);
    if (!frame)
    {
        free (payload_compressed);
        return NULL;
    }

    frame[0] = 0x80;
    frame[0] |= opcode;

    if (data_size <= 125)
    {
        /* length on one byte */
        frame[1] = data_size;
        index = 2;
    }
    else if (data_size <= 65535)
    {
        /* length on 2 bytes */
        frame[1] = 126;
        frame[2] = (data_size >> 8) & 0xFF;
        frame[3] = data_size & 0xFF;
        index = 4;
    }
    else
    {
        /* length on 8 bytes */
        frame[1] = 127;
        frame[2] = (data_size >> 56) & 0xFF;
        frame[3] = (data_size >> 48) & 0xFF;
        frame[4] = (data_size >> 40) & 0xFF;
        frame[5] = (data_size >> 32) & 0xFF;
        frame[6] = (data_size >> 24) & 0xFF;
        frame[7] = (data_size >> 16) & 0xFF;
        frame[8] = (data_size >> 8) & 0xFF;
        frame[9] = data_size & 0xFF;
        index = 10;
    }

    if (mask_frame)
    {
        frame[1] |= 128;
        ptr_mask = frame + index;
        gcry_create_nonce (ptr_mask, 4);
        index += 4;
    }

    /* copy buffer after data_size */
    memcpy (frame + index, ptr_data, data_size);

    /* mask frame */
    if (mask_frame)
    {
        for (i = 0; i < data_size; i++)
        {
            frame[index + i] = frame[index + i] ^ ptr_mask[i % 4];
        }
    }

    *length_frame = index + data_size;

    free (payload_compressed);

    return (char *)frame;
}

/*
 * Print websocket deflate data in WeeChat log file (usually for crash dump).
 */

void
relay_websocket_deflate_print_log (struct t_relay_websocket_deflate *ws_deflate,
                                   const char *prefix)
{
    weechat_log_printf ("%s  ws_deflate:", prefix);
    weechat_log_printf ("%s    enabled. . . . . . . . . . : %d", prefix, ws_deflate->enabled);
    weechat_log_printf ("%s    server_context_takeover. . : %d", prefix, ws_deflate->server_context_takeover);
    weechat_log_printf ("%s    client_context_takeover. . : %d", prefix, ws_deflate->client_context_takeover);
    weechat_log_printf ("%s    window_bits_deflate. . . . : %d", prefix, ws_deflate->window_bits_deflate);
    weechat_log_printf ("%s    window_bits_inflate. . . . : %d", prefix, ws_deflate->window_bits_inflate);
    weechat_log_printf ("%s    server_max_window_bits_recv: %d", prefix, ws_deflate->server_max_window_bits_recv);
    weechat_log_printf ("%s    client_max_window_bits_recv: %d", prefix, ws_deflate->client_max_window_bits_recv);
    weechat_log_printf ("%s    strm_deflate . . . . . . . : %p", prefix, ws_deflate->strm_deflate);
    weechat_log_printf ("%s    strm_inflate . . . . . . . : %p", prefix, ws_deflate->strm_inflate);
}
