From eacb7512822983203c5812ade6e8f776b7247ca9 Mon Sep 17 00:00:00 2001 From: Pierre Jacobs Date: Sat, 29 Jul 2023 22:49:53 +0200 Subject: [PATCH 1/9] #692 Add possibility to specific Sec-WebSocket-Protocol --- include/crow/routing.h | 11 +++++++++-- include/crow/utility.h | 18 ++++++++++++++++++ include/crow/websocket.h | 28 +++++++++++++++++++++++++++- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/include/crow/routing.h b/include/crow/routing.h index 7998e49a4..1b58d9587 100644 --- a/include/crow/routing.h +++ b/include/crow/routing.h @@ -451,12 +451,12 @@ namespace crow void handle_upgrade(const request& req, response&, SocketAdaptor&& adaptor) override { max_payload_ = max_payload_override_ ? max_payload_ : app_->websocket_max_payload(); - new crow::websocket::Connection(req, std::move(adaptor), app_, max_payload_, open_handler_, message_handler_, close_handler_, error_handler_, accept_handler_); + new crow::websocket::Connection(req, std::move(adaptor), app_, max_payload_, subprotocols_, open_handler_, message_handler_, close_handler_, error_handler_, accept_handler_); } #ifdef CROW_ENABLE_SSL void handle_upgrade(const request& req, response&, SSLAdaptor&& adaptor) override { - new crow::websocket::Connection(req, std::move(adaptor), app_, max_payload_, open_handler_, message_handler_, close_handler_, error_handler_, accept_handler_); + new crow::websocket::Connection(req, std::move(adaptor), app_, max_payload_, subprotocols_, open_handler_, message_handler_, close_handler_, error_handler_, accept_handler_); } #endif @@ -468,6 +468,12 @@ namespace crow return *this; } + self_t& subprotocols(std::vector&& subprotocols) + { + subprotocols_ = subprotocols; + return *this; + } + template self_t& onopen(Func f) { @@ -512,6 +518,7 @@ namespace crow std::function accept_handler_; uint64_t max_payload_; bool max_payload_override_ = false; + std::vector subprotocols_; }; /// Allows the user to assign parameters using functions. diff --git a/include/crow/utility.h b/include/crow/utility.h index be5b08cdb..c3aa9da58 100644 --- a/include/crow/utility.h +++ b/include/crow/utility.h @@ -897,5 +897,23 @@ namespace crow return v.substr(begin, end - begin); } + + /** + * @brief splits a string based on a separator + */ + inline static std::vector split(const std::string& v, const std::string& separator) + { + std::vector result; + size_t startPos = 0; + + for (size_t foundPos = v.find(separator); foundPos != std::string::npos; foundPos = v.find(separator, startPos)) + { + result.push_back(v.substr(startPos, foundPos - startPos)); + startPos = foundPos + separator.size(); + } + + result.push_back(v.substr(startPos)); + return result; + } } // namespace utility } // namespace crow diff --git a/include/crow/websocket.h b/include/crow/websocket.h index c7d8f0638..04540e39c 100644 --- a/include/crow/websocket.h +++ b/include/crow/websocket.h @@ -71,7 +71,8 @@ namespace crow /// /// Requires a request with an "Upgrade: websocket" header.
/// Automatically handles the handshake. - Connection(const crow::request& req, Adaptor&& adaptor, Handler* handler, uint64_t max_payload, + Connection(const crow::request& req, Adaptor&& adaptor, Handler* handler, + uint64_t max_payload, const std::vector& subprotocols, std::function open_handler, std::function message_handler, std::function close_handler, @@ -94,6 +95,24 @@ namespace crow return; } + std::string requested_subprotocols_header = req.get_header_value("Sec-WebSocket-Protocol"); + if (!subprotocols.empty() || !requested_subprotocols_header.empty()) + { + auto requested_subprotocols = utility::split(requested_subprotocols_header, ", "); + auto subprotocol = std::find_first_of(subprotocols.begin(), subprotocols.end(), requested_subprotocols.begin(), requested_subprotocols.end()); + if (subprotocol == subprotocols.end()) + { + adaptor_.close(); + handler_->remove_websocket(this); + delete this; + return; + } + else + { + subprotocol_ = *subprotocol; + } + } + if (accept_handler_) { void* ud = nullptr; @@ -265,6 +284,12 @@ namespace crow write_buffers_.emplace_back(header); write_buffers_.emplace_back(std::move(hello)); write_buffers_.emplace_back(crlf); + if (!subprotocol_.empty()) + { + write_buffers_.emplace_back("Sec-WebSocket-Protocol: "); + write_buffers_.emplace_back(subprotocol_); + write_buffers_.emplace_back(crlf); + } write_buffers_.emplace_back(crlf); do_write(); if (open_handler_) @@ -721,6 +746,7 @@ namespace crow uint16_t remaining_length16_{0}; uint64_t remaining_length_{0}; uint64_t max_payload_bytes_{UINT64_MAX}; + std::string subprotocol_; bool close_connection_{false}; bool is_reading{false}; bool has_mask_{false}; From b44310e179ca8cb3fe6d9a9f771249e15c4bcee9 Mon Sep 17 00:00:00 2001 From: Pierre Jacobs Date: Sat, 29 Jul 2023 23:24:17 +0200 Subject: [PATCH 2/9] #692 Add documentation --- docs/guides/websockets.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/guides/websockets.md b/docs/guides/websockets.md index b09eb685b..3ecbceba6 100644 --- a/docs/guides/websockets.md +++ b/docs/guides/websockets.md @@ -45,6 +45,11 @@ The maximum payload size that a connection accepts can be adjusted either global By default, this limit is disabled. To disable the global setting in specific routes, you only need to call `#!cpp CROW_WEBSOCKET_ROUTE(app, "/url").max_payload(UINT64_MAX)`. +## Subprotocols +[:octicons-feed-tag-16: master](https://github.com/CrowCpp/Crow) + +Specifies the possible subprotocols that are available for the client. If specified, the first match with the client's requested subprotocols will be returned in the "Sec-WebSocket-Protocol" header of the handshake response. Otherwise, the connection will be closed. If no subprotocol are specified on both the client and the server side, the connection process will continue normally. It can be specified by using `#!cpp CROW_WEBSOCKET_ROUTE(app, "/url").subprotocols()`. + For more info about websocket routes go [here](../reference/classcrow_1_1_web_socket_rule.html). From 0241466ce1e4b7c6d327e046ca721e445ea56ff4 Mon Sep 17 00:00:00 2001 From: Pierre Jacobs Date: Sun, 30 Jul 2023 10:05:16 +0200 Subject: [PATCH 3/9] #692 Not using find_first_of as it is a C++ 17 function --- include/crow/utility.h | 24 ++++++++++++++++++++++++ include/crow/websocket.h | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/include/crow/utility.h b/include/crow/utility.h index c3aa9da58..ea736dd21 100644 --- a/include/crow/utility.h +++ b/include/crow/utility.h @@ -915,5 +915,29 @@ namespace crow result.push_back(v.substr(startPos)); return result; } + + /** + * @brief Returns the first occurence that matches between two ranges of iterators + * @param first1 begin() iterator of the first range + * @param last1 end() iterator of the first range + * @param first2 begin() iterator of the second range + * @param last2 end() iterator of the second range + * @return first occurence that matches between two ranges of iterators + */ + template + inline static Iter1 find_first_of(Iter1 first1, Iter1 last1, Iter2 first2, Iter2 last2) + { + for (; first1 != last1; ++first1) + { + for (auto it = first2; it != last2; ++it) + { + if (*first1 == *it) + { + return first1; + } + } + } + return last1; + } } // namespace utility } // namespace crow diff --git a/include/crow/websocket.h b/include/crow/websocket.h index 04540e39c..b8da05a58 100644 --- a/include/crow/websocket.h +++ b/include/crow/websocket.h @@ -99,7 +99,7 @@ namespace crow if (!subprotocols.empty() || !requested_subprotocols_header.empty()) { auto requested_subprotocols = utility::split(requested_subprotocols_header, ", "); - auto subprotocol = std::find_first_of(subprotocols.begin(), subprotocols.end(), requested_subprotocols.begin(), requested_subprotocols.end()); + auto subprotocol = utility::find_first_of(subprotocols.begin(), subprotocols.end(), requested_subprotocols.begin(), requested_subprotocols.end()); if (subprotocol == subprotocols.end()) { adaptor_.close(); From e3da955cc0bdc3e991114b1bcd3ed7d96a18c45b Mon Sep 17 00:00:00 2001 From: Pierre Jacobs Date: Sun, 30 Jul 2023 10:13:11 +0200 Subject: [PATCH 4/9] #692 Not using find_first_of as it is a C++ 17 function --- include/crow/utility.h | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/include/crow/utility.h b/include/crow/utility.h index ea736dd21..b81e862a5 100644 --- a/include/crow/utility.h +++ b/include/crow/utility.h @@ -929,12 +929,9 @@ namespace crow { for (; first1 != last1; ++first1) { - for (auto it = first2; it != last2; ++it) + if (std::find(first2, last2, *first1) != last2) { - if (*first1 == *it) - { - return first1; - } + return first1; } } return last1; From 4b48a13265c0b252c2cee6d7067da8db03613d86 Mon Sep 17 00:00:00 2001 From: Pierre Jacobs Date: Sun, 30 Jul 2023 12:08:57 +0200 Subject: [PATCH 5/9] #692 Added get_subprotocol getter --- include/crow/websocket.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/include/crow/websocket.h b/include/crow/websocket.h index b8da05a58..94e663cf6 100644 --- a/include/crow/websocket.h +++ b/include/crow/websocket.h @@ -28,6 +28,7 @@ namespace crow virtual void send_pong(std::string msg) = 0; virtual void close(std::string const& msg = "quit") = 0; virtual std::string get_remote_ip() = 0; + virtual std::string get_subprotocol() const = 0; virtual ~connection() = default; void userdata(void* u) { userdata_ = u; } @@ -245,6 +246,12 @@ namespace crow max_payload_bytes_ = payload; } + /// Returns the matching client/server subprotocol, empty string if none matched. + std::string get_subprotocol() const override + { + return subprotocol_; + } + protected: /// Generate the websocket headers using an opcode and the message size (in bytes). std::string build_header(int opcode, size_t size) From 5342dd67141461662813b69b3b0640793b6df51d Mon Sep 17 00:00:00 2001 From: Pierre Jacobs Date: Tue, 8 Aug 2023 18:43:24 +0200 Subject: [PATCH 6/9] #642 CR : use const& instead of && for subprotocols parameter --- include/crow/routing.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/crow/routing.h b/include/crow/routing.h index 1b58d9587..64aeab22a 100644 --- a/include/crow/routing.h +++ b/include/crow/routing.h @@ -468,7 +468,7 @@ namespace crow return *this; } - self_t& subprotocols(std::vector&& subprotocols) + self_t& subprotocols(const std::vector& subprotocols) { subprotocols_ = subprotocols; return *this; From ebd1708251dec95ece3521aa06114e76245e5473 Mon Sep 17 00:00:00 2001 From: Pierre Jacobs Date: Sun, 14 Jan 2024 17:30:11 +0100 Subject: [PATCH 7/9] #692 Remove closing of the WebSocket if no subprotocol is matching --- include/crow/websocket.h | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/include/crow/websocket.h b/include/crow/websocket.h index 94e663cf6..222bed5b2 100644 --- a/include/crow/websocket.h +++ b/include/crow/websocket.h @@ -101,14 +101,7 @@ namespace crow { auto requested_subprotocols = utility::split(requested_subprotocols_header, ", "); auto subprotocol = utility::find_first_of(subprotocols.begin(), subprotocols.end(), requested_subprotocols.begin(), requested_subprotocols.end()); - if (subprotocol == subprotocols.end()) - { - adaptor_.close(); - handler_->remove_websocket(this); - delete this; - return; - } - else + if (subprotocol != subprotocols.end()) { subprotocol_ = *subprotocol; } From 3287ee9895df5a0ca9a6af8cb67513d64c3e992f Mon Sep 17 00:00:00 2001 From: Pierre Jacobs Date: Tue, 16 Jan 2024 22:26:00 +0100 Subject: [PATCH 8/9] #692 Fix missing include --- include/crow/utility.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/crow/utility.h b/include/crow/utility.h index b81e862a5..b951aa6b1 100644 --- a/include/crow/utility.h +++ b/include/crow/utility.h @@ -11,6 +11,7 @@ #include #include #include +#include #include "crow/settings.h" From 8192bdb3e5c2e165c6b83748a97f1ab9abdf7b7c Mon Sep 17 00:00:00 2001 From: Pierre Jacobs Date: Sat, 7 Sep 2024 22:13:46 +0200 Subject: [PATCH 9/9] #692 Add unit test for subprotocol --- tests/unittest.cpp | 51 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/unittest.cpp b/tests/unittest.cpp index bbf402177..fe48576f5 100644 --- a/tests/unittest.cpp +++ b/tests/unittest.cpp @@ -3203,6 +3203,57 @@ TEST_CASE("websocket_max_payload") app.stop(); } // websocket_max_payload +TEST_CASE("websocket_subprotocols") +{ + 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-Protocol: myprotocol\r\nSec-WebSocket-Version: 13\r\nHost: localhost\r\n\r\n"; + + static websocket::connection* connection = nullptr; + static bool connected{false}; + + SimpleApp app; + + CROW_WEBSOCKET_ROUTE(app, "/ws") + .subprotocols({"anotherprotocol", "myprotocol"}) + .onaccept([&](const crow::request& req, void**) { + CROW_LOG_INFO << "Accepted websocket with URL " << req.url; + return true; + }) + .onopen([&](websocket::connection& con) { + connected = true; + connection = &con; + CROW_LOG_INFO << "Connected websocket and subprotocol is " << con.get_subprotocol(); + }) + .onclose([&](websocket::connection&, const std::string&, uint16_t) { + CROW_LOG_INFO << "Closing websocket"; + }); + + app.validate(); + + auto _ = app.bindaddr(LOCALHOST_ADDRESS).port(45451).run_async(); + 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); + CHECK(connection->get_subprotocol() == "myprotocol"); + } + + app.stop(); +} + #ifdef CROW_ENABLE_COMPRESSION TEST_CASE("zlib_compression") {