aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md109
-rw-r--r--config.c74
-rw-r--r--config.h8
-rw-r--r--makefile2
-rw-r--r--plugins/backend_file.c22
-rw-r--r--websocksy.c68
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);
diff --git a/makefile b/makefile
index 20b6579..0e0dead 100644
--- a/makefile
+++ b/makefile
@@ -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){