diff --git a/15.pro b/15.pro index 16c568c..da2b2a9 100644 --- a/15.pro +++ b/15.pro @@ -18,7 +18,8 @@ HEADERS += \ src/Loop.h \ src/App.h \ src/HttpSocket.h \ - src/HttpParser.h + src/HttpParser.h \ + src/libwshandshake.hpp INCLUDEPATH += uSockets/src src #QMAKE_CXXFLAGS += -fsanitize=address diff --git a/main.cpp b/main.cpp index 9aa2f34..288f576 100644 --- a/main.cpp +++ b/main.cpp @@ -62,9 +62,39 @@ int main(int argc, char **argv) { uWS::App app; #endif - // todo: add timeouts and socket shutdown! + struct UserData { - auto serve = [](auto *s, auto *req, auto *args) { + }; + + // serve a chat page with pub/sub and index.html and get (should be the main app of use) + // basically take the old web chat page and upgrade it + + uWS::App a; // or uWS::SSLApp(options) for SSL + + a/*.onGet("/", [](auto *s, auto *req, auto *args) { + std::cout << "URL: /" << std::endl; + std::cout << "user-agent: " << req->getHeader("user-agent") << std::endl; + + std::cout << "upgrade: " << req->getHeader("upgrade") << std::endl; + + + })*/.onWebSocket("/", [](auto *ws, auto *req, auto *args) { + + std::cout << "WebSocket connected to /wsApi" << std::endl; + + }).onMessage([](auto *ws, auto message/*, auto opCode*/) { + + std::cout << "WebSocket data: " << message << std::endl; + + //ws->send(message, opCode); + + }).onClose([](/*auto *ws, int code, auto message*/) { + + std::cout << "WebSocket disconnected from /wsApi" << std::endl; + + }).listen("localhost", 3000, 0); + + /*auto serve = [](auto *s, auto *req, auto *args) { //std::cout << "URL: " << req->getUrl() << std::endl; @@ -82,7 +112,7 @@ int main(int argc, char **argv) { } } - /*writeHeader("Content-type", "text/html; charset=utf-8")->*/s->write([file](int offset) { + s->write([file](int offset) { return std::string_view(file.data() + offset, file.size() - offset); }, file.size()); @@ -112,7 +142,7 @@ int main(int argc, char **argv) { std::cout << "Connections: " << ++connections << std::endl; }).onHttpDisconnection([](auto *s) { std::cout << "Connections: " << --connections << std::endl; - }).listen(nullptr, 3000, 0); + }).listen(nullptr, 3000, 0);*/ uWS::run(); // loop.run(); diff --git a/src/App.h b/src/App.h index 2a6c005..89419d7 100644 --- a/src/App.h +++ b/src/App.h @@ -6,6 +6,7 @@ #include "Loop.h" #include "HttpSocket.h" #include "HttpRouter.h" +#include "libwshandshake.hpp" namespace uWS { @@ -25,9 +26,11 @@ protected: } } + 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() { @@ -51,96 +54,116 @@ protected: std::function *, HttpRequest *)> onHttpRequest; } *data; + struct WebSocketServerContextData { + + WebSocketServerContextData() { + + } + + std::function *, std::string_view)> onMessage; + + } *webSocketServerContextData; + // server protocols SOCKET_CONTEXT_TYPE *httpServerContext; SOCKET_CONTEXT_TYPE *webSocketServerContext; - // a us_socket_context can be constructed from another us_socket_context and will then act purely as a behavior (shared SSL_CTX) - // only socket contexts that come from construction from the start can be listened or connected to + // client protocols (todo?) - // us_create_derived_socket_context(us_socket_context) + void initWebSocketContexts(us_loop *loop) { + new (webSocketServerContextData = (WebSocketServerContextData *) static_dispatch(us_ssl_socket_context_ext, us_socket_context_ext)(webSocketServerContext)) WebSocketServerContextData(); - // client protocols + 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)); - void init(us_loop *loop) { - new (data = (Data *) static_dispatch(us_ssl_socket_context_ext, us_socket_context_ext)(httpServerContext)) Data(); + // this is set via the setter? + webSocketServerContextData->onMessage((HttpSocket *) s, std::string_view(data, length)); - 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)); + + return s; + }); + } + + void initHttpContexts(us_loop *loop) { + 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); + } - new (static_dispatch(us_ssl_socket_ext, us_socket_ext)(s)) HTTP_SOCKET_DATA_TYPE; + return s; - 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: @@ -178,8 +201,49 @@ public: return *this; } - // for client and server - AppBase &onWebSocket(std::string pattern, std::function handler) { + // for client and server? fuck no! + template + AppBase &onWebSocket(std::string pattern, std::function *, HttpRequest *, std::vector *)> handler) { + + // todo: GET should probably be get since the parser only leaves lower case + data->r.add("GET", pattern.c_str(), [this, handler](typename 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, 0); + + handler(user->httpSocket, user->httpRequest, args); + } else { + // note: this calls the http close handler inline + user->httpSocket->close(); + } + }); + + return *this; + } + + AppBase &onMessage(std::function *, std::string_view)> handler) { + + // actually we dynamically create a new server websocket context when we call onWebSocket! + webSocketServerContextData->onMessage = handler; + + return *this; + } + + AppBase &onClose(std::function) { + return *this; } @@ -192,7 +256,10 @@ class App : public AppBase { public: App() { httpServerContext = us_create_socket_context(defaultLoop.loop, sizeof(Data)); - init(defaultLoop.loop); + initHttpContexts(defaultLoop.loop); + + webSocketServerContext = us_create_socket_context(defaultLoop.loop, sizeof(Data)); // this should not have Data! WebSocketData! + initWebSocketContexts(defaultLoop.loop); } }; @@ -225,7 +292,10 @@ class SSLApp : public AppBase { public: SSLApp(SSLOptions &sslOptions) { httpServerContext = us_create_ssl_socket_context(defaultLoop.loop, sizeof(Data), sslOptions.options); - init(defaultLoop.loop); + initHttpContexts(defaultLoop.loop); + + webSocketServerContext = us_create_ssl_socket_context(defaultLoop.loop, sizeof(Data), sslOptions.options); // this should not have Data! WebSocketData! + initWebSocketContexts(defaultLoop.loop); } }; diff --git a/src/HttpSocket.h b/src/HttpSocket.h index 00ce6d0..5abd052 100644 --- a/src/HttpSocket.h +++ b/src/HttpSocket.h @@ -174,6 +174,10 @@ struct HttpSocket { httpData->inStream = stream; } + void close() { + static_dispatch(us_ssl_socket_close, us_socket_close)((SOCKET_TYPE *) this); + } + HttpSocket() = delete; }; diff --git a/src/libwshandshake.hpp b/src/libwshandshake.hpp new file mode 100644 index 0000000..ac5381a --- /dev/null +++ b/src/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