From 15842b8290c52b368dce625f8f782eec3dc71f7c Mon Sep 17 00:00:00 2001 From: Bram Matthys Date: Sat, 24 Dec 2016 13:35:37 +0100 Subject: [PATCH] Added "websocket" module. This module provides support for WebSocket (RFC6455) so you can use IRC directly from HTML5/JS. It is still considered experimental but feel free to test it out. To do so, add this to your unrealircd.conf: loadmodule "websocket"; This module was sponsored by Aberrant Software Inc. --- makefile.win32 | 4 + src/modules/Makefile.in | 7 +- src/modules/websocket.c | 603 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 613 insertions(+), 1 deletion(-) create mode 100644 src/modules/websocket.c diff --git a/makefile.win32 b/makefile.win32 index 92bed9fbb..14e5591b3 100644 --- a/makefile.win32 +++ b/makefile.win32 @@ -206,6 +206,7 @@ DLL_FILES=SRC/MODULES/M_CHGHOST.DLL SRC/MODULES/M_SDESC.DLL SRC/MODULES/M_SETIDE SRC/MODULES/CERTFP.DLL \ SRC/MODULES/CLOAK.DLL \ SRC/MODULES/WEBIRC.DLL \ + SRC/MODULES/WEBSOCKET.DLL \ SRC/MODULES/BLACKLIST.DLL \ SRC/MODULES/JOINTHROTTLE.DLL \ SRC/MODULES/CHANMODES/CENSOR.DLL \ @@ -780,6 +781,9 @@ src/modules/cloak.dll: src/modules/cloak.c $(INCLUDES) src/modules/webirc.dll: src/modules/webirc.c $(INCLUDES) $(CC) $(MODCFLAGS) src/modules/webirc.c $(MODLFLAGS) +src/modules/websocket.dll: src/modules/websocket.c $(INCLUDES) + $(CC) $(MODCFLAGS) src/modules/websocket.c $(MODLFLAGS) + src/modules/blacklist.dll: src/modules/blacklist.c $(INCLUDES) $(CC) $(MODCFLAGS) src/modules/blacklist.c $(MODLFLAGS) diff --git a/src/modules/Makefile.in b/src/modules/Makefile.in index 8535fe830..71cff9517 100644 --- a/src/modules/Makefile.in +++ b/src/modules/Makefile.in @@ -58,7 +58,8 @@ R_MODULES= \ m_botmotd.so m_lusers.so m_names.so m_svsnolag.so m_addmotd.so \ m_svslusers.so m_starttls.so m_nopost.so m_cap.so \ m_sasl.so m_md.so certfp.so \ - ssl_antidos.so webirc.so blacklist.so jointhrottle.so + ssl_antidos.so webirc.so websocket.so \ + blacklist.so jointhrottle.so MODULES=cloak.so $(R_MODULES) MODULEFLAGS=@MODULEFLAGS@ @@ -469,6 +470,10 @@ webirc.so: webirc.c $(INCLUDES) $(CC) $(CFLAGS) $(MODULEFLAGS) -DDYNAMIC_LINKING \ -o webirc.so webirc.c +websocket.so: websocket.c $(INCLUDES) + $(CC) $(CFLAGS) $(MODULEFLAGS) -DDYNAMIC_LINKING \ + -o websocket.so websocket.c + blacklist.so: blacklist.c $(INCLUDES) $(CC) $(CFLAGS) $(MODULEFLAGS) -DDYNAMIC_LINKING \ -o blacklist.so blacklist.c diff --git a/src/modules/websocket.c b/src/modules/websocket.c new file mode 100644 index 000000000..dbb8830f3 --- /dev/null +++ b/src/modules/websocket.c @@ -0,0 +1,603 @@ +/* + * websocket - WebSocket support (RFC6455) + * (C)Copyright 2016 Bram Matthys and the UnrealIRCd team + * License: GPLv2 + * This module was sponsored by Aberrant Software Inc. + */ + +#include "unrealircd.h" +#include + +#define WEBSOCKET_VERSION "0.9.0" + +ModuleHeader MOD_HEADER(websocket) + = { + "websocket", + WEBSOCKET_VERSION, + "WebSocket support (RFC6455)", + "3.2-b8-1", + NULL + }; + +#if CHAR_MIN < 0 + #error "In UnrealIRCd char should always be unsigned. Check your compiler" +#endif + +struct _websocketuser { + char get; /**< GET initiated */ + char handshake_completed; /**< Handshake completed, use data frames */ + char *handshake_key; /**< Handshake key (used during handshake) */ + char *lefttoparse; /**< Leftover buffer to parse */ + int lefttoparselen; /**< Length of lefttoparse buffer */ +}; +typedef struct _websocketuser WebSocketUser; + +#define WSU(cptr) ((WebSocketUser *)moddata_client(cptr, websocket_md).ptr) + +#define WEBSOCKET_MAGIC_KEY "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" /* see RFC6455 */ + +#define WSOP_CONTINUATION 0x00 +#define WSOP_TEXT 0x01 +#define WSOP_BINARY 0x02 +#define WSOP_CLOSE 0x08 +#define WSOP_PING 0x09 +#define WSOP_PONG 0x0a + +/* Forward declarations */ +int websocket_packet_out(aClient *from, aClient *to, char **msg, int *length); +int websocket_packet_in(aClient *sptr, char *readbuf, int *length); +void websocket_mdata_free(ModData *m); +int websocket_handle_packet(aClient *sptr, char *readbuf, int length); +int websocket_handle_handshake(aClient *sptr, char *readbuf, int *length); +int websocket_complete_handshake(aClient *sptr); +int websocket_handle_packet_ping(aClient *sptr, char *buf, int len); +int websocket_handle_packet_pong(aClient *sptr, char *buf, int len); +int websocket_create_frame(int opcode, char **buf, int *len); +int websocket_send_frame(aClient *sptr, int opcode, char *buf, int len); + +/* Global variables */ +ModDataInfo *websocket_md; + +MOD_INIT(websocket) +{ + ModDataInfo mreq; + + MARK_AS_OFFICIAL_MODULE(modinfo); + + HookAdd(modinfo->handle, HOOKTYPE_PACKET, 0, websocket_packet_out); + HookAdd(modinfo->handle, HOOKTYPE_RAWPACKET_IN, 0, websocket_packet_in); + + memset(&mreq, 0, sizeof(mreq)); + mreq.name = "websocket"; + mreq.serialize = NULL; + mreq.unserialize = NULL; + mreq.free = websocket_mdata_free; + mreq.sync = 0; + mreq.type = MODDATATYPE_CLIENT; + websocket_md = ModDataAdd(modinfo->handle, mreq); + + return MOD_SUCCESS; +} + +MOD_LOAD(websocket) +{ + if (SHOWCONNECTINFO) + { + config_warn("I'm disabling set::options::show-connect-info for you " + "as this setting is incompatible with the websocket module."); + SHOWCONNECTINFO = 0; + } + + return MOD_SUCCESS; +} + +MOD_UNLOAD(websocket) +{ + return MOD_SUCCESS; +} + +/** UnrealIRCd internals: free WebSocketUser object. */ +void websocket_mdata_free(ModData *m) +{ + WebSocketUser *wsu = (WebSocketUser *)m->ptr; + if (wsu) + { + safefree(wsu->handshake_key); + safefree(wsu->lefttoparse); + MyFree(m->ptr); + } +} + +/** Outgoing packet hook. + * This transforms the output to be Websocket-compliant, if necessary. + */ +int websocket_packet_out(aClient *from, aClient *to, char **msg, int *length) +{ + if (MyConnect(to) && WSU(to) && WSU(to)->handshake_completed) + { + websocket_create_frame(WSOP_BINARY, msg, length); + return 0; + } + return 0; +} + +int websocket_handle_websocket(aClient *sptr, char *readbuf2, int length2) +{ + int n; + char *ptr; + int length; + int length1 = WSU(sptr)->lefttoparselen; + char readbuf[4096]; + + length = length1 + length2; + if (length > sizeof(readbuf)-1) + { + dead_link(sptr, "Illegal buffer stacking/Excess flood"); + return 0; + } + + if (length1 > 0) + memcpy(readbuf, WSU(sptr)->lefttoparse, length1); + memcpy(readbuf+length1, readbuf2, length2); + + safefree(WSU(sptr)->lefttoparse); + WSU(sptr)->lefttoparselen = 0; + + ptr = readbuf; + do { + n = websocket_handle_packet(sptr, ptr, length); + if (n < 0) + return -1; /* killed -- STOP processing */ + if (n == 0) + { + /* Short read. Stop processing for now, but save data for next time */ + safefree(WSU(sptr)->lefttoparse); + WSU(sptr)->lefttoparse = MyMallocEx(length); + WSU(sptr)->lefttoparselen = length; + memcpy(WSU(sptr)->lefttoparse, ptr, length); + return 0; + } + length -= n; + ptr += n; + if (length < 0) + abort(); /* less than 0 is impossible */ + } while(length > 0); + + return 0; +} + +/** Incoming packet hook. + * This processes Websocket frames, if this is a websocket connection. + * NOTE The different return values: + * -1 means: don't touch this client anymore, it has or might have been killed! + * 0 means: don't process this data, but you can read another packet if you want + * >0 means: process this data (regular IRC data, non-websocket stuff) + */ +int websocket_packet_in(aClient *sptr, char *readbuf, int *length) +{ + if ((sptr->local->receiveM == 0) && (*length > 8) && !strncmp(readbuf, "GET ", 4)) + { + /* Allocate a new WebSocketUser struct for this session */ + moddata_client(sptr, websocket_md).ptr = MyMallocEx(sizeof(WebSocketUser)); + WSU(sptr)->get = 1; + } + + if (!WSU(sptr)) + return 1; /* "normal" IRC client */ + + if (WSU(sptr)->handshake_completed) + return websocket_handle_websocket(sptr, readbuf, *length); + /* else.. */ + return websocket_handle_handshake(sptr, readbuf, length); +} + +/** Helper function to parse the HTTP header consisting of multiple 'Key: value' pairs */ +int websocket_handshake_helper(char *buffer, int len, char **key, char **value, char **lastloc, int *end_of_request) +{ + static char buf[4096], *nextptr; + char *p; + char *k = NULL, *v = NULL; + int foundlf = 0; + + if (buffer) + { + /* Initialize */ + if (len > sizeof(buf) - 1) + len = sizeof(buf) - 1; + + memcpy(buf, buffer, len); + buf[len] = '\0'; + nextptr = buf; + } + + *end_of_request = 0; + + p = nextptr; + + if (!p) + { + *key = *value = NULL; + return 0; /* done processing data */ + } + + if (!strncmp(p, "\n", 1) || !strncmp(p, "\r\n", 2)) + { + *key = *value = NULL; + *end_of_request = 1; + return 0; + } + /* Note: p *could* point to the NUL byte ('\0') */ + + /* Special handling for GET line itself. */ + if (!strncmp(p, "GET ", 4)) + { + k = "GET"; + p += 4; + v = p; /* SET VALUE */ + nextptr = NULL; /* set to "we are done" in case next for loop fails */ + for (; *p; *p++) + { + if (*p == ' ') + { + *p = '\0'; /* terminate before "HTTP/1.X" part */ + } + else if (*p == '\r') + { + *p = '\0'; /* eat silently, but don't consider EOL */ + } + else if (*p == '\n') + { + *p = '\0'; + nextptr = p+1; /* safe, there is data or at least a \0 there */ + break; + } + } + *key = k; + *value = v; + return 1; + } + + /* Header parsing starts here. + * Example line "Host: www.unrealircd.org" + */ + k = p; /* SET KEY */ + + /* First check if the line contains a terminating \n. If not, don't process it + * as it may have been a cut header. + */ + for (p; *p; p++) + { + if (*p == '\n') + { + foundlf = 1; + break; + } + } + + if (!foundlf) + { + *key = *value = NULL; + *lastloc = k; + return 0; + } + + p = k; + + for (p; *p; p++) + { + if ((*p == '\n') || (*p == '\r')) + { + /* Reached EOL but 'value' not found */ + *p = '\0'; + break; + } + if (*p == ':') + { + *p++ = '\0'; + if (*p++ != ' ') + break; /* missing mandatory space after ':' */ + + v = p; /* SET VALUE */ + nextptr = NULL; /* set to "we are done" in case next for loop fails */ + for (; *p; *p++) + { + if (*p == '\r') + { + *p = '\0'; /* eat silently, but don't consider EOL */ + } + else if (*p == '\n') + { + *p = '\0'; + nextptr = p+1; /* safe, there is data or at least a \0 there */ + break; + } + } + /* A key-value pair was succesfully parsed, return it */ + *key = k; + *value = v; + return 1; + } + } + + /* Fatal parse error */ + *key = *value = NULL; + return 0; +} + +/** Handle client GET WebSocket handshake. + * Yes, I'm going to assume that the header fits in one packet and one packet only. + */ +int websocket_handle_handshake(aClient *sptr, char *readbuf, int *length) +{ + char *key, *value; + int r, end_of_request; + char netbuf[1024]; /* 2*512 */ + char *lastloc = NULL; + + if (WSU(sptr)->lefttoparse) + { + int n, maxcopy, nprefix; + strlcpy(netbuf, WSU(sptr)->lefttoparse, sizeof(netbuf)); + /* Need to some manual checking here as strlen() can't be safely used + * on readbuf. Same is true for strlncat since it uses strlen(). + */ + nprefix = strlen(netbuf); + maxcopy = sizeof(netbuf) - nprefix - 1; + n = *length; + if (n > maxcopy) + n = maxcopy; + memcpy(netbuf+nprefix, readbuf, n); /* SAFE: see checking above */ + netbuf[n+nprefix] = '\0'; + + safefree(WSU(sptr)->lefttoparse); + } else { + strlcpy(netbuf, readbuf, sizeof(netbuf)); + } + + for (r = websocket_handshake_helper(netbuf, strlen(netbuf), &key, &value, &lastloc, &end_of_request); + r; + r = websocket_handshake_helper(NULL, 0, &key, &value, &lastloc, &end_of_request)) + { + if (!stricmp(key, "Sec-WebSocket-Key")) + { + if (strchr(value, ':')) + { + /* This would cause unserialization issues. Should be base64 anyway */ + dead_link(sptr, "Invalid characters in Sec-WebSocket-Key"); + return -1; + } + safestrdup(WSU(sptr)->handshake_key, value); + } + } + + if (end_of_request) + { + if (!WSU(sptr)->handshake_key) + { + dead_link(sptr, "Invalid WebSocket request"); + return -1; + } + websocket_complete_handshake(sptr); + return 0; + } + + if (lastloc) + { + /* Last line was cut somewhere, save it for next round. */ + safefree(WSU(sptr)->lefttoparse); + WSU(sptr)->lefttoparse = strdup(lastloc); + } + return 0; /* don't let UnrealIRCd process this */ +} + +/** Complete the handshake by sending the appropriate HTTP 101 response etc. */ +int websocket_complete_handshake(aClient *sptr) +{ + char buf[512], hashbuf[64]; + SHA_CTX hash; + char sha1out[20]; /* 160 bits */ + + WSU(sptr)->handshake_completed = 1; + + snprintf(buf, sizeof(buf), "%s%s", WSU(sptr)->handshake_key, WEBSOCKET_MAGIC_KEY); + SHA1_Init(&hash); + SHA1_Update(&hash, buf, strlen(buf)); + SHA1_Final(sha1out, &hash); + + 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" + "\r\n", + hashbuf); + + /* 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(&sptr->local->sendQ, buf, strlen(buf)); + send_queued(sptr); + + return 0; +} + +/** WebSocket packet handler. + * For more information on the format, check out page 28 of RFC6455. + * @returns The number of bytes processed (the size of the frame) + * OR 0 to indicate a possible short read (want more data) + * OR -1 in case of an error. + */ +int websocket_handle_packet(aClient *sptr, char *readbuf, int length) +{ + char fin; /**< Final fragment */ + char opcode; /**< Opcode */ + char masked; /**< Masked */ + int len; /**< Length of the packet */ + char maskkey[4]; /**< Key used for masking */ + char *p, *payload; + int total_packet_size; + + if (length < 4) + { + /* WebSocket packet too short */ + return 0; + } + + fin = readbuf[0] & 0x80; + opcode = readbuf[0] & 0x7F; + masked = readbuf[1] & 0x80; + len = readbuf[1] & 0x7F; + p = &readbuf[2]; /* point to next element */ + + /* actually 'fin' is unused.. we don't care. */ + + if (!masked) + { + dead_link(sptr, "WebSocket packet not masked"); + return -1; /* Having the masked bit set is required (RFC6455 p29) */ + } + + if (len == 127) + { + dead_link(sptr, "WebSocket packet with insane size"); + return -1; /* Packets requiring 64bit lengths are not supported. Would be insane. */ + } + + total_packet_size = len + 2 + 4; /* 2 for header, 4 for mask key, rest for payload */ + + if (length < len + 2 + 4) + { + /* WebSocket frame too short */ + return 0; + } + + if (len == 126) + { + /* Extended payload length (16 bit). For packets of >=126 bytes */ + len = (readbuf[2] << 8) + readbuf[3]; + if (len < 126) + { + dead_link(sptr, "WebSocket protocol violation (extended payload length too short)"); + return -1; /* This is a violation (not a short read), see page 29 */ + } + p += 2; /* advance pointer 2 bytes */ + + /* Need to check the length again, now it has changed: */ + if (length < len + 4 + 4) + { + /* WebSocket frame too short */ + return 0; + } + /* And update the packet size */ + total_packet_size = len + 4 + 4; /* 4 for header, 4 for mask key, rest for payload */ + } + + memcpy(maskkey, p, 4); + p+= 4; + payload = (len > 0) ? p : NULL; + + if (len > 0) + { + /* Unmask this thing (page 33, section 5.3) */ + int n; + char v; + for (n = 0; n < len; n++) + { + v = *p; + *p++ = v ^ maskkey[n % 4]; + } + } + + switch(opcode) + { + case WSOP_CONTINUATION: + case WSOP_TEXT: + case WSOP_BINARY: + if (!process_packet(sptr, payload, len, 1)) /* let UnrealIRCd process this data */ + return -1; /* fatal error occured (such as flood kill) */ + return total_packet_size; + + case WSOP_CLOSE: + dead_link(sptr, "Connection closed"); /* TODO: Improve I guess */ + return -1; + + case WSOP_PING: + websocket_handle_packet_ping(sptr, payload, len); + return total_packet_size; + + case WSOP_PONG: + websocket_handle_packet_pong(sptr, payload, len); + return total_packet_size; + + default: + dead_link(sptr, "WebSocket: Unknown opcode"); + return -1; + } + + return -1; /* NOTREACHED */ +} + +int websocket_handle_packet_ping(aClient *sptr, char *buf, int len) +{ + /* This is UNTESTED */ + websocket_send_frame(sptr, WSOP_PONG, buf, len); + return -1; +} + +int websocket_handle_packet_pong(aClient *sptr, char *buf, int len) +{ + /* We don't care */ + return -1; +} + +/** Create a frame. Used for OUTGOING data. */ +int websocket_create_frame(int opcode, char **buf, int *len) +{ + static char sendbuf[8192]; + + sendbuf[0] = opcode | 0x80; /* text & final */ + + if (*len > sizeof(sendbuf) - 8) + abort(); /* should never happen (safety) */ + + if (*len < 126) + { + /* Short payload */ + sendbuf[1] = (char)*len; + memcpy(&sendbuf[2], *buf, *len); + *buf = sendbuf; + *len += 2; + } else { + /* Long payload */ + sendbuf[1] = 126; + sendbuf[2] = (char)((*len >> 8) & 0xFF); + sendbuf[3] = (char)(*len & 0xFF); + memcpy(&sendbuf[4], *buf, *len); + *buf = sendbuf; + *len += 4; + } + return 0; +} + +/** Create and send a frame */ +int websocket_send_frame(aClient *sptr, int opcode, char *buf, int len) +{ + char *b = buf; + int l = len; + + if (websocket_create_frame(opcode, &b, &l) < 0) + return -1; + + if (DBufLength(&sptr->local->sendQ) > get_sendq(sptr)) + { + dead_link(sptr, "Max SendQ exceeded"); + return -1; + } + + dbuf_put(&sptr->local->sendQ, b, l); + send_queued(sptr); + return 0; +}