Make websocket close include the status code (#854)

added status code to on_close function, status code will be sent now
This commit is contained in:
davidot
2024-07-29 10:20:22 +02:00
committed by GitHub
parent 823fe1689c
commit 31ca145716
7 changed files with 268 additions and 25 deletions

View File

@@ -9,7 +9,7 @@ A websocket route differs from a normal route quite a bit. It uses a slightly al
- `#!cpp onopen([&](crow::websocket::connection& conn){handler code goes here})`
- `#!cpp onmessage([&](crow::websocket::connection& conn, const std::string& message, bool is_binary){handler code goes here})`
- `#!cpp onerror([&](crow::websocket::connection& conn, const std::string& error_message){handler code goes here})`
- `#!cpp onclose([&](crow::websocket::connection& conn, const std::string& reason){handler code goes here})`
- `#!cpp onclose([&](crow::websocket::connection& conn, const std::string& reason, uint16_t with_status_code ){handler code goes here})`
!!! note
@@ -25,7 +25,7 @@ CROW_WEBSOCKET_ROUTE(app, "/ws")
.onopen([&](crow::websocket::connection& conn){
do_something();
})
.onclose([&](crow::websocket::connection& conn, const std::string& reason){
.onclose([&](crow::websocket::connection& conn, const std::string& reason, uint16_t){
do_something();
})
.onmessage([&](crow::websocket::connection& /*conn*/, const std::string& data, bool is_binary){

View File

@@ -16,7 +16,7 @@ int main()
std::lock_guard<std::mutex> _(mtx);
users.insert(&conn);
})
.onclose([&](crow::websocket::connection& conn, const std::string& reason) {
.onclose([&](crow::websocket::connection& conn, const std::string& reason, uint16_t) {
CROW_LOG_INFO << "websocket connection closed: " << reason;
std::lock_guard<std::mutex> _(mtx);
users.erase(&conn);

View File

@@ -107,7 +107,7 @@
* .onopen([&](crow::websocket::connection& conn){
* do_something();
* })
* .onclose([&](crow::websocket::connection& conn, const std::string& reason){
* .onclose([&](crow::websocket::connection& conn, const std::string& reason, uint16_t){
* do_something();
* })
* .onmessage([&](crow::websocket::connection&, const std::string& data, bool is_binary){

View File

@@ -517,7 +517,7 @@ namespace crow // NOTE: Already documented in "crow/app.h"
App* app_;
std::function<void(crow::websocket::connection&)> open_handler_;
std::function<void(crow::websocket::connection&, const std::string&, bool)> message_handler_;
std::function<void(crow::websocket::connection&, const std::string&)> close_handler_;
std::function<void(crow::websocket::connection&, const std::string&, uint16_t)> close_handler_;
std::function<void(crow::websocket::connection&, const std::string&)> error_handler_;
std::function<bool(const crow::request&, void**)> accept_handler_;
uint64_t max_payload_;

View File

@@ -33,6 +33,30 @@ namespace crow // NOTE: Already documented in "crow/app.h"
Payload,
};
// Codes taken from https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1
enum CloseStatusCode : uint16_t {
NormalClosure = 1000,
EndpointGoingAway = 1001,
ProtocolError = 1002,
UnacceptableData = 1003,
InconsistentData = 1007,
PolicyViolated = 1008,
MessageTooBig = 1009,
ExtensionsNotNegotiated = 1010,
UnexpectedCondition = 1011,
// Reserved for applications only, should not send/receive these to/from clients
NoStatusCodePresent = 1005,
ClosedAbnormally = 1006,
TLSHandshakeFailure = 1015,
StartStatusCodesForLibraries = 3000,
StartStatusCodesForPrivateUse = 4000,
// Status code should be between 1000 and 4999 inclusive
StartStatusCodes = NormalClosure,
EndStatusCodes = 4999,
};
/// A base class for websocket connection.
struct connection
{
@@ -40,7 +64,7 @@ namespace crow // NOTE: Already documented in "crow/app.h"
virtual void send_text(std::string msg) = 0;
virtual void send_ping(std::string msg) = 0;
virtual void send_pong(std::string msg) = 0;
virtual void close(std::string const& msg = "quit") = 0;
virtual void close(std::string const& msg = "quit", uint16_t status_code = CloseStatusCode::NormalClosure) = 0;
virtual std::string get_remote_ip() = 0;
virtual ~connection() = default;
@@ -88,7 +112,7 @@ namespace crow // NOTE: Already documented in "crow/app.h"
Connection(const crow::request& req, Adaptor&& adaptor, Handler* handler, uint64_t max_payload,
std::function<void(crow::websocket::connection&)> open_handler,
std::function<void(crow::websocket::connection&, const std::string&, bool)> message_handler,
std::function<void(crow::websocket::connection&, const std::string&)> close_handler,
std::function<void(crow::websocket::connection&, const std::string&, uint16_t)> close_handler,
std::function<void(crow::websocket::connection&, const std::string&)> error_handler,
std::function<bool(const crow::request&, void**)> accept_handler):
adaptor_(std::move(adaptor)),
@@ -213,18 +237,22 @@ namespace crow // NOTE: Already documented in "crow/app.h"
///
/// Sets a flag to destroy the object once the message is sent.
void close(std::string const& msg) override
void close(std::string const& msg, uint16_t status_code) override
{
dispatch([this, msg]() mutable {
dispatch([this, msg, status_code]() mutable {
has_sent_close_ = true;
if (has_recv_close_ && !is_close_handler_called_)
{
is_close_handler_called_ = true;
if (close_handler_)
close_handler_(*this, msg);
close_handler_(*this, msg, status_code);
}
auto header = build_header(0x8, msg.size());
auto header = build_header(0x8, msg.size() + 2);
char status_buf[2];
*(uint16_t*)(status_buf) = htons(status_code);
write_buffers_.emplace_back(std::move(header));
write_buffers_.emplace_back(std::string(status_buf, 2));
write_buffers_.emplace_back(msg);
do_write();
});
@@ -344,7 +372,7 @@ namespace crow // NOTE: Already documented in "crow/app.h"
adaptor_.close();
if (error_handler_)
error_handler_(*this, "Client connection not masked.");
check_destroy();
check_destroy(CloseStatusCode::UnacceptableData);
#endif
}
@@ -455,7 +483,7 @@ namespace crow // NOTE: Already documented in "crow/app.h"
adaptor_.close();
if (error_handler_)
error_handler_(*this, "Message length exceeds maximum payload.");
check_destroy();
check_destroy(MessageTooBig);
}
else if (has_mask_)
{
@@ -601,21 +629,36 @@ namespace crow // NOTE: Already documented in "crow/app.h"
case 0x8: // Close
{
has_recv_close_ = true;
uint16_t status_code = NoStatusCodePresent;
std::string::size_type message_start = 2;
if (fragment_.size() >= 2)
{
status_code = ntohs(((uint16_t*)fragment_.data())[0]);
} else {
// no message will crash substr
message_start = 0;
}
if (!has_sent_close_)
{
close(fragment_);
close(fragment_.substr(message_start), status_code);
}
else
{
adaptor_.shutdown_readwrite();
adaptor_.close();
close_connection_ = true;
if (!is_close_handler_called_)
{
if (close_handler_)
close_handler_(*this, fragment_);
close_handler_(*this, fragment_.substr(message_start), status_code);
is_close_handler_called_ = true;
}
adaptor_.shutdown_readwrite();
adaptor_.close();
// Close handler must have been called at this point so code does not matter
check_destroy();
return false;
}
@@ -678,12 +721,13 @@ namespace crow // NOTE: Already documented in "crow/app.h"
}
/// Destroy the Connection.
void check_destroy()
void check_destroy(websocket::CloseStatusCode code = CloseStatusCode::ClosedAbnormally)
{
//if (has_sent_close_ && has_recv_close_)
// Note that if the close handler was not yet called at this point we did not receive a close packet (or send one)
// and thus we use ClosedAbnormally unless instructed otherwise
if (!is_close_handler_called_)
if (close_handler_)
close_handler_(*this, "uncleanly");
close_handler_(*this, "uncleanly", code);
handler_->remove_websocket(this);
if (sending_buffers_.empty() && !is_reading)
delete this;
@@ -750,7 +794,7 @@ namespace crow // NOTE: Already documented in "crow/app.h"
std::function<void(crow::websocket::connection&)> open_handler_;
std::function<void(crow::websocket::connection&, const std::string&, bool)> message_handler_;
std::function<void(crow::websocket::connection&, const std::string&)> close_handler_;
std::function<void(crow::websocket::connection&, const std::string&, uint16_t status_code)> close_handler_;
std::function<void(crow::websocket::connection&, const std::string&)> error_handler_;
std::function<bool(const crow::request&, void**)> accept_handler_;
};

View File

@@ -12,7 +12,7 @@ void define_endpoints(crow::SimpleApp& app)
return true;
})
.onopen([](crow::websocket::connection&) {})
.onclose([](crow::websocket::connection&, const std::string&) {});
.onclose([](crow::websocket::connection&, const std::string&, uint16_t) {});
}
int main()

View File

@@ -2669,10 +2669,12 @@ TEST_CASE("websocket")
conn.send_ping("");
else if (!isbin && message == "Hello")
conn.send_text("Hello back");
else if (!isbin && message.empty())
conn.send_text("");
else if (isbin && message == "Hello bin")
conn.send_binary("Hello back bin");
})
.onclose([&](websocket::connection&, const std::string&) {
.onclose([&](websocket::connection&, const std::string&, uint16_t) {
CROW_LOG_INFO << "Closing websocket";
});
@@ -2731,6 +2733,17 @@ TEST_CASE("websocket")
std::string checkstring(std::string(buf).substr(0, 12));
CHECK(checkstring == "\x81\x0AHello back");
}
//----------Empty Text----------
{
std::fill_n(buf, 2048, 0);
char text_message[2 + 0 + 1]("\x81\x00");
c.send(asio::buffer(text_message, 2));
c.receive(asio::buffer(buf, 2048));
std::this_thread::sleep_for(std::chrono::milliseconds(5));
std::string checkstring(std::string(buf).substr(0, 2));
CHECK(checkstring == text_message);
}
//----------Binary----------
{
std::fill_n(buf, 2048, 0);
@@ -2809,6 +2822,192 @@ TEST_CASE("websocket")
app.stop();
} // websocket
TEST_CASE("websocket_close")
{
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\nHost: localhost\r\n\r\n";
static bool connected{false};
websocket::connection* connection = nullptr;
uint32_t close_calls = 0;
uint16_t last_status_code = 0;
CROW_LOG_INFO << "Setting up app!\n";
SimpleApp app;
CROW_WEBSOCKET_ROUTE(app, "/ws")
.onaccept([&](const crow::request& req, void**) {
CROW_LOG_INFO << "Accepted websocket with URL " << req.url;
return true;
})
.onopen([&](websocket::connection& conn) {
connected = true;
connection = &conn;
CROW_LOG_INFO << "Connected websocket and value is " << connected;
})
.onmessage([&](websocket::connection& conn, const std::string& message, bool) {
CROW_LOG_INFO << "Message is \"" << message << '\"';
if (message == "quit-default")
conn.close();
else if (message == "quit-custom")
conn.close("custom", crow::websocket::StartStatusCodesForPrivateUse + 10u);
})
.onclose([&](websocket::connection& conn, const std::string&, uint16_t status_code) {
// There should just be one connection
CHECK(&conn == connection);
CHECK_FALSE(conn.get_remote_ip().empty());
++close_calls;
last_status_code = status_code;
CROW_LOG_INFO << "Closing websocket";
});
app.validate();
CROW_LOG_WARNING << "Starting app!\n";
auto _ = app.bindaddr(LOCALHOST_ADDRESS).port(45453).run_async();
app.wait_for_server_start();
CROW_LOG_WARNING << "App started!\n";
asio::io_service is;
asio::ip::tcp::socket c(is);
c.connect(asio::ip::tcp::endpoint(
asio::ip::address::from_string(LOCALHOST_ADDRESS), 45453));
CROW_LOG_WARNING << "Connected!\n";
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);
}
CHECK(close_calls == 0);
SECTION("normal close from client")
{
std::fill_n(buf, 2048, 0);
// Close message with, len = 2, status code = 1000
char close_message[5]("\x88\x02\x03\xE8");
c.send(asio::buffer(close_message, 4));
c.receive(asio::buffer(buf, 2048));
std::this_thread::sleep_for(std::chrono::milliseconds(5));
CHECK((int)(unsigned char)buf[0] == 0x88);
CHECK((int)(unsigned char)buf[1] == 0x02);
CHECK((int)(unsigned char)buf[2] == 0x03);
CHECK((int)(unsigned char)buf[3] == 0xE8);
CHECK(close_calls == 1);
CHECK(last_status_code == 1000);
}
SECTION("empty close from client")
{
std::fill_n(buf, 2048, 0);
// Close message with, len = 0, status code = N/A -> To application give no status code present
char close_message[3]("\x88\x00");
c.send(asio::buffer(close_message, 2));
c.receive(asio::buffer(buf, 2048));
std::this_thread::sleep_for(std::chrono::milliseconds(5));
CHECK((int)(unsigned char)buf[0] == 0x88);
CHECK(close_calls == 1);
CHECK(last_status_code == crow::websocket::NoStatusCodePresent);
}
SECTION("close with message from client")
{
std::fill_n(buf, 2048, 0);
// Close message with, len = 2, status code = 1001
char close_message[9]("\x88\x06\x03\xE9" "fail");
c.send(asio::buffer(close_message, 8));
c.receive(asio::buffer(buf, 2048));
std::this_thread::sleep_for(std::chrono::milliseconds(5));
CHECK((int)(unsigned char)buf[0] == 0x88);
CHECK((int)(unsigned char)buf[1] == 0x06);
CHECK((int)(unsigned char)buf[2] == 0x03);
CHECK((int)(unsigned char)buf[3] == 0xE9);
std::string checkstring(std::string(buf).substr(4, 4));
CHECK(checkstring == "fail");
CHECK(close_calls == 1);
CHECK(last_status_code == 1001);
}
SECTION("normal close from server")
{
//----------Text----------
std::fill_n(buf, 2048, 0);
char text_message[2 + 12 + 1]("\x81\x0C"
"quit-default");
c.send(asio::buffer(text_message, 14));
c.receive(asio::buffer(buf, 2048));
std::this_thread::sleep_for(std::chrono::milliseconds(5));
CHECK((int)(unsigned char)buf[0] == 0x88);
// length is message + 2 for status code
CHECK((int)(unsigned char)buf[1] == 0x6);
uint16_t expected_code = websocket::NormalClosure;
CHECK((int)(unsigned char)buf[2] == expected_code >> 8);
CHECK((int)(unsigned char)buf[3] == (expected_code & 0xff));
std::string checkstring(std::string(buf).substr(4, 4));
CHECK(checkstring == "quit");
CHECK(close_calls == 0);
// Reply with client close
char client_close_response[9]("\x88\x06\x0\x0quit");
client_close_response[2] = buf[2];
client_close_response[3] = buf[3];
c.send(asio::buffer(client_close_response, 8));
std::this_thread::sleep_for(std::chrono::milliseconds(5));
CHECK(close_calls == 1);
CHECK(last_status_code == expected_code);
}
SECTION("custom close from server")
{
//----------Text----------
std::fill_n(buf, 2048, 0);
char text_message[2 + 11 + 1]("\x81\x0B"
"quit-custom");
c.send(asio::buffer(text_message, 13));
c.receive(asio::buffer(buf, 2048));
std::this_thread::sleep_for(std::chrono::milliseconds(5));
CHECK((int)(unsigned char)buf[0] == 0x88);
// length is message + 2 for status code
CHECK((int)(unsigned char)buf[1] == 0x8);
uint16_t expected_code = 4010;
CHECK((int)(unsigned char)buf[2] == expected_code >> 8);
CHECK((int)(unsigned char)buf[3] == (expected_code & 0xff));
std::string checkstring(std::string(buf).substr(4, 6));
CHECK(checkstring == "custom");
CHECK(close_calls == 0);
// Reply with client close
char client_close_response[11]("\x88\x08\x0\x0" "custom");
client_close_response[2] = buf[2];
client_close_response[3] = buf[3];
c.send(asio::buffer(client_close_response, 10));
std::this_thread::sleep_for(std::chrono::milliseconds(5));
CHECK(close_calls == 1);
CHECK(last_status_code == expected_code);
}
CROW_LOG_WARNING << "Stopping app!\n";
app.stop();
}
TEST_CASE("websocket_missing_host")
{
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";
@@ -2835,7 +3034,7 @@ TEST_CASE("websocket_missing_host")
else if (isbin && message == "Hello bin")
conn.send_binary("Hello back bin");
})
.onclose([&](websocket::connection&, const std::string&) {
.onclose([&](websocket::connection&, const std::string&, uint16_t) {
CROW_LOG_INFO << "Closing websocket";
});
@@ -2887,7 +3086,7 @@ TEST_CASE("websocket_max_payload")
else if (isbin && message == "Hello bin")
conn.send_binary("Hello back bin");
})
.onclose([&](websocket::connection&, const std::string&) {
.onclose([&](websocket::connection&, const std::string&, uint16_t) {
CROW_LOG_INFO << "Closing websocket";
});