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 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); diff --git a/include/crow/socket_adaptors.h b/include/crow/socket_adaptors.h index a3df1de2b..e27a48445 100644 --- a/include/crow/socket_adaptors.h +++ b/include/crow/socket_adaptors.h @@ -108,31 +108,43 @@ namespace crow bool is_open() { - return raw_socket().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() diff --git a/include/crow/websocket.h b/include/crow/websocket.h index 8ca963883..f55173828 100644 --- a/include/crow/websocket.h +++ b/include/crow/websocket.h @@ -18,10 +18,13 @@ namespace crow Payload, }; + ///A base class for websocket connection. struct connection { 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(){} @@ -32,10 +35,35 @@ namespace crow void* userdata_; }; + // 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) | + // |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,29 +100,49 @@ 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); } - void send_pong(const std::string& msg) + /// Send a "Ping" message. + + /// + /// Usually invoked to check if the other point is still online. + void send_ping(const std::string& msg) override + { + dispatch([this, msg]{ + auto header = build_header(0x9, msg.size()); + write_buffers_.emplace_back(std::move(header)); + write_buffers_.emplace_back(msg); + do_write(); + }); + } + + /// Send a "Pong" message. + + /// + /// Usually automatically invoked as a response to a "Ping" message. + 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(); }); } + /// Send a binary encoded message. void send_binary(const std::string& msg) override { dispatch([this, msg]{ @@ -105,6 +153,7 @@ namespace crow }); } + /// Send a plaintext message. void send_text(const std::string& msg) override { dispatch([this, msg]{ @@ -115,6 +164,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 +187,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 +211,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 +232,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; @@ -181,8 +246,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 @@ -200,8 +266,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; @@ -300,34 +369,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: { @@ -365,21 +442,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()) { @@ -454,6 +540,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 +575,7 @@ namespace crow } } + /// Destroy the Connection. void check_destroy() { //if (has_sent_close_ && has_recv_close_) @@ -509,6 +600,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}; 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(); +}