weechat/src/plugins/relay/relay-websocket.c

411 lines
11 KiB
C

/*
* relay-websocket.c - websocket server functions for relay plugin (RFC 6455)
*
* Copyright (C) 2013-2020 Sébastien Helleu <flashcode@flashtux.org>
*
* 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/>.
*/
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include "../weechat-plugin.h"
#include "relay.h"
#include "relay-client.h"
#include "relay-config.h"
#include "relay-websocket.h"
/*
* globally unique identifier that is concatenated to HTTP header
* "Sec-WebSocket-Key"
*/
#define WEBSOCKET_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
/*
* Checks if a message is a HTTP GET with resource "/weechat".
*
* Returns:
* 1: message is a HTTP GET with resource "/weechat"
* 0: message is NOT a HTTP GET with resource "/weechat"
*/
int
relay_websocket_is_http_get_weechat (const char *message)
{
/* the message must start with "GET /weechat" */
if (strncmp (message, "GET /weechat", 12) != 0)
return 0;
/* after "GET /weechat", only a new line or " HTTP" is allowed */
if ((message[12] != '\r') && (message[12] != '\n')
&& (strncmp (message + 12, " HTTP", 5) != 0))
{
return 0;
}
/* valid HTTP GET for resource "/weechat" */
return 1;
}
/*
* Saves a HTTP header in hashtable "http_header" of client.
*/
void
relay_websocket_save_header (struct t_relay_client *client,
const char *message)
{
char *pos, *name;
const char *ptr_value;
/* ignore the "GET" request */
if (strncmp (message, "GET ", 4) == 0)
return;
pos = strchr (message, ':');
/* not a valid header */
if (!pos || (pos == message))
return;
/* get header name, which is case-insensitive */
name = weechat_strndup (message, pos - message);
if (!name)
return;
weechat_string_tolower (name);
/* get pointer on header value */
ptr_value = pos + 1;
while (ptr_value[0] == ' ')
{
ptr_value++;
}
/* add header in the hashtable */
weechat_hashtable_set (client->http_headers, name, ptr_value);
free (name);
}
/*
* Checks if a client handshake is valid.
*
* A websocket query looks like:
* 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
*
* 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.
*
* Returns:
* 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_client *client)
{
const char *value;
/* check if we have header "Upgrade" with value "websocket" */
value = weechat_hashtable_get (client->http_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 (client->http_headers, "sec-websocket-key");
if (!value || !value[0])
return -1;
if (relay_config_regex_websocket_allowed_origins)
{
value = weechat_hashtable_get (client->http_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;
}
/*
* Builds the handshake that will be returned to client, to initialize and use
* the websocket.
*
* Returns 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_client *client)
{
const char *sec_websocket_key;
char *key, sec_websocket_accept[128], handshake[1024], hash[160 / 8];
int length, length_hash;
sec_websocket_key = weechat_hashtable_get (client->http_headers,
"sec-websocket-key");
if (!sec_websocket_key || !sec_websocket_key[0])
return NULL;
length = strlen (sec_websocket_key) + strlen (WEBSOCKET_GUID) + 1;
key = malloc (length);
if (!key)
return NULL;
/*
* concatenate header "Sec-WebSocket-Key" with the GUID
* (globally unique identifier)
*/
snprintf (key, length, "%s%s", sec_websocket_key, WEBSOCKET_GUID);
/* compute 160-bit SHA1 on the key and encode it with base64 */
if (!weechat_crypto_hash (key, strlen (key), "sha1", hash, &length_hash))
{
free (key);
return NULL;
}
if (weechat_string_base_encode (64, hash, length_hash,
sec_websocket_accept) < 0)
{
sec_websocket_accept[0] = '\0';
}
free (key);
/* 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"
"\r\n",
sec_websocket_accept);
return strdup (handshake);
}
/*
* Sends a HTTP message to client.
*
* Argument "http" is a HTTP code + message, for example:
* "403 Forbidden".
*/
void
relay_websocket_send_http (struct t_relay_client *client,
const char *http)
{
char *message;
int length;
length = 32 + strlen (http) + 1;
message = malloc (length);
if (message)
{
snprintf (message, length, "HTTP/1.1 %s\r\n\r\n", http);
relay_client_send (client, RELAY_CLIENT_MSG_STANDARD,
message, strlen (message), NULL);
free (message);
}
}
/*
* Decodes a websocket frame.
*
* Returns:
* 1: frame 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,
unsigned char *decoded,
unsigned long long *decoded_length)
{
unsigned long long i, index_buffer, length_frame_size, length_frame;
unsigned char opcode;
*decoded_length = 0;
index_buffer = 0;
/* loop to decode all frames in message */
while (index_buffer + 2 <= buffer_length)
{
opcode = buffer[index_buffer] & 15;
/*
* check if frame is masked: client MUST send a masked frame; if frame is
* not masked, we MUST reject it and close the connection (see RFC 6455)
*/
if (!(buffer[index_buffer + 1] & 128))
return 0;
/* decode frame */
length_frame_size = 1;
length_frame = buffer[index_buffer + 1] & 127;
index_buffer += 2;
if ((length_frame == 126) || (length_frame == 127))
{
length_frame_size = (length_frame == 126) ? 2 : 8;
if (buffer_length < 1 + length_frame_size)
return 0;
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;
}
if (buffer_length < 1 + length_frame_size + 4 + length_frame)
return 0;
/* read masks (4 bytes) */
int masks[4];
for (i = 0; i < 4; i++)
{
masks[i] = (int)((unsigned char)buffer[index_buffer + i]);
}
index_buffer += 4;
/* copy opcode in decoded data */
switch (opcode)
{
case WEBSOCKET_FRAME_OPCODE_PING:
decoded[*decoded_length] = RELAY_CLIENT_MSG_PING;
break;
case WEBSOCKET_FRAME_OPCODE_CLOSE:
decoded[*decoded_length] = RELAY_CLIENT_MSG_CLOSE;
break;
default:
decoded[*decoded_length] = RELAY_CLIENT_MSG_STANDARD;
break;
}
*decoded_length += 1;
/* decode data using masks */
for (i = 0; i < length_frame; i++)
{
decoded[*decoded_length + i] = (int)((unsigned char)buffer[index_buffer + i]) ^ masks[i % 4];
}
decoded[*decoded_length + length_frame] = '\0';
*decoded_length += length_frame + 1;
index_buffer += length_frame;
}
return 1;
}
/*
* Encodes data in a websocket frame.
*
* Returns websocket frame, NULL if error.
* Argument "length_frame" is set with the length of frame built.
*
* Note: result must be freed after use.
*/
char *
relay_websocket_encode_frame (int opcode,
const char *buffer,
unsigned long long length,
unsigned long long *length_frame)
{
unsigned char *frame;
unsigned long long index;
*length_frame = 0;
frame = malloc (length + 10);
if (!frame)
return NULL;
frame[0] = 0x80;
frame[0] |= opcode;
if (length <= 125)
{
/* length on one byte */
frame[1] = length;
index = 2;
}
else if (length <= 65535)
{
/* length on 2 bytes */
frame[1] = 126;
frame[2] = (length >> 8) & 0xFF;
frame[3] = length & 0xFF;
index = 4;
}
else
{
/* length on 8 bytes */
frame[1] = 127;
frame[2] = (length >> 56) & 0xFF;
frame[3] = (length >> 48) & 0xFF;
frame[4] = (length >> 40) & 0xFF;
frame[5] = (length >> 32) & 0xFF;
frame[6] = (length >> 24) & 0xFF;
frame[7] = (length >> 16) & 0xFF;
frame[8] = (length >> 8) & 0xFF;
frame[9] = length & 0xFF;
index = 10;
}
/* copy buffer after length */
memcpy (frame + index, buffer, length);
*length_frame = index + length;
return (char *)frame;
}