diff options
-rw-r--r-- | websocksy.c | 71 | ||||
-rw-r--r-- | websocksy.h | 111 | ||||
-rw-r--r-- | ws_proto.c | 55 |
3 files changed, 181 insertions, 56 deletions
diff --git a/websocksy.c b/websocksy.c index 0e4028f..55e8d35 100644 --- a/websocksy.c +++ b/websocksy.c @@ -10,6 +10,24 @@ #include <fcntl.h> #include <ctype.h> +/* TODO + * - TLS + * - config parsing + * - backend config + * - framing config / per peer? + * - per-connection framing state + */ + +/* + * Main loop condition, to be set from signal handler + */ +static volatile sig_atomic_t shutdown_requested = 0; + +#include "websocksy.h" + +/* + * Lowercase input string in-place + */ char* xstr_lower(char* in){ size_t n; for(n = 0; n < strlen(in); n++){ @@ -18,24 +36,33 @@ char* xstr_lower(char* in){ return in; } -#include "websocksy.h" #include "network.c" #include "ws_proto.c" -/* TODO - * - TLS - * - config parsing - * - Prevent http overrun +/* + * WebSocket interface & peer discovery configuration + */ +static struct { + char* host; + char* port; + ws_backend backend; +} config = { + .host = "::", + .port = "8001" +}; + +/* + * Signal handler, attached to SIGINT */ - -static volatile sig_atomic_t shutdown_requested = 0; - void signal_handler(int signum){ shutdown_requested = 1; } +/* + * Usage info + */ int usage(char* fn){ - fprintf(stderr, "\nwebsocksy - Proxy between websockets and 'real' sockets\n"); + fprintf(stderr, "\nwebsocksy v%s - Proxy between websockets and 'real' sockets\n", WEBSOCKSY_VERSION); fprintf(stderr, "Usage:\n"); fprintf(stderr, "\t%s [-p <port>] [-l <listen address>] [-b <targeting backend>]\n", fn); return EXIT_FAILURE; @@ -56,6 +83,7 @@ int main(int argc, char** argv){ size_t n; int listen_fd = -1, status, max_fd; + //parse command line arguments if(args_parse(argc - 1, argv + 1)){ exit(usage(argv[0])); } @@ -66,6 +94,7 @@ int main(int argc, char** argv){ exit(usage(argv[0])); } + //attach signal handler to catch Ctrl-C signal(SIGINT, signal_handler); //core loop @@ -77,16 +106,16 @@ int main(int argc, char** argv){ //push all fds to the select set for(n = 0; n < socks; n++){ - if(sock[n].fd >= 0){ - FD_SET(sock[n].fd, &read_fds); - if(max_fd < sock[n].fd){ - max_fd = sock[n].fd; + if(sock[n].ws_fd >= 0){ + FD_SET(sock[n].ws_fd, &read_fds); + if(max_fd < sock[n].ws_fd){ + max_fd = sock[n].ws_fd; } - if(sock[n].peer >= 0){ - FD_SET(sock[n].peer, &read_fds); - if(max_fd < sock[n].peer){ - max_fd = sock[n].peer; + if(sock[n].peer_fd >= 0){ + FD_SET(sock[n].peer_fd, &read_fds); + if(max_fd < sock[n].peer_fd){ + max_fd = sock[n].peer_fd; } } } @@ -109,16 +138,16 @@ int main(int argc, char** argv){ } } - //websocket & peer data + //websocket or peer data ready for(n = 0; n < socks; n++){ - if(sock[n].fd >= 0){ - if(FD_ISSET(sock[n].fd, &read_fds)){ + if(sock[n].ws_fd >= 0){ + if(FD_ISSET(sock[n].ws_fd, &read_fds)){ if(ws_data(sock + n)){ break; } } - if(sock[n].peer >= 0 && FD_ISSET(sock[n].peer, &read_fds)){ + if(sock[n].peer_fd >= 0 && FD_ISSET(sock[n].peer_fd, &read_fds)){ if(ws_peer_data(sock + n)){ break; } diff --git a/websocksy.h b/websocksy.h index 2073287..5a4f24c 100644 --- a/websocksy.h +++ b/websocksy.h @@ -3,16 +3,29 @@ #include <nettle/sha1.h> #include <nettle/base64.h> +/* Version defines */ +#define WEBSOCKSY_API_VERSION 1 +#define WEBSOCKSY_VERSION "0.1" + +/* HTTP/WS read buffer size & limit */ #define WS_MAX_LINE 16384 +/* Maximum number of HTTP headers to accept */ +#define WS_HEADER_LIMIT 10 +/* + * State machine for WebSocket connections + */ typedef enum { - ws_new = 0, - ws_http, - ws_open, - ws_closed + ws_new = 0, /* Initial state */ + ws_http, /* HTTP Headers */ + ws_open, /* Upgrade performed, forwarding */ + ws_closed /* Close frame sent */ } ws_state; -//RFC Section 5.2 +/* + * WebSocket frame opcodes + * RFC Section 5.2 + */ typedef enum { ws_frame_continuation = 0, ws_frame_text = 1, @@ -22,7 +35,10 @@ typedef enum { ws_frame_pong = 10 } ws_operation; -//RFC Section 7.4.1 +/* + * WebSocket close reasons / response codes + * RFC Section 7.4.1 + */ typedef enum { ws_close_http = 100, ws_close_normal = 1000, @@ -35,30 +51,91 @@ typedef enum { ws_close_unexpected = 1011 } ws_close_reason; -struct { +/* + * HTTP header split into tag and value + */ +typedef struct /*_ws_http_header*/ { + char* tag; + char* value; +} ws_http_header; + +/* + * Peer stream framing function + * + * Since the WebSocket protocol transfers its payload using discrete frames, unlike the peer's + * underlying TCP/Unix sockets (UDP is a different matter and may be framed directly), we need + * a method to indicate whether a complete frame to be forwarded has been received from the peer + * and with what WebSocket frame type to forward it (binary/text). Since this is largely + * protocol-dependent, this functionality needs to be user-extendable. + * + * We do however provide some default framing functions: + * * binary: Always forward all reads as binary frames + * * auto: Based on the content, forward every read result as binary/text + * * separator: Separate binary frames on a sequence of bytes + * * newline: Forward text frames separated by newlines (\r\n) + * + * The separator function is called once for every succesful read from the peer socket and called + * again when it indicates a frame boundary but there is still data in the buffer. + */ +typedef int64_t (*ws_framing)(void); + +/* + * Peer connection model + */ +typedef struct /*_ws_peer_info*/ { + /* Peer protocol data */ + int transport; char* host; char* port; - struct { - char* name; - } backend; -} config = { - .host = "::", - .port = "8001", - .backend.name = "internal" -}; + /* Framing function for this peer */ + ws_framing framing; + + /* WebSocket subprotocol indication index*/ + size_t protocol; +} ws_peer_info; + +/* + * Core connection model + */ typedef struct /*_web_socket*/ { - int fd; - int peer; + /* WebSocket state & data */ + int ws_fd; uint8_t read_buffer[WS_MAX_LINE]; size_t read_buffer_offset; ws_state state; + /* HTTP request headers */ + size_t headers; + ws_http_header header[WS_HEADER_LIMIT]; + + /* WebSocket parameters */ char* request_path; unsigned websocket_version; char* socket_key; unsigned want_upgrade; + /* WebSocket indicated subprotocols*/ size_t protocols; char** protocol; + + /* Peer data */ + ws_peer_info peer; + int peer_fd; } websocket; + +/* + * Peer discovery backend + * + * Used to dynamically select the peer based on parameters supplied by the WebSocket connection. + * The backend maps WebSocket characteristics (such as the endpoint used, the supported protocols and + * HTTP client headers) to a TCP/UDP/Unix socket peer endpoint using some form of user-configurable + * provider (such as databases, files, crystal balls or sheer guesses). + * + * The return value is a structure containing the destination address and transport to be connected to + * the Web Socket, as well as the indicated subprotocol to use (or none, if set to the provided maximum + * number of protocols). + * The fields within the structure should be allocated with `calloc` and will be free'd by websocky + * after use. + */ +typedef ws_peer_info (*ws_backend)(char* endpoint, size_t protocols, char** protocol, size_t headers, ws_http_header* header, websocket* ws); @@ -17,16 +17,24 @@ int ws_close(websocket* ws, ws_close_reason code, char* reason){ } ws->state = ws_closed; - if(ws->fd >= 0){ - close(ws->fd); - ws->fd = -1; + if(ws->ws_fd >= 0){ + close(ws->ws_fd); + ws->ws_fd = -1; } - if(ws->peer >= 0){ - close(ws->peer); - ws->peer = -1; + if(ws->peer_fd >= 0){ + close(ws->peer_fd); + ws->peer_fd = -1; } + 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]); } @@ -51,13 +59,13 @@ int ws_accept(int listen_fd){ size_t n = 0; websocket ws = { - .fd = accept(listen_fd, NULL, NULL), - .peer = -1 + .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].fd == -1){ + if(sock[n].ws_fd == -1){ break; } } @@ -66,7 +74,7 @@ int ws_accept(int listen_fd){ if(n == socks){ sock = realloc(sock, (socks + 1) * sizeof(websocket)); if(!sock){ - close(ws.fd); + close(ws.ws_fd); fprintf(stderr, "Failed to allocate memory\n"); return 1; } @@ -109,9 +117,9 @@ int ws_upgrade_http(websocket* ws){ if(ws->websocket_version == 13 && ws->socket_key && ws->want_upgrade == 3){ - if(network_send_str(ws->fd, "HTTP/1.1 101 Upgrading\r\n") - || network_send_str(ws->fd, "Upgrade: websocket\r\n") - || network_send_str(ws->fd, "Connection: Upgrade\r\n")){ + 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; } @@ -134,8 +142,8 @@ int ws_upgrade_http(websocket* ws){ memcpy(ws_accept_key + encode_offset, "\r\n\0", 3); //send websocket accept key - if(network_send_str(ws->fd, "Sec-WebSocket-Accept: ") - || network_send_str(ws->fd, ws_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; } @@ -144,7 +152,7 @@ int ws_upgrade_http(websocket* ws){ //TODO find/connect peer ws->state = ws_open; - if(network_send_str(ws->fd, "\r\n")){ + if(network_send_str(ws->ws_fd, "\r\n")){ ws_close(ws, ws_close_http, NULL); return 0; } @@ -196,7 +204,18 @@ int ws_handle_http(websocket* ws){ else if(!strcmp(header, "Connection") && strstr(xstr_lower(value), "upgrade")){ ws->want_upgrade |= 2; } - //TODO parse websocket protocol offers + else if(!strcmp(header, "Sec-WebSocket-Protocol")){ + //TODO parse websocket protocol offers + } + 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; } @@ -316,7 +335,7 @@ 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->fd, ws->read_buffer + ws->read_buffer_offset, bytes_left - 1, 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); |