X-Git-Url: https://jfr.im/git/solanum.git/blobdiff_plain/9b24cbdecc72337e825764f39cdf59ca23f41788..1f8c7ce5ca3ca09092ecce0efe73f2bd7bbc424b:/wsockd/wsockd.c diff --git a/wsockd/wsockd.c b/wsockd/wsockd.c index 7c6bbab2..66fa4efc 100644 --- a/wsockd/wsockd.c +++ b/wsockd/wsockd.c @@ -1,8 +1,8 @@ /* - * wsockd.c: charybdis websockets helper + * wsockd.c: solanum websockets helper * Copyright (C) 2007 Aaron Sethman * Copyright (C) 2007 ircd-ratbox development team - * Copyright (C) 2016 William Pitcock + * Copyright (C) 2016 Ariadne Conill * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,12 +21,17 @@ */ #include "stdinc.h" +#include "sha1.h" #define MAXPASSFD 4 #ifndef READBUF_SIZE #define READBUF_SIZE 16384 #endif +#define WEBSOCKET_SERVER_KEY "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" +#define WEBSOCKET_ANSWER_STRING_1 "HTTP/1.1 101 Switching Protocols\r\nAccess-Control-Allow-Origin: *\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: " +#define WEBSOCKET_ANSWER_STRING_2 "\r\n\r\n" + static void setup_signals(void); static pid_t ppid; @@ -70,8 +75,12 @@ typedef struct _conn { rb_dlink_node node; mod_ctl_t *ctl; + rawbuf_head_t *modbuf_out; - rawbuf_head_t *plainbuf_out; + rawbuf_head_t *modbuf_in; + + buf_head_t plainbuf_out; + buf_head_t plainbuf_in; uint32_t id; @@ -82,49 +91,101 @@ typedef struct _conn uint64_t plain_in; uint64_t plain_out; uint8_t flags; + + char client_key[37]; /* maximum 36 bytes + nul */ } conn_t; +#define WEBSOCKET_OPCODE_TEXT_FRAME 1 + +#define WEBSOCKET_MASK_LENGTH 4 + +#define WEBSOCKET_MAX_UNEXTENDED_PAYLOAD_DATA_LENGTH 125 + +typedef struct { + uint8_t opcode_rsv_fin; // opcode: 4, rsv1: 1, rsv2: 1, rsv3: 1, fin: 1 + uint8_t payload_length_mask; // payload_length: 7, mask: 1 +} ws_frame_hdr_t; + +#define WEBSOCKET_FRAME_HDR_INIT ((ws_frame_hdr_t) { 0, 0 }) + +typedef struct { + ws_frame_hdr_t header; + uint8_t payload_data[WEBSOCKET_MAX_UNEXTENDED_PAYLOAD_DATA_LENGTH]; +} ws_frame_payload_t; + +typedef struct { + ws_frame_hdr_t header; +} ws_frame_t; + +typedef struct { + ws_frame_hdr_t header; + uint16_t payload_length_extended; +} ws_frame_ext_t; + +#define WEBSOCKET_FRAME_EXT_INIT ((ws_frame_ext_t) { WEBSOCKET_FRAME_HDR_INIT, 0 }) + +typedef struct { + ws_frame_hdr_t header; + uint64_t payload_length_extended; +} ws_frame_ext2_t; + +static inline void +ws_frame_set_opcode(ws_frame_hdr_t *header, int opcode) +{ + header->opcode_rsv_fin &= ~0xF; + header->opcode_rsv_fin |= opcode & 0xF; +} + +static inline void +ws_frame_set_fin(ws_frame_hdr_t *header, int fin) +{ + header->opcode_rsv_fin &= ~(0x1 << 7); + header->opcode_rsv_fin |= (fin << 7) & (0x1 << 7); +} + +static void close_conn(conn_t * conn, int wait_plain, const char *fmt, ...); +static void conn_mod_read_cb(rb_fde_t *fd, void *data); +static void conn_plain_read_cb(rb_fde_t *fd, void *data); +static void conn_plain_process_recvq(conn_t *conn); + #define FLAG_CORK 0x01 #define FLAG_DEAD 0x02 #define FLAG_WSOCK 0x04 +#define FLAG_KEYED 0x08 #define IsCork(x) ((x)->flags & FLAG_CORK) #define IsDead(x) ((x)->flags & FLAG_DEAD) -#define IsWS(x) ((x)->flags & FLAG_WSOCK) +#define IsKeyed(x) ((x)->flags & FLAG_KEYED) #define SetCork(x) ((x)->flags |= FLAG_CORK) #define SetDead(x) ((x)->flags |= FLAG_DEAD) #define SetWS(x) ((x)->flags |= FLAG_WSOCK) +#define SetKeyed(x) ((x)->flags |= FLAG_KEYED) #define ClearCork(x) ((x)->flags &= ~FLAG_CORK) -#define ClearDead(x) ((x)->flags &= ~FLAG_DEAD) -#define ClearWS(x) ((x)->flags &= ~FLAG_WSOCK) #define NO_WAIT 0x0 #define WAIT_PLAIN 0x1 -#define HASH_WALK_SAFE(i, max, ptr, next, table) for(i = 0; i < max; i++) { RB_DLINK_FOREACH_SAFE(ptr, next, table[i].head) -#define HASH_WALK_END } #define CONN_HASH_SIZE 2000 #define connid_hash(x) (&connid_hash_table[(x % CONN_HASH_SIZE)]) +static const char *remote_closed = "Remote host closed the connection"; + static rb_dlink_list connid_hash_table[CONN_HASH_SIZE]; static rb_dlink_list dead_list; static void conn_plain_read_shutdown_cb(rb_fde_t *fd, void *data); -#ifndef _WIN32 static void dummy_handler(int sig) { return; } -#endif static void setup_signals() { -#ifndef _WIN32 struct sigaction act; act.sa_flags = 0; @@ -147,38 +208,20 @@ setup_signals() act.sa_handler = dummy_handler; sigaction(SIGALRM, &act, 0); -#endif } static int maxconn(void) { -#if defined(RLIMIT_NOFILE) && defined(HAVE_SYS_RESOURCE_H) struct rlimit limit; if(!getrlimit(RLIMIT_NOFILE, &limit)) { return limit.rlim_cur; } -#endif /* RLIMIT_FD_MAX */ return MAXCONNECTIONS; } -static conn_t * -conn_find_by_id(uint32_t id) -{ - rb_dlink_node *ptr; - conn_t *conn; - - RB_DLINK_FOREACH(ptr, (connid_hash(id))->head) - { - conn = ptr->data; - if(conn->id == id && !IsDead(conn)) - return conn; - } - return NULL; -} - static void conn_add_id_hash(conn_t * conn, uint32_t id) { @@ -189,8 +232,12 @@ conn_add_id_hash(conn_t * conn, uint32_t id) static void free_conn(conn_t * conn) { + rb_linebuf_donebuf(&conn->plainbuf_in); + rb_linebuf_donebuf(&conn->plainbuf_out); + + rb_free_rawbuffer(conn->modbuf_in); rb_free_rawbuffer(conn->modbuf_out); - rb_free_rawbuffer(conn->plainbuf_out); + rb_free(conn); } @@ -209,6 +256,116 @@ clean_dead_conns(void *unused) dead_list.tail = dead_list.head = NULL; } +static void +conn_plain_write_sendq(rb_fde_t *fd, void *data) +{ + conn_t *conn = data; + int retlen; + + if(IsDead(conn)) + return; + + while((retlen = rb_linebuf_flush(fd, &conn->plainbuf_out)) > 0) + conn->plain_out += retlen; + + if(retlen == 0 || (retlen < 0 && !rb_ignore_errno(errno))) + { + close_conn(data, NO_WAIT, NULL); + return; + } + + if(rb_linebuf_alloclen(&conn->plainbuf_out) > 0) + rb_setselect(conn->plain_fd, RB_SELECT_WRITE, conn_plain_write_sendq, conn); + else + rb_setselect(conn->plain_fd, RB_SELECT_WRITE, NULL, NULL); +} + +static void +conn_mod_write_sendq(rb_fde_t *fd, void *data) +{ + conn_t *conn = data; + const char *err; + int retlen; + + if(IsDead(conn)) + return; + + while((retlen = rb_rawbuf_flush(conn->modbuf_out, fd)) > 0) + conn->mod_out += retlen; + + if(retlen == 0 || (retlen < 0 && !rb_ignore_errno(errno))) + { + if(retlen == 0) + close_conn(conn, WAIT_PLAIN, "%s", remote_closed); + err = strerror(errno); + close_conn(conn, WAIT_PLAIN, "Write error: %s", err); + return; + } + + if(rb_rawbuf_length(conn->modbuf_out) > 0) + rb_setselect(conn->mod_fd, RB_SELECT_WRITE, conn_mod_write_sendq, conn); + else + rb_setselect(conn->mod_fd, RB_SELECT_WRITE, NULL, NULL); + + if(IsCork(conn) && rb_rawbuf_length(conn->modbuf_out) == 0) + { + ClearCork(conn); + conn_plain_read_cb(conn->plain_fd, conn); + } +} + +static void +conn_mod_write(conn_t * conn, void *data, size_t len) +{ + if(IsDead(conn)) /* no point in queueing to a dead man */ + return; + rb_rawbuf_append(conn->modbuf_out, data, len); +} + +static void +conn_mod_write_short_frame(conn_t * conn, void *data, int len) +{ + ws_frame_hdr_t hdr = WEBSOCKET_FRAME_HDR_INIT; + + ws_frame_set_opcode(&hdr, WEBSOCKET_OPCODE_TEXT_FRAME); + ws_frame_set_fin(&hdr, 1); + hdr.payload_length_mask = (len + 2) & 0x7f; + + conn_mod_write(conn, &hdr, sizeof(hdr)); + conn_mod_write(conn, data, len); + conn_mod_write(conn, "\r\n", 2); +} + +static void +conn_mod_write_long_frame(conn_t * conn, void *data, int len) +{ + ws_frame_ext_t hdr = WEBSOCKET_FRAME_EXT_INIT; + + ws_frame_set_opcode(&hdr.header, WEBSOCKET_OPCODE_TEXT_FRAME); + ws_frame_set_fin(&hdr.header, 1); + hdr.header.payload_length_mask = 126; + hdr.payload_length_extended = htons(len + 2); + + conn_mod_write(conn, &hdr, sizeof(hdr)); + conn_mod_write(conn, data, len); + conn_mod_write(conn, "\r\n", 2); +} + +static void +conn_mod_write_frame(conn_t *conn, void *data, int len) +{ + if(IsDead(conn)) /* no point in queueing to a dead man */ + return; + + if (len < 123) + { + conn_mod_write_short_frame(conn, data, len); + return; + } + + conn_mod_write_long_frame(conn, data, len); +} + static void mod_write_ctl(rb_fde_t *F, void *data) { @@ -262,8 +419,11 @@ close_conn(conn_t * conn, int wait_plain, const char *fmt, ...) if(IsDead(conn)) return; + if (IsKeyed(conn)) + conn_plain_process_recvq(conn); + rb_rawbuf_flush(conn->modbuf_out, conn->mod_fd); - rb_rawbuf_flush(conn->plainbuf_out, conn->plain_fd); + rb_linebuf_flush(conn->plain_fd, &conn->plainbuf_out); rb_close(conn->mod_fd); SetDead(conn); @@ -295,13 +455,18 @@ make_conn(mod_ctl_t * ctl, rb_fde_t *mod_fd, rb_fde_t *plain_fd) { conn_t *conn = rb_malloc(sizeof(conn_t)); conn->ctl = ctl; - conn->modbuf_out = rb_new_rawbuffer(); - conn->plainbuf_out = rb_new_rawbuffer(); conn->mod_fd = mod_fd; conn->plain_fd = plain_fd; conn->id = -1; rb_set_nb(mod_fd); rb_set_nb(plain_fd); + + rb_linebuf_newbuf(&conn->plainbuf_in); + rb_linebuf_newbuf(&conn->plainbuf_out); + + conn->modbuf_in = rb_new_rawbuffer(); + conn->modbuf_out = rb_new_rawbuffer(); + return conn; } @@ -316,9 +481,196 @@ cleanup_bad_message(mod_ctl_t * ctl, mod_ctl_buf_t * ctlb) } static void -conn_mod_handshake_cb(rb_fde_t *fd, void *data) +ws_frame_unmask(char *msg, int length, uint8_t maskval[WEBSOCKET_MASK_LENGTH]) +{ + int i; + + for (i = 0; i < length; i++) + msg[i] = msg[i] ^ maskval[i % 4]; +} + +static void +conn_mod_process_frame(conn_t *conn, ws_frame_hdr_t *hdr, int masked) +{ + char msg[WEBSOCKET_MAX_UNEXTENDED_PAYLOAD_DATA_LENGTH]; + uint8_t maskval[WEBSOCKET_MASK_LENGTH]; + int dolen; + + /* if we're masked, we get to collect the masking key for this frame */ + if (masked) + { + dolen = rb_rawbuf_get(conn->modbuf_in, maskval, sizeof(maskval)); + if (!dolen) + { + close_conn(conn, WAIT_PLAIN, "websocket error: fault unpacking unmask key"); + return; + } + } + + dolen = rb_rawbuf_get(conn->modbuf_in, msg, hdr->payload_length_mask); + if (!dolen) + { + close_conn(conn, WAIT_PLAIN, "websocket error: fault unpacking message"); + return; + } + + if (masked) + ws_frame_unmask(msg, dolen, maskval); + + rb_linebuf_parse(&conn->plainbuf_out, msg, dolen, 1); +} + +static void +conn_mod_process_large(conn_t *conn, ws_frame_hdr_t *hdr, int masked) +{ + char msg[READBUF_SIZE]; + uint16_t msglen; + uint8_t maskval[WEBSOCKET_MASK_LENGTH]; + int dolen; + + memset(msg, 0, sizeof msg); + + dolen = rb_rawbuf_get(conn->modbuf_in, &msglen, sizeof(msglen)); + if (!dolen) + { + close_conn(conn, WAIT_PLAIN, "websocket error: fault unpacking message size"); + return; + } + + msglen = ntohs(msglen); + + if (masked) + { + dolen = rb_rawbuf_get(conn->modbuf_in, maskval, sizeof(maskval)); + if (!dolen) + { + close_conn(conn, WAIT_PLAIN, "websocket error: fault unpacking unmask key"); + return; + } + } + + dolen = rb_rawbuf_get(conn->modbuf_in, msg, msglen); + if (!dolen) + { + close_conn(conn, WAIT_PLAIN, "websocket error: fault unpacking message"); + return; + } + + if (masked) + ws_frame_unmask(msg, dolen, maskval); + + rb_linebuf_parse(&conn->plainbuf_out, msg, dolen, 1); +} + +static void +conn_mod_process_huge(conn_t *conn, ws_frame_hdr_t *hdr, int masked) +{ + /* XXX implement me */ +} + +static void +conn_mod_process(conn_t *conn) +{ + ws_frame_hdr_t hdr; + + while (1) + { + int masked; + int dolen = rb_rawbuf_get(conn->modbuf_in, &hdr, sizeof(hdr)); + if (dolen != sizeof(hdr)) + break; + + masked = (hdr.payload_length_mask >> 7) == 1; + + hdr.payload_length_mask &= 0x7f; + switch (hdr.payload_length_mask) + { + case 126: + conn_mod_process_large(conn, &hdr, masked); + break; + case 127: + conn_mod_process_huge(conn, &hdr, masked); + break; + default: + conn_mod_process_frame(conn, &hdr, masked); + break; + } + } + + conn_plain_write_sendq(conn->plain_fd, conn); +} + +static void +conn_mod_handshake_process(conn_t *conn) +{ + char inbuf[READBUF_SIZE]; + + memset(inbuf, 0, sizeof inbuf); + + while (1) + { + char *p = NULL; + + int dolen = rb_rawbuf_get(conn->modbuf_in, inbuf, sizeof inbuf); + if (!dolen) + break; + + if ((p = rb_strcasestr(inbuf, "Sec-WebSocket-Key:")) != NULL) + { + char *start, *end; + + start = p + strlen("Sec-WebSocket-Key:"); + + for (; start < (inbuf + READBUF_SIZE) && *start; start++) + { + if (*start != ' ' && *start != '\t') + break; + } + + for (end = start; end < (inbuf + READBUF_SIZE) && *end; end++) + { + if (*end == '\r' || *end == '\n') + { + *end = '\0'; + break; + } + } + + rb_strlcpy(conn->client_key, start, sizeof(conn->client_key)); + SetKeyed(conn); + } + } + + if (IsKeyed(conn)) + { + SHA1 sha1; + uint8_t digest[SHA1_DIGEST_LENGTH]; + char *resp; + + sha1_init(&sha1); + sha1_update(&sha1, (uint8_t *) conn->client_key, strlen(conn->client_key)); + sha1_update(&sha1, (uint8_t *) WEBSOCKET_SERVER_KEY, strlen(WEBSOCKET_SERVER_KEY)); + sha1_final(&sha1, digest); + + resp = (char *) rb_base64_encode(digest, SHA1_DIGEST_LENGTH); + + conn_mod_write(conn, WEBSOCKET_ANSWER_STRING_1, strlen(WEBSOCKET_ANSWER_STRING_1)); + conn_mod_write(conn, resp, strlen(resp)); + conn_mod_write(conn, WEBSOCKET_ANSWER_STRING_2, strlen(WEBSOCKET_ANSWER_STRING_2)); + + rb_free(resp); + } + + conn_mod_write_sendq(conn->mod_fd, conn); +} + +static void +conn_mod_read_cb(rb_fde_t *fd, void *data) { char inbuf[READBUF_SIZE]; + + memset(inbuf, 0, sizeof inbuf); + conn_t *conn = data; int length = 0; if (conn == NULL) @@ -332,23 +684,125 @@ conn_mod_handshake_cb(rb_fde_t *fd, void *data) if (IsDead(conn)) return; - length = rb_read(conn->plain_fd, inbuf, sizeof(inbuf)); - if (length == 0 || (length < 0 && !rb_ignore_errno(errno))) + length = rb_read(fd, inbuf, sizeof(inbuf)); + + if (length < 0) + { + if (rb_ignore_errno(errno)) + { + rb_setselect(fd, RB_SELECT_READ, conn_mod_read_cb, conn); + conn_plain_write_sendq(conn->plain_fd, conn); + } + else + close_conn(conn, NO_WAIT, "Connection closed"); + + return; + } + else if (length == 0) { close_conn(conn, NO_WAIT, "Connection closed"); return; } + + rb_rawbuf_append(conn->modbuf_in, inbuf, length); + if (!IsKeyed(conn)) + conn_mod_handshake_process(conn); + else + conn_mod_process(conn); + + if ((size_t) length < sizeof(inbuf)) + { + rb_setselect(fd, RB_SELECT_READ, conn_mod_read_cb, conn); + return; + } + } +} + +static bool +plain_check_cork(conn_t * conn) +{ + if(rb_rawbuf_length(conn->modbuf_out) >= 4096) + { + /* if we have over 4k pending outbound, don't read until + * we've cleared the queue */ + SetCork(conn); + rb_setselect(conn->plain_fd, RB_SELECT_READ, NULL, NULL); + /* try to write */ + if (IsKeyed(conn)) + conn_mod_write_sendq(conn->mod_fd, conn); + return true; } + + return false; } static void -conn_mod_read_cb(rb_fde_t *fd, void *data) +conn_plain_process_recvq(conn_t *conn) { + char inbuf[READBUF_SIZE]; + + memset(inbuf, 0, sizeof inbuf); + + while (1) + { + int dolen = rb_linebuf_get(&conn->plainbuf_in, inbuf, sizeof inbuf, LINEBUF_COMPLETE, LINEBUF_PARSED); + if (!dolen) + break; + + conn_mod_write_frame(conn, inbuf, dolen); + } + + if (IsKeyed(conn)) + conn_mod_write_sendq(conn->mod_fd, conn); } static void conn_plain_read_cb(rb_fde_t *fd, void *data) { + char inbuf[READBUF_SIZE]; + + memset(inbuf, 0, sizeof inbuf); + + conn_t *conn = data; + int length = 0; + if(conn == NULL) + return; + + if(IsDead(conn)) + return; + + if(plain_check_cork(conn)) + return; + + while(1) + { + if(IsDead(conn)) + return; + + length = rb_read(conn->plain_fd, inbuf, sizeof(inbuf)); + + if(length == 0 || (length < 0 && !rb_ignore_errno(errno))) + { + close_conn(conn, NO_WAIT, NULL); + return; + } + + if(length < 0) + { + rb_setselect(conn->plain_fd, RB_SELECT_READ, conn_plain_read_cb, conn); + if (IsKeyed(conn)) + conn_plain_process_recvq(conn); + return; + } + conn->plain_in += length; + + (void) rb_linebuf_parse(&conn->plainbuf_in, inbuf, length, 0); + + if(IsDead(conn)) + return; + if(plain_check_cork(conn)) + return; + } } static void @@ -398,7 +852,8 @@ wsock_process(mod_ctl_t * ctl, mod_ctl_buf_t * ctlb) if(rb_get_type(conn->plain_fd) == RB_FD_UNKNOWN) rb_set_type(conn->plain_fd, RB_FD_SOCKET); - conn_mod_handshake_cb(conn->mod_fd, conn); + conn_mod_read_cb(conn->mod_fd, conn); + conn_plain_read_cb(conn->plain_fd, conn); } static void @@ -490,7 +945,7 @@ int main(int argc, char **argv) { const char *s_ctlfd, *s_pipe, *s_pid; - int ctlfd, pipefd, x, maxfd; + int ctlfd, pipefd, maxfd, x; maxfd = maxconn(); s_ctlfd = getenv("CTL_FD"); @@ -500,7 +955,7 @@ main(int argc, char **argv) if(s_ctlfd == NULL || s_pipe == NULL || s_pid == NULL) { fprintf(stderr, - "This is the charybdis wsockd for internal ircd use.\n"); + "This is the solanum wsockd for internal ircd use.\n"); fprintf(stderr, "You aren't supposed to run me directly. Exiting.\n"); exit(1); @@ -509,8 +964,7 @@ main(int argc, char **argv) ctlfd = atoi(s_ctlfd); pipefd = atoi(s_pipe); ppid = atoi(s_pid); - x = 0; -#ifndef _WIN32 + for(x = 0; x < maxfd; x++) { if(x != ctlfd && x != pipefd && x > 2) @@ -529,10 +983,11 @@ main(int argc, char **argv) if(x > 2) close(x); } -#endif + setup_signals(); rb_lib_init(NULL, NULL, NULL, 0, maxfd, 1024, 4096); - rb_init_rawbuffers(1024); + rb_linebuf_init(4096); + rb_init_rawbuffers(4096); mod_ctl = rb_malloc(sizeof(mod_ctl_t)); mod_ctl->F = rb_open(ctlfd, RB_FD_SOCKET, "ircd control socket");