diff options
-rw-r--r-- | README.md | 109 | ||||
-rw-r--r-- | config.c | 74 | ||||
-rw-r--r-- | config.h | 8 | ||||
-rw-r--r-- | makefile | 2 | ||||
-rw-r--r-- | plugins/backend_file.c | 22 | ||||
-rw-r--r-- | websocksy.c | 68 |
6 files changed, 210 insertions, 73 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..10a2f73 --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ +# websocksy + +`websocksy` is a highly configurable [WebSocket (RFC6455)](https://tools.ietf.org/html/rfc6455) to 'normal' networking transport (TCP/UDP) bridge. +It is written in C and supports pluggable modules for bridge peer selection modules (for dynamic bridging) and stream framing. + +Connecting WebSockets to 'real' sockets may seem like an easy task at first, but the WebSocket protocol has some fundamental differences to TCP and UDP. + +### Framing + +Data sent over WebSockets is explicitly framed - you get told how much data to expect for any one package. This is similar to UDP, which operates on Datagrams, +which are always received as one full message and carry an integrated length field. + +In contrast to that, TCP operates on a 'stream' basis, where any message boundaries need to be established by an upper layer protocol, and any messages sent +may be fragmented into multiple receive operations. + +Bridging data _from_ the WebSocket _to_ the network peer is thus not the problem - one can simply write out any complete message frame received. +For TCP, the other direction needs some kind of indication when to send the currently buffered data from the stream as one message to the WebSocket client. + +### Frame typing + +WebSocket frames contain an `opcode`, which indicates the type of data the frame contains, for example `binary`, `text`, or `ping` frames. + +Normal sockets only transfer bytes of data as payload, without any indication or information on what they signify - that is dependent on the upper layer protocols +that use these transport protocols. + +### Contextual information + +WebSockets carry in their connection establishment additional metadata, such as HTTP headers, an endpoint address and a list of supported subprotocols, +from which the server may select one it supports. + +Normal sockets only differentiate connections by a tuple consisting of source and destination addresses and ports, with the destination port number +being the primary discriminator between services. + +## Dynamic proxying + +To allow `websocksy` to connect any protocol to a WebSocket endpoint despite these differences, there are two avenues of extensibility. + +### Peer discovery backend + +Peer discovery backends map the metadata from a WebSocket connection, such as HTTP headers (e.g. Cookies), the connection endpoint and any indicated +subprotocols to a peer address naming a 'normal' socket. + +Backends can be loaded from shared libraries and may use any facilities available to them in order to find a peer - for example they may query a database +or [scan a file](plugins/backend_file.md) based on the supplied metadata. This allows the creation of dynamically configured bridge connections. + +Currently, the following peer address schemes are supported: + +* `tcp://<host>[:<port>]` - TCP client +* `udp://<host>[:<port>]` - UDP client +* `unix://<file>` - Unix socket, stream mode +* `unix-dgram://<file>` - Unix socket, datagram mode + +The default backend integrated into `websocksy` returns the same (configurable) peer for any connection. + +### Peer stream framing + +To solve the problem of framing the data stream from TCP peers and selecting the correct WebSocket frame type, `websocksy` uses "framing functions", +which can be loaded as plugins from shared objects. These are called when data was received from the network peer to decide if and how many bytes are +to be framed and sent to the WebSocket, and with what frame type. + +The framing function to be used for a connection is returned dynamically by the peer discovery backend. The backend may select from a library of different +framing functions (both built in and loaded from plugins). + +`websocksy` comes with the following framing functions built in: + +* `auto`: Send all data immediately, with the `text` type if the content was detected as valid UTF-8 string, otherwise use a `binary` frame +* `binary`: Send all data immediately as a `binary` frame + +# Configuration / Usage + +`websocksy` may either be configured by passing command line arguments or by specifying a configuration file to be read. + +The listen port may either be exposed to incoming WebSocket clients directly or via a proxying webserver such as nginx. + +### Command line arguments + +* `-p <port>`: Set the listen port for incoming WebSocket connections +* `-l <host>`: Set the host for listening for incoming WebSocket connections +* `-b <backend>`: Select external backend +* `-c <option>=<value>`: Pass configuration option to backend + +### Configuration file + +TBD + +## Default backend + +The integrated default backend takes the following configuration arguments: + +* `host`: The peer address to connect to +* `port`: An explicit port specification for the peer (may be inferred from the host if not specified or ignored if not required) +* `framing`: The name of a framing function to be used (`auto` is used when none is specified) +* `protocol`: The subprotocol to negotiate with the WebSocket peer. If not set, only the empty protocol set is accepted, which fails clients indicating + an explicitly supported subprotocol. The special value `*` matches the first available protocol. +* `framing-config`: Arguments to the framing function + +# Building + +To build `websocksy`, you need the following things + +* A working C compiler +* `make` +* `gnutls` and `libnettle` development packages (`libnettle-dev` and `libgnutls28-dev` for Debian, respectively) + +Run `make` in the project directory to build the core binary as well as the default plugins. + +# Development + +TBD diff --git a/config.c b/config.c new file mode 100644 index 0000000..7da08eb --- /dev/null +++ b/config.c @@ -0,0 +1,74 @@ +#include <string.h> +#include <stdio.h> + +#include "websocksy.h" +#include "config.h" +#include "plugin.h" + +static enum /*_config_file_section*/ { + cfg_main, + cfg_backend +} config_section = cfg_main; + +int config_parse_file(ws_config* config, char* filename){ + return 1; +} + +int config_parse_arguments(ws_config* config, int argc, char** argv){ + size_t u; + char* option = NULL, *value = NULL; + + //if exactly one argument, treat it as config file + if(argc == 1){ + return config_parse_file(config, argv[0]); + } + + if(argc % 2){ + return 1; + } + + for(u = 0; u < argc; u += 2){ + if(argv[u][0] != '-'){ + return 1; + } + switch(argv[u][1]){ + case 'p': + config->port = argv[u + 1]; + break; + case 'l': + config->host = argv[u + 1]; + break; + case 'b': + //clean up the previously registered backend + if(config->backend.cleanup){ + config->backend.cleanup(); + } + //load the backend plugin + if(plugin_backend_load(PLUGINS, argv[u + 1], &(config->backend))){ + return 1; + } + if(config->backend.init() != WEBSOCKSY_API_VERSION){ + fprintf(stderr, "Loaded backend %s was built for a different API version\n", argv[u + 1]); + return 1; + } + break; + case 'c': + if(!strchr(argv[u + 1], '=')){ + return 1; + } + if(!config->backend.config){ + continue; + } + option = strdup(argv[u + 1]); + value = strchr(option, '='); + *value = 0; + value++; + config->backend.config(option, value); + free(option); + option = NULL; + value = NULL; + break; + } + } + return 0; +} diff --git a/config.h b/config.h new file mode 100644 index 0000000..87502fb --- /dev/null +++ b/config.h @@ -0,0 +1,8 @@ +typedef struct /*_websocksy_config*/ { + char* host; + char* port; + ws_backend backend; +} ws_config; + +int config_parse_file(ws_config* config, char* filename); +int config_parse_arguments(ws_config* config, int argc, char** argv); @@ -4,7 +4,7 @@ PLUGINPATH?=plugins/ CFLAGS=-g -Wall -Wpedantic -DPLUGINS=\"$(PLUGINPATH)\" LDLIBS=-lnettle -ldl -OBJECTS=builtins.o network.o websocket.o plugin.o +OBJECTS=builtins.o network.o websocket.o plugin.o config.o all: websocksy diff --git a/plugins/backend_file.c b/plugins/backend_file.c index a858ef4..9976170 100644 --- a/plugins/backend_file.c +++ b/plugins/backend_file.c @@ -55,9 +55,11 @@ uint64_t configure(char* key, char* value){ } static int expression_replace(char* buffer, size_t buffer_length, size_t variable_length, char* content, size_t content_length){ + size_t u; + //check whether the replacement fits if(variable_length < content_length && strlen(buffer) + (content_length - variable_length) >= buffer_length){ - fprintf(stderr, "Expression replacement buffer overrun\n"); + fprintf(stderr, "Expression replacement buffer overrun: replacing %lu bytes with %lu bytes, current buffer used %lu, max %lu\n", variable_length, content_length, strlen(buffer), buffer_length); return 1; } @@ -66,7 +68,13 @@ static int expression_replace(char* buffer, size_t buffer_length, size_t variabl //insert replacement memcpy(buffer, content, content_length); - + + //sanitize replacement + for(u = 0; u < content_length; u++){ + if(buffer[u] == '/'){ + buffer[u] = '_'; + } + } return 0; } @@ -89,15 +97,8 @@ static int expression_resolve(char* template, size_t length, char* endpoint, siz endpoint[strlen(endpoint) - 1] = 0; } - //replace with sanitized endpoint string - for(p = 0; endpoint[p]; p++){ - if(endpoint[p] == '/'){ - endpoint[p] = '_'; - } - } - value = endpoint + 1; - value_len = p - 1; + value_len = strlen(value); variable_len = 10; } else if(!strncmp(template + u, "%cookie:", 8)){ @@ -263,6 +264,7 @@ ws_peer_info query(char* endpoint, size_t protocols, char** protocol, size_t hea continue; } + //copy data to peer info structure peer.host = strdup(line); peer.framing = core_framing(components[1]); peer.framing_config = components[2] ? strdup(components[2]) : NULL; diff --git a/websocksy.c b/websocksy.c index 7727ef0..f948727 100644 --- a/websocksy.c +++ b/websocksy.c @@ -15,15 +15,15 @@ #include "network.h" #include "websocket.h" #include "plugin.h" +#include "config.h" #define DEFAULT_HOST "::" #define DEFAULT_PORT "8001" /* TODO * - TLS - * - plugin loading * - config file - * - WS p2p + * - pings */ /* Main loop condition, to be set from signal handler */ @@ -43,11 +43,7 @@ char* xstr_lower(char* in){ } /* Daemon configuration */ -static struct { - char* host; - char* port; - ws_backend backend; -} config = { +static ws_config config = { .host = DEFAULT_HOST, .port = DEFAULT_PORT, /* Assign the built-in defaultpeer backend by default */ @@ -214,60 +210,6 @@ static int usage(char* fn){ return EXIT_FAILURE; } -static int args_parse(int argc, char** argv){ - size_t u; - char* option = NULL, *value = NULL; - - if(argc % 2){ - return 1; - } - - for(u = 0; u < argc; u += 2){ - if(argv[u][0] != '-'){ - return 1; - } - switch(argv[u][1]){ - case 'p': - config.port = argv[u + 1]; - break; - case 'l': - config.host = argv[u + 1]; - break; - case 'b': - //clean up the previously registered backend - if(config.backend.cleanup){ - config.backend.cleanup(); - } - //load the backend plugin - if(plugin_backend_load(PLUGINS, argv[u + 1], &(config.backend))){ - return 1; - } - if(config.backend.init() != WEBSOCKSY_API_VERSION){ - fprintf(stderr, "Loaded backend %s was built for a different API version\n", argv[u + 1]); - return 1; - } - break; - case 'c': - if(!strchr(argv[u + 1], '=')){ - return 1; - } - if(!config.backend.config){ - continue; - } - option = strdup(argv[u + 1]); - value = strchr(option, '='); - *value = 0; - value++; - config.backend.config(option, value); - free(option); - option = NULL; - value = NULL; - break; - } - } - return 0; -} - 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; @@ -350,7 +292,7 @@ int main(int argc, char** argv){ } //parse command line arguments - if(args_parse(argc - 1, argv + 1)){ + if(config_parse_arguments(&config, argc - 1, argv + 1)){ exit(usage(argv[0])); } @@ -362,6 +304,8 @@ int main(int argc, char** argv){ //attach signal handler to catch Ctrl-C signal(SIGINT, signal_handler); + //ignore broken pipes when writing + signal(SIGPIPE, SIG_IGN); //core loop while(!shutdown_requested){ |