diff --git a/src/http/HttpApp.h b/src/http/HttpApp.h new file mode 100644 index 0000000..9cbc117 --- /dev/null +++ b/src/http/HttpApp.h @@ -0,0 +1,178 @@ +#ifndef HTTPAPP_H +#define HTTPAPP_H + +#include +#include "Loop.h" +#include "HttpSocket.h" +#include "HttpRouter.h" +#include "websocket/libwshandshake.hpp" +#include "websocket/WebSocket.h" + +template +class HttpApp { + +protected: + + static const unsigned int HTTP_IDLE_TIMEOUT_S = 10; + + template + static constexpr typename std::conditional::type *static_dispatch(A *a, B *b) { + if constexpr(SSL) { + return a; + } else { + return b; + } + } + + typedef typename std::conditional::type SOCKET_TYPE; + typedef typename std::conditional::type SOCKET_CONTEXT_TYPE; + typedef typename HttpSocket::Data HTTP_SOCKET_DATA_TYPE; + + // todo_ rename to HttpContextData + struct Data { + Data() { + + // default http handler is a router + onHttpRequest = [this](auto *s, HttpRequest *req) { + UserData user = {s, req}; + r.route("GET", 3, req->getUrl().data(), req->getUrl().length(), &user); + }; + + } + + struct UserData { + HttpSocket *httpSocket; + HttpRequest *httpRequest; + }; + + HttpRouter r; + + std::function *)> onHttpConnection; + std::function *)> onHttpDisconnection; + std::function *, HttpRequest *)> onHttpRequest; + } *data; + + // server protocols + SOCKET_CONTEXT_TYPE *httpServerContext; + + HttpApp(SOCKET_CONTEXT_TYPE *httpServerContext) : httpServerContext(httpServerContext) { + new (data = (Data *) static_dispatch(us_ssl_socket_context_ext, us_socket_context_ext)(httpServerContext)) Data(); + + static_dispatch(us_ssl_socket_context_on_open, us_socket_context_on_open)(httpServerContext, [](auto *s, int is_client) { + Data *appData = (Data *) static_dispatch(us_ssl_socket_context_ext, us_socket_context_ext)(static_dispatch(us_ssl_socket_get_context, us_socket_get_context)(s)); + + static_dispatch(us_ssl_socket_timeout, us_socket_timeout)(s, HTTP_IDLE_TIMEOUT_S); + + new (static_dispatch(us_ssl_socket_ext, us_socket_ext)(s)) HTTP_SOCKET_DATA_TYPE; + + if (appData->onHttpConnection) { + appData->onHttpConnection((HttpSocket *) s); + } + + return s; + }); + + static_dispatch(us_ssl_socket_context_on_close, us_socket_context_on_close)(httpServerContext, [](auto *s) { + Data *appData = (Data *) static_dispatch(us_ssl_socket_context_ext, us_socket_context_ext)(static_dispatch(us_ssl_socket_get_context, us_socket_get_context)(s)); + + ((HTTP_SOCKET_DATA_TYPE *) static_dispatch(us_ssl_socket_ext, us_socket_ext)(s))->~HTTP_SOCKET_DATA_TYPE(); + + if (appData->onHttpDisconnection) { + appData->onHttpDisconnection((HttpSocket *) s); + } + + return s; + }); + + static_dispatch(us_ssl_socket_context_on_data, us_socket_context_on_data)(httpServerContext, [](auto *s, char *data, int length) { + Data *appData = (Data *) static_dispatch(us_ssl_socket_context_ext, us_socket_context_ext)(static_dispatch(us_ssl_socket_get_context, us_socket_get_context)(s)); + + // warning: should NOT reset timer on any data, ONLY reset data on full HTTP requests! + // warning: if we are in shutdown state, resetting the timer is a security issue! + static_dispatch(us_ssl_socket_timeout, us_socket_timeout)(s, HTTP_IDLE_TIMEOUT_S); + + // onHttpRequest should probably be hard-coded to HttpRouter + ((HttpSocket *) s)->onData(data, length, appData->onHttpRequest); + + // compared to routing directly + //typename Data::UserData user = {(HttpSocket *) s, nullptr}; + //appData->r.route("GET", 3, "/", 1, &user); + + return s; + }); + + static_dispatch(us_ssl_socket_context_on_writable, us_socket_context_on_writable)(httpServerContext, [](auto *s) { + + // what if the client + + // I think it's fair to never mind this one -> if we keep writing data after shutting down then that's an issue for us + static_dispatch(us_ssl_socket_timeout, us_socket_timeout)(s, HTTP_IDLE_TIMEOUT_S); + + ((HttpSocket *) s)->onWritable(); + + return s; + }); + + static_dispatch(us_ssl_socket_context_on_end, us_socket_context_on_end)(httpServerContext, [](auto *s) { + std::cout << "Socket was half-closed!" << std::endl; + + return s; + }); + + static_dispatch(us_ssl_socket_context_on_timeout, us_socket_context_on_timeout)(httpServerContext, [](auto *s) { + + if (static_dispatch(us_ssl_socket_is_shut_down, us_socket_is_shut_down)(s)) { + std::cout << "Forcefully closing socket since shutdown was not answered in time" << std::endl; + static_dispatch(us_ssl_socket_close, us_socket_close)(s); + } else { + std::cout << "Shutting down socket now" << std::endl; + static_dispatch(us_ssl_socket_timeout, us_socket_timeout)(s, HTTP_IDLE_TIMEOUT_S); + static_dispatch(us_ssl_socket_shutdown, us_socket_shutdown)(s); + } + + return s; + + }); + } + +public: + + // for server + void listen(const char *host, int port, int options) { + static_dispatch(us_ssl_socket_context_listen, us_socket_context_listen)(httpServerContext, host, port, options, sizeof(typename HttpSocket::Data)); + } + + HttpApp &onPost(std::string pattern, std::function *, HttpRequest *, std::vector *)> handler) { + data->r.add("POST", pattern.c_str(), [handler](typename Data::UserData *user, auto *args) { + handler(user->httpSocket, user->httpRequest, args); + }); + + return *this; + } + + CHILD &onGet(std::string pattern, std::function *, HttpRequest *, std::vector *)> handler) { + data->r.add("GET", pattern.c_str(), [handler](typename Data::UserData *user, auto *args) { + handler(user->httpSocket, user->httpRequest, args); + }); + + return *(CHILD *) this; + } + + // why even bother with these? + HttpApp &onHttpConnection(std::function *)> handler) { + data->onHttpConnection = handler; + + return *this; + } + + HttpApp &onHttpDisconnection(std::function *)> handler) { + data->onHttpDisconnection = handler; + + return *this; + } + + ~HttpApp() { + } +}; + +#endif // HTTPAPP_H diff --git a/src/http/HttpParser.h b/src/http/HttpParser.h new file mode 100644 index 0000000..d80650e --- /dev/null +++ b/src/http/HttpParser.h @@ -0,0 +1,205 @@ +#ifndef HTTPPARSER_H +#define HTTPPARSER_H + +#include +#include +#include + +class HttpRequest { + + friend class HttpParser; + +private: + const static int MAX_HEADERS = 50; + struct Header { + std::string_view key, value; + } headers[MAX_HEADERS]; + int querySeparator; + +public: + std::string_view getHeader(std::string_view header) { + for (Header *h = headers; (++h)->key.length(); ) { + if (h->key.length() == header.length() && !strncmp(h->key.data(), header.data(), header.length())) { + return h->value; + } + } + return std::string_view(nullptr, 0); + } + + // todo: implement this + /*int getHeader(std::string_view header) { + return 0; + }*/ + + std::string_view getUrl() { + return std::string_view(headers->value.data(), querySeparator); + } + + std::string_view getQuery() { + return std::string_view(headers->value.data() + querySeparator, headers->value.length() - querySeparator); + } + +}; + +class HttpParser { + +private: + std::string fallback; + int remainingStreamingBytes = 0; + + const size_t MAX_FALLBACK_SIZE = 1024 * 4; + + static unsigned int toUnsignedInteger(std::string_view str) { + int unsignedIntegerValue = 0; + for (unsigned char c : str) { + unsignedIntegerValue = unsignedIntegerValue * 10 + (c - '0'); + } + return unsignedIntegerValue; + } + + static unsigned int getHeaders(char *postPaddedBuffer, char *end, struct HttpRequest::Header *headers) { + char *preliminaryKey, *preliminaryValue, *start = postPaddedBuffer; + + for (unsigned int i = 0; i < HttpRequest::MAX_HEADERS; i++) { + for (preliminaryKey = postPaddedBuffer; (*postPaddedBuffer != ':') & (*postPaddedBuffer > 32); *(postPaddedBuffer++) |= 32); + if (*postPaddedBuffer == '\r') { + if ((postPaddedBuffer != end) & (postPaddedBuffer[1] == '\n') & (i > 0)) { + headers->key = std::string_view(nullptr, 0); + return (postPaddedBuffer + 2) - start; + } else { + return 0; + } + } else { + headers->key = std::string_view(preliminaryKey, (size_t) (postPaddedBuffer - preliminaryKey)); + for (postPaddedBuffer++; (*postPaddedBuffer == ':' || *postPaddedBuffer < 33) && *postPaddedBuffer != '\r'; postPaddedBuffer++); + preliminaryValue = postPaddedBuffer; + postPaddedBuffer = (char *) memchr(postPaddedBuffer, '\r', end - postPaddedBuffer); + if (postPaddedBuffer && postPaddedBuffer[1] == '\n') { + headers->value = std::string_view(preliminaryValue, (size_t) (postPaddedBuffer - preliminaryValue)); + postPaddedBuffer += 2; + headers++; + } else { + return 0; + } + } + } + return 0; + } + + // the only caller of getHeaders + template + int fenceAndConsumePostPadded(char *data, int length, void *user, HttpRequest *req, std::function &requestHandler, std::function &dataHandler) { + int consumedTotal = 0; + data[length] = '\r'; + + for (int consumed; length && (consumed = getHeaders(data, data + length, req->headers)); ) { + data += consumed; + length -= consumed; + consumedTotal += consumed; + + req->headers->value = std::string_view(req->headers->value.data(), std::max(0, req->headers->value.length() - 9)); + + // querySeparator is untested, todo: go through this + const char *querySeparatorPtr = (const char *) memchr(req->headers->value.data(), '?', req->headers->value.length()); + req->querySeparator = (querySeparatorPtr ? querySeparatorPtr : req->headers->value.data() + req->headers->value.length()) - req->headers->value.data(); + + requestHandler(user, req); + + std::string_view contentLengthString = req->getHeader("content-length"); + if (contentLengthString.length()) { + remainingStreamingBytes = toUnsignedInteger(contentLengthString); + + if (!CONSUME_MINIMALLY) { + int emittable = std::min(remainingStreamingBytes, length); + dataHandler(user, std::string_view(data, emittable)); + remainingStreamingBytes -= emittable; + + data += emittable; + length -= emittable; + consumedTotal += emittable; + } + } + + if (CONSUME_MINIMALLY) { + break; + } + } + return consumedTotal; + } + +public: + + // todo: what can we do with the socket inside the handlers? we need to check on return from any handler if we closed or terminated or upgraded the socket + void consumePostPadded(char *data, int length, void *user, std::function &&requestHandler, std::function &&dataHandler, std::function &&errorHandler) { + + HttpRequest req; + + if (remainingStreamingBytes) { + if (remainingStreamingBytes >= length) { + dataHandler(user, std::string_view(data, length)); + remainingStreamingBytes -= length; + return; + } else { + dataHandler(user, std::string_view(data, remainingStreamingBytes)); + + data += remainingStreamingBytes; + length -= remainingStreamingBytes; + + remainingStreamingBytes = 0; + } + } else if (fallback.length()) { + int had = fallback.length(); + + int maxCopyDistance = std::min(MAX_FALLBACK_SIZE - fallback.length(), (size_t) length); + + fallback.reserve(maxCopyDistance + 32); // padding should be same as libus + fallback.append(data, maxCopyDistance); + + int consumed = fenceAndConsumePostPadded(fallback.data(), fallback.length(), user, &req, requestHandler, dataHandler); + if (consumed) { + + fallback.clear(); + + data += consumed - had; + length -= consumed - had; + + // this is exactly the same as above! + if (remainingStreamingBytes) { + if (remainingStreamingBytes >= length) { + dataHandler(user, std::string_view(data, length)); + remainingStreamingBytes -= length; + return; + } else { + dataHandler(user, std::string_view(data, remainingStreamingBytes)); + + data += remainingStreamingBytes; + length -= remainingStreamingBytes; + + remainingStreamingBytes = 0; + } + } + + } else { + if (fallback.length() == MAX_FALLBACK_SIZE) { + errorHandler(user); + } + return; + } + } + + int consumed = fenceAndConsumePostPadded(data, length, user, &req, requestHandler, dataHandler); + + data += consumed; + length -= consumed; + + if (length) { + if (length < MAX_FALLBACK_SIZE) { + fallback.append(data, length); + } else { + errorHandler(user); + } + } + } +}; + +#endif // HTTPPARSER_H diff --git a/src/http/HttpRouter.h b/src/http/HttpRouter.h new file mode 100644 index 0000000..0eaa764 --- /dev/null +++ b/src/http/HttpRouter.h @@ -0,0 +1,171 @@ +#ifndef HTTPROUTER_HPP +#define HTTPROUTER_HPP + +// this header also needs testing and fixing as a separate module + +#include +#include +#include +#include +#include +#include + +template +class HttpRouter { +private: + std::vector *)>> handlers; + std::vector params; + + struct Node { + std::string name; + std::map children; + short handler; + }; + + Node *tree = new Node({"GET", {}, -1}); + std::string compiled_tree; + + void add(std::vector route, short handler) { + Node *parent = tree; + for (std::string node : route) { + if (parent->children.find(node) == parent->children.end()) { + parent->children[node] = new Node({node, {}, handler}); + } + parent = parent->children[node]; + } + } + + unsigned short compile_tree(Node *n) { + unsigned short nodeLength = 6 + n->name.length(); + for (auto c : n->children) { + nodeLength += compile_tree(c.second); + } + + unsigned short nodeNameLength = n->name.length(); + + std::string compiledNode; + compiledNode.append((char *) &nodeLength, sizeof(nodeLength)); + compiledNode.append((char *) &nodeNameLength, sizeof(nodeNameLength)); + compiledNode.append((char *) &n->handler, sizeof(n->handler)); + compiledNode.append(n->name.data(), n->name.length()); + + compiled_tree = compiledNode + compiled_tree; + return nodeLength; + } + + inline const char *find_node(const char *parent_node, const char *name, int name_length) { + unsigned short nodeLength = *(unsigned short *) &parent_node[0]; + unsigned short nodeNameLength = *(unsigned short *) &parent_node[2]; + + //std::cout << "Finding node: <" << std::string(name, name_length) << ">" << std::endl; + + const char *stoppp = parent_node + nodeLength; + for (const char *candidate = parent_node + 6 + nodeNameLength; candidate < stoppp; ) { + + unsigned short nodeLength = *(unsigned short *) &candidate[0]; + unsigned short nodeNameLength = *(unsigned short *) &candidate[2]; + + // whildcard, parameter, equal + if (nodeNameLength == 0) { + return candidate; + } else if (candidate[6] == ':') { + // parameter + + // todo: push this pointer on the stack of args! + params.push_back(std::string_view(name, name_length)); + + return candidate; + } else if (nodeNameLength == name_length && !memcmp(candidate + 6, name, name_length)) { + return candidate; + } + + candidate = candidate + nodeLength; + } + + return nullptr; + } + + // returns next slash from start or end + inline const char *getNextSegment(const char *start, const char *end) { + const char *stop = (const char *) memchr(start, '/', end - start); + return stop ? stop : end; + } + + // should take method also! + inline int lookup(const char *url, int length) { + // all urls start with / + url++; + length--; + + const char *treeStart = (char *) compiled_tree.data(); + + const char *stop, *start = url, *end_ptr = url + length; + do { + stop = getNextSegment(start, end_ptr); + + //std::cout << "Matching(" << std::string(start, stop - start) << ")" << std::endl; + + if(nullptr == (treeStart = find_node(treeStart, start, stop - start))) { + return -1; + } + + start = stop + 1; + } while (stop != end_ptr); + + return *(short *) &treeStart[4]; + } + +public: + HttpRouter() { + // maximum 100 parameters + params.reserve(100); + } + + HttpRouter *add(const char *method, const char *pattern, std::function *)> handler) { + + // step over any initial slash + if (pattern[0] == '/') { + pattern++; + } + + std::vector nodes; + //nodes.push_back(method); + + const char *stop, *start = pattern, *end_ptr = pattern + strlen(pattern); + do { + stop = getNextSegment(start, end_ptr); + + //std::cout << "Segment(" << std::string(start, stop - start) << ")" << std::endl; + + nodes.push_back(std::string(start, stop - start)); + + start = stop + 1; + } while (stop != end_ptr); + + + // if pattern starts with / then move 1+ and run inline slash parser + + add(nodes, handlers.size()); + handlers.push_back(handler); + + compile(); + return this; + } + + void compile() { + compiled_tree.clear(); + compile_tree(tree); + } + + void route(const char *method, unsigned int method_length, const char *url, unsigned int url_length, USERDATA userData) { + + int index = lookup(url, url_length); + if (index != -1) { + handlers[index](userData, ¶ms); + } + + params.clear(); + } +}; + +#endif // HTTPROUTER_HPP diff --git a/src/http/HttpSocket.h b/src/http/HttpSocket.h new file mode 100644 index 0000000..1960932 --- /dev/null +++ b/src/http/HttpSocket.h @@ -0,0 +1,176 @@ +#ifndef HTTP_H +#define HTTP_H + +#include "Socket.h" +#include "HttpParser.h" + +#include +#include +#include + +template +struct HttpSocket : Socket { + + const size_t MAX_FALLBACK_SIZE = 4096; + + typedef typename Socket::SOCKET_TYPE SOCKET_TYPE; + using Socket::static_dispatch; + + int u32toa(uint32_t value, char *dst) { + char temp[10]; + char *p = temp; + do { + *p++ = (char) (value % 10) + '0'; + value /= 10; + } while (value > 0); + + int ret = p - temp; + + do { + *dst++ = *--p; + } while (p != temp); + + return ret; + } + + // chunked response will be tricky with this buffering scheme + // if we do not fit, we can always use the header buffer for this (both in and out!) + // put first 8kb chunk in the http buffer, then from there it's the stream's job! + // httpheaders should only have 1 stream in and 1 stream out, but we can have helper wrappers + + struct Data { + HttpParser httpParser; + + std::function inStream; + + // out streaming (.end should be a wrapper of this!) + int offset = 0; + std::function outStream; + }; + + // only this one should be used! + void writeToCorkBuffer(const char *src, int length) { + uWS::Loop::Data *loopData = (uWS::Loop::Data *) us_loop_ext(us_socket_context_loop(us_socket_get_context((us_socket *) this))); + + memcpy(loopData->corkBuffer + loopData->corkOffset, src, length); + loopData->corkOffset += length; + } + + // never rely on this one! + int writeToCorkBufferAndReset(const char *src, int length, int contentLength, bool expectMore) { + uWS::Loop::Data *loopData = (uWS::Loop::Data *) us_loop_ext(us_socket_context_loop(us_socket_get_context((us_socket *) this))); + + memcpy(loopData->corkBuffer + loopData->corkOffset, "Content-Length: ", 16); + loopData->corkOffset += 16; + + loopData->corkOffset += u32toa(contentLength, loopData->corkBuffer + loopData->corkOffset); + + memcpy(loopData->corkBuffer + loopData->corkOffset, "\r\n\r\n", 4); + loopData->corkOffset += 4; + + + memcpy(loopData->corkBuffer + loopData->corkOffset, src, length); + loopData->corkOffset += length; + + int written = static_dispatch(us_ssl_socket_write, us_socket_write)((SOCKET_TYPE *) this, loopData->corkBuffer, loopData->corkOffset, expectMore); + + loopData->corkOffset = 0; + return written; + } + + HttpSocket *writeStatus(std::string_view status) { + writeToCorkBuffer("HTTP/1.1 ", 9); + writeToCorkBuffer(status.data(), status.length()); + writeToCorkBuffer("\r\n", 2); + return this; + } + + HttpSocket *writeHeader(std::string_view key, std::string_view value) { + writeToCorkBuffer(key.data(), key.length()); + writeToCorkBuffer(": ", 2); + writeToCorkBuffer(value.data(), value.length()); + writeToCorkBuffer("\r\n", 2); + return this; + } + + // this should not be anything other than a simple convenience wrapper of streams! + void end(std::string_view data) { + // end should not explicitly flush the cork buffer! delay to when done with all http data! + writeToCorkBufferAndReset(data.data(), data.length(), data.length(), false); + } + + // stream out (todo: fix up large sends and benchmark it again) + void write(std::function cb, int length) { + + std::string_view chunk = cb(0); + + // kopiera upp till (SSL eller icke-ssl) max copy distance + + // om mer än detta, fortsätt skicka + + + // this strategy can be simplified to one, we can even have MAX_COPY_DISTANCE_SSL and MAX_COPY_DISTANCE + if (length < uWS::Loop::MAX_COPY_DISTANCE) { + // what if the streamer cannot return any data? + // then it should return something to pause write, and then start it again + // basically we need throttling + writeToCorkBufferAndReset(chunk.data(), chunk.length(), length, false); + } else { + // copying some data with the headers is a good idea for SSL but probably not for non-SSL + writeToCorkBufferAndReset(chunk.data(), uWS::Loop::MAX_COPY_DISTANCE, length, true); + + // just assume this went fine + Data *httpData = (Data *) static_dispatch(us_ssl_socket_ext, us_socket_ext)((SOCKET_TYPE *) this); + + // write that off! + static_dispatch(us_ssl_socket_write, us_socket_write)((SOCKET_TYPE *) this, chunk.data() + uWS::Loop::MAX_COPY_DISTANCE, chunk.length() - uWS::Loop::MAX_COPY_DISTANCE, 0); + + // if offset is at the end, we are done + if (httpData->offset < length) { + httpData->outStream = cb; + } + } + } + + // this thing should only be reachable from App! + void onWritable() { + Data *httpData = (Data *) static_dispatch(us_ssl_socket_ext, us_socket_ext)((SOCKET_TYPE *) this); + + + // now we start streaming as much as possible in each call! + std::string_view chunk = httpData->outStream(httpData->offset); + + // write that off! + static_dispatch(us_ssl_socket_write, us_socket_write)((SOCKET_TYPE *) this, chunk.data(), chunk.length(), 0); + } + + void onData(char *data, int length, std::function *, HttpRequest *)> &onHttpRequest) { + Data *httpData = (Data *) static_dispatch(us_ssl_socket_ext, us_socket_ext)((SOCKET_TYPE *) this); + + // todo: this is where the HttpSocket binds together HttpParser and HttpRouter into one + httpData->httpParser.consumePostPadded(data, length, this, [&onHttpRequest](void *user, HttpRequest *httpRequest) { + onHttpRequest((HttpSocket *) user, httpRequest); + }, [httpData](void *user, std::string_view data) { + if (httpData->inStream) { + httpData->inStream(data); + } + }, [](void *user) { + std::cout << "INVALID HTTP!" << std::endl; + }); + } + + void read(decltype(Data::inStream) stream) { + Data *httpData = (Data *) static_dispatch(us_ssl_socket_ext, us_socket_ext)((SOCKET_TYPE *) this); + + httpData->inStream = stream; + } + + // typical shared function? + void close() { + static_dispatch(us_ssl_socket_close, us_socket_close)((SOCKET_TYPE *) this); + } + + HttpSocket() = delete; +}; + +#endif // HTTP_H diff --git a/src/websocket/WebSocket.h b/src/websocket/WebSocket.h new file mode 100644 index 0000000..ae431ed --- /dev/null +++ b/src/websocket/WebSocket.h @@ -0,0 +1,53 @@ +#ifndef WEBSOCKET_H +#define WEBSOCKET_H + +#include "libusockets.h" +#include "Socket.h" +#include "WebSocketProtocol.h" + +// client or server? +template +struct WebSocket : public Socket { + + // this needs to hold + struct Data : uWS::WebSocketState { + + }; + + static bool setCompressed(uWS::WebSocketState *wState) { + return true; + } + + static void forceClose(uWS::WebSocketState *wState) { + + } + + static bool handleFragment(char *data, size_t length, unsigned int remainingBytes, int opCode, bool fin, uWS::WebSocketState *webSocketState) { + + std::cout << std::string_view(data, length) << std::endl; + + //Data *webSocketData = (Data *) static_dispatch(us_ssl_socket_ext, us_socket_ext)((SOCKET_TYPE *) this); + + + //Socket::getSocketContextExt(); + + + + } + + static bool refusePayloadLength(uint64_t length, uWS::WebSocketState *wState) { + return false; + } + + //why is this here? events are handled and emitted from the app, the app depends on websocket, not two way deps! + void onData(char *data, int length) { + + Data *webSocketData = (Data *) Socket::static_dispatch(us_ssl_socket_ext, us_socket_ext)((typename Socket::SOCKET_TYPE *) this); + + uWS::WebSocketProtocol>::consume(data, length, webSocketData); + } + + WebSocket() = delete; +}; + +#endif // WEBSOCKET_H diff --git a/src/websocket/WebSocketApp.h b/src/websocket/WebSocketApp.h new file mode 100644 index 0000000..261974a --- /dev/null +++ b/src/websocket/WebSocketApp.h @@ -0,0 +1,114 @@ +#ifndef WEBSOCKETAPP_H +#define WEBSOCKETAPP_H + +#include "http/HttpApp.h" +#include + +// basically you have one of this for server, one for client!? +template +struct WebSocketApp : HttpApp> { + + // usings + using HttpApp>::static_dispatch; + typedef typename HttpApp>::SOCKET_CONTEXT_TYPE SOCKET_CONTEXT_TYPE; + typedef typename HttpApp>::SOCKET_TYPE SOCKET_TYPE; + + // constructor + WebSocketApp(SOCKET_CONTEXT_TYPE *httpServerContext) : HttpApp>(httpServerContext) { + + } + + // per-context "WebSocketApp" data + template + struct WebSocketServerContextData { + + WebSocketServerContextData() { + + } + + std::function *, std::string_view)> onMessage; + }; + + // all server contexts created with below functions + std::vector webSocketServerContexts; + bool lastContextIsServer; + + // register a new (server) protocol + template + WebSocketApp &onWebSocket(std::string pattern, std::function *, HttpRequest *, std::vector *)> handler) { + + // we are going to push a server context + lastContextIsServer = true; + + // create a new websocket child context + SOCKET_CONTEXT_TYPE *webSocketServerContext = static_dispatch(us_create_child_ssl_socket_context, us_create_child_socket_context)(HttpApp>::httpServerContext, sizeof(WebSocketServerContextData)); + new ((WebSocketServerContextData *) static_dispatch(us_ssl_socket_context_ext, us_socket_context_ext)(webSocketServerContext)) WebSocketServerContextData(); + WebSocketApp::webSocketServerContexts.push_back(webSocketServerContext); + + // add the behavior of it + static_dispatch(us_ssl_socket_context_on_data, us_socket_context_on_data)(webSocketServerContext, [](auto *s, char *data, int length) { + //WebSocketServerContextData *webSocketServerContextData = (WebSocketServerContextData *) static_dispatch(us_ssl_socket_context_ext, us_socket_context_ext)(static_dispatch(us_ssl_socket_get_context, us_socket_get_context)(s)); + + ((WebSocket *) s)->onData(data, length/*, webSocketServerContextData->onMessage*/); + + return s; + }); + + // todo: GET should probably be get since the parser only leaves lower case + HttpApp>::data->r.add("GET", pattern.c_str(), [webSocketServerContext, handler](typename HttpApp>::Data::UserData *user, auto *args) { + + std::string_view secWebSocketKey = user->httpRequest->getHeader("sec-websocket-key"); + if (secWebSocketKey.length()) { + + // note: OpenSSL can be used here to speed this up somewhat + char secWebSocketAccept[29] = {}; + WebSocketHandshake::generate(secWebSocketKey.data(), secWebSocketAccept); + + user->httpSocket->writeStatus("101 Switching Protocols") + ->writeHeader("Upgrade", "websocket") + ->writeHeader("Connection", "Upgrade") + ->writeHeader("Sec-WebSocket-Accept", secWebSocketAccept) + ->end(""); + + // todo: transform the socket into a websocket and hand it over + static_dispatch(us_ssl_socket_context_adopt_socket, us_socket_context_adopt_socket)(webSocketServerContext, (SOCKET_TYPE *) user->httpSocket, sizeof(typename WebSocket::Data) + sizeof(UserData)); + + // init the websocket data + + handler(user->httpSocket, user->httpRequest, args); + } else { + + // maybe pass this one to a HTTP handler on the websocket + + // note: this calls the http close handler inline + user->httpSocket->close(); + } + }); + + return *this; + } + + // this function does in fact determine whether we are client or not based on the websocket type passed! + template + WebSocketApp &onMessage(std::function *, std::string_view)> handler) { + + // pop last context on the stack + SOCKET_CONTEXT_TYPE *context = lastContextIsServer ? webSocketServerContexts.back() : nullptr; + + // get its data + if (lastContextIsServer) { + WebSocketServerContextData *data = (WebSocketServerContextData *) static_dispatch(us_ssl_socket_context_ext, us_socket_context_ext)(context); + + data->onMessage = handler; + } + + return *this; + } + + WebSocketApp &onClose(std::function) { + + return *this; + } +}; + +#endif // WEBSOCKETAPP_H diff --git a/src/websocket/WebSocketProtocol.h b/src/websocket/WebSocketProtocol.h new file mode 100644 index 0000000..108b5d0 --- /dev/null +++ b/src/websocket/WebSocketProtocol.h @@ -0,0 +1,392 @@ +#ifndef WEBSOCKETPROTOCOL_UWS_H +#define WEBSOCKETPROTOCOL_UWS_H + +#ifdef __linux +#include +#include +#elif __APPLE__ +#include +#define htobe64(x) OSSwapHostToBigInt64(x) +#define be64toh(x) OSSwapBigToHostInt64(x) +#else +#ifdef __MINGW32__ +// Windows has always been tied to LE +#define htobe64(x) __builtin_bswap64(x) +#define be64toh(x) __builtin_bswap64(x) +#else +#define htobe64(x) htonll(x) +#define be64toh(x) ntohll(x) +#endif +#endif + +#include +#include + +namespace uWS { + +enum OpCode : unsigned char { + TEXT = 1, + BINARY = 2, + CLOSE = 8, + PING = 9, + PONG = 10 +}; + +enum { + CLIENT, + SERVER +}; + +// 24 bytes perfectly +template +struct WebSocketState { +public: + static const unsigned int SHORT_MESSAGE_HEADER = isServer ? 6 : 2; + static const unsigned int MEDIUM_MESSAGE_HEADER = isServer ? 8 : 4; + static const unsigned int LONG_MESSAGE_HEADER = isServer ? 14 : 10; + + // 16 bytes + struct State { + unsigned int wantsHead : 1; + unsigned int spillLength : 4; + int opStack : 2; // -1, 0, 1 + unsigned int lastFin : 1; + + // 15 bytes + unsigned char spill[LONG_MESSAGE_HEADER - 1]; + OpCode opCode[2]; + + State() { + wantsHead = true; + spillLength = 0; + opStack = -1; + lastFin = true; + } + + } state; + + // 8 bytes + unsigned int remainingBytes = 0; + char mask[isServer ? 4 : 1]; +}; + +template +class WIN32_EXPORT WebSocketProtocol { +public: + static const unsigned int SHORT_MESSAGE_HEADER = isServer ? 6 : 2; + static const unsigned int MEDIUM_MESSAGE_HEADER = isServer ? 8 : 4; + static const unsigned int LONG_MESSAGE_HEADER = isServer ? 14 : 10; + +protected: + static inline bool isFin(char *frame) {return *((unsigned char *) frame) & 128;} + static inline unsigned char getOpCode(char *frame) {return *((unsigned char *) frame) & 15;} + static inline unsigned char payloadLength(char *frame) {return ((unsigned char *) frame)[1] & 127;} + static inline bool rsv23(char *frame) {return *((unsigned char *) frame) & 48;} + static inline bool rsv1(char *frame) {return *((unsigned char *) frame) & 64;} + + static inline void unmaskImprecise(char *dst, char *src, char *mask, unsigned int length) { + for (unsigned int n = (length >> 2) + 1; n; n--) { + *(dst++) = *(src++) ^ mask[0]; + *(dst++) = *(src++) ^ mask[1]; + *(dst++) = *(src++) ^ mask[2]; + *(dst++) = *(src++) ^ mask[3]; + } + } + + static inline void unmaskImpreciseCopyMask(char *dst, char *src, char *maskPtr, unsigned int length) { + char mask[4] = {maskPtr[0], maskPtr[1], maskPtr[2], maskPtr[3]}; + unmaskImprecise(dst, src, mask, length); + } + + static inline void rotateMask(unsigned int offset, char *mask) { + char originalMask[4] = {mask[0], mask[1], mask[2], mask[3]}; + mask[(0 + offset) % 4] = originalMask[0]; + mask[(1 + offset) % 4] = originalMask[1]; + mask[(2 + offset) % 4] = originalMask[2]; + mask[(3 + offset) % 4] = originalMask[3]; + } + + static inline void unmaskInplace(char *data, char *stop, char *mask) { + while (data < stop) { + *(data++) ^= mask[0]; + *(data++) ^= mask[1]; + *(data++) ^= mask[2]; + *(data++) ^= mask[3]; + } + } + + enum { + SND_CONTINUATION = 1, + SND_NO_FIN = 2, + SND_COMPRESSED = 64 + }; + + template + static inline bool consumeMessage(T payLength, char *&src, unsigned int &length, WebSocketState *wState) { + if (getOpCode(src)) { + if (wState->state.opStack == 1 || (!wState->state.lastFin && getOpCode(src) < 2)) { + Impl::forceClose(wState); + return true; + } + wState->state.opCode[++wState->state.opStack] = (OpCode) getOpCode(src); + } else if (wState->state.opStack == -1) { + Impl::forceClose(wState); + return true; + } + wState->state.lastFin = isFin(src); + + if (Impl::refusePayloadLength(payLength, wState)) { + Impl::forceClose(wState); + return true; + } + + if (payLength + MESSAGE_HEADER <= length) { + if (isServer) { + unmaskImpreciseCopyMask(src + MESSAGE_HEADER - 4, src + MESSAGE_HEADER, src + MESSAGE_HEADER - 4, (unsigned int) payLength); + if (Impl::handleFragment(src + MESSAGE_HEADER - 4, payLength, 0, wState->state.opCode[wState->state.opStack], isFin(src), wState)) { + return true; + } + } else { + if (Impl::handleFragment(src + MESSAGE_HEADER, payLength, 0, wState->state.opCode[wState->state.opStack], isFin(src), wState)) { + return true; + } + } + + if (isFin(src)) { + wState->state.opStack--; + } + + src += payLength + MESSAGE_HEADER; + length -= payLength + MESSAGE_HEADER; + wState->state.spillLength = 0; + return false; + } else { + wState->state.spillLength = 0; + wState->state.wantsHead = false; + wState->remainingBytes = (unsigned int) (payLength - length + MESSAGE_HEADER); + bool fin = isFin(src); + if (isServer) { + memcpy(wState->mask, src + MESSAGE_HEADER - 4, 4); + unmaskImprecise(src, src + MESSAGE_HEADER, wState->mask, length - MESSAGE_HEADER); + rotateMask(4 - (length - MESSAGE_HEADER) % 4, wState->mask); + } else { + src += MESSAGE_HEADER; + } + Impl::handleFragment(src, length - MESSAGE_HEADER, wState->remainingBytes, wState->state.opCode[wState->state.opStack], fin, wState); + return true; + } + } + + static inline bool consumeContinuation(char *&src, unsigned int &length, WebSocketState *wState) { + if (wState->remainingBytes <= length) { + if (isServer) { + int n = wState->remainingBytes >> 2; + unmaskInplace(src, src + n * 4, wState->mask); + for (int i = 0, s = wState->remainingBytes % 4; i < s; i++) { + src[n * 4 + i] ^= wState->mask[i]; + } + } + + if (Impl::handleFragment(src, wState->remainingBytes, 0, wState->state.opCode[wState->state.opStack], wState->state.lastFin, wState)) { + return false; + } + + if (wState->state.lastFin) { + wState->state.opStack--; + } + + src += wState->remainingBytes; + length -= wState->remainingBytes; + wState->state.wantsHead = true; + return true; + } else { + if (isServer) { + unmaskInplace(src, src + ((length >> 2) + 1) * 4, wState->mask); + } + + wState->remainingBytes -= length; + if (Impl::handleFragment(src, length, wState->remainingBytes, wState->state.opCode[wState->state.opStack], wState->state.lastFin, wState)) { + return false; + } + + if (isServer && length % 4) { + rotateMask(4 - (length % 4), wState->mask); + } + return false; + } + } + +public: + WebSocketProtocol() { + + } + + // Based on utf8_check.c by Markus Kuhn, 2005 + // https://www.cl.cam.ac.uk/~mgk25/ucs/utf8_check.c + // Optimized for predominantly 7-bit content by Alex Hultman, 2016 + // Licensed as Zlib, like the rest of this project + static bool isValidUtf8(unsigned char *s, size_t length) + { + for (unsigned char *e = s + length; s != e; ) { + if (s + 4 <= e && ((*(uint32_t *) s) & 0x80808080) == 0) { + s += 4; + } else { + while (!(*s & 0x80)) { + if (++s == e) { + return true; + } + } + + if ((s[0] & 0x60) == 0x40) { + if (s + 1 >= e || (s[1] & 0xc0) != 0x80 || (s[0] & 0xfe) == 0xc0) { + return false; + } + s += 2; + } else if ((s[0] & 0xf0) == 0xe0) { + if (s + 2 >= e || (s[1] & 0xc0) != 0x80 || (s[2] & 0xc0) != 0x80 || + (s[0] == 0xe0 && (s[1] & 0xe0) == 0x80) || (s[0] == 0xed && (s[1] & 0xe0) == 0xa0)) { + return false; + } + s += 3; + } else if ((s[0] & 0xf8) == 0xf0) { + if (s + 3 >= e || (s[1] & 0xc0) != 0x80 || (s[2] & 0xc0) != 0x80 || (s[3] & 0xc0) != 0x80 || + (s[0] == 0xf0 && (s[1] & 0xf0) == 0x80) || (s[0] == 0xf4 && s[1] > 0x8f) || s[0] > 0xf4) { + return false; + } + s += 4; + } else { + return false; + } + } + } + return true; + } + + struct CloseFrame { + uint16_t code; + char *message; + size_t length; + }; + + static inline CloseFrame parseClosePayload(char *src, size_t length) { + CloseFrame cf = {}; + if (length >= 2) { + memcpy(&cf.code, src, 2); + cf = {ntohs(cf.code), src + 2, length - 2}; + if (cf.code < 1000 || cf.code > 4999 || (cf.code > 1011 && cf.code < 4000) || + (cf.code >= 1004 && cf.code <= 1006) || !isValidUtf8((unsigned char *) cf.message, cf.length)) { + return {}; + } + } + return cf; + } + + static inline size_t formatClosePayload(char *dst, uint16_t code, const char *message, size_t length) { + if (code) { + code = htons(code); + memcpy(dst, &code, 2); + memcpy(dst + 2, message, length); + return length + 2; + } + return 0; + } + + static inline size_t formatMessage(char *dst, const char *src, size_t length, OpCode opCode, size_t reportedLength, bool compressed) { + size_t messageLength; + size_t headerLength; + if (reportedLength < 126) { + headerLength = 2; + dst[1] = reportedLength; + } else if (reportedLength <= UINT16_MAX) { + headerLength = 4; + dst[1] = 126; + *((uint16_t *) &dst[2]) = htons(reportedLength); + } else { + headerLength = 10; + dst[1] = 127; + *((uint64_t *) &dst[2]) = htobe64(reportedLength); + } + + int flags = 0; + dst[0] = (flags & SND_NO_FIN ? 0 : 128) | (compressed ? SND_COMPRESSED : 0); + if (!(flags & SND_CONTINUATION)) { + dst[0] |= opCode; + } + + char mask[4]; + if (!isServer) { + dst[1] |= 0x80; + uint32_t random = rand(); + memcpy(mask, &random, 4); + memcpy(dst + headerLength, &random, 4); + headerLength += 4; + } + + messageLength = headerLength + length; + memcpy(dst + headerLength, src, length); + + if (!isServer) { + + // overwrites up to 3 bytes outside of the given buffer! + //WebSocketProtocol::unmaskInplace(dst + headerLength, dst + headerLength + length, mask); + + // this is not optimal + char *start = dst + headerLength; + char *stop = start + length; + int i = 0; + while (start != stop) { + (*start++) ^= mask[i++ % 4]; + } + } + return messageLength; + } + + static inline void consume(char *src, unsigned int length, WebSocketState *wState) { + if (wState->state.spillLength) { + src -= wState->state.spillLength; + length += wState->state.spillLength; + memcpy(src, wState->state.spill, wState->state.spillLength); + } + if (wState->state.wantsHead) { + parseNext: + while (length >= SHORT_MESSAGE_HEADER) { + + // invalid reserved bits / invalid opcodes / invalid control frames / set compressed frame + if ((rsv1(src) && !Impl::setCompressed(wState)) || rsv23(src) || (getOpCode(src) > 2 && getOpCode(src) < 8) || + getOpCode(src) > 10 || (getOpCode(src) > 2 && (!isFin(src) || payloadLength(src) > 125))) { + Impl::forceClose(wState); + return; + } + + if (payloadLength(src) < 126) { + if (consumeMessage(payloadLength(src), src, length, wState)) { + return; + } + } else if (payloadLength(src) == 126) { + if (length < MEDIUM_MESSAGE_HEADER) { + break; + } else if(consumeMessage(ntohs(*(uint16_t *) &src[2]), src, length, wState)) { + return; + } + } else if (length < LONG_MESSAGE_HEADER) { + break; + } else if (consumeMessage(be64toh(*(uint64_t *) &src[2]), src, length, wState)) { + return; + } + } + if (length) { + memcpy(wState->state.spill, src, length); + wState->state.spillLength = length; + } + } else if (consumeContinuation(src, length, wState)) { + goto parseNext; + } + } + + static const int CONSUME_POST_PADDING = 4; + static const int CONSUME_PRE_PADDING = LONG_MESSAGE_HEADER - 1; +}; + +} + +#endif // WEBSOCKETPROTOCOL_UWS_H diff --git a/src/websocket/libwshandshake.hpp b/src/websocket/libwshandshake.hpp new file mode 100644 index 0000000..ac5381a --- /dev/null +++ b/src/websocket/libwshandshake.hpp @@ -0,0 +1,131 @@ +// Copyright (c) 2016 Alex Hultman and contributors + +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. + +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: + +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgement in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. + +#ifndef LIBWSHANDSHAKE_H +#define LIBWSHANDSHAKE_H + +#include +#include + +class WebSocketHandshake { + template + struct static_for { + void operator()(uint32_t *a, uint32_t *b) { + static_for()(a, b); + T::template f(a, b); + } + }; + + template + struct static_for<0, T> { + void operator()(uint32_t *a, uint32_t *hash) {} + }; + + template + struct Sha1Loop { + static inline uint32_t rol(uint32_t value, size_t bits) {return (value << bits) | (value >> (32 - bits));} + static inline uint32_t blk(uint32_t b[16], size_t i) { + return rol(b[(i + 13) & 15] ^ b[(i + 8) & 15] ^ b[(i + 2) & 15] ^ b[i], 1); + } + + template + static inline void f(uint32_t *a, uint32_t *b) { + switch (state) { + case 1: + a[i % 5] += ((a[(3 + i) % 5] & (a[(2 + i) % 5] ^ a[(1 + i) % 5])) ^ a[(1 + i) % 5]) + b[i] + 0x5a827999 + rol(a[(4 + i) % 5], 5); + a[(3 + i) % 5] = rol(a[(3 + i) % 5], 30); + break; + case 2: + b[i] = blk(b, i); + a[(1 + i) % 5] += ((a[(4 + i) % 5] & (a[(3 + i) % 5] ^ a[(2 + i) % 5])) ^ a[(2 + i) % 5]) + b[i] + 0x5a827999 + rol(a[(5 + i) % 5], 5); + a[(4 + i) % 5] = rol(a[(4 + i) % 5], 30); + break; + case 3: + b[(i + 4) % 16] = blk(b, (i + 4) % 16); + a[i % 5] += (a[(3 + i) % 5] ^ a[(2 + i) % 5] ^ a[(1 + i) % 5]) + b[(i + 4) % 16] + 0x6ed9eba1 + rol(a[(4 + i) % 5], 5); + a[(3 + i) % 5] = rol(a[(3 + i) % 5], 30); + break; + case 4: + b[(i + 8) % 16] = blk(b, (i + 8) % 16); + a[i % 5] += (((a[(3 + i) % 5] | a[(2 + i) % 5]) & a[(1 + i) % 5]) | (a[(3 + i) % 5] & a[(2 + i) % 5])) + b[(i + 8) % 16] + 0x8f1bbcdc + rol(a[(4 + i) % 5], 5); + a[(3 + i) % 5] = rol(a[(3 + i) % 5], 30); + break; + case 5: + b[(i + 12) % 16] = blk(b, (i + 12) % 16); + a[i % 5] += (a[(3 + i) % 5] ^ a[(2 + i) % 5] ^ a[(1 + i) % 5]) + b[(i + 12) % 16] + 0xca62c1d6 + rol(a[(4 + i) % 5], 5); + a[(3 + i) % 5] = rol(a[(3 + i) % 5], 30); + break; + case 6: + b[i] += a[4 - i]; + } + } + }; + + static inline void sha1(uint32_t hash[5], uint32_t b[16]) { + uint32_t a[5] = {hash[4], hash[3], hash[2], hash[1], hash[0]}; + static_for<16, Sha1Loop<1>>()(a, b); + static_for<4, Sha1Loop<2>>()(a, b); + static_for<20, Sha1Loop<3>>()(a, b); + static_for<20, Sha1Loop<4>>()(a, b); + static_for<20, Sha1Loop<5>>()(a, b); + static_for<5, Sha1Loop<6>>()(a, hash); + } + + static inline void base64(unsigned char *src, char *dst) { + const char *b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + for (int i = 0; i < 18; i += 3) { + *dst++ = b64[(src[i] >> 2) & 63]; + *dst++ = b64[((src[i] & 3) << 4) | ((src[i + 1] & 240) >> 4)]; + *dst++ = b64[((src[i + 1] & 15) << 2) | ((src[i + 2] & 192) >> 6)]; + *dst++ = b64[src[i + 2] & 63]; + } + *dst++ = b64[(src[18] >> 2) & 63]; + *dst++ = b64[((src[18] & 3) << 4) | ((src[19] & 240) >> 4)]; + *dst++ = b64[((src[19] & 15) << 2)]; + *dst++ = '='; + } + +public: + static inline void generate(const char input[24], char output[28]) { + uint32_t b_output[5] = { + 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0 + }; + uint32_t b_input[16] = { + 0, 0, 0, 0, 0, 0, 0x32353845, 0x41464135, 0x2d453931, 0x342d3437, 0x44412d39, + 0x3543412d, 0x43354142, 0x30444338, 0x35423131, 0x80000000 + }; + + for (int i = 0; i < 6; i++) { + b_input[i] = (input[4 * i + 3] & 0xff) | (input[4 * i + 2] & 0xff) << 8 | (input[4 * i + 1] & 0xff) << 16 | (input[4 * i + 0] & 0xff) << 24; + } + sha1(b_output, b_input); + uint32_t last_b[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 480}; + sha1(b_output, last_b); + for (int i = 0; i < 5; i++) { + uint32_t tmp = b_output[i]; + char *bytes = (char *) &b_output[i]; + bytes[3] = tmp & 0xff; + bytes[2] = (tmp >> 8) & 0xff; + bytes[1] = (tmp >> 16) & 0xff; + bytes[0] = (tmp >> 24) & 0xff; + } + base64((unsigned char *) b_output, output); + } +}; + +#endif // LIBWSHANDSHAKE_H