From 777dfa31af619e430021d6f12850c7a1e3dbe7f7 Mon Sep 17 00:00:00 2001 From: The-EDev Date: Wed, 4 Nov 2020 06:20:09 +0300 Subject: [PATCH 01/10] added documentation and illustration from RFC 6455 --- include/crow/websocket.h | 55 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/include/crow/websocket.h b/include/crow/websocket.h index 8ca963883..534648bbf 100644 --- a/include/crow/websocket.h +++ b/include/crow/websocket.h @@ -18,6 +18,7 @@ namespace crow Payload, }; + ///A base class for websocket connection. struct connection { virtual void send_binary(const std::string& msg) = 0; @@ -32,10 +33,35 @@ namespace crow void* userdata_; }; + // 0 1 2 3 + // 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 + // +-+-+-+-+-------+-+-------------+-------------------------------+ + // |F|R|R|R| opcode|M| Payload len | Extended payload length | + // |I|S|S|S| (4) |A| (7) | (16/64) | + // |N|V|V|V| |S| | (if payload len==126/127) | + // | |1|2|3| |K| | | + // +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + + // | Extended payload length continued, if payload len == 127 | + // + - - - - - - - - - - - - - - - +-------------------------------+ + // | |Masking-key, if MASK set to 1 | + // +-------------------------------+-------------------------------+ + // | Masking-key (continued) | Payload Data | + // +-------------------------------- - - - - - - - - - - - - - - - + + // : Payload Data continued ... : + // + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + // | Payload Data continued ... | + // +---------------------------------------------------------------+ + + ///A websocket connection. template class Connection : public connection { public: + /// Constructor for a connection. + + /// + /// Requires a request with an "Upgrade: websocket" header.
+ /// Automatically handles the handshake. Connection(const crow::request& req, Adaptor&& adaptor, std::function open_handler, std::function message_handler, @@ -72,18 +98,24 @@ namespace crow start(crow::utility::base64encode((char*)digest, 20)); } + /// Send data through the socket. template void dispatch(CompletionHandler handler) { adaptor_.get_io_service().dispatch(handler); } + /// Send data through the socket and return immediately. template void post(CompletionHandler handler) { adaptor_.get_io_service().post(handler); } + /// Send a "Pong" message. + + /// + /// Usually automatically invoked as a response to a "Ping" message. void send_pong(const std::string& msg) { dispatch([this, msg]{ @@ -95,6 +127,7 @@ namespace crow }); } + /// Send a binary encoded message. void send_binary(const std::string& msg) override { dispatch([this, msg]{ @@ -105,6 +138,7 @@ namespace crow }); } + /// Send a plaintext message. void send_text(const std::string& msg) override { dispatch([this, msg]{ @@ -115,6 +149,10 @@ namespace crow }); } + /// Send a close signal. + + /// + /// Sets a flag to destroy the object once the message is sent. void close(const std::string& msg) override { dispatch([this, msg]{ @@ -134,6 +172,7 @@ namespace crow protected: + /// Generate the websocket headers using an opcode and the message size (in bytes). std::string build_header(int opcode, size_t size) { char buf[2+8] = "\x80\x00"; @@ -157,6 +196,10 @@ namespace crow } } + /// Send the HTTP upgrade response. + + /// + /// Finishes the handshake process, then starts reading messages from the socket. void start(std::string&& hello) { static std::string header = "HTTP/1.1 101 Switching Protocols\r\n" @@ -174,6 +217,13 @@ namespace crow do_read(); } + /// Read a websocket message. + + /// + /// Involves:
+ /// Handling headers (opcodes, size).
+ /// Unmasking the payload.
+ /// Reading the actual payload.
void do_read() { is_reading = true; @@ -454,6 +504,10 @@ namespace crow fragment_.clear(); } + /// Send the buffers' data through the socket. + + /// + /// Also destroyes the object if the Close flag is set. void do_write() { if (sending_buffers_.empty()) @@ -485,6 +539,7 @@ namespace crow } } + ///Destroy the Connection. void check_destroy() { //if (has_sent_close_ && has_recv_close_) From cc715f4245f50b1ed06c6ef979af248cac0bd597 Mon Sep 17 00:00:00 2001 From: The-EDev Date: Wed, 4 Nov 2020 06:21:52 +0300 Subject: [PATCH 02/10] cleaned up app.h documentation --- include/crow/app.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/include/crow/app.h b/include/crow/app.h index e7969e164..fe9c5e5d9 100644 --- a/include/crow/app.h +++ b/include/crow/app.h @@ -121,12 +121,12 @@ namespace crow ///Set the server's log level /// - /// Possible values are: - /// crow::LogLevel::Debug (0) - /// crow::LogLevel::Info (1) - /// crow::LogLevel::Warning (2) - /// crow::LogLevel::Error (3) - /// crow::LogLevel::Critical (4) + /// Possible values are:
+ /// crow::LogLevel::Debug (0)
+ /// crow::LogLevel::Info (1)
+ /// crow::LogLevel::Warning (2)
+ /// crow::LogLevel::Error (3)
+ /// crow::LogLevel::Critical (4)
self_t& loglevel(crow::LogLevel level) { crow::logger::setLogLevel(level); From 463917a17f964753ef90e9aeadd97851d5e876a0 Mon Sep 17 00:00:00 2001 From: The-EDev Date: Wed, 4 Nov 2020 07:16:03 +0300 Subject: [PATCH 03/10] added option to have unmasked messages --- include/crow/websocket.h | 77 +++++++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 28 deletions(-) diff --git a/include/crow/websocket.h b/include/crow/websocket.h index 534648bbf..958b229c3 100644 --- a/include/crow/websocket.h +++ b/include/crow/websocket.h @@ -250,8 +250,11 @@ namespace crow } #endif - if (!ec && ((mini_header_ & 0x80) == 0x80)) + if (!ec) { + if ((mini_header_ & 0x80) == 0x80) + has_mask_ = true; + if ((mini_header_ & 0x7f) == 127) { state_ = WebSocketReadState::Len64; @@ -350,34 +353,42 @@ namespace crow } break; case WebSocketReadState::Mask: - boost::asio::async_read(adaptor_.socket(), boost::asio::buffer((char*)&mask_, 4), - [this](const boost::system::error_code& ec, std::size_t -#ifdef CROW_ENABLE_DEBUG - bytes_transferred -#endif - ) - { - is_reading = false; + if (has_mask_) + { + boost::asio::async_read(adaptor_.socket(), boost::asio::buffer((char*)&mask_, 4), + [this](const boost::system::error_code& ec, std::size_t #ifdef CROW_ENABLE_DEBUG - if (!ec && bytes_transferred != 4) + bytes_transferred +#endif + ) { - throw std::runtime_error("WebSocket:Mask:async_read fail:asio bug?"); - } + is_reading = false; +#ifdef CROW_ENABLE_DEBUG + if (!ec && bytes_transferred != 4) + { + throw std::runtime_error("WebSocket:Mask:async_read fail:asio bug?"); + } #endif - if (!ec) - { - state_ = WebSocketReadState::Payload; - do_read(); - } - else - { - close_connection_ = true; - if (error_handler_) - error_handler_(*this); - adaptor_.close(); - } - }); + if (!ec) + { + state_ = WebSocketReadState::Payload; + do_read(); + } + else + { + close_connection_ = true; + if (error_handler_) + error_handler_(*this); + adaptor_.close(); + } + }); + } + else + { + state_ = WebSocketReadState::Payload; + do_read(); + } break; case WebSocketReadState::Payload: { @@ -415,21 +426,30 @@ namespace crow } } + /// Check if the FIN bit is set. bool is_FIN() { return mini_header_ & 0x8000; } + /// Extract the opcode from the header. int opcode() { return (mini_header_ & 0x0f00) >> 8; } + /// Process the payload fragment. + + /// + /// Unmasks the fragment, checks the opcode, merges fragments into 1 message body, and calls the appropriate handler. void handle_fragment() { - for(decltype(fragment_.length()) i = 0; i < fragment_.length(); i ++) + if (has_mask_) { - fragment_[i] ^= ((char*)&mask_)[i%4]; + for(decltype(fragment_.length()) i = 0; i < fragment_.length(); i ++) + { + fragment_[i] ^= ((char*)&mask_)[i%4]; + } } switch(opcode()) { @@ -539,7 +559,7 @@ namespace crow } } - ///Destroy the Connection. + /// Destroy the Connection. void check_destroy() { //if (has_sent_close_ && has_recv_close_) @@ -564,6 +584,7 @@ namespace crow uint64_t remaining_length_{0}; bool close_connection_{false}; bool is_reading{false}; + bool has_mask_{false}; uint32_t mask_; uint16_t mini_header_; bool has_sent_close_{false}; From 8dc61959f259a27a8cd9bc43ea802b9765a6762e Mon Sep 17 00:00:00 2001 From: The-EDev Date: Thu, 5 Nov 2020 04:10:33 +0300 Subject: [PATCH 04/10] added method to send ping message --- include/crow/websocket.h | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/include/crow/websocket.h b/include/crow/websocket.h index 958b229c3..78410994c 100644 --- a/include/crow/websocket.h +++ b/include/crow/websocket.h @@ -112,6 +112,21 @@ namespace crow adaptor_.get_io_service().post(handler); } + /// Send a "Ping" message. + + /// + /// Usually invoked to check if the other point is still online. + void send_ping(const std::string& msg) + { + dispatch([this, msg]{ + char buf[3] = "\x89\x00"; + buf[1] += msg.size(); + write_buffers_.emplace_back(buf, buf+2); + write_buffers_.emplace_back(msg); + do_write(); + }); + } + /// Send a "Pong" message. /// From 5c7ef21211e7eed9e5f3cc98e549777bb8ee57b7 Mon Sep 17 00:00:00 2001 From: The-EDev Date: Mon, 9 Nov 2020 04:41:55 +0300 Subject: [PATCH 05/10] websocket improvements replaced base10 bits to base8 in comments added send_ping and send_pong to API (users can now send them) reset header value before every read --- include/crow/websocket.h | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/include/crow/websocket.h b/include/crow/websocket.h index 78410994c..c4b61ae3f 100644 --- a/include/crow/websocket.h +++ b/include/crow/websocket.h @@ -23,6 +23,8 @@ namespace crow { virtual void send_binary(const std::string& msg) = 0; virtual void send_text(const std::string& msg) = 0; + virtual void send_ping(const std::string& msg) = 0; + virtual void send_pong(const std::string& msg) = 0; virtual void close(const std::string& msg = "quit") = 0; virtual ~connection(){} @@ -33,8 +35,8 @@ namespace crow void* userdata_; }; - // 0 1 2 3 - // 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 + // 0 1 2 3 -byte + // 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 -bit // +-+-+-+-+-------+-+-------------+-------------------------------+ // |F|R|R|R| opcode|M| Payload len | Extended payload length | // |I|S|S|S| (4) |A| (7) | (16/64) | @@ -116,7 +118,7 @@ namespace crow /// /// Usually invoked to check if the other point is still online. - void send_ping(const std::string& msg) + void send_ping(const std::string& msg) override { dispatch([this, msg]{ char buf[3] = "\x89\x00"; @@ -131,7 +133,7 @@ namespace crow /// /// Usually automatically invoked as a response to a "Ping" message. - void send_pong(const std::string& msg) + void send_pong(const std::string& msg) override { dispatch([this, msg]{ char buf[3] = "\x8A\x00"; @@ -246,8 +248,9 @@ namespace crow { case WebSocketReadState::MiniHeader: { + mini_header_ = 0; //boost::asio::async_read(adaptor_.socket(), boost::asio::buffer(&mini_header_, 1), - adaptor_.socket().async_read_some(boost::asio::buffer(&mini_header_, 2), + adaptor_.socket().async_read_some(boost::asio::buffer(&mini_header_, 2), [this](const boost::system::error_code& ec, std::size_t #ifdef CROW_ENABLE_DEBUG bytes_transferred From 76af4167669d9f243a1ad223462b67d3638df368 Mon Sep 17 00:00:00 2001 From: The-EDev Date: Mon, 9 Nov 2020 04:47:41 +0300 Subject: [PATCH 06/10] added websocket unit test --- tests/unittest.cpp | 134 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/tests/unittest.cpp b/tests/unittest.cpp index 730623807..7b4b35978 100644 --- a/tests/unittest.cpp +++ b/tests/unittest.cpp @@ -1371,3 +1371,137 @@ TEST_CASE("stream_response") }); runTest.join(); } + +TEST_CASE("websocket") +{ + static std::string http_message = "GET /ws HTTP/1.1\r\nConnection: keep-alive, Upgrade\r\nupgrade: websocket\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n"; + + static bool connected{false}; + + SimpleApp app; + + CROW_ROUTE(app, "/ws").websocket() + .onopen([&](websocket::connection&){ + connected = true; + CROW_LOG_INFO << "Connected websocket and value is " << connected; + }) + .onmessage([&](websocket::connection& conn, const std::string& message, bool isbin){ + CROW_LOG_INFO << "Message is \"" << message << '\"'; + if (!isbin && message == "PINGME") + conn.send_ping(""); + else if (!isbin && message == "Hello") + conn.send_text("Hello back"); + else if (isbin && message == "Hello bin") + conn.send_binary("Hello back bin"); + }) + .onclose([&](websocket::connection&, const std::string&){ + CROW_LOG_INFO << "Closing websocket"; + }); + + app.validate(); + + auto _ = async(launch::async, + [&] { app.bindaddr(LOCALHOST_ADDRESS).port(45451).run(); }); + app.wait_for_server_start(); + asio::io_service is; + + asio::ip::tcp::socket c(is); + c.connect(asio::ip::tcp::endpoint( + asio::ip::address::from_string(LOCALHOST_ADDRESS), 45451)); + + + char buf[2048]; + + //----------Handshake---------- + { + std::fill_n (buf, 2048, 0); + c.send(asio::buffer(http_message)); + + c.receive(asio::buffer(buf, 2048)); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + CHECK(connected); + } + //----------Pong---------- + { + std::fill_n (buf, 2048, 0); + char ping_message[2]("\x89"); + + c.send(asio::buffer(ping_message, 2)); + c.receive(asio::buffer(buf, 2048)); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + CHECK((int)(unsigned char)buf[0] == 0x8A); + } + //----------Ping---------- + { + std::fill_n (buf, 2048, 0); + char not_ping_message[2+6+1]("\x81\x06" + "PINGME"); + + c.send(asio::buffer(not_ping_message, 8)); + c.receive(asio::buffer(buf, 2048)); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + CHECK((int)(unsigned char)buf[0] == 0x89); + } + //----------Text---------- + { + std::fill_n (buf, 2048, 0); + char text_message[2+5+1]("\x81\x05" + "Hello"); + + c.send(asio::buffer(text_message, 7)); + c.receive(asio::buffer(buf, 2048)); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + std::string checkstring(std::string(buf).substr(0, 12)); + CHECK(checkstring == "\x81\x0AHello back"); + } + //----------Binary---------- + { + std::fill_n (buf, 2048, 0); + char bin_message[2+9+1]("\x82\x09" + "Hello bin"); + + c.send(asio::buffer(bin_message, 11)); + c.receive(asio::buffer(buf, 2048)); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + std::string checkstring2(std::string(buf).substr(0, 16)); + CHECK(checkstring2 == "\x82\x0EHello back bin"); + } + //----------Masked Text---------- + { + std::fill_n (buf, 2048, 0); + char text_masked_message[2+4+5+1]("\x81\x85" + "\x67\xc6\x69\x73" + "\x2f\xa3\x05\x1f\x08"); + + c.send(asio::buffer(text_masked_message, 11)); + c.receive(asio::buffer(buf, 2048)); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + std::string checkstring3(std::string(buf).substr(0, 12)); + CHECK(checkstring3 == "\x81\x0AHello back"); + } + //----------Masked Binary---------- + { + std::fill_n (buf, 2048, 0); + char bin_masked_message[2+4+9+1]("\x82\x89" + "\x67\xc6\x69\x73" + "\x2f\xa3\x05\x1f\x08\xe6\x0b\x1a\x09"); + + c.send(asio::buffer(bin_masked_message, 15)); + c.receive(asio::buffer(buf, 2048)); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + std::string checkstring4(std::string(buf).substr(0, 16)); + CHECK(checkstring4 == "\x82\x0EHello back bin"); + } + //----------Close---------- + { + std::fill_n (buf, 2048, 0); + char close_message[10]("\x88"); //I do not know why, but the websocket code does not read this unless it's longer than 4 or so bytes + + c.send(asio::buffer(close_message, 10)); + c.receive(asio::buffer(buf, 2048)); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + CHECK((int)(unsigned char)buf[0] == 0x88); + } + + app.stop(); +} From ca46844782080fc60583d6f95c4b38f737afc468 Mon Sep 17 00:00:00 2001 From: The-EDev Date: Mon, 9 Nov 2020 05:00:49 +0300 Subject: [PATCH 07/10] Added check for SSL adaptor to fix potential crash when SSL websockets are used (ipkn#328) --- include/crow/socket_adaptors.h | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/include/crow/socket_adaptors.h b/include/crow/socket_adaptors.h index a3df1de2b..8330b6fa6 100644 --- a/include/crow/socket_adaptors.h +++ b/include/crow/socket_adaptors.h @@ -111,28 +111,45 @@ namespace crow return raw_socket().is_open(); } + bool is_open() + { + return ssl_socket_ ? raw_socket().is_open() : false; + } + void close() { - boost::system::error_code ec; - raw_socket().close(ec); + if (is_open()) + { + boost::system::error_code ec; + raw_socket().close(ec); + } } void shutdown_readwrite() { - boost::system::error_code ec; - raw_socket().shutdown(boost::asio::socket_base::shutdown_type::shutdown_both, ec); + if (is_open()) + { + boost::system::error_code ec; + raw_socket().shutdown(boost::asio::socket_base::shutdown_type::shutdown_both, ec); + } } void shutdown_write() { - boost::system::error_code ec; - raw_socket().shutdown(boost::asio::socket_base::shutdown_type::shutdown_send, ec); + if (is_open()) + { + boost::system::error_code ec; + raw_socket().shutdown(boost::asio::socket_base::shutdown_type::shutdown_send, ec); + } } void shutdown_read() { - boost::system::error_code ec; - raw_socket().shutdown(boost::asio::socket_base::shutdown_type::shutdown_receive, ec); + if (is_open()) + { + boost::system::error_code ec; + raw_socket().shutdown(boost::asio::socket_base::shutdown_type::shutdown_receive, ec); + } } boost::asio::io_service& get_io_service() From b54b5a4c6e5a251b71f47ffc008272546508c6e5 Mon Sep 17 00:00:00 2001 From: The-EDev Date: Mon, 9 Nov 2020 05:18:34 +0300 Subject: [PATCH 08/10] obligatory simple messup fix removed extra is_open method --- include/crow/socket_adaptors.h | 5 ----- 1 file changed, 5 deletions(-) diff --git a/include/crow/socket_adaptors.h b/include/crow/socket_adaptors.h index 8330b6fa6..e27a48445 100644 --- a/include/crow/socket_adaptors.h +++ b/include/crow/socket_adaptors.h @@ -106,11 +106,6 @@ namespace crow return raw_socket().remote_endpoint(); } - bool is_open() - { - return raw_socket().is_open(); - } - bool is_open() { return ssl_socket_ ? raw_socket().is_open() : false; From fab264070221bd0aade11a148bbfd7509d603d76 Mon Sep 17 00:00:00 2001 From: The-EDev Date: Mon, 9 Nov 2020 17:36:03 +0300 Subject: [PATCH 09/10] Ping and Pong messages shouldn't fail with messages larger than 127 bytes anymore Has the added advantage of making the code look more consistent, while sacrificing some performance (very little though) --- include/crow/websocket.h | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/include/crow/websocket.h b/include/crow/websocket.h index c4b61ae3f..f55173828 100644 --- a/include/crow/websocket.h +++ b/include/crow/websocket.h @@ -54,7 +54,7 @@ namespace crow // | Payload Data continued ... | // +---------------------------------------------------------------+ - ///A websocket connection. + /// A websocket connection. template class Connection : public connection { @@ -121,9 +121,8 @@ namespace crow void send_ping(const std::string& msg) override { dispatch([this, msg]{ - char buf[3] = "\x89\x00"; - buf[1] += msg.size(); - write_buffers_.emplace_back(buf, buf+2); + auto header = build_header(0x9, msg.size()); + write_buffers_.emplace_back(std::move(header)); write_buffers_.emplace_back(msg); do_write(); }); @@ -136,9 +135,8 @@ namespace crow void send_pong(const std::string& msg) override { dispatch([this, msg]{ - char buf[3] = "\x8A\x00"; - buf[1] += msg.size(); - write_buffers_.emplace_back(buf, buf+2); + auto header = build_header(0xA, msg.size()); + write_buffers_.emplace_back(std::move(header)); write_buffers_.emplace_back(msg); do_write(); }); From 5150fcdf05ddf79152701c5d6090e7c5d05f048c Mon Sep 17 00:00:00 2001 From: The-EDev Date: Mon, 9 Nov 2020 19:14:50 +0300 Subject: [PATCH 10/10] bump version in doc --- Doxyfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 14e2777eb..432e7aa5b 100644 --- a/Doxyfile +++ b/Doxyfile @@ -38,13 +38,13 @@ PROJECT_NAME = Crow # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 0.1 +PROJECT_NUMBER = 0.2 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a # quick idea about the purpose of the project. Keep the description short. -PROJECT_BRIEF = "C++ microframework for the web" +PROJECT_BRIEF = "A C++ microframework for the web" # With the PROJECT_LOGO tag one can specify a logo or an icon that is included # in the documentation. The maximum height of the logo should not exceed 55