From 20323140f55607d51199af8060b3d971451ae262 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 25 May 2019 15:57:20 +0200 Subject: Implement pongs, move to multiple TLUs --- .gitignore | 1 + builtins.c | 5 + builtins.h | 11 ++ makefile | 9 +- network.c | 27 +++- network.h | 7 + websocket.c | 491 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ websocket.h | 6 + websocksy.c | 76 ++++++--- websocksy.h | 8 + ws_proto.c | 513 ------------------------------------------------------------ 11 files changed, 617 insertions(+), 537 deletions(-) create mode 100644 builtins.h create mode 100644 network.h create mode 100644 websocket.c create mode 100644 websocket.h delete mode 100644 ws_proto.c diff --git a/.gitignore b/.gitignore index 0ac4e99..23d0915 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.swp websocksy +*.o diff --git a/builtins.c b/builtins.c index fa73260..d5b081f 100644 --- a/builtins.c +++ b/builtins.c @@ -1,3 +1,8 @@ +#include + +#include "websocksy.h" +#include "builtins.h" + #define UTF8_BYTE(a) ((a & 0xC0) == 0x80) /* diff --git a/builtins.h b/builtins.h new file mode 100644 index 0000000..926d5bb --- /dev/null +++ b/builtins.h @@ -0,0 +1,11 @@ +/* The builtin `defaultpeer` backend */ +uint64_t backend_defaultpeer_init(); +uint64_t backend_defaultpeer_configure(char* key, char* value); +ws_peer_info backend_defaultpeer_query(char* endpoint, size_t protocols, char** protocol, size_t headers, ws_http_header* header, websocket* ws); +void backend_defaultpeer_cleanup(); + +/* Built-in framing functions */ +int64_t framing_auto(uint8_t* data, size_t length, size_t last_read, ws_operation* opcode, void** framing_data, char* config); +int64_t framing_binary(uint8_t* data, size_t length, size_t last_read, ws_operation* opcode, void** framing_data, char* config); +int64_t framing_separator(uint8_t* data, size_t length, size_t last_read, ws_operation* opcode, void** framing_data, char* config); +int64_t framing_newline(uint8_t* data, size_t length, size_t last_read, ws_operation* opcode, void** framing_data, char* config); diff --git a/makefile b/makefile index 0a9c4ae..4623e49 100644 --- a/makefile +++ b/makefile @@ -1,10 +1,13 @@ CFLAGS=-g -Wall -Wpedantic LDLIBS=-lnettle +OBJECTS=builtins.o network.o websocket.o + all: websocksy -websocksy: websocksy.c websocksy.h ws_proto.c builtins.c - $(CC) $(CFLAGS) $(LDLIBS) $< -o $@ +websocksy: websocksy.c websocksy.h $(OBJECTS) + $(CC) $(CFLAGS) $(LDLIBS) $< -o $@ $(OBJECTS) clean: - rm websocksy + $(RM) $(OBJECTS) + $(RM) websocksy diff --git a/network.c b/network.c index 9719b27..2ecee4b 100644 --- a/network.c +++ b/network.c @@ -1,3 +1,19 @@ +#include "network.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * Create a file descriptor connected to a socket peer. + * Client sockets will be connected, listening sockets will be bound. + * Returns -1 in case of failure, a valid fd otherwise. + */ int network_socket(char* host, char* port, int socktype, int listener){ int fd = -1, status, yes = 1, flags; struct addrinfo hints = { @@ -81,7 +97,12 @@ int network_socket(char* host, char* port, int socktype, int listener){ return fd; } +/* + * Send arbitrary data over multiple writes if necessary. + * Returns 0 on success + */ int network_send(int fd, uint8_t* data, size_t length){ + //TODO probably should introduce send buffering at some point ssize_t total = 0, sent; while(total < length){ sent = send(fd, data + total, length - total, 0); @@ -94,6 +115,10 @@ int network_send(int fd, uint8_t* data, size_t length){ return 0; } +/* + * Send string data over multiple writes if necessary. + * Returns 0 on success + */ int network_send_str(int fd, char* data){ return network_send(fd, (uint8_t*) data, strlen(data)); -} \ No newline at end of file +} diff --git a/network.h b/network.h new file mode 100644 index 0000000..50c8e18 --- /dev/null +++ b/network.h @@ -0,0 +1,7 @@ +#include +#include + +/* Socket interface convenience functions */ +int network_socket(char* host, char* port, int socktype, int listener); +int network_send(int fd, uint8_t* data, size_t length); +int network_send_str(int fd, char* data); diff --git a/websocket.c b/websocket.c new file mode 100644 index 0000000..c90c71e --- /dev/null +++ b/websocket.c @@ -0,0 +1,491 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "websocket.h" +#include "network.h" + +#define RFC6455_MAGIC_KEY "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" +#define WS_FRAME_HEADER_LEN 16 + +#define WS_FLAG_FIN 0x80 +#define WS_GET_FIN(a) ((a & WS_FLAG_FIN) >> 7) +#define WS_GET_RESERVED(a) ((a & 0xE0) >> 4) +#define WS_GET_OP(a) ((a & 0x0F)) +#define WS_GET_MASK(a) ((a & 0x80) >> 7) +#define WS_GET_LEN(a) ((a & 0x7F)) + +int ws_close(websocket* ws, ws_close_reason code, char* reason){ + size_t p; + ws_peer_info empty_peer = { + 0 + }; + + if(ws->state == ws_open && reason){ + //TODO send close frame + } + ws->state = ws_closed; + + if(ws->ws_fd >= 0){ + close(ws->ws_fd); + ws->ws_fd = -1; + } + + if(ws->peer_fd >= 0){ + close(ws->peer_fd); + ws->peer_fd = -1; + //clean up framing data + if(ws->peer_framing_data){ + ws->peer.framing(NULL, 0, 0, NULL, &(ws->peer_framing_data), ws->peer.framing_config); + ws->peer_framing_data = NULL; + } + } + + for(p = 0; p < ws->headers; p++){ + free(ws->header[p].tag); + ws->header[p].tag = NULL; + free(ws->header[p].value); + ws->header[p].value = NULL; + } + ws->headers = 0; + + for(p = 0; p < ws->protocols; p++){ + free(ws->protocol[p]); + } + ws->protocols = 0; + ws->protocol = NULL; + + ws->read_buffer_offset = 0; + ws->peer_buffer_offset = 0; + + free(ws->request_path); + ws->request_path = NULL; + + free(ws->socket_key); + ws->socket_key = NULL; + + ws->websocket_version = 0; + ws->want_upgrade = 0; + + free(ws->peer.host); + free(ws->peer.port); + free(ws->peer.framing_config); + ws->peer = empty_peer; + + return 0; +} + +int ws_accept(int listen_fd){ + websocket ws = { + .ws_fd = accept(listen_fd, NULL, NULL), + .peer_fd = -1 + }; + + return client_register(&ws); +} + +static int ws_handle_new(websocket* ws){ + size_t u; + char* path, *proto; + + if(!strncmp((char*) ws->read_buffer, "GET ", 4)){ + path = (char*) ws->read_buffer + 4; + for(u = 0; path[u] && !isspace(path[u]); u++){ + } + path[u] = 0; + proto = path + u + 1; + } + //TODO handle other methods + else{ + fprintf(stderr, "Unknown HTTP method in request\n"); + return 1; + } + + if(strncmp(proto, "HTTP/", 5)){ + fprintf(stderr, "Malformed HTTP initiation\n"); + return 1; + } + + ws->state = ws_http; + ws->request_path = strdup(path); + return 0; +} + +static int ws_upgrade_http(websocket* ws){ + if(ws->websocket_version == 13 + && ws->socket_key + && ws->want_upgrade == 3){ + + //find and connect peer + if(client_connect(ws)){ + ws_close(ws, ws_close_http, "500 Peer connection failed"); + return 0; + } + + if(network_send_str(ws->ws_fd, "HTTP/1.1 101 Upgrading\r\n") + || network_send_str(ws->ws_fd, "Upgrade: websocket\r\n") + || network_send_str(ws->ws_fd, "Connection: Upgrade\r\n")){ + ws_close(ws, ws_close_http, NULL); + return 0; + } + + //calculate the websocket accept key, which for some reason is defined as + //base64(sha1(concat(trim(client-key), "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))) + //requiring not one but 2 unnecessarily complex operations + size_t encode_offset = 0; + struct sha1_ctx ws_accept_ctx; + struct base64_encode_ctx ws_accept_encode; + uint8_t ws_accept_digest[SHA1_DIGEST_SIZE]; + char ws_accept_key[BASE64_ENCODE_LENGTH(SHA1_DIGEST_SIZE) + 3] = ""; + sha1_init(&ws_accept_ctx); + sha1_update(&ws_accept_ctx, strlen(ws->socket_key), (uint8_t*) ws->socket_key); + sha1_update(&ws_accept_ctx, strlen(RFC6455_MAGIC_KEY), (uint8_t*) RFC6455_MAGIC_KEY); + sha1_digest(&ws_accept_ctx, sizeof(ws_accept_digest), (uint8_t*) &ws_accept_digest); + base64_encode_init(&ws_accept_encode); + encode_offset = base64_encode_update(&ws_accept_encode, (uint8_t*) ws_accept_key, SHA1_DIGEST_SIZE, ws_accept_digest); + encode_offset += base64_encode_final(&ws_accept_encode, (uint8_t*) ws_accept_key + encode_offset); + memcpy(ws_accept_key + encode_offset, "\r\n\0", 3); + + //send websocket accept key + if(network_send_str(ws->ws_fd, "Sec-WebSocket-Accept: ") + || network_send_str(ws->ws_fd, ws_accept_key)){ + ws_close(ws, ws_close_http, NULL); + return 0; + } + + //acknowledge selected protocol + if(ws->peer.protocol < ws->protocols){ + if(network_send_str(ws->ws_fd, "Sec-WebSocket-Protocol: ") + || network_send_str(ws->ws_fd, ws->protocol[ws->peer.protocol]) + || network_send_str(ws->ws_fd, "\r\n")){ + ws_close(ws, ws_close_http, NULL); + return 0; + } + } + + ws->state = ws_open; + if(network_send_str(ws->ws_fd, "\r\n")){ + ws_close(ws, ws_close_http, NULL); + return 0; + } + return 0; + } + //RFC 4.2.2.4: An unsupported version must be answered with HTTP 426 + if(ws->websocket_version != 13){ + ws_close(ws, ws_close_http, "426 Unsupported protocol version"); + return 0; + } + return 1; +} + +static int ws_handle_http(websocket* ws){ + char* header, *value; + ssize_t p; + + if(!ws->read_buffer[0]){ + return ws_upgrade_http(ws); + } + else if(isspace(ws->read_buffer[0])){ + //i hate header folding + ws_close(ws, ws_close_http, "500 Header folding"); + return 0; + } + else{ + header = (char*) ws->read_buffer; + value = strchr(header, ':'); + if(!value){ + ws_close(ws, ws_close_http, "500 Header format"); + return 0; + } + *value = 0; + value++; + for(; isspace(*value); value++){ + } + } + + //RFC 4.2.1 checks + if(!strcmp(header, "Sec-WebSocket-Version")){ + ws->websocket_version = strtoul(value, NULL, 10); + } + else if(!strcmp(header, "Sec-WebSocket-Key")){ + //right-trim the key + for(p = strlen(value) - 1; p >= 0 && isspace(value[p]); p--){ + value[p] = 0; + } + ws->socket_key = strdup(value); + } + else if(!strcmp(header, "Upgrade") && !strcasecmp(value, "websocket")){ + ws->want_upgrade |= 1; + } + else if(!strcmp(header, "Connection") && strstr(xstr_lower(value), "upgrade")){ + ws->want_upgrade |= 2; + } + else if(!strcmp(header, "Sec-WebSocket-Protocol")){ + //parse websocket protocol offers + for(value = strtok(value, ","); value; value = strtok(NULL, ",")){ + //ltrim + for(; *value && isspace(*value); value++){ + } + + //rtrim + for(p = strlen(value) - 1; p >= 0 && isspace(value[p]); p--){ + value[p] = 0; + } + + for(p = 0; p < ws->protocols; p++){ + if(!strcmp(ws->protocol[p], value)){ + break; + } + } + + //add new protocol + if(p == ws->protocols){ + ws->protocol = realloc(ws->protocol, (ws->protocols + 1) * sizeof(char*)); + if(!ws->protocol){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + ws->protocol[ws->protocols] = strdup(value); + ws->protocols++; + } + } + } + else if(ws->headers < WS_HEADER_LIMIT){ + ws->header[ws->headers].tag = strdup(header); + ws->header[ws->headers].value = strdup(value); + ws->headers++; + } + else{ + //limit the number of stored headers to prevent abuse + //ws_close(ws, ws_close_http, "500 Header limit"); + } + return 0; +} + +//returns bytes handled +static size_t ws_frame(websocket* ws){ + size_t u; + uint64_t payload_length = 0; + uint16_t* payload_len16 = (uint16_t*) (ws->read_buffer + 2); + uint64_t* payload_len64 = (uint64_t*) (ws->read_buffer + 2); + uint8_t* masking_key = NULL, *payload = ws->read_buffer + 2; + + //need at least the header bits + if(ws->read_buffer_offset < 2){ + return 0; + } + + if(WS_GET_RESERVED(ws->read_buffer[0])){ + //reserved bits set without any extensions + //RFC 5.2 says we MUST close the connection + //ignoring it for now + } + + //calculate the payload length (could've used a uint64 and be done with it...) + //TODO test this for the bigger frames + payload_length = WS_GET_LEN(ws->read_buffer[1]); + if(WS_GET_MASK(ws->read_buffer[1])){ + if(ws->read_buffer_offset < 6){ + return 0; + } + masking_key = ws->read_buffer + 2; + payload = ws->read_buffer + 6; + } + + if(payload_length == 126){ + //16-bit payload length + if(ws->read_buffer_offset < 4){ + return 0; + } + payload_length = htobe16(*payload_len16); + payload = ws->read_buffer + 4; + if(WS_GET_MASK(ws->read_buffer[1])){ + if(ws->read_buffer_offset < 8){ + return 0; + } + masking_key = ws->read_buffer + 4; + payload = ws->read_buffer + 8; + } + } + else if(payload_length == 127){ + //64-bit payload length + if(ws->read_buffer_offset < 10){ + return 0; + } + payload_length = htobe64(*payload_len64); + payload = ws->read_buffer + 10; + if(WS_GET_MASK(ws->read_buffer[1])){ + if(ws->read_buffer_offset < 14){ + return 0; + } + masking_key = ws->read_buffer + 10; + payload = ws->read_buffer + 14; + } + } + + //check for complete WS frame + if(ws->read_buffer_offset < (payload - ws->read_buffer) + payload_length){ + //fprintf(stderr, "Incomplete payload: offset %lu, want %lu\n", ws->read_buffer_offset, (payload - ws->read_buffer) + payload_length); + return 0; + } + + //RFC Section 5.1: If the client sends an unmasked frame, close the connection + if(!WS_GET_MASK(ws->read_buffer[1])){ + ws_close(ws, ws_close_proto, "Unmasked client frame"); + return 0; + } + + //unmask data + if(WS_GET_MASK(ws->read_buffer[1])){ + for(u = 0; u < payload_length; u++){ + payload[u] = payload[u] ^ masking_key[u % 4]; + } + } + + //TODO handle fragmentation + //TODO handle control frames within fragmented frames + + fprintf(stderr, "Incoming websocket data: %s %s OP %02X LEN %u %lu\n", + WS_GET_FIN(ws->read_buffer[0]) ? "FIN" : "CONT", + WS_GET_MASK(ws->read_buffer[1]) ? "MASK" : "CLEAR", + WS_GET_OP(ws->read_buffer[0]), + WS_GET_LEN(ws->read_buffer[1]), + payload_length); + + //handle data + switch(WS_GET_OP(ws->read_buffer[0])){ + case ws_frame_text: + fprintf(stderr, "Text payload: %.*s\n", (int) payload_length, (char*) payload); + case ws_frame_binary: + //forward to peer + if(ws->peer_fd >= 0){ + fprintf(stderr, "WS -> Peer %lu bytes\n", payload_length); + if(network_send(ws->peer_fd, payload, payload_length)){ + ws_close(ws, ws_close_unexpected, "Failed to forward"); + } + } + break; + case ws_frame_close: + ws_close(ws, ws_close_normal, "Client requested termination"); + break; + case ws_frame_ping: + break; + case ws_frame_pong: + if(ws_send_frame(ws, ws_frame_pong, payload, payload_length)){ + ws_close(ws, ws_close_unexpected, "Failed to send ping"); + } + break; + default: + //unknown frame type received + fprintf(stderr, "Unknown WebSocket opcode %02X in frame\n", WS_GET_OP(ws->read_buffer[0])); + ws_close(ws, ws_close_proto, "Invalid opcode"); + break; + } + + return ((payload - ws->read_buffer) + payload_length); +} + +int ws_send_frame(websocket* ws, ws_operation opcode, uint8_t* data, size_t len){ + fprintf(stderr, "Peer -> WS %lu bytes\n", len); + uint8_t frame_header[WS_FRAME_HEADER_LEN]; + size_t header_bytes = 2; + uint16_t* payload_len16 = (uint16_t*) (frame_header + 2); + uint64_t* payload_len64 = (uint64_t*) (frame_header + 2); + + //FIXME might want to support segmented transfers here + //set up the basic frame header + frame_header[0] = WS_FLAG_FIN | opcode; + if(len <= 125){ + frame_header[1] = len; + } + else if(len <= 0xFFFF){ + frame_header[1] = 126; + *payload_len16 = htobe16(len); + header_bytes += 2; + } + else{ + frame_header[1] = 127; + *payload_len64 = htobe64(len); + header_bytes += 8; + } + + if(network_send(ws->ws_fd, frame_header, header_bytes) + || network_send(ws->ws_fd, data, len)){ + return 1; + } + + return 0; +} + +int ws_data(websocket* ws){ + ssize_t bytes_read, n, bytes_left = sizeof(ws->read_buffer) - ws->read_buffer_offset; + int rv = 0; + + bytes_read = recv(ws->ws_fd, ws->read_buffer + ws->read_buffer_offset, bytes_left - 1, 0); + if(bytes_read < 0){ + fprintf(stderr, "Failed to receive from websocket: %s\n", strerror(errno)); + ws_close(ws, ws_close_unexpected, NULL); + return 0; + } + else if(bytes_read == 0){ + //client closed connection + ws_close(ws, ws_close_unexpected, NULL); + return 0; + } + + //terminate new data + ws->read_buffer[ws->read_buffer_offset + bytes_read] = 0; + + switch(ws->state){ + case ws_new: + case ws_http: + //scan for newline, handle line + for(n = 0; n < bytes_read - 1; n++){ + if(!strncmp((char*) ws->read_buffer + ws->read_buffer_offset + n, "\r\n", 2)){ + //terminate line + ws->read_buffer[ws->read_buffer_offset + n] = 0; + + if(ws->state == ws_new){ + ws_handle_new(ws); + } + else{ + ws_handle_http(ws); + } + + //remove from buffer + bytes_read -= (n + 2); + memmove(ws->read_buffer, ws->read_buffer + ws->read_buffer_offset + n + 2, bytes_read); + ws->read_buffer_offset = 0; + + //restart from the beginning + n = -1; + } + } + //update read buffer offset + ws->read_buffer_offset = bytes_read; + break; + case ws_open: + ws->read_buffer_offset += bytes_read; + for(n = ws_frame(ws); n > 0 && ws->read_buffer_offset > 0; n = ws_frame(ws)){ + memmove(ws->read_buffer, ws->read_buffer + n, ws->read_buffer_offset - n); + ws->read_buffer_offset -= n; + } + break; + //this should never be reached, as ws_close also closes the client fd + case ws_closed: + fprintf(stderr, "This should not have happened\n"); + break; + } + + //disconnect spammy clients + if(sizeof(ws->read_buffer) - ws->read_buffer_offset < 2){ + fprintf(stderr, "Disconnecting misbehaving client\n"); + ws_close(ws, ws_close_limit, "Receive size limit exceeded"); + return 0; + } + return rv; +} diff --git a/websocket.h b/websocket.h new file mode 100644 index 0000000..f2a4500 --- /dev/null +++ b/websocket.h @@ -0,0 +1,6 @@ +#include "websocksy.h" + +int ws_close(websocket* ws, ws_close_reason code, char* reason); +int ws_accept(int listen_fd); +int ws_send_frame(websocket* ws, ws_operation opcode, uint8_t* data, size_t len); +int ws_data(websocket* ws); diff --git a/websocksy.c b/websocksy.c index 6ab0f81..aa0cf77 100644 --- a/websocksy.c +++ b/websocksy.c @@ -10,6 +10,11 @@ #include #include +#include "websocksy.h" +#include "builtins.h" +#include "network.h" +#include "websocket.h" + /* TODO * - TLS * - config parsing @@ -19,16 +24,14 @@ * - framing function discovery / registry */ -/* - * Main loop condition, to be set from signal handler - */ +/* Main loop condition, to be set from signal handler */ static volatile sig_atomic_t shutdown_requested = 0; -#include "websocksy.h" +/* Core client registry */ +static size_t socks = 0; +static websocket* sock = NULL; -/* - * Lowercase input string in-place - */ +/* Lowercase input string in-place */ char* xstr_lower(char* in){ size_t n; for(n = 0; n < strlen(in); n++){ @@ -37,12 +40,7 @@ char* xstr_lower(char* in){ return in; } -#include "network.c" -#include "builtins.c" - -/* - * WebSocket interface & peer discovery configuration - */ +/* Daemon configuration */ static struct { char* host; char* port; @@ -57,7 +55,46 @@ static struct { .backend.cleanup = backend_defaultpeer_cleanup }; -int connect_peer(websocket* ws){ +/* Push a new client to the registry */ +int client_register(websocket* ws){ + size_t n = 0; + + //try to find a slot to occupy + for(n = 0; n < socks; n++){ + if(sock[n].ws_fd == -1){ + break; + } + } + + //none found, need to extend + if(n == socks){ + sock = realloc(sock, (socks + 1) * sizeof(websocket)); + if(!sock){ + close(ws->ws_fd); + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + socks++; + } + + sock[n] = *ws; + + return 0; +} + +void client_cleanup(){ + size_t n; + for(n = 0; n < socks; n++){ + ws_close(sock + n, ws_close_shutdown, "Shutting down"); + } + + free(sock); + sock = NULL; + socks = 0; +} + +/* Establish peer connection for negotiated websocket */ +int client_connect(websocket* ws){ int rv = 1; ws->peer = config.backend.query(ws->request_path, ws->protocols, ws->protocol, ws->headers, ws->header, ws); @@ -71,6 +108,7 @@ int connect_peer(websocket* ws){ ws->peer.framing = framing_auto; } + //TODO connection establishment should be async in the future switch(ws->peer.transport){ case peer_tcp_client: ws->peer_fd = network_socket(ws->peer.host, ws->peer.port, SOCK_STREAM, 0); @@ -100,19 +138,17 @@ int connect_peer(websocket* ws){ return rv; } -#include "ws_proto.c" - /* * Signal handler, attached to SIGINT */ -void signal_handler(int signum){ +static void signal_handler(int signum){ shutdown_requested = 1; } /* * Usage info */ -int usage(char* fn){ +static int usage(char* fn){ fprintf(stderr, "\nwebsocksy v%s - Proxy between websockets and 'real' sockets\n", WEBSOCKSY_VERSION); fprintf(stderr, "Usage:\n"); fprintf(stderr, "\t%s [-p ] [-l ] [-b ]\n", fn); @@ -124,7 +160,7 @@ int args_parse(int argc, char** argv){ return 0; } -int ws_peer_data(websocket* ws){ +static int ws_peer_data(websocket* ws){ ssize_t bytes_read, bytes_left = sizeof(ws->peer_buffer) - ws->peer_buffer_offset; int64_t bytes_framed; //default to a binary frame @@ -268,7 +304,7 @@ int main(int argc, char** argv){ if(config.backend.cleanup){ config.backend.cleanup(); } - ws_cleanup(); + client_cleanup(); close(listen_fd); return 0; } diff --git a/websocksy.h b/websocksy.h index 59ec5b2..f50bb46 100644 --- a/websocksy.h +++ b/websocksy.h @@ -1,3 +1,5 @@ +#ifndef WEBSOCKSY_HEADER_INCLUDED +#define WEBSOCKSY_HEADER_INCLUDED #include #include #include @@ -199,3 +201,9 @@ typedef struct /*_ws_backend*/ { ws_backend_query query; ws_backend_cleanup cleanup; } ws_backend; + +/* Internal helper functions */ +char* xstr_lower(char* in); +int client_register(websocket* ws); +int client_connect(websocket* ws); +#endif diff --git a/ws_proto.c b/ws_proto.c deleted file mode 100644 index c75fe22..0000000 --- a/ws_proto.c +++ /dev/null @@ -1,513 +0,0 @@ -#define RFC6455_MAGIC_KEY "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" -#define WS_FRAME_HEADER_LEN 16 - -#define WS_FLAG_FIN 0x80 -#define WS_GET_FIN(a) ((a & WS_FLAG_FIN) >> 7) -#define WS_GET_RESERVED(a) ((a & 0xE0) >> 4) -#define WS_GET_OP(a) ((a & 0x0F)) -#define WS_GET_MASK(a) ((a & 0x80) >> 7) -#define WS_GET_LEN(a) ((a & 0x7F)) - -static size_t socks = 0; -static websocket* sock = NULL; - -int ws_close(websocket* ws, ws_close_reason code, char* reason){ - size_t p; - ws_peer_info empty_peer = { - 0 - }; - - if(ws->state == ws_open && reason){ - //TODO send close frame - } - ws->state = ws_closed; - - if(ws->ws_fd >= 0){ - close(ws->ws_fd); - ws->ws_fd = -1; - } - - if(ws->peer_fd >= 0){ - close(ws->peer_fd); - ws->peer_fd = -1; - //clean up framing data - if(ws->peer_framing_data){ - ws->peer.framing(NULL, 0, 0, NULL, &(ws->peer_framing_data), ws->peer.framing_config); - ws->peer_framing_data = NULL; - } - } - - for(p = 0; p < ws->headers; p++){ - free(ws->header[p].tag); - ws->header[p].tag = NULL; - free(ws->header[p].value); - ws->header[p].value = NULL; - } - ws->headers = 0; - - for(p = 0; p < ws->protocols; p++){ - free(ws->protocol[p]); - } - ws->protocols = 0; - ws->protocol = NULL; - - ws->read_buffer_offset = 0; - ws->peer_buffer_offset = 0; - - free(ws->request_path); - ws->request_path = NULL; - - free(ws->socket_key); - ws->socket_key = NULL; - - ws->websocket_version = 0; - ws->want_upgrade = 0; - - free(ws->peer.host); - free(ws->peer.port); - free(ws->peer.framing_config); - ws->peer = empty_peer; - - return 0; -} - -int ws_accept(int listen_fd){ - size_t n = 0; - - websocket ws = { - .ws_fd = accept(listen_fd, NULL, NULL), - .peer_fd = -1 - }; - - //try to find a slot to occupy - for(n = 0; n < socks; n++){ - if(sock[n].ws_fd == -1){ - break; - } - } - - //none found, need to extend - if(n == socks){ - sock = realloc(sock, (socks + 1) * sizeof(websocket)); - if(!sock){ - close(ws.ws_fd); - fprintf(stderr, "Failed to allocate memory\n"); - return 1; - } - socks++; - } - - sock[n] = ws; - - return 0; -} - -int ws_handle_new(websocket* ws){ - size_t u; - char* path, *proto; - - if(!strncmp((char*) ws->read_buffer, "GET ", 4)){ - path = (char*) ws->read_buffer + 4; - for(u = 0; path[u] && !isspace(path[u]); u++){ - } - path[u] = 0; - proto = path + u + 1; - } - //TODO handle other methods - else{ - fprintf(stderr, "Unknown HTTP method in request\n"); - return 1; - } - - if(strncmp(proto, "HTTP/", 5)){ - fprintf(stderr, "Malformed HTTP initiation\n"); - return 1; - } - - ws->state = ws_http; - ws->request_path = strdup(path); - return 0; -} - -int ws_upgrade_http(websocket* ws){ - if(ws->websocket_version == 13 - && ws->socket_key - && ws->want_upgrade == 3){ - - //find and connect peer - if(connect_peer(ws)){ - ws_close(ws, ws_close_http, "500 Peer connection failed"); - return 0; - } - - if(network_send_str(ws->ws_fd, "HTTP/1.1 101 Upgrading\r\n") - || network_send_str(ws->ws_fd, "Upgrade: websocket\r\n") - || network_send_str(ws->ws_fd, "Connection: Upgrade\r\n")){ - ws_close(ws, ws_close_http, NULL); - return 0; - } - - //calculate the websocket accept key, which for some reason is defined as - //base64(sha1(concat(trim(client-key), "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))) - //requiring not one but 2 unnecessarily complex operations - size_t encode_offset = 0; - struct sha1_ctx ws_accept_ctx; - struct base64_encode_ctx ws_accept_encode; - uint8_t ws_accept_digest[SHA1_DIGEST_SIZE]; - char ws_accept_key[BASE64_ENCODE_LENGTH(SHA1_DIGEST_SIZE) + 3] = ""; - sha1_init(&ws_accept_ctx); - sha1_update(&ws_accept_ctx, strlen(ws->socket_key), (uint8_t*) ws->socket_key); - sha1_update(&ws_accept_ctx, strlen(RFC6455_MAGIC_KEY), (uint8_t*) RFC6455_MAGIC_KEY); - sha1_digest(&ws_accept_ctx, sizeof(ws_accept_digest), (uint8_t*) &ws_accept_digest); - base64_encode_init(&ws_accept_encode); - encode_offset = base64_encode_update(&ws_accept_encode, (uint8_t*) ws_accept_key, SHA1_DIGEST_SIZE, ws_accept_digest); - encode_offset += base64_encode_final(&ws_accept_encode, (uint8_t*) ws_accept_key + encode_offset); - memcpy(ws_accept_key + encode_offset, "\r\n\0", 3); - - //send websocket accept key - if(network_send_str(ws->ws_fd, "Sec-WebSocket-Accept: ") - || network_send_str(ws->ws_fd, ws_accept_key)){ - ws_close(ws, ws_close_http, NULL); - return 0; - } - - //acknowledge selected protocol - if(ws->peer.protocol < ws->protocols){ - if(network_send_str(ws->ws_fd, "Sec-WebSocket-Protocol: ") - || network_send_str(ws->ws_fd, ws->protocol[ws->peer.protocol]) - || network_send_str(ws->ws_fd, "\r\n")){ - ws_close(ws, ws_close_http, NULL); - return 0; - } - } - - ws->state = ws_open; - if(network_send_str(ws->ws_fd, "\r\n")){ - ws_close(ws, ws_close_http, NULL); - return 0; - } - return 0; - } - //RFC 4.2.2.4: An unsupported version must be answered with HTTP 426 - if(ws->websocket_version != 13){ - ws_close(ws, ws_close_http, "426 Unsupported protocol version"); - return 0; - } - return 1; -} - -int ws_handle_http(websocket* ws){ - char* header, *value; - ssize_t p; - - if(!ws->read_buffer[0]){ - return ws_upgrade_http(ws); - } - else if(isspace(ws->read_buffer[0])){ - //i hate header folding - ws_close(ws, ws_close_http, "500 Header folding"); - return 0; - } - else{ - header = (char*) ws->read_buffer; - value = strchr(header, ':'); - if(!value){ - ws_close(ws, ws_close_http, "500 Header format"); - return 0; - } - *value = 0; - value++; - for(; isspace(*value); value++){ - } - } - - //RFC 4.2.1 checks - if(!strcmp(header, "Sec-WebSocket-Version")){ - ws->websocket_version = strtoul(value, NULL, 10); - } - else if(!strcmp(header, "Sec-WebSocket-Key")){ - //right-trim the key - for(p = strlen(value) - 1; p >= 0 && isspace(value[p]); p--){ - value[p] = 0; - } - ws->socket_key = strdup(value); - } - else if(!strcmp(header, "Upgrade") && !strcasecmp(value, "websocket")){ - ws->want_upgrade |= 1; - } - else if(!strcmp(header, "Connection") && strstr(xstr_lower(value), "upgrade")){ - ws->want_upgrade |= 2; - } - else if(!strcmp(header, "Sec-WebSocket-Protocol")){ - //parse websocket protocol offers - for(value = strtok(value, ","); value; value = strtok(NULL, ",")){ - //ltrim - for(; *value && isspace(*value); value++){ - } - - //rtrim - for(p = strlen(value) - 1; p >= 0 && isspace(value[p]); p--){ - value[p] = 0; - } - - for(p = 0; p < ws->protocols; p++){ - if(!strcmp(ws->protocol[p], value)){ - break; - } - } - - //add new protocol - if(p == ws->protocols){ - ws->protocol = realloc(ws->protocol, (ws->protocols + 1) * sizeof(char*)); - if(!ws->protocol){ - fprintf(stderr, "Failed to allocate memory\n"); - return 1; - } - ws->protocol[ws->protocols] = strdup(value); - ws->protocols++; - } - } - } - else if(ws->headers < WS_HEADER_LIMIT){ - ws->header[ws->headers].tag = strdup(header); - ws->header[ws->headers].value = strdup(value); - ws->headers++; - } - else{ - //limit the number of stored headers to prevent abuse - ws_close(ws, ws_close_http, "500 Header limit"); - } - return 0; -} - -//returns bytes handled -size_t ws_frame(websocket* ws){ - size_t u; - uint64_t payload_length = 0; - uint16_t* payload_len16 = (uint16_t*) (ws->read_buffer + 2); - uint64_t* payload_len64 = (uint64_t*) (ws->read_buffer + 2); - uint8_t* masking_key = NULL, *payload = ws->read_buffer + 2; - - //need at least the header bits - if(ws->read_buffer_offset < 2){ - return 0; - } - - if(WS_GET_RESERVED(ws->read_buffer[0])){ - //reserved bits set without any extensions - //RFC 5.2 says we MUST close the connection - //ignoring it for now - } - - //calculate the payload length (could've used a uint64 and be done with it...) - //TODO test this for the bigger frames - payload_length = WS_GET_LEN(ws->read_buffer[1]); - if(WS_GET_MASK(ws->read_buffer[1])){ - if(ws->read_buffer_offset < 6){ - return 0; - } - masking_key = ws->read_buffer + 2; - payload = ws->read_buffer + 6; - } - - if(payload_length == 126){ - //16-bit payload length - if(ws->read_buffer_offset < 4){ - return 0; - } - payload_length = htobe16(*payload_len16); - payload = ws->read_buffer + 4; - if(WS_GET_MASK(ws->read_buffer[1])){ - if(ws->read_buffer_offset < 8){ - return 0; - } - masking_key = ws->read_buffer + 4; - payload = ws->read_buffer + 8; - } - } - else if(payload_length == 127){ - //64-bit payload length - if(ws->read_buffer_offset < 10){ - return 0; - } - payload_length = htobe64(*payload_len64); - payload = ws->read_buffer + 10; - if(WS_GET_MASK(ws->read_buffer[1])){ - if(ws->read_buffer_offset < 14){ - return 0; - } - masking_key = ws->read_buffer + 10; - payload = ws->read_buffer + 14; - } - } - - //check for complete WS frame - if(ws->read_buffer_offset < (payload - ws->read_buffer) + payload_length){ - //fprintf(stderr, "Incomplete payload: offset %lu, want %lu\n", ws->read_buffer_offset, (payload - ws->read_buffer) + payload_length); - return 0; - } - - //RFC Section 5.1: If the client sends an unmasked frame, close the connection - if(!WS_GET_MASK(ws->read_buffer[1])){ - ws_close(ws, ws_close_proto, "Unmasked client frame"); - return 0; - } - - //unmask data - if(WS_GET_MASK(ws->read_buffer[1])){ - for(u = 0; u < payload_length; u++){ - payload[u] = payload[u] ^ masking_key[u % 4]; - } - } - - //TODO handle fragmentation - //TODO handle control frames within fragmented frames - - fprintf(stderr, "Incoming websocket data: %s %s OP %02X LEN %u %lu\n", - WS_GET_FIN(ws->read_buffer[0]) ? "FIN" : "CONT", - WS_GET_MASK(ws->read_buffer[1]) ? "MASK" : "CLEAR", - WS_GET_OP(ws->read_buffer[0]), - WS_GET_LEN(ws->read_buffer[1]), - payload_length); - - //handle data - switch(WS_GET_OP(ws->read_buffer[0])){ - case ws_frame_text: - fprintf(stderr, "Text payload: %.*s\n", (int) payload_length, (char*) payload); - case ws_frame_binary: - //forward to peer - if(ws->peer_fd >= 0){ - fprintf(stderr, "WS -> Peer %lu bytes\n", payload_length); - if(network_send(ws->peer_fd, payload, payload_length)){ - ws_close(ws, ws_close_unexpected, "Failed to forward"); - } - } - break; - case ws_frame_close: - ws_close(ws, ws_close_normal, "Client requested termination"); - break; - case ws_frame_ping: - break; - case ws_frame_pong: - break; - default: - //unknown frame type received - fprintf(stderr, "Unknown WebSocket opcode %02X in frame\n", WS_GET_OP(ws->read_buffer[0])); - ws_close(ws, ws_close_proto, "Invalid opcode"); - break; - } - - return ((payload - ws->read_buffer) + payload_length); -} - -int ws_send_frame(websocket* ws, ws_operation opcode, uint8_t* data, size_t len){ - fprintf(stderr, "Peer -> WS %lu bytes\n", len); - uint8_t frame_header[WS_FRAME_HEADER_LEN]; - size_t header_bytes = 2; - uint16_t* payload_len16 = (uint16_t*) (frame_header + 2); - uint64_t* payload_len64 = (uint64_t*) (frame_header + 2); - - //FIXME might want to support segmented transfers here - //set up the basic frame header - frame_header[0] = WS_FLAG_FIN | opcode; - if(len <= 125){ - frame_header[1] = len; - } - else if(len <= 0xFFFF){ - frame_header[1] = 126; - *payload_len16 = htobe16(len); - header_bytes += 2; - } - else{ - frame_header[1] = 127; - *payload_len64 = htobe64(len); - header_bytes += 8; - } - - if(network_send(ws->ws_fd, frame_header, header_bytes) - || network_send(ws->ws_fd, data, len)){ - return 1; - } - - return 0; -} - -int ws_data(websocket* ws){ - ssize_t bytes_read, n, bytes_left = sizeof(ws->read_buffer) - ws->read_buffer_offset; - int rv = 0; - - bytes_read = recv(ws->ws_fd, ws->read_buffer + ws->read_buffer_offset, bytes_left - 1, 0); - if(bytes_read < 0){ - fprintf(stderr, "Failed to receive from websocket: %s\n", strerror(errno)); - ws_close(ws, ws_close_unexpected, NULL); - return 0; - } - else if(bytes_read == 0){ - //client closed connection - ws_close(ws, ws_close_unexpected, NULL); - return 0; - } - - //terminate new data - ws->read_buffer[ws->read_buffer_offset + bytes_read] = 0; - - switch(ws->state){ - case ws_new: - case ws_http: - //scan for newline, handle line - for(n = 0; n < bytes_read - 1; n++){ - if(!strncmp((char*) ws->read_buffer + ws->read_buffer_offset + n, "\r\n", 2)){ - //terminate line - ws->read_buffer[ws->read_buffer_offset + n] = 0; - - if(ws->state == ws_new){ - ws_handle_new(ws); - } - else{ - ws_handle_http(ws); - } - - //remove from buffer - bytes_read -= (n + 2); - memmove(ws->read_buffer, ws->read_buffer + ws->read_buffer_offset + n + 2, bytes_read); - ws->read_buffer_offset = 0; - - //restart from the beginning - n = -1; - } - } - //update read buffer offset - ws->read_buffer_offset = bytes_read; - break; - case ws_open: - ws->read_buffer_offset += bytes_read; - for(n = ws_frame(ws); n > 0 && ws->read_buffer_offset > 0; n = ws_frame(ws)){ - memmove(ws->read_buffer, ws->read_buffer + n, ws->read_buffer_offset - n); - ws->read_buffer_offset -= n; - } - break; - //this should never be reached, as ws_close also closes the client fd - case ws_closed: - fprintf(stderr, "This should not have happened\n"); - break; - } - - //disconnect spammy clients - if(sizeof(ws->read_buffer) - ws->read_buffer_offset < 2){ - fprintf(stderr, "Disconnecting misbehaving client\n"); - ws_close(ws, ws_close_limit, "Receive size limit exceeded"); - return 0; - } - return rv; -} - -void ws_cleanup(){ - size_t n; - for(n = 0; n < socks; n++){ - ws_close(sock + n, ws_close_shutdown, "Shutting down"); - } - - free(sock); - sock = NULL; - socks = 0; -} -- cgit v1.2.3