mirror of https://github.com/pissnet/pissircd.git
489 lines
15 KiB
C
489 lines
15 KiB
C
/*
|
|
* websocket - WebSocket support (RFC6455)
|
|
* (C)Copyright 2016 Bram Matthys and the UnrealIRCd team
|
|
* License: GPLv2 or later
|
|
* This module was sponsored by Aberrant Software Inc.
|
|
*/
|
|
|
|
#include "unrealircd.h"
|
|
|
|
#define WEBSOCKET_VERSION "1.1.0"
|
|
|
|
ModuleHeader MOD_HEADER
|
|
= {
|
|
"websocket",
|
|
WEBSOCKET_VERSION,
|
|
"WebSocket support (RFC6455)",
|
|
"UnrealIRCd Team",
|
|
"unrealircd-6",
|
|
};
|
|
|
|
#if CHAR_MIN < 0
|
|
#error "In UnrealIRCd char should always be unsigned. Check your compiler"
|
|
#endif
|
|
|
|
#ifndef WEBSOCKET_SEND_BUFFER_SIZE
|
|
#define WEBSOCKET_SEND_BUFFER_SIZE 16384
|
|
#endif
|
|
|
|
#define WSU(client) ((WebSocketUser *)moddata_client(client, websocket_md).ptr)
|
|
#define WEB(client) ((WebRequest *)moddata_client(client, webserver_md).ptr)
|
|
|
|
#define WEBSOCKET_PORT(client) ((client->local && client->local->listener) ? client->local->listener->websocket_options : 0)
|
|
#define WEBSOCKET_TYPE(client) (WSU(client)->type)
|
|
|
|
/* Forward declarations */
|
|
int websocket_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
|
|
int websocket_config_posttest(int *);
|
|
int websocket_config_run_ex(ConfigFile *cf, ConfigEntry *ce, int type, void *ptr);
|
|
int websocket_packet_out(Client *from, Client *to, Client *intended_to, char **msg, int *length);
|
|
int websocket_handle_handshake(Client *client, const char *readbuf, int *length);
|
|
int websocket_handshake_send_response(Client *client);
|
|
int websocket_handle_body_websocket(Client *client, WebRequest *web, const char *readbuf2, int length2);
|
|
int websocket_secure_connect(Client *client);
|
|
int websocket_handle_request(Client *client, WebRequest *web);
|
|
int websocket_config_listener(ConfigItem_listen *listener);
|
|
|
|
/* Global variables */
|
|
ModDataInfo *websocket_md = NULL; /* (by us) */
|
|
ModDataInfo *webserver_md = NULL; /* (external module, looked up) */
|
|
static int ws_text_mode_available = 1;
|
|
|
|
MOD_TEST()
|
|
{
|
|
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, websocket_config_test);
|
|
HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, websocket_config_posttest);
|
|
|
|
/* Call MOD_INIT very early, since we manage sockets, but depend on websocket_common */
|
|
ModuleSetOptions(modinfo->handle, MOD_OPT_PRIORITY, WEBSOCKET_MODULE_PRIORITY_INIT+1);
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
MOD_INIT()
|
|
{
|
|
ModDataInfo mreq;
|
|
|
|
MARK_AS_OFFICIAL_MODULE(modinfo);
|
|
|
|
websocket_md = findmoddata_byname("websocket", MODDATATYPE_CLIENT);
|
|
if (!websocket_md)
|
|
config_warn("The 'websocket_common' module is not loaded, even though it was promised to be ???");
|
|
|
|
HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN_EX, 0, websocket_config_run_ex);
|
|
HookAdd(modinfo->handle, HOOKTYPE_PACKET, INT_MAX, websocket_packet_out);
|
|
HookAdd(modinfo->handle, HOOKTYPE_SECURE_CONNECT, 0, websocket_secure_connect);
|
|
HookAdd(modinfo->handle, HOOKTYPE_CONFIG_LISTENER, 0, websocket_config_listener);
|
|
|
|
/* Call MOD_LOAD very late, since we manage sockets, but depend on websocket_common */
|
|
ModuleSetOptions(modinfo->handle, MOD_OPT_PRIORITY, WEBSOCKET_MODULE_PRIORITY_UNLOAD-1);
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
MOD_LOAD()
|
|
{
|
|
webserver_md = findmoddata_byname("web", MODDATATYPE_CLIENT);
|
|
|
|
if (non_utf8_nick_chars_in_use || (iConf.allowed_channelchars == ALLOWED_CHANNELCHARS_ANY))
|
|
ws_text_mode_available = 0;
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
MOD_UNLOAD()
|
|
{
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
int websocket_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
|
|
{
|
|
int errors = 0;
|
|
ConfigEntry *cep;
|
|
int has_type = 0;
|
|
static char errored_once_nick = 0;
|
|
|
|
if (type != CONFIG_LISTEN_OPTIONS)
|
|
return 0;
|
|
|
|
/* We are only interrested in listen::options::websocket.. */
|
|
if (!ce || !ce->name || strcmp(ce->name, "websocket"))
|
|
return 0;
|
|
|
|
for (cep = ce->items; cep; cep = cep->next)
|
|
{
|
|
if (!strcmp(cep->name, "type"))
|
|
{
|
|
CheckNull(cep);
|
|
has_type = 1;
|
|
if (!strcmp(cep->value, "text"))
|
|
{
|
|
if (non_utf8_nick_chars_in_use && !errored_once_nick)
|
|
{
|
|
/* This one is a hard error, since the consequences are grave */
|
|
config_error("You have a websocket listener with type 'text' AND your set::allowed-nickchars contains at least one non-UTF8 character set.");
|
|
config_error("This is a very BAD idea as this makes your websocket vulnerable to UTF8 conversion attacks. "
|
|
"This can cause things like unkickable users and 'ghosts' for websocket users.");
|
|
config_error("You have 4 options: 1) Remove the websocket listener, 2) Use websocket type 'binary', "
|
|
"3) Remove the non-UTF8 character set from set::allowed-nickchars, 4) Replace the non-UTF8 with an UTF8 character set in set::allowed-nickchars");
|
|
config_error("For more details see https://www.unrealircd.org/docs/WebSocket_support#websockets-and-non-utf8");
|
|
errored_once_nick = 1;
|
|
errors++;
|
|
}
|
|
}
|
|
else if (!strcmp(cep->value, "binary"))
|
|
{
|
|
}
|
|
else
|
|
{
|
|
config_error("%s:%i: listen::options::websocket::type must be either 'binary' or 'text' (not '%s')",
|
|
cep->file->filename, cep->line_number, cep->value);
|
|
errors++;
|
|
}
|
|
} else if (!strcmp(cep->name, "forward"))
|
|
{
|
|
config_error("%s:%i: this functionality has been moved to the proxy { } block. "
|
|
"See https://www.unrealircd.org/docs/Proxy_block",
|
|
cep->file->filename, cep->line_number);
|
|
errors++;
|
|
} else if (!strcmp(cep->name, "allow-origin"))
|
|
{
|
|
} else
|
|
{
|
|
config_error("%s:%i: unknown directive listen::options::websocket::%s",
|
|
cep->file->filename, cep->line_number, cep->name);
|
|
errors++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (!has_type)
|
|
{
|
|
config_error("%s:%i: websocket set, but type unspecified. Use something like: listen { ip *; port 443; websocket { type text; } }",
|
|
ce->file->filename, ce->line_number);
|
|
errors++;
|
|
}
|
|
|
|
*errs = errors;
|
|
return errors ? -1 : 1;
|
|
}
|
|
|
|
int websocket_config_run_ex(ConfigFile *cf, ConfigEntry *ce, int type, void *ptr)
|
|
{
|
|
ConfigEntry *cep, *cepp;
|
|
ConfigItem_listen *l;
|
|
static char warned_once_channel = 0;
|
|
|
|
if (type != CONFIG_LISTEN_OPTIONS)
|
|
return 0;
|
|
|
|
/* We are only interrested in listen::options::websocket.. */
|
|
if (!ce || !ce->name || strcmp(ce->name, "websocket"))
|
|
return 0;
|
|
|
|
l = (ConfigItem_listen *)ptr;
|
|
l->webserver = safe_alloc(sizeof(WebServer));
|
|
l->webserver->handle_request = websocket_handle_request;
|
|
l->webserver->handle_body = websocket_handle_body_websocket;
|
|
|
|
for (cep = ce->items; cep; cep = cep->next)
|
|
{
|
|
if (!strcmp(cep->name, "type"))
|
|
{
|
|
if (!strcmp(cep->value, "binary"))
|
|
l->websocket_options = WEBSOCKET_TYPE_BINARY;
|
|
else if (!strcmp(cep->value, "text"))
|
|
{
|
|
l->websocket_options = WEBSOCKET_TYPE_TEXT;
|
|
if ((tempiConf.allowed_channelchars == ALLOWED_CHANNELCHARS_ANY) && !warned_once_channel)
|
|
{
|
|
/* This one is a warning, since the consequences are less grave than with nicks */
|
|
config_warn("You have a websocket listener with type 'text' AND your set::allowed-channelchars is set to 'any'.");
|
|
config_warn("This is not a recommended combination as this makes your websocket vulnerable to UTF8 conversion attacks. "
|
|
"This can cause things like unpartable channels for websocket users.");
|
|
config_warn("It is highly recommended that you use set { allowed-channelchars utf8; }");
|
|
config_warn("For more details see https://www.unrealircd.org/docs/WebSocket_support#websockets-and-non-utf8");
|
|
warned_once_channel = 1;
|
|
}
|
|
}
|
|
} else if (!strcmp(cep->name, "allow-origin"))
|
|
{
|
|
for (cepp = cep->items; cepp; cepp = cepp->next)
|
|
add_name_list(l->websocket_origin, cepp->name);
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
int websocket_config_listener(ConfigItem_listen *listener)
|
|
{
|
|
if (listener->websocket_options)
|
|
{
|
|
listener->webserver->handle_request = websocket_handle_request;
|
|
listener->webserver->handle_body = websocket_handle_body_websocket;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int websocket_config_posttest(int *errs)
|
|
{
|
|
int errors = 0;
|
|
char webserver_module = 1, websocket_common_module = 1;
|
|
|
|
if (!is_module_loaded("webserver"))
|
|
{
|
|
config_error("The 'websocket' module requires the 'webserver' module to be loaded, otherwise websocket connections will not work!");
|
|
webserver_module = 0;
|
|
errors++;
|
|
}
|
|
|
|
if (!is_module_loaded("websocket_common"))
|
|
{
|
|
config_error("The 'websocket' module requires the 'websocket_common' module to be loaded, otherwise websocket connections will not work!");
|
|
websocket_common_module = 0;
|
|
errors++;
|
|
}
|
|
|
|
/* Is nicer for the admin when these are grouped... */
|
|
if (!webserver_module)
|
|
config_error("Add the following line to your config file: loadmodule \"webserver\";");
|
|
if (!websocket_common_module)
|
|
config_error("Add the following line to your config file: loadmodule \"websocket_common\";");
|
|
|
|
*errs = errors;
|
|
return errors ? -1 : 1;
|
|
}
|
|
|
|
/* Add LF (if needed) to a buffer. Max 4K. */
|
|
void add_lf_if_needed(char **buf, int *len)
|
|
{
|
|
static char newbuf[MAXLINELENGTH];
|
|
char *b = *buf;
|
|
int l = *len;
|
|
|
|
if (l <= 0)
|
|
return; /* too short */
|
|
|
|
if (b[l - 1] == '\n')
|
|
return; /* already contains \n */
|
|
|
|
if (l >= sizeof(newbuf)-2)
|
|
l = sizeof(newbuf)-2; /* cut-off if necessary */
|
|
|
|
memcpy(newbuf, b, l);
|
|
newbuf[l] = '\n';
|
|
newbuf[l + 1] = '\0'; /* not necessary, but I like zero termination */
|
|
l++;
|
|
*buf = newbuf; /* new buffer */
|
|
*len = l; /* new length */
|
|
}
|
|
|
|
/** Called on decoded websocket frame (INPUT).
|
|
* Should contain exactly 1 IRC line (command)
|
|
*/
|
|
int websocket_irc_callback(Client *client, char *buf, int len)
|
|
{
|
|
add_lf_if_needed(&buf, &len);
|
|
if (!process_packet(client, buf, len, 1)) /* Let UnrealIRCd handle this as usual */
|
|
return 0; /* client killed */
|
|
return 1;
|
|
}
|
|
|
|
int websocket_handle_body_websocket(Client *client, WebRequest *web, const char *readbuf2, int length2)
|
|
{
|
|
return websocket_handle_websocket(client, web, readbuf2, length2, websocket_irc_callback);
|
|
}
|
|
|
|
/** Outgoing packet hook.
|
|
* This transforms the output to be Websocket-compliant, if necessary.
|
|
*/
|
|
int websocket_packet_out(Client *from, Client *to, Client *intended_to, char **msg, int *length)
|
|
{
|
|
static char utf8buf[510];
|
|
|
|
if (MyConnect(to) && !IsRPC(to) && websocket_md && WSU(to) && WSU(to)->handshake_completed)
|
|
{
|
|
if (WEBSOCKET_TYPE(to) == WEBSOCKET_TYPE_BINARY)
|
|
websocket_create_packet(WSOP_BINARY, msg, length);
|
|
else if (WEBSOCKET_TYPE(to) == WEBSOCKET_TYPE_TEXT)
|
|
{
|
|
/* Some more conversions are needed */
|
|
char *safe_msg = unrl_utf8_make_valid(*msg, utf8buf, sizeof(utf8buf), 1);
|
|
*msg = safe_msg;
|
|
*length = *msg ? strlen(safe_msg) : 0;
|
|
websocket_create_packet(WSOP_TEXT, msg, length);
|
|
}
|
|
return 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/** We got a HTTP(S) request and we need to check if we can upgrade the connection
|
|
* to a websocket connection.
|
|
*/
|
|
int websocket_handle_request(Client *client, WebRequest *web)
|
|
{
|
|
NameValuePrioList *r;
|
|
const char *key, *value;
|
|
|
|
/* Allocate a new WebSocketUser struct for this session */
|
|
moddata_client(client, websocket_md).ptr = safe_alloc(sizeof(WebSocketUser));
|
|
/* ...and set the default protocol (text or binary) */
|
|
WSU(client)->type = client->local->listener->websocket_options;
|
|
|
|
/** Now step through the lines.. **/
|
|
for (r = web->headers; r; r = r->next)
|
|
{
|
|
key = r->name;
|
|
value = r->value;
|
|
if (!strcasecmp(key, "Sec-WebSocket-Key"))
|
|
{
|
|
if (strchr(value, ':'))
|
|
{
|
|
/* This would cause unserialization issues. Should be base64 anyway */
|
|
webserver_send_response(client, 400, "Invalid characters in Sec-WebSocket-Key");
|
|
return -1;
|
|
}
|
|
safe_strdup(WSU(client)->handshake_key, value);
|
|
} else
|
|
if (!strcasecmp(key, "Sec-WebSocket-Protocol"))
|
|
{
|
|
/* Save it here, will be processed later */
|
|
safe_strdup(WSU(client)->sec_websocket_protocol, value);
|
|
}
|
|
}
|
|
|
|
/** Finally, validate the websocket request (handshake) and proceed or reject. */
|
|
if (client->local->listener->websocket_origin)
|
|
{
|
|
const char *origin = get_nvplist(web->headers, "Origin");
|
|
char origin_host[256];
|
|
|
|
*origin_host = '\0';
|
|
if (origin)
|
|
{
|
|
char *start = strstr(origin, "://");
|
|
char *p;
|
|
if (start)
|
|
{
|
|
start += 3;
|
|
p = strchr(start, '/');
|
|
if (p)
|
|
strlncpy(origin_host, start, sizeof(origin_host), p - start);
|
|
else
|
|
strlcpy(origin_host, start, sizeof(origin_host));
|
|
}
|
|
}
|
|
if (!find_name_list_match(client->local->listener->websocket_origin, origin_host))
|
|
{
|
|
webserver_send_response(client, 403, "This site is not permitted to connect to us with websockets");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/* Not websocket and webredir loaded? Let that module serve a redirect. */
|
|
if (!WSU(client)->handshake_key)
|
|
{
|
|
if (is_module_loaded("webredir"))
|
|
{
|
|
const char *parx[2] = { NULL, NULL };
|
|
do_cmd(client, NULL, "GET", 1, parx);
|
|
}
|
|
webserver_send_response(client, 404, "This port is for IRC WebSocket only");
|
|
return 0;
|
|
}
|
|
|
|
/* Sec-WebSocket-Protocol (optional) */
|
|
if (WSU(client)->sec_websocket_protocol)
|
|
{
|
|
char *p = NULL, *name;
|
|
int negotiated = 0;
|
|
|
|
for (name = strtoken(&p, WSU(client)->sec_websocket_protocol, ",");
|
|
name;
|
|
name = strtoken(&p, NULL, ","))
|
|
{
|
|
skip_whitespace(&name);
|
|
if (!strcmp(name, "binary.ircv3.net"))
|
|
{
|
|
negotiated = WEBSOCKET_TYPE_BINARY;
|
|
break; /* First hit wins */
|
|
} else
|
|
if (!strcmp(name, "text.ircv3.net") && ws_text_mode_available)
|
|
{
|
|
negotiated = WEBSOCKET_TYPE_TEXT;
|
|
break; /* First hit wins */
|
|
}
|
|
}
|
|
if (negotiated == WEBSOCKET_TYPE_BINARY)
|
|
{
|
|
WSU(client)->type = WEBSOCKET_TYPE_BINARY;
|
|
safe_strdup(WSU(client)->sec_websocket_protocol, "binary.ircv3.net");
|
|
} else
|
|
if (negotiated == WEBSOCKET_TYPE_TEXT)
|
|
{
|
|
WSU(client)->type = WEBSOCKET_TYPE_TEXT;
|
|
safe_strdup(WSU(client)->sec_websocket_protocol, "text.ircv3.net");
|
|
} else
|
|
{
|
|
/* Negotiation failed, fallback to the default (don't set it here) */
|
|
safe_free(WSU(client)->sec_websocket_protocol);
|
|
}
|
|
}
|
|
|
|
websocket_handshake_send_response(client);
|
|
return 1;
|
|
}
|
|
|
|
int websocket_secure_connect(Client *client)
|
|
{
|
|
/* Remove secure mode (-z) if the WEBIRC gateway did not ensure
|
|
* us that their [client]--[webirc gateway] connection is also
|
|
* secure (eg: using https)
|
|
*/
|
|
if (IsSecureConnect(client) && websocket_md && WSU(client) &&
|
|
WEB(client)->forwarded && !WEB(client)->forwarded->secure)
|
|
{
|
|
client->umodes &= ~UMODE_SECURE;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/** Complete the handshake by sending the appropriate HTTP 101 response etc. */
|
|
int websocket_handshake_send_response(Client *client)
|
|
{
|
|
char buf[512], hashbuf[64];
|
|
char sha1out[20]; /* 160 bits */
|
|
|
|
WSU(client)->handshake_completed = 1;
|
|
|
|
snprintf(buf, sizeof(buf), "%s%s", WSU(client)->handshake_key, WEBSOCKET_MAGIC_KEY);
|
|
sha1hash_binary(sha1out, buf, strlen(buf));
|
|
b64_encode(sha1out, sizeof(sha1out), hashbuf, sizeof(hashbuf));
|
|
|
|
snprintf(buf, sizeof(buf),
|
|
"HTTP/1.1 101 Switching Protocols\r\n"
|
|
"Upgrade: websocket\r\n"
|
|
"Connection: Upgrade\r\n"
|
|
"Sec-WebSocket-Accept: %s\r\n",
|
|
hashbuf);
|
|
|
|
if (WSU(client)->sec_websocket_protocol)
|
|
{
|
|
/* using strlen() is safe here since above buffer will not
|
|
* cause it to be >=512 and thus we won't get into negatives.
|
|
*/
|
|
snprintf(buf+strlen(buf), sizeof(buf)-strlen(buf),
|
|
"Sec-WebSocket-Protocol: %s\r\n",
|
|
WSU(client)->sec_websocket_protocol);
|
|
}
|
|
|
|
strlcat(buf, "\r\n", sizeof(buf));
|
|
|
|
/* Caution: we bypass sendQ flood checking by doing it this way.
|
|
* Risk is minimal, though, as we only permit limited text only
|
|
* once per session.
|
|
*/
|
|
dbuf_put(&client->local->sendQ, buf, strlen(buf));
|
|
send_queued(client);
|
|
|
|
return 0;
|
|
}
|