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();
+}