diff --git a/net/cpp/include/ftl/net/ws_internal.hpp b/net/cpp/include/ftl/net/ws_internal.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..5390f10281c860f91d35e3adf71d1101c1a0b275
--- /dev/null
+++ b/net/cpp/include/ftl/net/ws_internal.hpp
@@ -0,0 +1,53 @@
+#ifndef _FTL_NET_WS_INTERNAL_HPP_
+#define _FTL_NET_WS_INTERNAL_HPP_
+
+#include <stdint.h>
+#include <cstddef>
+#include <functional>
+#include <ftl/uri.hpp>
+
+using std::size_t;
+
+namespace ftl {
+namespace net {
+
+/* Taken from easywsclient */
+struct wsheader_type {
+	unsigned header_size;
+	bool fin;
+	bool mask;
+	enum opcode_type {
+		CONTINUATION = 0x0,
+		TEXT_FRAME = 0x1,
+		BINARY_FRAME = 0x2,
+		CLOSE = 8,
+		PING = 9,
+		PONG = 0xa,
+	} opcode;
+	int N0;
+	uint64_t N;
+	uint8_t masking_key[4];
+};
+
+/**
+ * Websocket dispatch parser. Given a raw socket buffer and its length, this
+ * function parses the websocket header and if valid and containing enough data
+ * in the buffer, it calls the passed function. If not enough data available
+ * to complete the dispatch, -1 is returned. Otherwise the total amount read
+ * from the buffer is returned.
+ */
+int ws_dispatch(const char *data, size_t len, std::function<void(const wsheader_type&,const char*,size_t)> d);
+
+/**
+ * Websocket header constructor. Fills a buffer with the correct websocket
+ * header for a given opcode, mask setting and message length.
+ */
+int ws_prepare(wsheader_type::opcode_type, bool useMask, size_t len, char *buffer, size_t maxlen);
+
+bool ws_connect(int sockfd, const ftl::URI &uri);
+
+};
+};
+
+#endif  // _FTL_NET_WS_INTERNAL_HPP_
+
diff --git a/net/cpp/src/ws_internal.cpp b/net/cpp/src/ws_internal.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..5b1aece52126a854425a099701fdb939f7d90d7c
--- /dev/null
+++ b/net/cpp/src/ws_internal.cpp
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2019 Nicolas Pope.
+ */
+
+#define GLOG_NO_ABBREVIATED_SEVERITIES
+#include <glog/logging.h>
+
+#include <cstring>
+#include <ftl/net/ws_internal.hpp>
+#include <memory>
+
+#ifndef WIN32
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <netdb.h>
+#include <arpa/inet.h>
+#define INVALID_SOCKET -1
+#define SOCKET_ERROR -1
+#endif
+
+#ifdef WIN32
+#include <windows.h>
+#include <winsock2.h>
+#include <Ws2tcpip.h>
+#endif
+
+#include <string>
+#include <iostream>
+
+using std::size_t;
+using ftl::net::wsheader_type;
+using ftl::URI;
+using std::string;
+
+/* Taken from easywsclient. */
+int ftl::net::ws_dispatch(const char *data, size_t len, std::function<void(const wsheader_type&,const char*,size_t)> d) {
+	wsheader_type ws;
+	if (len < 2) return -1;
+	
+	ws.fin = (data[0] & 0x80) == 0x80;
+	ws.opcode = (wsheader_type::opcode_type) (data[0] & 0x0f);
+	ws.mask = (data[1] & 0x80) == 0x80;
+	ws.N0 = (data[1] & 0x7f);
+	ws.header_size = 2 + (ws.N0 == 126? 2 : 0) + (ws.N0 == 127? 8 : 0) + (ws.mask? 4 : 0);
+
+	if (len < ws.header_size) return -1;
+
+	int i = 0;
+	if (ws.N0 < 126) {
+		ws.N = ws.N0;
+		i = 2;
+	} else if (ws.N0 == 126) {
+		ws.N = 0;
+		ws.N |= ((uint64_t) data[2]) << 8;
+		ws.N |= ((uint64_t) data[3]) << 0;
+		i = 4;
+	} else if (ws.N0 == 127) {
+		ws.N = 0;
+		ws.N |= ((uint64_t) data[2]) << 56;
+		ws.N |= ((uint64_t) data[3]) << 48;
+		ws.N |= ((uint64_t) data[4]) << 40;
+		ws.N |= ((uint64_t) data[5]) << 32;
+		ws.N |= ((uint64_t) data[6]) << 24;
+		ws.N |= ((uint64_t) data[7]) << 16;
+		ws.N |= ((uint64_t) data[8]) << 8;
+		ws.N |= ((uint64_t) data[9]) << 0;
+		i = 10;
+	}
+	
+	if (ws.mask) {
+		ws.masking_key[0] = ((uint8_t) data[i+0]) << 0;
+		ws.masking_key[1] = ((uint8_t) data[i+1]) << 0;
+		ws.masking_key[2] = ((uint8_t) data[i+2]) << 0;
+		ws.masking_key[3] = ((uint8_t) data[i+3]) << 0;
+	} else {
+		ws.masking_key[0] = 0;
+		ws.masking_key[1] = 0;
+		ws.masking_key[2] = 0;
+		ws.masking_key[3] = 0;
+	}
+	
+	if (len < ws.header_size+ws.N) return -1;
+
+	// Perform dispatch
+	d(ws, &data[ws.header_size], ws.N);
+	return ws.header_size+ws.N;
+}
+
+int ftl::net::ws_prepare(wsheader_type::opcode_type op, bool useMask, size_t len, char *data, size_t maxlen) {
+	// TODO:
+	// Masking key should (must) be derived from a high quality random
+	// number generator, to mitigate attacks on non-WebSocket friendly
+	// middleware:
+	const uint8_t masking_key[4] = { 0x12, 0x34, 0x56, 0x78 };
+
+	char *header = data;
+	size_t header_size = 2 + (len >= 126 ? 2 : 0) + (len >= 65536 ? 6 : 0) + (useMask ? 4 : 0);
+	if (header_size > maxlen) return -1;
+
+	memset(header, 0, header_size);
+	header[0] = 0x80 | op;
+	if (false) { }
+	else if (len < 126) {
+		header[1] = (len & 0xff) | (useMask ? 0x80 : 0);
+		if (useMask) {
+		    header[2] = masking_key[0];
+		    header[3] = masking_key[1];
+		    header[4] = masking_key[2];
+		    header[5] = masking_key[3];
+		}
+	} else if (len < 65536) {
+		header[1] = 126 | (useMask ? 0x80 : 0);
+		header[2] = (len >> 8) & 0xff;
+		header[3] = (len >> 0) & 0xff;
+		if (useMask) {
+		    header[4] = masking_key[0];
+		    header[5] = masking_key[1];
+		    header[6] = masking_key[2];
+		    header[7] = masking_key[3];
+		}
+	} else {
+		header[1] = 127 | (useMask ? 0x80 : 0);
+		header[2] = (len >> 56) & 0xff;
+		header[3] = (len >> 48) & 0xff;
+		header[4] = (len >> 40) & 0xff;
+		header[5] = (len >> 32) & 0xff;
+		header[6] = (len >> 24) & 0xff;
+		header[7] = (len >> 16) & 0xff;
+		header[8] = (len >>  8) & 0xff;
+		header[9] = (len >>  0) & 0xff;
+		if (useMask) {
+		    header[10] = masking_key[0];
+		    header[11] = masking_key[1];
+		    header[12] = masking_key[2];
+		    header[13] = masking_key[3];
+		}
+	}
+	
+	return header_size;
+}
+
+bool ftl::net::ws_connect(int sockfd, const URI &uri) {
+	string http = "";
+	int status;
+	int i;
+	char line[256];
+	
+	http += "GET "+uri.getPath()+" HTTP/1.1\r\n";
+	if (uri.getPort() == 80) {
+		http += "Host: "+uri.getHost()+"\r\n";
+	} else {
+		http += "Host: "+uri.getHost()+":"+std::to_string(uri.getPort())+"\r\n";
+	}
+	http += "Upgrade: websocket\r\n";
+	http += "Connection: Upgrade\r\n";
+	http += "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n";
+	http += "Sec-WebSocket-Version: 13\r\n";
+	http += "\r\n";
+	int rc = ::send(sockfd, http.c_str(), http.length(), 0);
+	if (rc != http.length()) {
+		LOG(ERROR) << "Could not send Websocket http request...";
+		std::cout << http;
+		return false;
+	}
+	
+	for (i = 0; i < 2 || (i < 255 && line[i-2] != '\r' && line[i-1] != '\n'); ++i) { if (recv(sockfd, line+i, 1, 0) == 0) { return false; } }
+	line[i] = 0;
+	if (i == 255) { fprintf(stderr, "ERROR: Got invalid status line connecting to: %s\n", uri.getHost().c_str()); return false; }
+	if (sscanf(line, "HTTP/1.1 %d", &status) != 1 || status != 101) { fprintf(stderr, "ERROR: Got bad status connecting to %s: %s", uri.getHost().c_str(), line); return false; }
+	// TODO: verify response headers,
+	while (true) {
+		for (i = 0; i < 2 || (i < 255 && line[i-2] != '\r' && line[i-1] != '\n'); ++i) { if (recv(sockfd, line+i, 1, 0) == 0) { return false; } }
+		if (line[0] == '\r' && line[1] == '\n') { break; }
+	}
+	return true;
+}
+