From fa121e17e5ce9f9e36640d088e1b04a57885a592 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 16 Mar 2023 17:13:55 -0700 Subject: [PATCH 01/53] Use a deque --- .../include/launchdarkly/sse/sse.hpp | 3 ++- libs/server-sent-events/src/sse.cpp | 13 ++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp index 2f1090125..09caad221 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp @@ -12,6 +12,7 @@ #include #include #include +#include namespace launchdarkly { namespace sse { @@ -66,7 +67,7 @@ class client : public std::enable_shared_from_this { std::string m_host; std::string m_port; boost::optional m_buffered_line; - std::vector m_complete_lines; + std::deque m_complete_lines; std::vector m_events; bool m_begin_CR; boost::optional m_event_data; diff --git a/libs/server-sent-events/src/sse.cpp b/libs/server-sent-events/src/sse.cpp index af66ff42f..82437564a 100644 --- a/libs/server-sent-events/src/sse.cpp +++ b/libs/server-sent-events/src/sse.cpp @@ -189,7 +189,8 @@ boost::optional> parse_field(std::string fie size_t colon_index = field.find(':'); switch (colon_index) { case 0: - return std::make_pair(std::string{"comment"}, field.erase(0, 1)); + field.erase(0, 1); + return std::make_pair(std::string{"comment"}, std::move(field)); case std::string::npos: return std::make_pair(std::move(field), std::string{}); default: @@ -207,11 +208,9 @@ void client::parse_events() { while(true) { bool seen_empty_line = false; - auto it = m_complete_lines.begin(); - while (it != m_complete_lines.end()) { - - std::string line = *it; - it = m_complete_lines.erase(it); + while (!m_complete_lines.empty()) { + std::string line = std::move(m_complete_lines.front()); + m_complete_lines.pop_front(); if (line.empty()) { if (m_event_data.has_value()) { @@ -221,7 +220,7 @@ void client::parse_events() { continue; } - if (auto field = parse_field(line)) { + if (auto field = parse_field(std::move(line))) { if (field->first == "comment") { m_events.emplace_back(field->second); From 4670ab127e7b9ac0e98ba17516b031d3cb0f4a93 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 20 Mar 2023 15:52:20 -0700 Subject: [PATCH 02/53] mess: working on contract tests --- CMakeLists.txt | 10 ++ apps/CMakeLists.txt | 3 +- apps/hello-cpp/main.cpp | 7 +- apps/sse-contract-tests/CMakeLists.txt | 17 ++ apps/sse-contract-tests/definitions.hpp | 75 +++++++++ apps/sse-contract-tests/entity_manager.hpp | 38 +++++ apps/sse-contract-tests/http_connection.hpp | 152 ++++++++++++++++++ apps/sse-contract-tests/main.cpp | 23 +++ apps/sse-contract-tests/server.hpp | 81 ++++++++++ apps/sse-contract-tests/stream_entity.hpp | 27 ++++ cmake/json.cmake | 11 ++ .../include/launchdarkly/sse/sse.hpp | 11 +- libs/server-sent-events/src/sse.cpp | 17 +- 13 files changed, 460 insertions(+), 12 deletions(-) create mode 100644 apps/sse-contract-tests/CMakeLists.txt create mode 100644 apps/sse-contract-tests/definitions.hpp create mode 100644 apps/sse-contract-tests/entity_manager.hpp create mode 100644 apps/sse-contract-tests/http_connection.hpp create mode 100644 apps/sse-contract-tests/main.cpp create mode 100644 apps/sse-contract-tests/server.hpp create mode 100644 apps/sse-contract-tests/stream_entity.hpp create mode 100644 cmake/json.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 91995c3ae..9e1ac959d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,16 @@ project( LANGUAGES CXX C ) + +if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.24") + # Affects robustness of timestamp checking on FetchContent dependencies. + cmake_policy(SET CMP0135 NEW) +endif() + +# All projects in this repo should share the same version of 3rd party depends. +# It's the only way to remain sane. +set(CMAKE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + add_subdirectory(libs/server-sent-events) add_subdirectory(libs/client-sdk) add_subdirectory(apps) diff --git a/apps/CMakeLists.txt b/apps/CMakeLists.txt index a5c954802..2ece71cae 100644 --- a/apps/CMakeLists.txt +++ b/apps/CMakeLists.txt @@ -1,2 +1,3 @@ #add_subdirectory(hello-c) -add_subdirectory(hello-cpp) +#add_subdirectory(hello-cpp) +add_subdirectory(sse-contract-tests) diff --git a/apps/hello-cpp/main.cpp b/apps/hello-cpp/main.cpp index b3d5486cb..20a68e514 100644 --- a/apps/hello-cpp/main.cpp +++ b/apps/hello-cpp/main.cpp @@ -16,8 +16,7 @@ int main() { net::io_context ioc; - // curl "https://stream-stg.launchdarkly.com/all?filter=even-flags-2" -H "Authorization: sdk-66a5dbe0-8b26-445a-9313-761e7e3d381b" -v - auto client = launchdarkly::sse::builder(ioc, "https://stream-stg.launchdarkly.com/all") + auto client = launchdarkly::sse::builder(&ioc, "https://stream-stg.launchdarkly.com/all") .header("Authorization", "sdk-66a5dbe0-8b26-445a-9313-761e7e3d381b") .build(); @@ -31,6 +30,10 @@ int main() { ioc.run(); }); + client->on_event([](launchdarkly::sse::event_data e){ + std::cout << "Got[" << e.get_type() << "] = <" << e.get_data() << ">\n"; + }); + client->run(); } diff --git a/apps/sse-contract-tests/CMakeLists.txt b/apps/sse-contract-tests/CMakeLists.txt new file mode 100644 index 000000000..a333b0f54 --- /dev/null +++ b/apps/sse-contract-tests/CMakeLists.txt @@ -0,0 +1,17 @@ +# Required for Apple Silicon support. +cmake_minimum_required(VERSION 3.19) + +project( + LaunchDarklyCPPSSETestHarness + VERSION 0.1 + DESCRIPTION "LaunchDarkly CPP SSE Test Harness" + LANGUAGES CXX +) + +include(${CMAKE_FILES}/json.cmake) + +add_executable(sse-tests main.cpp) +target_link_libraries(sse-tests PRIVATE + launchdarkly::sse + nlohmann_json::nlohmann_json +) diff --git a/apps/sse-contract-tests/definitions.hpp b/apps/sse-contract-tests/definitions.hpp new file mode 100644 index 000000000..2d79f2d61 --- /dev/null +++ b/apps/sse-contract-tests/definitions.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include + +#include +#include +#include + +namespace nlohmann { + + template + void to_json(nlohmann::json& j, const std::optional& v) + { + if (v.has_value()) + j = *v; + else + j = nullptr; + } + + template + void from_json(const nlohmann::json& j, std::optional& v) + { + if (j.is_null()) + v = std::nullopt; + else + v = j.get(); + } +} // namespace nlohmann + + +struct config_params { + std::string streamUrl; + std::string callbackUrl; + std::string tag; + std::optional initialDelayMs; + std::optional readTimeoutMs; + std::optional lastEventId; + std::optional> headers; + std::optional method; + std::optional body; +}; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(config_params, + streamUrl, + callbackUrl, + tag, + initialDelayMs, + readTimeoutMs, + lastEventId, + headers, + method, + body +); + +struct event { + std::string type; + std::string data; + std::string id; +}; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(event, type, data, id); + +struct event_message { + std::string kind; + event event; +}; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(event_message, kind, event); + +struct comment_message { + std::string kind; + std::string comment; +}; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(comment_message, kind, comment); diff --git a/apps/sse-contract-tests/entity_manager.hpp b/apps/sse-contract-tests/entity_manager.hpp new file mode 100644 index 000000000..0a48c0473 --- /dev/null +++ b/apps/sse-contract-tests/entity_manager.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "stream_entity.hpp" +#include "definitions.hpp" + +#include +#include +#include + +class entity_manager { + std::unordered_map entities_; + std::size_t counter_; + std::mutex lock_; + boost::asio::any_io_executor executor_; +public: + entity_manager(boost::asio::any_io_executor executor): + entities_{}, + counter_{0}, + lock_{}, + executor_{executor}{ + } + + std::string create(config_params params) { + std::lock_guard guard{lock_}; + auto id = std::to_string(counter_++); + entities_.emplace(id, stream_entity{executor_, std::move(params)}); + return id; + } + + bool destroy(std::string const& id) { + std::lock_guard guard{lock_}; + auto it = entities_.find(id); + if (it == entities_.end()) { + return false; + } + return true; + } +}; diff --git a/apps/sse-contract-tests/http_connection.hpp b/apps/sse-contract-tests/http_connection.hpp new file mode 100644 index 000000000..6e76ad383 --- /dev/null +++ b/apps/sse-contract-tests/http_connection.hpp @@ -0,0 +1,152 @@ +#pragma once + +#include "entity_manager.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace beast = boost::beast; // from +namespace http = beast::http; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from + + + + +class http_connection : public std::enable_shared_from_this +{ +public: + explicit http_connection(tcp::socket socket, entity_manager& manager, std::vector caps): + socket_{std::move(socket)}, + manager_{manager}, + capabilities_{std::move(caps)} + { + } + + void start() { + read_request(); + } + +private: + // The socket for the currently connected client. + tcp::socket socket_; + + // The buffer for performing reads. + beast::flat_buffer buffer_{8192}; + + // The request message. + http::request request_; + + // The response message. + http::response response_; + + entity_manager& manager_; + + std::vector capabilities_; + + + + // Asynchronously receive a complete request message. + void + read_request() + { + auto self = shared_from_this(); + + http::async_read( + socket_, + buffer_, + request_, + [self](beast::error_code ec, + std::size_t bytes_transferred) + { + boost::ignore_unused(bytes_transferred); + if(!ec) { + self->process_request(); + } + }); + } + + // Determine what needs to be done with the request message. + void + process_request() + { + response_.version(request_.version()); + response_.keep_alive(false); + + if (create_response()) { + write_response(); + } else { + socket_.shutdown(boost::asio::socket_base::shutdown_both); + socket_.close(); + std::raise(SIGTERM); + } + } + + // Construct a response message based on the program state. + bool + create_response() + { + if (request_.target() == "/") { + if (request_.method() == http::verb::get) { + response_.set(http::field::content_type, "application/json"); + response_.result(http::status::ok); + + nlohmann::json status { + {"capabilities", capabilities_} + }; + beast::ostream(response_.body()) << status.dump(); + return true; + } + else if (request_.method() == http::verb::delete_) { + return false; + } else if (request_.method() == http::verb::post) { + std::string id = manager_.create(config_params{}); + response_.result(http::status::ok); + response_.set("Location", "/shutdown/" + id); + std::cout << "creating entity " << id << '\n'; + return true; + } + } else if (request_.target().starts_with("/shutdown/")) { + auto id = request_.target(); + id.remove_prefix(std::strlen("/shutdown/")); + std::cout << "<"<< id << ">" << std::endl; + if (manager_.destroy(id)) { + response_.result(http::status::ok); + } else { + response_.result(http::status::not_found); + } + return true; + } + + response_.result(http::status::bad_request); + response_.set(http::field::content_type, "text/plain"); + beast::ostream(response_.body()) << "400 Bad Request"; + return true; + } + void + write_response() + { + auto self = shared_from_this(); + + response_.content_length(response_.body().size()); + + http::async_write( + socket_, + response_, + [self](beast::error_code ec, std::size_t) + { + self->socket_.shutdown(tcp::socket::shutdown_send, ec); + }); + } +}; diff --git a/apps/sse-contract-tests/main.cpp b/apps/sse-contract-tests/main.cpp new file mode 100644 index 000000000..52d293e16 --- /dev/null +++ b/apps/sse-contract-tests/main.cpp @@ -0,0 +1,23 @@ +#include "server.hpp" + +int +main(int argc, char* argv[]) +{ + try + { + auto address = net::ip::make_address("0.0.0.0"); + unsigned short port = 8123; + net::io_context ioc; + + server s{ioc.get_executor(), std::move(address), port}; + s.add_capability("headers"); + + s.start_accepting(); + ioc.run(); + } + catch(std::exception const& e) + { + std::cerr << "Error: " << e.what() << std::endl; + return EXIT_FAILURE; + } +} diff --git a/apps/sse-contract-tests/server.hpp b/apps/sse-contract-tests/server.hpp new file mode 100644 index 000000000..1f5385039 --- /dev/null +++ b/apps/sse-contract-tests/server.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include "stream_entity.hpp" +#include "http_connection.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace beast = boost::beast; // from +namespace http = beast::http; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from + + +class server +{ +public: + server(net::any_io_executor executor, boost::asio::ip::address const& address, unsigned short port): + acceptor_(executor, {address, port}), + stopped_(false), + signals_{executor}, + manager_{executor} + { + signals_.add(SIGTERM); + signals_.add(SIGINT); + signals_.async_wait(boost::bind(&server::stop, this)); + acceptor_.set_option(tcp::acceptor::reuse_address(true)); + } + + void add_capability(std::string cap) { + + } + + void start_accepting() + { + acceptor_.listen(); + accept_loop(); + } + + void stop() + { + boost::asio::post(acceptor_.get_executor(), [this]() + { + std::cout << "Stopping server\n"; + acceptor_.cancel(); + stopped_ = true; + std::cout << "Server stopped\n"; + }); + } + +private: + void accept_loop() + { + acceptor_.async_accept([this](beast::error_code ec, tcp::socket peer){ + if (!ec) { + if (!stopped_) { + std::make_shared(std::move(peer), manager_, caps_)->start(); + accept_loop(); + } + } + }); + } + + tcp::acceptor acceptor_; + boost::asio::signal_set signals_; + bool stopped_; + entity_manager manager_; + std::vector caps_; +}; diff --git a/apps/sse-contract-tests/stream_entity.hpp b/apps/sse-contract-tests/stream_entity.hpp new file mode 100644 index 000000000..762481689 --- /dev/null +++ b/apps/sse-contract-tests/stream_entity.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "definitions.hpp" + +#include +#include + + +class stream_entity { + std::string callback_url_; + size_t callback_counter_; + std::shared_ptr client_; +public: + stream_entity(boost::asio::any_io_executor executor, config_params params): + callback_url_{params.callbackUrl}, + callback_counter_{0}, + client_{} + { + auto builder = launchdarkly::sse::builder{executor, params.streamUrl}; + if (params.headers) { + for (auto h: *params.headers) { + builder.header(h.first, h.second); + } + } + client_ = builder.build(); + } +}; diff --git a/cmake/json.cmake b/cmake/json.cmake new file mode 100644 index 000000000..ee1c9d42d --- /dev/null +++ b/cmake/json.cmake @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.11) + +include(FetchContent) + +set(JSON_ImplicitConversions OFF) + +FetchContent_Declare(json + URL https://github.com/nlohmann/json/releases/download/v3.11.2/json.tar.xz +) + +FetchContent_MakeAvailable(json) diff --git a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp index 09caad221..5e9b882eb 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include namespace launchdarkly { @@ -27,13 +28,13 @@ class client; class builder { public: - builder(net::io_context& ioc, std::string url); + builder(net::any_io_executor ioc, std::string url); builder& header(const std::string& name, const std::string& value); builder& method(http::verb verb); std::shared_ptr build(); private: std::string m_url; - net::io_context& m_executor; + net::any_io_executor m_executor; ssl::context m_ssl_ctx; http::request m_request; }; @@ -71,13 +72,19 @@ class client : public std::enable_shared_from_this { std::vector m_events; bool m_begin_CR; boost::optional m_event_data; + std::function m_cb; void complete_line(); + size_t append_up_to(std::string_view body, const std::string& search); std::size_t parse_stream(std::uint64_t remain, std::string_view body, beast::error_code& ec); void parse_events(); public: explicit client(net::any_io_executor ex, ssl::context &ctx, http::request req, std::string host, std::string port); void run(); + template + void on_event(Callback event_cb) { + m_cb = event_cb; + } }; diff --git a/libs/server-sent-events/src/sse.cpp b/libs/server-sent-events/src/sse.cpp index 82437564a..8451ee5d4 100644 --- a/libs/server-sent-events/src/sse.cpp +++ b/libs/server-sent-events/src/sse.cpp @@ -1,9 +1,7 @@ #include #include -#include #include #include -#include #include #include #include @@ -16,7 +14,7 @@ namespace net = boost::asio; // from namespace ssl = boost::asio::ssl; // from using tcp = boost::asio::ip::tcp; // from -builder::builder(net::io_context& ctx, std::string url): +builder::builder(net::any_io_executor ctx, std::string url): m_url{std::move(url)}, m_ssl_ctx{ssl::context::tlsv12_client}, m_executor{ctx} { @@ -95,7 +93,10 @@ client::client(net::any_io_executor ex, ssl::context &ctx, http::request\n"; + }}{ // The HTTP response body is of potentially infinite length. m_parser.body_limit(boost::none); @@ -223,7 +224,10 @@ void client::parse_events() { if (auto field = parse_field(std::move(line))) { if (field->first == "comment") { - m_events.emplace_back(field->second); + event_data e{boost::none}; + e.set_type("comment"); + e.append_data(field->second); + m_cb(std::move(e)); continue; } @@ -248,8 +252,7 @@ void client::parse_events() { m_event_data.reset(); if (data.has_value()) { - std::cout << "Got event:\n"; - std::cout << "Type = <" << data->get_type() << ">, Data = <" << data->get_data() << ">\n"; + m_cb(std::move(data.get())); } continue; From b6b643f471662f0ec4a97019a334d5c36b162741 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 20 Mar 2023 17:16:38 -0700 Subject: [PATCH 03/53] bug in http session handling --- apps/sse-contract-tests/definitions.hpp | 29 ++++++++------ apps/sse-contract-tests/entity_manager.hpp | 1 + apps/sse-contract-tests/main.cpp | 10 +++-- apps/sse-contract-tests/server.hpp | 6 +-- .../{http_connection.hpp => session.hpp} | 17 ++++---- apps/sse-contract-tests/stream_entity.hpp | 40 ++++++++++++++++++- 6 files changed, 73 insertions(+), 30 deletions(-) rename apps/sse-contract-tests/{http_connection.hpp => session.hpp} (87%) diff --git a/apps/sse-contract-tests/definitions.hpp b/apps/sse-contract-tests/definitions.hpp index 2d79f2d61..fb53b9da0 100644 --- a/apps/sse-contract-tests/definitions.hpp +++ b/apps/sse-contract-tests/definitions.hpp @@ -8,23 +8,26 @@ namespace nlohmann { - template - void to_json(nlohmann::json& j, const std::optional& v) - { - if (v.has_value()) - j = *v; - else + template + struct adl_serializer> { + static void to_json(json& j, const std::optional& opt) { + if (opt == std::nullopt) { j = nullptr; + } else { + j = *opt; // this will call adl_serializer::to_json which will + // find the free function to_json in T's namespace! + } } - template - void from_json(const nlohmann::json& j, std::optional& v) - { - if (j.is_null()) - v = std::nullopt; - else - v = j.get(); + static void from_json(const json& j, std::optional& opt) { + if (j.is_null()) { + opt = std::nullopt; + } else { + opt = j.get(); // same as above, but with + // adl_serializer::from_json + } } +}; } // namespace nlohmann diff --git a/apps/sse-contract-tests/entity_manager.hpp b/apps/sse-contract-tests/entity_manager.hpp index 0a48c0473..d045b9c2b 100644 --- a/apps/sse-contract-tests/entity_manager.hpp +++ b/apps/sse-contract-tests/entity_manager.hpp @@ -33,6 +33,7 @@ class entity_manager { if (it == entities_.end()) { return false; } + entities_.erase(it); return true; } }; diff --git a/apps/sse-contract-tests/main.cpp b/apps/sse-contract-tests/main.cpp index 52d293e16..a2cb5577f 100644 --- a/apps/sse-contract-tests/main.cpp +++ b/apps/sse-contract-tests/main.cpp @@ -5,14 +5,16 @@ main(int argc, char* argv[]) { try { - auto address = net::ip::make_address("0.0.0.0"); - unsigned short port = 8123; + const auto address = net::ip::make_address("0.0.0.0"); + unsigned short port = 8111; net::io_context ioc; - server s{ioc.get_executor(), std::move(address), port}; + server s{ioc.get_executor(), address, port}; s.add_capability("headers"); - s.start_accepting(); + + std::cout << "listening on " << address << ":" << port << std::endl; + ioc.run(); } catch(std::exception const& e) diff --git a/apps/sse-contract-tests/server.hpp b/apps/sse-contract-tests/server.hpp index 1f5385039..e5e66b0ad 100644 --- a/apps/sse-contract-tests/server.hpp +++ b/apps/sse-contract-tests/server.hpp @@ -1,7 +1,7 @@ #pragma once #include "stream_entity.hpp" -#include "http_connection.hpp" +#include "session.hpp" #include #include @@ -40,7 +40,7 @@ class server } void add_capability(std::string cap) { - + caps_.push_back(cap); } void start_accepting() @@ -66,7 +66,7 @@ class server acceptor_.async_accept([this](beast::error_code ec, tcp::socket peer){ if (!ec) { if (!stopped_) { - std::make_shared(std::move(peer), manager_, caps_)->start(); + std::make_shared(std::move(peer), manager_, caps_)->start(); accept_loop(); } } diff --git a/apps/sse-contract-tests/http_connection.hpp b/apps/sse-contract-tests/session.hpp similarity index 87% rename from apps/sse-contract-tests/http_connection.hpp rename to apps/sse-contract-tests/session.hpp index 6e76ad383..2f65dda21 100644 --- a/apps/sse-contract-tests/http_connection.hpp +++ b/apps/sse-contract-tests/session.hpp @@ -24,10 +24,10 @@ using tcp = boost::asio::ip::tcp; // from -class http_connection : public std::enable_shared_from_this +class session : public std::enable_shared_from_this { public: - explicit http_connection(tcp::socket socket, entity_manager& manager, std::vector caps): + explicit session(tcp::socket socket, entity_manager& manager, std::vector caps): socket_{std::move(socket)}, manager_{manager}, capabilities_{std::move(caps)} @@ -46,10 +46,10 @@ class http_connection : public std::enable_shared_from_this beast::flat_buffer buffer_{8192}; // The request message. - http::request request_; + http::request request_; // The response message. - http::response response_; + http::response response_; entity_manager& manager_; @@ -105,13 +105,15 @@ class http_connection : public std::enable_shared_from_this nlohmann::json status { {"capabilities", capabilities_} }; - beast::ostream(response_.body()) << status.dump(); + response_.body() = status.dump(); return true; } else if (request_.method() == http::verb::delete_) { return false; } else if (request_.method() == http::verb::post) { - std::string id = manager_.create(config_params{}); + auto json = nlohmann::json::parse(request_.body()); + auto params = json.get(); + std::string id = manager_.create(params); response_.result(http::status::ok); response_.set("Location", "/shutdown/" + id); std::cout << "creating entity " << id << '\n'; @@ -131,7 +133,6 @@ class http_connection : public std::enable_shared_from_this response_.result(http::status::bad_request); response_.set(http::field::content_type, "text/plain"); - beast::ostream(response_.body()) << "400 Bad Request"; return true; } void @@ -144,7 +145,7 @@ class http_connection : public std::enable_shared_from_this http::async_write( socket_, response_, - [self](beast::error_code ec, std::size_t) + [self](beast::error_code ec, std::size_t bytes_written) { self->socket_.shutdown(tcp::socket::shutdown_send, ec); }); diff --git a/apps/sse-contract-tests/stream_entity.hpp b/apps/sse-contract-tests/stream_entity.hpp index 762481689..fe2eed274 100644 --- a/apps/sse-contract-tests/stream_entity.hpp +++ b/apps/sse-contract-tests/stream_entity.hpp @@ -2,19 +2,28 @@ #include "definitions.hpp" +#include #include #include +namespace beast = boost::beast; // from +namespace http = beast::http; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from + + class stream_entity { std::string callback_url_; size_t callback_counter_; std::shared_ptr client_; + net::any_io_executor executor_; public: - stream_entity(boost::asio::any_io_executor executor, config_params params): + stream_entity(net::any_io_executor executor, config_params params): callback_url_{params.callbackUrl}, callback_counter_{0}, - client_{} + client_{}, + executor_{executor} { auto builder = launchdarkly::sse::builder{executor, params.streamUrl}; if (params.headers) { @@ -23,5 +32,32 @@ class stream_entity { } } client_ = builder.build(); + client_->on_event([this, url = params.callbackUrl](launchdarkly::sse::event_data ev) { + tcp::resolver resolver{executor_}; + beast::tcp_stream stream{executor_}; + + // Look up the domain name + auto const results = resolver.resolve(url, "80"); + + // Make the connection on the IP address we get from a lookup + stream.connect(results); + + // Set up an HTTP GET request message + http::request req{ + http::verb::get, url+"/" + std::to_string(++callback_counter_), 11}; + + if (ev.get_type() == "comment") { + // comment_message thing2 + req.body() = nlohmann::json{ + comment_message{"comment",ev.get_data()} + }.dump(); + } else { + req.body() = nlohmann::json{ + event_message{"event", event{ev.get_type(), ev.get_data()}} + }.dump(); + } + // Send the HTTP request to the remote host + http::write(stream, req); + }); } }; From 807a063da8a9503228282fe692225a660fc1d0e0 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 21 Mar 2023 11:52:09 -0700 Subject: [PATCH 04/53] More work on the contract test server --- apps/sse-contract-tests/entity_manager.hpp | 13 +- apps/sse-contract-tests/session.hpp | 249 +++++++++++------- apps/sse-contract-tests/stream_entity.hpp | 62 +++-- .../include/launchdarkly/sse/sse.hpp | 2 +- libs/server-sent-events/src/sse.cpp | 3 +- 5 files changed, 204 insertions(+), 125 deletions(-) diff --git a/apps/sse-contract-tests/entity_manager.hpp b/apps/sse-contract-tests/entity_manager.hpp index d045b9c2b..bc1d91139 100644 --- a/apps/sse-contract-tests/entity_manager.hpp +++ b/apps/sse-contract-tests/entity_manager.hpp @@ -20,9 +20,20 @@ class entity_manager { executor_{executor}{ } - std::string create(config_params params) { + std::optional create(config_params params) { std::lock_guard guard{lock_}; auto id = std::to_string(counter_++); + + auto builder = launchdarkly::sse::builder{executor_, params.streamUrl}; + if (params.headers) { + for (auto h: *params.headers) { + builder.header(h.first, h.second); + } + } + auto client = builder.build(); + if (!client) { + return std::nullopt; + } entities_.emplace(id, stream_entity{executor_, std::move(params)}); return id; } diff --git a/apps/sse-contract-tests/session.hpp b/apps/sse-contract-tests/session.hpp index 2f65dda21..73a6fa509 100644 --- a/apps/sse-contract-tests/session.hpp +++ b/apps/sse-contract-tests/session.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -24,23 +25,12 @@ using tcp = boost::asio::ip::tcp; // from +const std::string ENTITY_PATH = "/entity/"; + class session : public std::enable_shared_from_this { -public: - explicit session(tcp::socket socket, entity_manager& manager, std::vector caps): - socket_{std::move(socket)}, - manager_{manager}, - capabilities_{std::move(caps)} - { - } - - void start() { - read_request(); - } - -private: // The socket for the currently connected client. - tcp::socket socket_; + beast::tcp_stream stream_; // The buffer for performing reads. beast::flat_buffer buffer_{8192}; @@ -48,106 +38,177 @@ class session : public std::enable_shared_from_this // The request message. http::request request_; - // The response message. - http::response response_; - entity_manager& manager_; std::vector capabilities_; - - // Asynchronously receive a complete request message. - void - read_request() + template + http::message_generator + handle_request(http::request>&& req) { - auto self = shared_from_this(); - - http::async_read( - socket_, - buffer_, - request_, - [self](beast::error_code ec, - std::size_t bytes_transferred) + + std::cout << "handling " << req.method() << " <" << req.target() << ">\n"; + // Returns a bad request response + auto const bad_request = + [&req](beast::string_view why) { - boost::ignore_unused(bytes_transferred); - if(!ec) { - self->process_request(); - } - }); - } + http::response res{http::status::bad_request, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "application/json"); + res.keep_alive(req.keep_alive()); + res.body() = nlohmann::json{"error", why}.dump(); + res.prepare_payload(); + return res; + }; - // Determine what needs to be done with the request message. - void - process_request() - { - response_.version(request_.version()); - response_.keep_alive(false); - - if (create_response()) { - write_response(); - } else { - socket_.shutdown(boost::asio::socket_base::shutdown_both); - socket_.close(); - std::raise(SIGTERM); + // Returns a not found response + auto const not_found = + [&req](beast::string_view target) + { + http::response res{http::status::not_found, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "text/html"); + res.keep_alive(req.keep_alive()); + res.body() = "The resource '" + std::string(target) + "' was not found."; + res.prepare_payload(); + return res; + }; + + // Returns a server error response + auto const server_error = + [&req](beast::string_view what) + { + http::response res{http::status::internal_server_error, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "text/html"); + res.keep_alive(req.keep_alive()); + res.body() = "An error occurred: '" + std::string(what) + "'"; + res.prepare_payload(); + return res; + }; + + auto const capabilities_response = [&req](std::vector const& caps) { + http::response res{http::status::ok, req.version()}; + res.set(http::field::content_type, "application/json"); + res.keep_alive(req.keep_alive()); + res.body() = nlohmann::json{{"capabilities", caps}}.dump(); + res.prepare_payload(); + return res; + }; + + auto const create_entity_response = [&req](std::string id) { + http::response res{http::status::ok, req.version()}; + res.keep_alive(req.keep_alive()); + res.set("Location", ENTITY_PATH + id); + res.prepare_payload(); + return res; + }; + + auto const destroy_entity_response = [&req](bool erased) { + auto status = erased? http::status::ok : http::status::not_found; + http::response res{status, req.version()}; + res.keep_alive(req.keep_alive()); + res.prepare_payload(); + return res; + }; + + + if (req.method() == http::verb::get && req.target() == "/") { + return capabilities_response(capabilities_); } - } - // Construct a response message based on the program state. - bool - create_response() - { - if (request_.target() == "/") { - if (request_.method() == http::verb::get) { - response_.set(http::field::content_type, "application/json"); - response_.result(http::status::ok); + if (req.method() == http::verb::head && req.target() == "/") { + http::response res{http::status::ok, req.version()}; + return res; + } - nlohmann::json status { - {"capabilities", capabilities_} - }; - response_.body() = status.dump(); - return true; - } - else if (request_.method() == http::verb::delete_) { - return false; - } else if (request_.method() == http::verb::post) { + if (req.method() == http::verb::delete_ && req.target() == "/") { + // not clean, but doesn't matter from the test-harness's perspective. + std::raise(SIGTERM); + } + + if (req.method() == http::verb::post && req.target() == "/") { + try { auto json = nlohmann::json::parse(request_.body()); auto params = json.get(); - std::string id = manager_.create(params); - response_.result(http::status::ok); - response_.set("Location", "/shutdown/" + id); - std::cout << "creating entity " << id << '\n'; - return true; - } - } else if (request_.target().starts_with("/shutdown/")) { - auto id = request_.target(); - id.remove_prefix(std::strlen("/shutdown/")); - std::cout << "<"<< id << ">" << std::endl; - if (manager_.destroy(id)) { - response_.result(http::status::ok); - } else { - response_.result(http::status::not_found); + std::string id = manager_.create(std::move(params)); + return create_entity_response(id); + } catch(nlohmann::json::exception& e) { + return bad_request("unable to parse config JSON"); } - return true; } - response_.result(http::status::bad_request); - response_.set(http::field::content_type, "text/plain"); - return true; + if (req.method() == http::verb::delete_ && req.target().starts_with(ENTITY_PATH)) { + std::string id = req.target(); + boost::erase_first(id, ENTITY_PATH); + bool erased = manager_.destroy(id); + return destroy_entity_response(erased); + } + + return bad_request("unknown route"); } +public: + explicit session(tcp::socket&& socket, entity_manager& manager, std::vector caps): + stream_{std::move(socket)}, + manager_{manager}, + capabilities_{std::move(caps)} + { + } + + void start() { + net::dispatch(stream_.get_executor(), beast::bind_front_handler(&session::do_read, shared_from_this())); + } + + + void do_read() { + request_ = {}; + http::async_read(stream_, buffer_, request_, + beast::bind_front_handler(&session::on_read, shared_from_this()) + ); + } + + void on_read(beast::error_code ec, std::size_t bytes_transferred) + { + boost::ignore_unused(bytes_transferred); + + if (ec == http::error::end_of_stream) { + return do_close(); + } + if (ec) { + std::cout << "read failed\n"; + return; + } + send_response(handle_request(std::move(request_))); + } + + void do_close() { + beast::error_code ec; + stream_.socket().shutdown(tcp::socket::shutdown_send, ec); + } + + void send_response(http::message_generator&& msg) + { + beast::async_write( + stream_, + std::move(msg), + beast::bind_front_handler(&session::on_write, shared_from_this(), request_.keep_alive())); + } + void - write_response() + on_write(bool keep_alive, beast::error_code ec, std::size_t bytes_transferred) { - auto self = shared_from_this(); + boost::ignore_unused(bytes_transferred); - response_.content_length(response_.body().size()); + if (ec) { + std::cout << "write failed\n"; + return; + } - http::async_write( - socket_, - response_, - [self](beast::error_code ec, std::size_t bytes_written) - { - self->socket_.shutdown(tcp::socket::shutdown_send, ec); - }); + if (!keep_alive) { + return do_close(); + } + + do_read(); } }; diff --git a/apps/sse-contract-tests/stream_entity.hpp b/apps/sse-contract-tests/stream_entity.hpp index fe2eed274..09b9dae30 100644 --- a/apps/sse-contract-tests/stream_entity.hpp +++ b/apps/sse-contract-tests/stream_entity.hpp @@ -5,7 +5,7 @@ #include #include #include - +#include namespace beast = boost::beast; // from namespace http = beast::http; // from @@ -31,33 +31,39 @@ class stream_entity { builder.header(h.first, h.second); } } + + client_ = builder.build(); - client_->on_event([this, url = params.callbackUrl](launchdarkly::sse::event_data ev) { - tcp::resolver resolver{executor_}; - beast::tcp_stream stream{executor_}; - - // Look up the domain name - auto const results = resolver.resolve(url, "80"); - - // Make the connection on the IP address we get from a lookup - stream.connect(results); - - // Set up an HTTP GET request message - http::request req{ - http::verb::get, url+"/" + std::to_string(++callback_counter_), 11}; - - if (ev.get_type() == "comment") { - // comment_message thing2 - req.body() = nlohmann::json{ - comment_message{"comment",ev.get_data()} - }.dump(); - } else { - req.body() = nlohmann::json{ - event_message{"event", event{ev.get_type(), ev.get_data()}} - }.dump(); - } - // Send the HTTP request to the remote host - http::write(stream, req); - }); + if (client_) { + client_->on_event([this, url = params.callbackUrl](launchdarkly::sse::event_data ev) { + tcp::resolver resolver{executor_}; + beast::tcp_stream stream{executor_}; + + // Look up the domain name + auto const results = resolver.resolve(url, "80"); + + // Make the connection on the IP address we get from a lookup + stream.connect(results); + + // Set up an HTTP GET request message + http::request req{ + http::verb::get, url + "/" + std::to_string(++callback_counter_), 11}; + + if (ev.get_type() == "comment") { + // comment_message thing2 + req.body() = nlohmann::json{ + comment_message{"comment", ev.get_data()} + }.dump(); + } else { + req.body() = nlohmann::json{ + event_message{"event", event{ev.get_type(), ev.get_data()}} + }.dump(); + } + // Send the HTTP request to the remote host + http::write(stream, req); + }); + + client_->read() + } } }; diff --git a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp index 5e9b882eb..abaa080c7 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp @@ -80,7 +80,7 @@ class client : public std::enable_shared_from_this { void parse_events(); public: explicit client(net::any_io_executor ex, ssl::context &ctx, http::request req, std::string host, std::string port); - void run(); + void read(); template void on_event(Callback event_cb) { m_cb = event_cb; diff --git a/libs/server-sent-events/src/sse.cpp b/libs/server-sent-events/src/sse.cpp index 8451ee5d4..d33147236 100644 --- a/libs/server-sent-events/src/sse.cpp +++ b/libs/server-sent-events/src/sse.cpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace launchdarkly::sse { @@ -104,7 +105,7 @@ client::client(net::any_io_executor ex, ssl::context &ctx, http::request Date: Tue, 21 Mar 2023 17:07:20 -0700 Subject: [PATCH 05/53] Changing sse client to be totally async --- apps/hello-cpp/main.cpp | 19 +-- apps/sse-contract-tests/entity_manager.hpp | 4 +- apps/sse-contract-tests/session.hpp | 7 +- .../launchdarkly/sse/detail/sse_stream.hpp | 84 ++++++++----- .../include/launchdarkly/sse/sse.hpp | 9 +- libs/server-sent-events/src/sse.cpp | 118 ++++++++++-------- 6 files changed, 141 insertions(+), 100 deletions(-) diff --git a/apps/hello-cpp/main.cpp b/apps/hello-cpp/main.cpp index 2adf0109d..8a06545c5 100644 --- a/apps/hello-cpp/main.cpp +++ b/apps/hello-cpp/main.cpp @@ -14,9 +14,13 @@ int main() { net::io_context ioc; - // curl "https://stream-stg.launchdarkly.com/all?filter=even-flags-2" -H "Authorization: sdk-66a5dbe0-8b26-445a-9313-761e7e3d381b" -v + const char* key = std::getenv("STG_SDK_KEY"); + if (!key){ + std::cout << "Set environment variable STG_SDK_KEY to the sdk key\n"; + return 1; + } auto client = launchdarkly::sse::builder(ioc.get_executor(), "https://stream-stg.launchdarkly.com/all") - .header("Authorization", "sdk-66a5dbe0-8b26-445a-9313-761e7e3d381b") + .header("Authorization", key) .build(); if (!client) { @@ -24,11 +28,12 @@ int main() { return 1; } - std::thread t([&]() { ioc.run(); }); + client->read(); + - client->on_event([](launchdarkly::sse::event_data e){ - std::cout << "Got[" << e.get_type() << "] = <" << e.get_data() << ">\n"; - }); +// client->on_event([](launchdarkly::sse::event_data e){ +// std::cout << "Got[" << e.get_type() << "] = <" << e.get_data() << ">\n"; +// }); - client->run(); + ioc.run(); } diff --git a/apps/sse-contract-tests/entity_manager.hpp b/apps/sse-contract-tests/entity_manager.hpp index bc1d91139..85532ae5e 100644 --- a/apps/sse-contract-tests/entity_manager.hpp +++ b/apps/sse-contract-tests/entity_manager.hpp @@ -13,11 +13,11 @@ class entity_manager { std::mutex lock_; boost::asio::any_io_executor executor_; public: - entity_manager(boost::asio::any_io_executor executor): + explicit entity_manager(boost::asio::any_io_executor executor): entities_{}, counter_{0}, lock_{}, - executor_{executor}{ + executor_{std::move(executor)}{ } std::optional create(config_params params) { diff --git a/apps/sse-contract-tests/session.hpp b/apps/sse-contract-tests/session.hpp index 73a6fa509..18f78656a 100644 --- a/apps/sse-contract-tests/session.hpp +++ b/apps/sse-contract-tests/session.hpp @@ -132,8 +132,11 @@ class session : public std::enable_shared_from_this try { auto json = nlohmann::json::parse(request_.body()); auto params = json.get(); - std::string id = manager_.create(std::move(params)); - return create_entity_response(id); + if (auto id = manager_.create(std::move(params))) { + return create_entity_response(*id); + } else { + return server_error("couldn't create client entity"); + } } catch(nlohmann::json::exception& e) { return bad_request("unable to parse config JSON"); } diff --git a/libs/server-sent-events/include/launchdarkly/sse/detail/sse_stream.hpp b/libs/server-sent-events/include/launchdarkly/sse/detail/sse_stream.hpp index 746bb89f0..9613d79ca 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/detail/sse_stream.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/detail/sse_stream.hpp @@ -14,10 +14,10 @@ namespace net = boost::asio; // from // A layered stream which implements the SSE protocol. template class sse_stream { - NextLayer m_nextLayer; - boost::optional m_bufferedLine; - bool m_lastCharWasCR; - std::vector m_completeLines; + NextLayer next_layer_; + bool begin_CR_; + boost::optional buffered_line_; + std::deque complete_lines_; // This is the "initiation" object passed to async_initiate to start the // operation @@ -44,12 +44,14 @@ class sse_stream { : base(std::move(handler), stream.get_executor()), stream_(stream) { // Start the asynchronous operation + std::cout << "op sse_stream: async_read_some\n"; stream_.next_layer().async_read_some(buffers, std::move(*this)); } void operator()(beast::error_code ec, std::size_t bytes_transferred) { + std::cout << "op sse_stream: completion handler\n"; this->complete_now(ec, bytes_transferred); } }; @@ -83,12 +85,15 @@ class sse_stream { : base(std::move(handler), stream.get_executor()), stream_(stream) { // Start the asynchronous operation + std::cout << "sse_stream: async_write_some\n"; stream_.next_layer().async_write_some(buffers, std::move(*this)); } void operator()(beast::error_code ec, std::size_t bytes_transferred) { + + std::cout << "sse_stream: completion handler\n"; this->complete_now(ec, bytes_transferred); } }; @@ -97,36 +102,46 @@ class sse_stream { } }; - void push_buffered() { - if (m_bufferedLine) { - m_completeLines.push_back(*m_bufferedLine); - m_bufferedLine.reset(); + void complete_line() { + if (buffered_line_.has_value()) { + complete_lines_.push_back(buffered_line_.value()); + std::cout << "Line: <" << buffered_line_.value() << ">" + << std::endl; + buffered_line_.reset(); } } - void append_buffered(std::string token) { - if (m_bufferedLine) { - m_bufferedLine->append(token); + size_t append_up_to(std::string_view body, std::string const& search) { + std::size_t index = body.find_first_of(search); + if (index != std::string::npos) { + body.remove_suffix(body.size() - index); + } + if (buffered_line_.has_value()) { + buffered_line_->append(body); } else { - m_bufferedLine.emplace(token); + buffered_line_ = std::string{body}; } + return index == std::string::npos ? body.size() : index; } template - void decode_and_buffer_lines(MutableBufferSequence const& chunk) { - boost::char_separator sep{"", "\r\n|\r|\n"}; - boost::tokenizer> tokens_all{chunk, sep}; - - std::vector tokens{std::begin(tokens_all), - std::end(tokens_all)}; - - for (auto tok = tokens.begin(); tok != tokens.end(); tok = tok++) { - if (*tok == "\r" || *tok == "\n" || *tok == "\r\n") { - this->push_buffered(); - } else { - this->append_buffered(*tok); + void decode_and_buffer_lines(MutableBufferSequence const& body) { + size_t i = 0; + while (i < body.length()) { + i += this->append_up_to(body.substr(i, body.length() - i), "\r\n"); + if (body[i] == '\r') { + if (this->begin_CR_) { + // todo: illegal token + } else { + this->begin_CR_ = true; + } + } else if (body[i] == '\n') { + this->begin_CR_ = false; + this->complete_line(); + i++; } } + return body.length(); } public: @@ -134,23 +149,25 @@ class sse_stream { template explicit sse_stream(Args&&... args) - : m_nextLayer{std::forward(args)...}, - m_lastCharWasCR{false}, - m_bufferedLine{} {} + : next_layer_{std::forward(args)...}, + begin_CR_{false}, + buffered_line_{}, + complete_lines_{} {} /// Returns an instance of the executor used to submit completion handlers - executor_type get_executor() noexcept { return m_nextLayer.get_executor(); } + executor_type get_executor() noexcept { return next_layer_.get_executor(); } /// Returns a reference to the next layer - NextLayer& next_layer() noexcept { return m_nextLayer; } + NextLayer& next_layer() noexcept { return next_layer_; } /// Returns a reference to the next layer - NextLayer const& next_layer() const noexcept { return m_nextLayer; } + NextLayer const& next_layer() const noexcept { return next_layer_; } /// Read some data from the stream template std::size_t read_some(MutableBufferSequence const& buffers) { - auto const bytes_transferred = m_nextLayer.read_some(buffers); + std::cout << "sse_stream: read_some\n"; + auto const bytes_transferred = next_layer_.read_some(buffers); this->decode_and_buffer_lines(buffers); return bytes_transferred; } @@ -159,7 +176,8 @@ class sse_stream { template std::size_t read_some(MutableBufferSequence const& buffers, beast::error_code& ec) { - auto const bytes_transferred = m_nextLayer.read_some(buffers, ec); + std::cout << "sse_stream: read_some\n"; + auto const bytes_transferred = next_layer_.read_some(buffers, ec); this->decode_and_buffer_lines(buffers); return bytes_transferred; } @@ -170,6 +188,7 @@ class sse_stream { async_read_some(MutableBufferSequence const& buffers, ReadHandler&& handler = net::default_completion_token{}) { + std::cout << "sse_stream: async_read_some\n"; return net::async_initiate( run_read_op{}, handler, this, buffers); @@ -182,6 +201,7 @@ class sse_stream { async_write_some(ConstBufferSequence const& buffers, WriteHandler&& handler = net::default_completion_token_t{}) { + std::cout << "sse_stream: async_write_some\n"; return net::async_initiate( run_write_op{}, handler, this, buffers); diff --git a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp index 5b47a844b..9160e3bb6 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp @@ -60,13 +60,13 @@ using event = std::variant; class client : public std::enable_shared_from_this { using parser = - http::response_parser>; + http::response_parser; tcp::resolver m_resolver; beast::ssl_stream m_stream; beast::flat_buffer m_buffer; http::request m_request; http::response m_response; - parser m_parser; + parser parser_; std::string m_host; std::string m_port; boost::optional m_buffered_line; @@ -79,6 +79,11 @@ class client : public std::enable_shared_from_this { size_t append_up_to(std::string_view body, const std::string& search); std::size_t parse_stream(std::uint64_t remain, std::string_view body, beast::error_code& ec); void parse_events(); + void on_resolve(beast::error_code, tcp::resolver::results_type); + void on_connect(beast::error_code, tcp::resolver::results_type::endpoint_type); + void on_handshake(beast::error_code); + void on_write(beast::error_code ec, std::size_t); + void on_read(beast::error_code, std::size_t); public: explicit client(net::any_io_executor ex, ssl::context &ctx, http::request req, std::string host, std::string port); void read(); diff --git a/libs/server-sent-events/src/sse.cpp b/libs/server-sent-events/src/sse.cpp index 5235b40be..e9a5a47da 100644 --- a/libs/server-sent-events/src/sse.cpp +++ b/libs/server-sent-events/src/sse.cpp @@ -18,7 +18,7 @@ using tcp = boost::asio::ip::tcp; // from builder::builder(net::any_io_executor ctx, std::string url): m_url{std::move(url)}, m_ssl_ctx{ssl::context::tlsv12_client}, - m_executor{ctx} { + m_executor{std::move(ctx)} { // This needs to be verify_peer in production!! m_ssl_ctx.set_verify_mode(ssl::verify_none); @@ -81,7 +81,7 @@ client::client(net::any_io_executor ex, ssl::context &ctx, http::request\n"; }}{ - // The HTTP response body is of potentially infinite length. - m_parser.body_limit(boost::none); + +} + +// Report a failure +void +fail(beast::error_code ec, char const* what) +{ + std::cerr << what << ": " << ec.message() << "\n"; } void client::read() { @@ -103,68 +109,70 @@ void client::read() { return; } - beast::get_lowest_layer(m_stream).expires_after(std::chrono::seconds(10)); + beast::get_lowest_layer(m_stream).expires_after(std::chrono::seconds(15)); - auto results = m_resolver.resolve(m_host, m_port); - beast::get_lowest_layer(m_stream).connect(results); - m_stream.handshake(ssl::stream_base::client); - http::write(m_stream, m_request); + m_resolver.async_resolve(m_host, m_port, beast::bind_front_handler(&client::on_resolve, shared_from_this())); +} - beast::get_lowest_layer(m_stream).expires_never(); - auto callback = [this](std::uint64_t remain, std::string_view body, - beast::error_code& ec) { - size_t read = this->parse_stream(remain, body, ec); - this->parse_events(); - return read; - }; +void client::on_resolve(beast::error_code ec, tcp::resolver::results_type results) { + if(ec) + return fail(ec, "resolve"); + // Make the connection on the IP address we get from a lookup + beast::get_lowest_layer(m_stream).async_connect( + results, + beast::bind_front_handler( + &client::on_connect, + shared_from_this())); +} - m_parser.on_chunk_body(callback); +void client::on_connect(beast::error_code ec, tcp::resolver::results_type::endpoint_type) { + if (ec) + return fail(ec, "connect"); - // Blocking call until the stream terminates. - http::read(m_stream, m_buffer, m_parser); + m_stream.async_handshake( + ssl::stream_base::client, + beast::bind_front_handler(&client::on_handshake, shared_from_this())); } -void client::complete_line() { - if (m_buffered_line.has_value()) { - m_complete_lines.push_back(m_buffered_line.value()); - std::cout << "Line: <" << m_buffered_line.value() << ">" << std::endl; - m_buffered_line.reset(); - } +void client::on_handshake(beast::error_code ec) { + if(ec) + return fail(ec, "handshake"); + + // Send the HTTP request to the remote host + http::async_write(m_stream, m_request, + beast::bind_front_handler( + &client::on_write, + shared_from_this())); } -size_t client::append_up_to(std::string_view body, std::string const& search) { - std::size_t index = body.find_first_of(search); - if (index != std::string::npos) { - body.remove_suffix(body.size() - index); - } - if (m_buffered_line.has_value()) { - m_buffered_line->append(body); - } else { - m_buffered_line = std::string{body}; +void client::on_write(beast::error_code ec, std::size_t) { + if(ec) + return fail(ec, "write"); + + beast::get_lowest_layer(m_stream).expires_after(std::chrono::seconds(10)); + + http::async_read_some(m_stream, m_buffer, parser_, + beast::bind_front_handler( + &client::on_read, + shared_from_this())); +} + +void client::on_read(beast::error_code ec, std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + if (ec) { + return fail(ec, "read"); } - return index == std::string::npos ? body.size() : index; -} - -std::size_t client::parse_stream(std::uint64_t remain, - std::string_view body, - beast::error_code& ec) { - size_t i = 0; - while (i < body.length()) { - i += this->append_up_to(body.substr(i, body.length() - i), "\r\n"); - if (body[i] == '\r') { - if (this->m_begin_CR) { - // todo: illegal token - } else { - this->m_begin_CR = true; - } - } else if (body[i] == '\n') { - this->m_begin_CR = false; - this->complete_line(); - i++; - } + + beast::get_lowest_layer(m_stream).expires_never(); + + if (bytes_transferred > 0) { + std::cout << "got bytes: " << bytes_transferred << "; async read\n"; } - return body.length(); + + http::async_read_some(m_stream, m_buffer, parser_, + beast::bind_front_handler(&client::on_read, shared_from_this())); + } boost::optional> parse_field( From 6432dc609d73b07112fca115a46b04d9be12aa43 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 21 Mar 2023 17:34:31 -0700 Subject: [PATCH 06/53] SSE works --- .../launchdarkly/sse/detail/sse_stream.hpp | 211 ------------------ .../include/launchdarkly/sse/sse.hpp | 40 ++-- libs/server-sent-events/src/sse.cpp | 172 +++++++++----- 3 files changed, 134 insertions(+), 289 deletions(-) delete mode 100644 libs/server-sent-events/include/launchdarkly/sse/detail/sse_stream.hpp diff --git a/libs/server-sent-events/include/launchdarkly/sse/detail/sse_stream.hpp b/libs/server-sent-events/include/launchdarkly/sse/detail/sse_stream.hpp deleted file mode 100644 index 9613d79ca..000000000 --- a/libs/server-sent-events/include/launchdarkly/sse/detail/sse_stream.hpp +++ /dev/null @@ -1,211 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include - -namespace launchdarkly::sse::detail { - -namespace beast = boost::beast; // from -namespace http = beast::http; // from -namespace net = boost::asio; // from - -// A layered stream which implements the SSE protocol. -template -class sse_stream { - NextLayer next_layer_; - bool begin_CR_; - boost::optional buffered_line_; - std::deque complete_lines_; - - // This is the "initiation" object passed to async_initiate to start the - // operation - struct run_read_op { - template - void operator()(ReadHandler&& handler, - sse_stream* stream, - MutableBufferSequence const& buffers) { - using handler_type = typename std::decay::type; - - // async_base handles all of the composed operation boilerplate for - // us - using base = beast::async_base>; - - // Our composed operation is implemented as a completion handler - // object - struct op : base { - sse_stream& stream_; - - op(sse_stream& stream, - handler_type&& handler, - MutableBufferSequence const& buffers) - : base(std::move(handler), stream.get_executor()), - stream_(stream) { - // Start the asynchronous operation - std::cout << "op sse_stream: async_read_some\n"; - stream_.next_layer().async_read_some(buffers, - std::move(*this)); - } - - void operator()(beast::error_code ec, - std::size_t bytes_transferred) { - std::cout << "op sse_stream: completion handler\n"; - this->complete_now(ec, bytes_transferred); - } - }; - - op(*stream, std::forward(handler), buffers); - } - }; - - // This is the "initiation" object passed to async_initiate to start the - // operation - struct run_write_op { - template - void operator()(WriteHandler&& handler, - sse_stream* stream, - ConstBufferSequence const& buffers) { - using handler_type = typename std::decay::type; - - // async_base handles all of the composed operation boilerplate for - // us - using base = beast::async_base>; - - // Our composed operation is implemented as a completion handler - // object - struct op : base { - sse_stream& stream_; - - op(sse_stream& stream, - handler_type&& handler, - ConstBufferSequence const& buffers) - : base(std::move(handler), stream.get_executor()), - stream_(stream) { - // Start the asynchronous operation - std::cout << "sse_stream: async_write_some\n"; - stream_.next_layer().async_write_some(buffers, - std::move(*this)); - } - - void operator()(beast::error_code ec, - std::size_t bytes_transferred) { - - std::cout << "sse_stream: completion handler\n"; - this->complete_now(ec, bytes_transferred); - } - }; - - op(*stream, std::forward(handler), buffers); - } - }; - - void complete_line() { - if (buffered_line_.has_value()) { - complete_lines_.push_back(buffered_line_.value()); - std::cout << "Line: <" << buffered_line_.value() << ">" - << std::endl; - buffered_line_.reset(); - } - } - - size_t append_up_to(std::string_view body, std::string const& search) { - std::size_t index = body.find_first_of(search); - if (index != std::string::npos) { - body.remove_suffix(body.size() - index); - } - if (buffered_line_.has_value()) { - buffered_line_->append(body); - } else { - buffered_line_ = std::string{body}; - } - return index == std::string::npos ? body.size() : index; - } - - template - void decode_and_buffer_lines(MutableBufferSequence const& body) { - size_t i = 0; - while (i < body.length()) { - i += this->append_up_to(body.substr(i, body.length() - i), "\r\n"); - if (body[i] == '\r') { - if (this->begin_CR_) { - // todo: illegal token - } else { - this->begin_CR_ = true; - } - } else if (body[i] == '\n') { - this->begin_CR_ = false; - this->complete_line(); - i++; - } - } - return body.length(); - } - - public: - using executor_type = beast::executor_type; - - template - explicit sse_stream(Args&&... args) - : next_layer_{std::forward(args)...}, - begin_CR_{false}, - buffered_line_{}, - complete_lines_{} {} - - /// Returns an instance of the executor used to submit completion handlers - executor_type get_executor() noexcept { return next_layer_.get_executor(); } - - /// Returns a reference to the next layer - NextLayer& next_layer() noexcept { return next_layer_; } - - /// Returns a reference to the next layer - NextLayer const& next_layer() const noexcept { return next_layer_; } - - /// Read some data from the stream - template - std::size_t read_some(MutableBufferSequence const& buffers) { - std::cout << "sse_stream: read_some\n"; - auto const bytes_transferred = next_layer_.read_some(buffers); - this->decode_and_buffer_lines(buffers); - return bytes_transferred; - } - - /// Read some data from the stream - template - std::size_t read_some(MutableBufferSequence const& buffers, - beast::error_code& ec) { - std::cout << "sse_stream: read_some\n"; - auto const bytes_transferred = next_layer_.read_some(buffers, ec); - this->decode_and_buffer_lines(buffers); - return bytes_transferred; - } - - template > - BOOST_BEAST_ASYNC_RESULT2(ReadHandler) - async_read_some(MutableBufferSequence const& buffers, - ReadHandler&& handler = - net::default_completion_token{}) { - std::cout << "sse_stream: async_read_some\n"; - return net::async_initiate( - run_read_op{}, handler, this, buffers); - } - - /// Write some data to the stream asynchronously - template > - BOOST_BEAST_ASYNC_RESULT2(WriteHandler) - async_write_some(ConstBufferSequence const& buffers, - WriteHandler&& handler = - net::default_completion_token_t{}) { - std::cout << "sse_stream: async_write_some\n"; - return net::async_initiate( - run_write_op{}, handler, this, buffers); - } -}; - -} // namespace launchdarkly::sse::detail diff --git a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp index 9160e3bb6..244d3b0a6 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp @@ -7,13 +7,11 @@ #include #include #include -#include +#include #include #include #include #include -#include -#include #include namespace launchdarkly::sse { @@ -27,9 +25,9 @@ using tcp = boost::asio::ip::tcp; // from class client; class builder { -public: + public: builder(net::any_io_executor ioc, std::string url); - builder& header(const std::string& name, const std::string& value); + builder& header(std::string const& name, std::string const& value); builder& method(http::verb verb); std::shared_ptr build(); @@ -59,8 +57,7 @@ using sse_comment = std::string; using event = std::variant; class client : public std::enable_shared_from_this { - using parser = - http::response_parser; + using parser = http::response_parser; tcp::resolver m_resolver; beast::ssl_stream m_stream; beast::flat_buffer m_buffer; @@ -69,25 +66,36 @@ class client : public std::enable_shared_from_this { parser parser_; std::string m_host; std::string m_port; - boost::optional m_buffered_line; - std::deque m_complete_lines; + boost::optional buffered_line_; + std::deque complete_lines_; std::vector m_events; - bool m_begin_CR; + bool begin_CR_; boost::optional m_event_data; std::function m_cb; void complete_line(); - size_t append_up_to(std::string_view body, const std::string& search); - std::size_t parse_stream(std::uint64_t remain, std::string_view body, beast::error_code& ec); + size_t append_up_to(boost::string_view body, std::string const& search); + std::size_t parse_stream(std::uint64_t remain, + boost::string_view body, + beast::error_code& ec); void parse_events(); void on_resolve(beast::error_code, tcp::resolver::results_type); - void on_connect(beast::error_code, tcp::resolver::results_type::endpoint_type); + void on_connect(beast::error_code, + tcp::resolver::results_type::endpoint_type); void on_handshake(beast::error_code); void on_write(beast::error_code ec, std::size_t); void on_read(beast::error_code, std::size_t); -public: - explicit client(net::any_io_executor ex, ssl::context &ctx, http::request req, std::string host, std::string port); + std::optional> + on_chunk_body_trampoline_; + + public: + explicit client(net::any_io_executor ex, + ssl::context& ctx, + http::request req, + std::string host, + std::string port); void read(); - template + template void on_event(Callback event_cb) { m_cb = event_cb; } diff --git a/libs/server-sent-events/src/sse.cpp b/libs/server-sent-events/src/sse.cpp index e9a5a47da..d5b5d3e88 100644 --- a/libs/server-sent-events/src/sse.cpp +++ b/libs/server-sent-events/src/sse.cpp @@ -1,24 +1,24 @@ -#include -#include -#include -#include #include +#include +#include +#include +#include +#include #include #include -#include namespace launchdarkly::sse { -namespace beast = boost::beast; // from -namespace http = beast::http; // from -namespace net = boost::asio; // from -namespace ssl = boost::asio::ssl; // from -using tcp = boost::asio::ip::tcp; // from +namespace beast = boost::beast; // from +namespace http = beast::http; // from +namespace net = boost::asio; // from +namespace ssl = boost::asio::ssl; // from +using tcp = boost::asio::ip::tcp; // from -builder::builder(net::any_io_executor ctx, std::string url): - m_url{std::move(url)}, - m_ssl_ctx{ssl::context::tlsv12_client}, - m_executor{std::move(ctx)} { +builder::builder(net::any_io_executor ctx, std::string url) + : m_url{std::move(url)}, + m_ssl_ctx{ssl::context::tlsv12_client}, + m_executor{std::move(ctx)} { // This needs to be verify_peer in production!! m_ssl_ctx.set_verify_mode(ssl::verify_none); @@ -75,28 +75,30 @@ std::string const& event_data::get_data() { return m_data; } -client::client(net::any_io_executor ex, ssl::context &ctx, http::request req, std::string host, std::string port): - m_resolver{ex}, - m_stream{ex, ctx}, - m_request{std::move(req)}, - m_host{std::move(host)}, - m_port{std::move(port)}, - parser_{}, - m_buffered_line{}, - m_complete_lines{}, - m_begin_CR{false}, - m_event_data{}, - m_events{}, - m_cb{[](event_data e){ - std::cout << "Event[" << e.get_type() << "] = <" << e.get_data() << ">\n"; - }}{ - -} +client::client(net::any_io_executor ex, + ssl::context& ctx, + http::request req, + std::string host, + std::string port) + : m_resolver{ex}, + m_stream{ex, ctx}, + m_request{std::move(req)}, + m_host{std::move(host)}, + m_port{std::move(port)}, + parser_{}, + buffered_line_{}, + complete_lines_{}, + begin_CR_{false}, + m_event_data{}, + m_events{}, + on_chunk_body_trampoline_{}, + m_cb{[](event_data e) { + std::cout << "Event[" << e.get_type() << "] = <" << e.get_data() + << ">\n"; + }} {} // Report a failure -void -fail(beast::error_code ec, char const* what) -{ +void fail(beast::error_code ec, char const* what) { std::cerr << what << ": " << ec.message() << "\n"; } @@ -111,22 +113,23 @@ void client::read() { beast::get_lowest_layer(m_stream).expires_after(std::chrono::seconds(15)); - m_resolver.async_resolve(m_host, m_port, beast::bind_front_handler(&client::on_resolve, shared_from_this())); + m_resolver.async_resolve( + m_host, m_port, + beast::bind_front_handler(&client::on_resolve, shared_from_this())); } - -void client::on_resolve(beast::error_code ec, tcp::resolver::results_type results) { - if(ec) +void client::on_resolve(beast::error_code ec, + tcp::resolver::results_type results) { + if (ec) return fail(ec, "resolve"); // Make the connection on the IP address we get from a lookup beast::get_lowest_layer(m_stream).async_connect( results, - beast::bind_front_handler( - &client::on_connect, - shared_from_this())); + beast::bind_front_handler(&client::on_connect, shared_from_this())); } -void client::on_connect(beast::error_code ec, tcp::resolver::results_type::endpoint_type) { +void client::on_connect(beast::error_code ec, + tcp::resolver::results_type::endpoint_type) { if (ec) return fail(ec, "connect"); @@ -136,26 +139,33 @@ void client::on_connect(beast::error_code ec, tcp::resolver::results_type::endpo } void client::on_handshake(beast::error_code ec) { - if(ec) + if (ec) return fail(ec, "handshake"); // Send the HTTP request to the remote host - http::async_write(m_stream, m_request, - beast::bind_front_handler( - &client::on_write, - shared_from_this())); + http::async_write( + m_stream, m_request, + beast::bind_front_handler(&client::on_write, shared_from_this())); } void client::on_write(beast::error_code ec, std::size_t) { - if(ec) + if (ec) return fail(ec, "write"); beast::get_lowest_layer(m_stream).expires_after(std::chrono::seconds(10)); - http::async_read_some(m_stream, m_buffer, parser_, - beast::bind_front_handler( - &client::on_read, - shared_from_this())); + on_chunk_body_trampoline_.emplace( + [self = this->shared_from_this()](auto remain, auto body, auto ec) { + auto consumed = self->parse_stream(remain, body, ec); + self->parse_events(); + return consumed; + }); + + parser_.on_chunk_body(*this->on_chunk_body_trampoline_); + + http::async_read_some( + m_stream, m_buffer, parser_, + beast::bind_front_handler(&client::on_read, shared_from_this())); } void client::on_read(beast::error_code ec, std::size_t bytes_transferred) { @@ -166,13 +176,9 @@ void client::on_read(beast::error_code ec, std::size_t bytes_transferred) { beast::get_lowest_layer(m_stream).expires_never(); - if (bytes_transferred > 0) { - std::cout << "got bytes: " << bytes_transferred << "; async read\n"; - } - - http::async_read_some(m_stream, m_buffer, parser_, - beast::bind_front_handler(&client::on_read, shared_from_this())); - + http::async_read_some( + m_stream, m_buffer, parser_, + beast::bind_front_handler(&client::on_read, shared_from_this())); } boost::optional> parse_field( @@ -202,9 +208,9 @@ void client::parse_events() { while (true) { bool seen_empty_line = false; - while (!m_complete_lines.empty()) { - std::string line = std::move(m_complete_lines.front()); - m_complete_lines.pop_front(); + while (!complete_lines_.empty()) { + std::string line = std::move(complete_lines_.front()); + complete_lines_.pop_front(); if (line.empty()) { if (m_event_data.has_value()) { @@ -254,4 +260,46 @@ void client::parse_events() { } } +void client::complete_line() { + if (buffered_line_.has_value()) { + complete_lines_.push_back(buffered_line_.value()); + buffered_line_.reset(); + } +} + +size_t client::append_up_to(boost::string_view body, + std::string const& search) { + std::size_t index = body.find_first_of(search); + if (index != std::string::npos) { + body.remove_suffix(body.size() - index); + } + if (buffered_line_.has_value()) { + buffered_line_->append(body.to_string()); + } else { + buffered_line_ = std::string{body}; + } + return index == std::string::npos ? body.size() : index; +} + +size_t client::parse_stream(std::uint64_t remain, + boost::string_view body, + beast::error_code& ec) { + size_t i = 0; + while (i < body.length()) { + i += this->append_up_to(body.substr(i, body.length() - i), "\r\n"); + if (body[i] == '\r') { + if (this->begin_CR_) { + // todo: illegal token + } else { + this->begin_CR_ = true; + } + } else if (body[i] == '\n') { + this->begin_CR_ = false; + this->complete_line(); + i++; + } + } + return body.length(); +} + } // namespace launchdarkly::sse From f3cee704d38960dea45242ca56ee3c6b252341f8 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 21 Mar 2023 19:18:01 -0700 Subject: [PATCH 07/53] inheritance works --- .../include/launchdarkly/sse/sse.hpp | 26 +- libs/server-sent-events/src/sse.cpp | 275 ++++++++++-------- 2 files changed, 159 insertions(+), 142 deletions(-) diff --git a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp index 244d3b0a6..ed9c76cde 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp @@ -57,15 +57,15 @@ using sse_comment = std::string; using event = std::variant; class client : public std::enable_shared_from_this { + protected: using parser = http::response_parser; tcp::resolver m_resolver; - beast::ssl_stream m_stream; beast::flat_buffer m_buffer; http::request m_request; http::response m_response; parser parser_; - std::string m_host; - std::string m_port; + std::string host_; + std::string port_; boost::optional buffered_line_; std::deque complete_lines_; std::vector m_events; @@ -78,27 +78,23 @@ class client : public std::enable_shared_from_this { boost::string_view body, beast::error_code& ec); void parse_events(); - void on_resolve(beast::error_code, tcp::resolver::results_type); - void on_connect(beast::error_code, - tcp::resolver::results_type::endpoint_type); - void on_handshake(beast::error_code); - void on_write(beast::error_code ec, std::size_t); - void on_read(beast::error_code, std::size_t); + std::optional> on_chunk_body_trampoline_; public: - explicit client(net::any_io_executor ex, - ssl::context& ctx, - http::request req, - std::string host, - std::string port); - void read(); + client(boost::asio::any_io_executor ex, + http::request req, + std::string host, + std::string port); + template void on_event(Callback event_cb) { m_cb = event_cb; } + + virtual void read() = 0; }; } // namespace launchdarkly::sse diff --git a/libs/server-sent-events/src/sse.cpp b/libs/server-sent-events/src/sse.cpp index d5b5d3e88..0a338daa7 100644 --- a/libs/server-sent-events/src/sse.cpp +++ b/libs/server-sent-events/src/sse.cpp @@ -15,49 +15,6 @@ namespace net = boost::asio; // from namespace ssl = boost::asio::ssl; // from using tcp = boost::asio::ip::tcp; // from -builder::builder(net::any_io_executor ctx, std::string url) - : m_url{std::move(url)}, - m_ssl_ctx{ssl::context::tlsv12_client}, - m_executor{std::move(ctx)} { - // This needs to be verify_peer in production!! - m_ssl_ctx.set_verify_mode(ssl::verify_none); - - m_request.version(11); - m_request.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); - m_request.method(http::verb::get); - m_request.set("Accept", "text/event-stream"); - m_request.set("Cache-Control", "no-cache"); -} - -builder& builder::header(std::string const& name, std::string const& value) { - m_request.set(name, value); - return *this; -} - -builder& builder::method(http::verb verb) { - m_request.method(verb); - return *this; -} - -std::shared_ptr builder::build() { - boost::system::result uri_components = - boost::urls::parse_uri(m_url); - if (!uri_components) { - return nullptr; - } - std::string port; - if (!uri_components->has_port() && - uri_components->scheme_id() == boost::urls::scheme::https) { - port = "443"; - } - - m_request.set(http::field::host, uri_components->host()); - m_request.target(uri_components->path()); - - return std::make_shared(net::make_strand(m_executor), m_ssl_ctx, - m_request, uri_components->host(), port); -} - event_data::event_data(boost::optional id) : m_type{}, m_data{}, m_id{std::move(id)} {} @@ -76,16 +33,14 @@ std::string const& event_data::get_data() { } client::client(net::any_io_executor ex, - ssl::context& ctx, http::request req, std::string host, std::string port) : m_resolver{ex}, - m_stream{ex, ctx}, - m_request{std::move(req)}, - m_host{std::move(host)}, - m_port{std::move(port)}, parser_{}, + host_{std::move(host)}, + port_{std::move(port)}, + m_request{std::move(req)}, buffered_line_{}, complete_lines_{}, begin_CR_{false}, @@ -102,85 +57,6 @@ void fail(beast::error_code ec, char const* what) { std::cerr << what << ": " << ec.message() << "\n"; } -void client::read() { - // Set SNI Hostname (many hosts need this to handshake successfully) - if (!SSL_set_tlsext_host_name(m_stream.native_handle(), m_host.c_str())) { - beast::error_code ec{static_cast(::ERR_get_error()), - net::error::get_ssl_category()}; - std::cerr << ec.message() << "\n"; - return; - } - - beast::get_lowest_layer(m_stream).expires_after(std::chrono::seconds(15)); - - m_resolver.async_resolve( - m_host, m_port, - beast::bind_front_handler(&client::on_resolve, shared_from_this())); -} - -void client::on_resolve(beast::error_code ec, - tcp::resolver::results_type results) { - if (ec) - return fail(ec, "resolve"); - // Make the connection on the IP address we get from a lookup - beast::get_lowest_layer(m_stream).async_connect( - results, - beast::bind_front_handler(&client::on_connect, shared_from_this())); -} - -void client::on_connect(beast::error_code ec, - tcp::resolver::results_type::endpoint_type) { - if (ec) - return fail(ec, "connect"); - - m_stream.async_handshake( - ssl::stream_base::client, - beast::bind_front_handler(&client::on_handshake, shared_from_this())); -} - -void client::on_handshake(beast::error_code ec) { - if (ec) - return fail(ec, "handshake"); - - // Send the HTTP request to the remote host - http::async_write( - m_stream, m_request, - beast::bind_front_handler(&client::on_write, shared_from_this())); -} - -void client::on_write(beast::error_code ec, std::size_t) { - if (ec) - return fail(ec, "write"); - - beast::get_lowest_layer(m_stream).expires_after(std::chrono::seconds(10)); - - on_chunk_body_trampoline_.emplace( - [self = this->shared_from_this()](auto remain, auto body, auto ec) { - auto consumed = self->parse_stream(remain, body, ec); - self->parse_events(); - return consumed; - }); - - parser_.on_chunk_body(*this->on_chunk_body_trampoline_); - - http::async_read_some( - m_stream, m_buffer, parser_, - beast::bind_front_handler(&client::on_read, shared_from_this())); -} - -void client::on_read(beast::error_code ec, std::size_t bytes_transferred) { - boost::ignore_unused(bytes_transferred); - if (ec) { - return fail(ec, "read"); - } - - beast::get_lowest_layer(m_stream).expires_never(); - - http::async_read_some( - m_stream, m_buffer, parser_, - beast::bind_front_handler(&client::on_read, shared_from_this())); -} - boost::optional> parse_field( std::string field) { if (field.empty()) { @@ -302,4 +178,149 @@ size_t client::parse_stream(std::uint64_t remain, return body.length(); } +class ssl_client : public client { + beast::ssl_stream stream_; + + std::shared_ptr shared() { + return std::static_pointer_cast(shared_from_this()); + } + + void on_resolve(beast::error_code ec, tcp::resolver::results_type results) { + if (ec) + return fail(ec, "resolve"); + // Make the connection on the IP address we get from a lookup + beast::get_lowest_layer(stream_).async_connect( + results, + beast::bind_front_handler(&ssl_client::on_connect, shared())); + } + + void on_connect(beast::error_code ec, + tcp::resolver::results_type::endpoint_type) { + if (ec) + return fail(ec, "connect"); + + stream_.async_handshake( + ssl::stream_base::client, + beast::bind_front_handler(&ssl_client::on_handshake, shared())); + } + + void on_handshake(beast::error_code ec) { + if (ec) + return fail(ec, "handshake"); + + // Send the HTTP request to the remote host + http::async_write( + stream_, m_request, + beast::bind_front_handler(&ssl_client::on_write, shared())); + } + + void on_write(beast::error_code ec, std::size_t) { + if (ec) + return fail(ec, "write"); + + beast::get_lowest_layer(stream_).expires_after( + std::chrono::seconds(10)); + + on_chunk_body_trampoline_.emplace( + [self = std::static_pointer_cast( + this->shared_from_this())](auto remain, auto body, auto ec) { + auto consumed = self->parse_stream(remain, body, ec); + self->parse_events(); + return consumed; + }); + + parser_.on_chunk_body(*this->on_chunk_body_trampoline_); + + http::async_read_some( + stream_, m_buffer, parser_, + beast::bind_front_handler(&ssl_client::on_read, shared())); + } + + void on_read(beast::error_code ec, std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + if (ec) { + return fail(ec, "read"); + } + + beast::get_lowest_layer(stream_).expires_never(); + + http::async_read_some( + stream_, m_buffer, parser_, + beast::bind_front_handler(&ssl_client::on_read, shared())); + } + + public: + ssl_client(net::any_io_executor ex, + ssl::context& ctx, + http::request req, + std::string host, + std::string port) + : client(ex, std::move(req), std::move(host), std::move(port)), + stream_{ex, ctx} { + + std::cout << "constructor\n"; + } + + void read() override { + // Set SNI Hostname (many hosts need this to handshake successfully) + if (!SSL_set_tlsext_host_name(stream_.native_handle(), host_.c_str())) { + beast::error_code ec{static_cast(::ERR_get_error()), + net::error::get_ssl_category()}; + std::cerr << ec.message() << "\n"; + return; + } + + beast::get_lowest_layer(stream_).expires_after( + std::chrono::seconds(15)); + + m_resolver.async_resolve( + host_, port_, + beast::bind_front_handler(&ssl_client::on_resolve, shared())); + } +}; + +builder::builder(net::any_io_executor ctx, std::string url) + : m_url{std::move(url)}, + m_ssl_ctx{ssl::context::tlsv12_client}, + m_executor{std::move(ctx)} { + // This needs to be verify_peer in production!! + m_ssl_ctx.set_verify_mode(ssl::verify_none); + + m_request.version(11); + m_request.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); + m_request.method(http::verb::get); + m_request.set("Accept", "text/event-stream"); + m_request.set("Cache-Control", "no-cache"); +} + +builder& builder::header(std::string const& name, std::string const& value) { + m_request.set(name, value); + return *this; +} + +builder& builder::method(http::verb verb) { + m_request.method(verb); + return *this; +} + +std::shared_ptr builder::build() { + boost::system::result uri_components = + boost::urls::parse_uri(m_url); + if (!uri_components) { + return nullptr; + } + std::string port; + if (!uri_components->has_port() && + uri_components->scheme_id() == boost::urls::scheme::https) { + port = "443"; + } + + m_request.set(http::field::host, uri_components->host()); + m_request.target(uri_components->path()); + + return std::make_shared(net::make_strand(m_executor), m_ssl_ctx, + m_request, uri_components->host(), + port); +} + } // namespace launchdarkly::sse From 192f4ae67dadb4fe1755479584594d13b2a15abb Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 21 Mar 2023 20:14:46 -0700 Subject: [PATCH 08/53] parsing works asynchronously --- apps/sse-contract-tests/entity_manager.hpp | 4 +- apps/sse-contract-tests/stream_entity.hpp | 119 ++++++++++++++---- .../include/launchdarkly/sse/sse.hpp | 3 + libs/server-sent-events/src/sse.cpp | 116 +++++++++++++++-- 4 files changed, 203 insertions(+), 39 deletions(-) diff --git a/apps/sse-contract-tests/entity_manager.hpp b/apps/sse-contract-tests/entity_manager.hpp index 85532ae5e..270b085cb 100644 --- a/apps/sse-contract-tests/entity_manager.hpp +++ b/apps/sse-contract-tests/entity_manager.hpp @@ -8,7 +8,7 @@ #include class entity_manager { - std::unordered_map entities_; + std::unordered_map> entities_; std::size_t counter_; std::mutex lock_; boost::asio::any_io_executor executor_; @@ -34,7 +34,7 @@ class entity_manager { if (!client) { return std::nullopt; } - entities_.emplace(id, stream_entity{executor_, std::move(params)}); + entities_.emplace(id, std::make_shared(executor_, std::move(params))); return id; } diff --git a/apps/sse-contract-tests/stream_entity.hpp b/apps/sse-contract-tests/stream_entity.hpp index d434b0ba5..13bd7a1cc 100644 --- a/apps/sse-contract-tests/stream_entity.hpp +++ b/apps/sse-contract-tests/stream_entity.hpp @@ -5,25 +5,41 @@ #include #include #include +#include #include +#include namespace beast = boost::beast; // from namespace http = beast::http; // from namespace net = boost::asio; // from using tcp = boost::asio::ip::tcp; // from +// Report a failure +void fail(beast::error_code ec, char const* what) { + std::cerr << what << ": " << ec.message() << "\n"; +} -class stream_entity { +class stream_entity :public std::enable_shared_from_this { std::string callback_url_; + std::string callback_port_; + std::string callback_host_; size_t callback_counter_; std::shared_ptr client_; net::any_io_executor executor_; + tcp::resolver resolver_; + beast::tcp_stream stream_; + http::request req_; public: stream_entity(net::any_io_executor executor, config_params params): callback_url_{params.callbackUrl}, + callback_host_{}, + callback_port_{}, callback_counter_{0}, client_{}, - executor_{executor} + executor_{executor}, + resolver_{executor}, + stream_{executor}, + req_{} { auto builder = launchdarkly::sse::builder{executor, params.streamUrl}; if (params.headers) { @@ -32,38 +48,93 @@ class stream_entity { } } + boost::system::result uri_components = + boost::urls::parse_uri(params.callbackUrl); - client_ = builder.build(); - if (client_) { - client_->on_event([this, url = params.callbackUrl](launchdarkly::sse::event_data ev) { - tcp::resolver resolver{executor_}; - beast::tcp_stream stream{executor_}; - // Look up the domain name - auto const results = resolver.resolve(url, "80"); + req_.set(http::field::host, uri_components->host()); + req_.method(http::verb::get); - // Make the connection on the IP address we get from a lookup - stream.connect(results); + callback_host_ = uri_components->host(); + callback_port_ = uri_components->port(); - // Set up an HTTP GET request message - http::request req{ - http::verb::get, url + "/" + std::to_string(++callback_counter_), 11}; + client_ = builder.build(); + if (client_) { + client_->on_event([this](launchdarkly::sse::event_data ev) { + req_.target(callback_url_+"/" + std::to_string(++callback_counter_)); if (ev.get_type() == "comment") { - // comment_message thing2 - req.body() = nlohmann::json{ - comment_message{"comment", ev.get_data()} - }.dump(); + nlohmann::json json = comment_message{"comment", ev.get_data()}; + req_.body() = json.dump(); } else { - req.body() = nlohmann::json{ - event_message{"event", event{ev.get_type(), ev.get_data()}} - }.dump(); + nlohmann::json json = event_message{"event", event{ev.get_type(), ev.get_data()}}; + req_.body() = json.dump(); } - // Send the HTTP request to the remote host - http::write(stream, req); - }); + req_.prepare_payload(); + resolver_.async_resolve( + callback_host_, callback_port_, + beast::bind_front_handler(&stream_entity::on_resolve, shared_from_this())); + }); +// client_->on_event([this, url = params.callbackUrl](launchdarkly::sse::event_data ev) { +// tcp::resolver resolver{executor_}; +// beast::tcp_stream stream{executor_}; +// +// // Look up the domain name +// auto const results = resolver.resolve(url, "80"); +// +// // Make the connection on the IP address we get from a lookup +// stream.connect(results); +// +// // Set up an HTTP GET request message +// http::request req{ +// http::verb::get, url + "/" + std::to_string(++callback_counter_), 11}; +// +// if (ev.get_type() == "comment") { +// // comment_message thing2 +// req.body() = nlohmann::json{ +// comment_message{"comment", ev.get_data()} +// }.dump(); +// } else { +// req.body() = nlohmann::json{ +// event_message{"event", event{ev.get_type(), ev.get_data()}} +// }.dump(); +// } +// // Send the HTTP request to the remote host +// http::write(stream, req); +// }); client_->read(); } } + + void on_resolve(beast::error_code ec, tcp::resolver::results_type results) { + if (ec) + return fail(ec, "resolve"); + // Make the connection on the IP address we get from a lookup + beast::get_lowest_layer(stream_).async_connect( + results, + beast::bind_front_handler(&stream_entity::on_connect, shared_from_this())); + } + + void on_connect(beast::error_code ec, + tcp::resolver::results_type::endpoint_type) { + if (ec) + return fail(ec, "connect"); + + http::async_write( + stream_, req_, + beast::bind_front_handler(&stream_entity::on_write, shared_from_this())); + } + + void on_write(beast::error_code ec, std::size_t) { + if (ec) + return fail(ec, "write"); + + stream_.socket().shutdown(tcp::socket::shutdown_both, ec); + + // not_connected happens sometimes so don't bother reporting it. + if(ec && ec != beast::errc::not_connected) + return fail(ec, "shutdown"); + } + }; diff --git a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp index ed9c76cde..bbfa02c55 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp @@ -29,6 +29,7 @@ class builder { builder(net::any_io_executor ioc, std::string url); builder& header(std::string const& name, std::string const& value); builder& method(http::verb verb); + builder& tls(ssl::context_base::method); std::shared_ptr build(); private: @@ -36,6 +37,8 @@ class builder { net::any_io_executor m_executor; ssl::context m_ssl_ctx; http::request m_request; + std::optional tls_version_; + }; class event_data { diff --git a/libs/server-sent-events/src/sse.cpp b/libs/server-sent-events/src/sse.cpp index 0a338daa7..379e7a0ed 100644 --- a/libs/server-sent-events/src/sse.cpp +++ b/libs/server-sent-events/src/sse.cpp @@ -222,8 +222,7 @@ class ssl_client : public client { std::chrono::seconds(10)); on_chunk_body_trampoline_.emplace( - [self = std::static_pointer_cast( - this->shared_from_this())](auto remain, auto body, auto ec) { + [self = shared()](auto remain, auto body, auto ec) { auto consumed = self->parse_stream(remain, body, ec); self->parse_events(); return consumed; @@ -257,8 +256,6 @@ class ssl_client : public client { std::string port) : client(ex, std::move(req), std::move(host), std::move(port)), stream_{ex, ctx} { - - std::cout << "constructor\n"; } void read() override { @@ -279,10 +276,91 @@ class ssl_client : public client { } }; +class plaintext_client : public client { + beast::tcp_stream stream_; + + std::shared_ptr shared() { + return std::static_pointer_cast(shared_from_this()); + } + + void on_resolve(beast::error_code ec, tcp::resolver::results_type results) { + if (ec) + return fail(ec, "resolve"); + // Make the connection on the IP address we get from a lookup + beast::get_lowest_layer(stream_).async_connect( + results, + beast::bind_front_handler(&plaintext_client::on_connect, shared())); + } + + void on_connect(beast::error_code ec, + tcp::resolver::results_type::endpoint_type) { + if (ec) + return fail(ec, "connect"); + + http::async_write( + stream_, m_request, + beast::bind_front_handler(&plaintext_client::on_write, shared())); + } + + void on_write(beast::error_code ec, std::size_t) { + if (ec) + return fail(ec, "write"); + + beast::get_lowest_layer(stream_).expires_after( + std::chrono::seconds(10)); + + on_chunk_body_trampoline_.emplace( + [self = shared()](auto remain, auto body, auto ec) { + auto consumed = self->parse_stream(remain, body, ec); + self->parse_events(); + return consumed; + }); + + parser_.on_chunk_body(*this->on_chunk_body_trampoline_); + + http::async_read_some( + stream_, m_buffer, parser_, + beast::bind_front_handler(&plaintext_client::on_read, shared())); + } + + void on_read(beast::error_code ec, std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + if (ec) { + return fail(ec, "read"); + } + + beast::get_lowest_layer(stream_).expires_never(); + + http::async_read_some( + stream_, m_buffer, parser_, + beast::bind_front_handler(&plaintext_client::on_read, shared())); + } + + public: + plaintext_client(net::any_io_executor ex, + ssl::context& ctx, + http::request req, + std::string host, + std::string port) + : client(ex, std::move(req), std::move(host), std::move(port)), + stream_{ex} { + } + + void read() override { + beast::get_lowest_layer(stream_).expires_after( + std::chrono::seconds(15)); + + m_resolver.async_resolve( + host_, port_, + beast::bind_front_handler(&plaintext_client::on_resolve, shared())); + } +}; + builder::builder(net::any_io_executor ctx, std::string url) : m_url{std::move(url)}, m_ssl_ctx{ssl::context::tlsv12_client}, - m_executor{std::move(ctx)} { + m_executor{std::move(ctx)}, + tls_version_{12}{ // This needs to be verify_peer in production!! m_ssl_ctx.set_verify_mode(ssl::verify_none); @@ -303,24 +381,36 @@ builder& builder::method(http::verb verb) { return *this; } +builder& builder::tls(ssl::context_base::method ctx) { + m_ssl_ctx = ssl::context{ctx}; + return *this; +} + + std::shared_ptr builder::build() { boost::system::result uri_components = boost::urls::parse_uri(m_url); if (!uri_components) { return nullptr; } - std::string port; - if (!uri_components->has_port() && - uri_components->scheme_id() == boost::urls::scheme::https) { - port = "443"; - } + m_request.set(http::field::host, uri_components->host()); m_request.target(uri_components->path()); - return std::make_shared(net::make_strand(m_executor), m_ssl_ctx, - m_request, uri_components->host(), - port); + if (uri_components->scheme_id() == boost::urls::scheme::https) { + std::string port = uri_components->has_port() ? uri_components->port() : "443"; + + return std::make_shared(net::make_strand(m_executor), + m_ssl_ctx, m_request, + uri_components->host(), port); + } else { + std::string port = uri_components->has_port() ? uri_components->port() : "80"; + + return std::make_shared(net::make_strand(m_executor), + m_ssl_ctx, m_request, + uri_components->host(), port); + } } } // namespace launchdarkly::sse From 86cc35e40bd58dbded8f420e1aa74ef67f7329ff Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 22 Mar 2023 08:17:07 -0700 Subject: [PATCH 09/53] store outgoing http requests in buffer --- apps/sse-contract-tests/stream_entity.hpp | 165 ++++++++---------- .../include/launchdarkly/sse/sse.hpp | 14 +- libs/server-sent-events/src/sse.cpp | 80 +++++---- 3 files changed, 128 insertions(+), 131 deletions(-) diff --git a/apps/sse-contract-tests/stream_entity.hpp b/apps/sse-contract-tests/stream_entity.hpp index 13bd7a1cc..f0b8cbebc 100644 --- a/apps/sse-contract-tests/stream_entity.hpp +++ b/apps/sse-contract-tests/stream_entity.hpp @@ -2,24 +2,27 @@ #include "definitions.hpp" +#include +#include +#include +#include #include #include -#include -#include #include -#include -namespace beast = boost::beast; // from -namespace http = beast::http; // from -namespace net = boost::asio; // from -using tcp = boost::asio::ip::tcp; // from +namespace beast = boost::beast; // from +namespace http = beast::http; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from // Report a failure void fail(beast::error_code ec, char const* what) { std::cerr << what << ": " << ec.message() << "\n"; } -class stream_entity :public std::enable_shared_from_this { +class stream_entity : public std::enable_shared_from_this { + using request_type = http::request; + std::string callback_url_; std::string callback_port_; std::string callback_host_; @@ -28,83 +31,25 @@ class stream_entity :public std::enable_shared_from_this { net::any_io_executor executor_; tcp::resolver resolver_; beast::tcp_stream stream_; - http::request req_; -public: - stream_entity(net::any_io_executor executor, config_params params): - callback_url_{params.callbackUrl}, - callback_host_{}, - callback_port_{}, - callback_counter_{0}, - client_{}, - executor_{executor}, - resolver_{executor}, - stream_{executor}, - req_{} - { - auto builder = launchdarkly::sse::builder{executor, params.streamUrl}; - if (params.headers) { - for (auto h: *params.headers) { - builder.header(h.first, h.second); - } - } - - boost::system::result uri_components = - boost::urls::parse_uri(params.callbackUrl); - - - req_.set(http::field::host, uri_components->host()); - req_.method(http::verb::get); - - callback_host_ = uri_components->host(); - callback_port_ = uri_components->port(); - - - client_ = builder.build(); - if (client_) { - client_->on_event([this](launchdarkly::sse::event_data ev) { - req_.target(callback_url_+"/" + std::to_string(++callback_counter_)); - if (ev.get_type() == "comment") { - nlohmann::json json = comment_message{"comment", ev.get_data()}; - req_.body() = json.dump(); - } else { - nlohmann::json json = event_message{"event", event{ev.get_type(), ev.get_data()}}; - req_.body() = json.dump(); - } - req_.prepare_payload(); - resolver_.async_resolve( - callback_host_, callback_port_, - beast::bind_front_handler(&stream_entity::on_resolve, shared_from_this())); - }); -// client_->on_event([this, url = params.callbackUrl](launchdarkly::sse::event_data ev) { -// tcp::resolver resolver{executor_}; -// beast::tcp_stream stream{executor_}; -// -// // Look up the domain name -// auto const results = resolver.resolve(url, "80"); -// -// // Make the connection on the IP address we get from a lookup -// stream.connect(results); -// -// // Set up an HTTP GET request message -// http::request req{ -// http::verb::get, url + "/" + std::to_string(++callback_counter_), 11}; -// -// if (ev.get_type() == "comment") { -// // comment_message thing2 -// req.body() = nlohmann::json{ -// comment_message{"comment", ev.get_data()} -// }.dump(); -// } else { -// req.body() = nlohmann::json{ -// event_message{"event", event{ev.get_type(), ev.get_data()}} -// }.dump(); -// } -// // Send the HTTP request to the remote host -// http::write(stream, req); -// }); - - client_->read(); + std::deque requests_; + + request_type build_request(std::size_t counter, + launchdarkly::sse::event_data ev) { + request_type req; + req.set(http::field::host, callback_host_); + req.method(http::verb::get); + req.target(callback_url_ + "/" + std::to_string(counter)); + if (ev.get_type() == "comment") { + nlohmann::json json = comment_message{"comment", ev.get_data()}; + req.body() = json.dump(); + } else { + nlohmann::json json = event_message{ + "event", + event{ev.get_type(), ev.get_data(), ev.get_id().value_or("")}}; + req.body() = json.dump(); } + req.prepare_payload(); + return req; } void on_resolve(beast::error_code ec, tcp::resolver::results_type results) { @@ -112,8 +57,8 @@ class stream_entity :public std::enable_shared_from_this { return fail(ec, "resolve"); // Make the connection on the IP address we get from a lookup beast::get_lowest_layer(stream_).async_connect( - results, - beast::bind_front_handler(&stream_entity::on_connect, shared_from_this())); + results, beast::bind_front_handler(&stream_entity::on_connect, + shared_from_this())); } void on_connect(beast::error_code ec, @@ -121,20 +66,60 @@ class stream_entity :public std::enable_shared_from_this { if (ec) return fail(ec, "connect"); - http::async_write( - stream_, req_, - beast::bind_front_handler(&stream_entity::on_write, shared_from_this())); + request_type& req = requests_.front(); + std::cout << "writing event: " << req.body() << '\n'; + http::async_write(stream_, req, + beast::bind_front_handler(&stream_entity::on_write, + shared_from_this())); } void on_write(beast::error_code ec, std::size_t) { if (ec) return fail(ec, "write"); + requests_.pop_front(); stream_.socket().shutdown(tcp::socket::shutdown_both, ec); // not_connected happens sometimes so don't bother reporting it. - if(ec && ec != beast::errc::not_connected) + if (ec && ec != beast::errc::not_connected) return fail(ec, "shutdown"); } + public: + stream_entity(net::any_io_executor executor, config_params params) + : callback_url_{params.callbackUrl}, + callback_host_{}, + callback_port_{}, + callback_counter_{0}, + client_{}, + executor_{executor}, + resolver_{executor}, + stream_{executor}, + requests_{} { + auto builder = launchdarkly::sse::builder{executor, params.streamUrl}; + if (params.headers) { + for (auto h : *params.headers) { + builder.header(h.first, h.second); + } + } + + boost::system::result uri_components = + boost::urls::parse_uri(params.callbackUrl); + + callback_host_ = uri_components->host(); + callback_port_ = uri_components->port(); + + client_ = builder.build(); + if (client_) { + client_->on_event([this](launchdarkly::sse::event_data ev) { + auto req = build_request(callback_counter_++, std::move(ev)); + requests_.push_back(req); + resolver_.async_resolve( + callback_host_, callback_port_, + beast::bind_front_handler(&stream_entity::on_resolve, + shared_from_this())); + }); + client_->read(); + } + } }; diff --git a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp index bbfa02c55..3783a194c 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp @@ -38,20 +38,21 @@ class builder { ssl::context m_ssl_ctx; http::request m_request; std::optional tls_version_; - }; class event_data { std::string m_type; std::string m_data; - boost::optional m_id; + std::optional m_id; public: - explicit event_data(boost::optional id); + explicit event_data(); void set_type(std::string); + void set_id(std::optional); + void append_data(std::string const&); std::string const& get_type(); std::string const& get_data(); - void append_data(std::string const&); + std::optional const& get_id(); }; using sse_event = event_data; @@ -69,11 +70,12 @@ class client : public std::enable_shared_from_this { parser parser_; std::string host_; std::string port_; - boost::optional buffered_line_; + std::optional buffered_line_; std::deque complete_lines_; std::vector m_events; + std::optional last_event_id_; bool begin_CR_; - boost::optional m_event_data; + std::optional m_event_data; std::function m_cb; void complete_line(); size_t append_up_to(boost::string_view body, std::string const& search); diff --git a/libs/server-sent-events/src/sse.cpp b/libs/server-sent-events/src/sse.cpp index 379e7a0ed..aaac92935 100644 --- a/libs/server-sent-events/src/sse.cpp +++ b/libs/server-sent-events/src/sse.cpp @@ -15,8 +15,7 @@ namespace net = boost::asio; // from namespace ssl = boost::asio::ssl; // from using tcp = boost::asio::ip::tcp; // from -event_data::event_data(boost::optional id) - : m_type{}, m_data{}, m_id{std::move(id)} {} +event_data::event_data() : m_type{}, m_data{}, m_id{} {} void event_data::set_type(std::string type) { m_type = std::move(type); @@ -25,6 +24,10 @@ void event_data::append_data(std::string const& data) { m_data.append(data); } +void event_data::set_id(std::optional id) { + m_id = std::move(id); +} + std::string const& event_data::get_type() { return m_type; } @@ -32,6 +35,10 @@ std::string const& event_data::get_data() { return m_data; } +std::optional const& event_data::get_id() { + return m_id; +} + client::client(net::any_io_executor ex, http::request req, std::string host, @@ -44,6 +51,7 @@ client::client(net::any_io_executor ex, buffered_line_{}, complete_lines_{}, begin_CR_{false}, + last_event_id_{}, m_event_data{}, m_events{}, on_chunk_body_trampoline_{}, @@ -57,8 +65,7 @@ void fail(beast::error_code ec, char const* what) { std::cerr << what << ": " << ec.message() << "\n"; } -boost::optional> parse_field( - std::string field) { +std::pair parse_field(std::string field) { if (field.empty()) { assert(0 && "should never parse an empty line"); } @@ -96,37 +103,42 @@ void client::parse_events() { continue; } - if (auto field = parse_field(std::move(line))) { - if (field->first == "comment") { - event_data e{boost::none}; - e.set_type("comment"); - e.append_data(field->second); - m_cb(std::move(e)); - continue; - } + auto field = parse_field(std::move(line)); + if (field.first == "comment") { + event_data e; + e.set_type("comment"); + e.append_data(field.second); + m_cb(std::move(e)); + continue; + } - if (!m_event_data.has_value()) { - m_event_data.emplace(event_data{boost::none}); - } + if (!m_event_data.has_value()) { + m_event_data.emplace(event_data{}); + } - if (field->first == "event") { - m_event_data->set_type(field->second); - } else if (field->first == "data") { - m_event_data->append_data(field->second); - } else if (field->first == "id") { - std::cout << "Got ID field\n"; - } else if (field->first == "retry") { - std::cout << "Got RETRY field\n"; + if (field.first == "event") { + m_event_data->set_type(field.second); + } else if (field.first == "data") { + m_event_data->append_data(field.second); + } else if (field.first == "id") { + if (field.second.find('\0') != std::string::npos) { + std::cout + << "Debug: ignoring event ID will null terminator\n"; + continue; } + last_event_id_ = field.second; + m_event_data->set_id(last_event_id_); + } else if (field.first == "retry") { + std::cout << "Got RETRY field\n"; } } if (seen_empty_line) { - boost::optional data = m_event_data; - m_event_data.reset(); + std::optional data = m_event_data; + m_event_data = std::nullopt; if (data.has_value()) { - m_cb(std::move(data.get())); + m_cb(std::move(*data)); } continue; @@ -255,8 +267,7 @@ class ssl_client : public client { std::string host, std::string port) : client(ex, std::move(req), std::move(host), std::move(port)), - stream_{ex, ctx} { - } + stream_{ex, ctx} {} void read() override { // Set SNI Hostname (many hosts need this to handshake successfully) @@ -343,8 +354,7 @@ class plaintext_client : public client { std::string host, std::string port) : client(ex, std::move(req), std::move(host), std::move(port)), - stream_{ex} { - } + stream_{ex} {} void read() override { beast::get_lowest_layer(stream_).expires_after( @@ -360,7 +370,7 @@ builder::builder(net::any_io_executor ctx, std::string url) : m_url{std::move(url)}, m_ssl_ctx{ssl::context::tlsv12_client}, m_executor{std::move(ctx)}, - tls_version_{12}{ + tls_version_{12} { // This needs to be verify_peer in production!! m_ssl_ctx.set_verify_mode(ssl::verify_none); @@ -386,7 +396,6 @@ builder& builder::tls(ssl::context_base::method ctx) { return *this; } - std::shared_ptr builder::build() { boost::system::result uri_components = boost::urls::parse_uri(m_url); @@ -394,18 +403,19 @@ std::shared_ptr builder::build() { return nullptr; } - m_request.set(http::field::host, uri_components->host()); m_request.target(uri_components->path()); if (uri_components->scheme_id() == boost::urls::scheme::https) { - std::string port = uri_components->has_port() ? uri_components->port() : "443"; + std::string port = + uri_components->has_port() ? uri_components->port() : "443"; return std::make_shared(net::make_strand(m_executor), m_ssl_ctx, m_request, uri_components->host(), port); } else { - std::string port = uri_components->has_port() ? uri_components->port() : "80"; + std::string port = + uri_components->has_port() ? uri_components->port() : "80"; return std::make_shared(net::make_strand(m_executor), m_ssl_ctx, m_request, From 92349f4e1747617b83df880e99041c82bece5107 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 22 Mar 2023 08:33:29 -0700 Subject: [PATCH 10/53] fixing various contract test-caught bugs --- libs/server-sent-events/include/launchdarkly/sse/sse.hpp | 1 + libs/server-sent-events/src/sse.cpp | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp index 3783a194c..28273d930 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp @@ -50,6 +50,7 @@ class event_data { void set_type(std::string); void set_id(std::optional); void append_data(std::string const&); + void trim_trailing_newline(); std::string const& get_type(); std::string const& get_data(); std::optional const& get_id(); diff --git a/libs/server-sent-events/src/sse.cpp b/libs/server-sent-events/src/sse.cpp index aaac92935..9fb36375d 100644 --- a/libs/server-sent-events/src/sse.cpp +++ b/libs/server-sent-events/src/sse.cpp @@ -22,6 +22,7 @@ void event_data::set_type(std::string type) { } void event_data::append_data(std::string const& data) { m_data.append(data); + m_data.append("\n"); } void event_data::set_id(std::optional id) { @@ -35,6 +36,12 @@ std::string const& event_data::get_data() { return m_data; } +void event_data::trim_trailing_newline() { + if (m_data[m_data.size()-1] == '\n') { + m_data.resize(m_data.size() - 1); + } +} + std::optional const& event_data::get_id() { return m_id; } @@ -114,6 +121,7 @@ void client::parse_events() { if (!m_event_data.has_value()) { m_event_data.emplace(event_data{}); + m_event_data->set_id(last_event_id_); } if (field.first == "event") { @@ -138,6 +146,7 @@ void client::parse_events() { m_event_data = std::nullopt; if (data.has_value()) { + data->trim_trailing_newline(); m_cb(std::move(*data)); } From 4b4b95cc70f1cf60020270205b0629bb2a4fef96 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 22 Mar 2023 10:14:49 -0700 Subject: [PATCH 11/53] passed all contract tests in basic suite --- apps/sse-contract-tests/entity_manager.hpp | 5 +- apps/sse-contract-tests/stream_entity.hpp | 66 ++++++++++++++++--- .../include/launchdarkly/sse/sse.hpp | 1 + libs/server-sent-events/src/sse.cpp | 28 +++++++- 4 files changed, 90 insertions(+), 10 deletions(-) diff --git a/apps/sse-contract-tests/entity_manager.hpp b/apps/sse-contract-tests/entity_manager.hpp index 270b085cb..b9999e19e 100644 --- a/apps/sse-contract-tests/entity_manager.hpp +++ b/apps/sse-contract-tests/entity_manager.hpp @@ -34,7 +34,9 @@ class entity_manager { if (!client) { return std::nullopt; } - entities_.emplace(id, std::make_shared(executor_, std::move(params))); + auto entity = std::make_shared(executor_, std::move(params)); + entity->run(); + entities_.emplace(id, entity); return id; } @@ -44,6 +46,7 @@ class entity_manager { if (it == entities_.end()) { return false; } + it->second->close(); entities_.erase(it); return true; } diff --git a/apps/sse-contract-tests/stream_entity.hpp b/apps/sse-contract-tests/stream_entity.hpp index f0b8cbebc..f0ec9171e 100644 --- a/apps/sse-contract-tests/stream_entity.hpp +++ b/apps/sse-contract-tests/stream_entity.hpp @@ -2,6 +2,9 @@ #include "definitions.hpp" +#include +#include +#include #include #include #include @@ -31,7 +34,9 @@ class stream_entity : public std::enable_shared_from_this { net::any_io_executor executor_; tcp::resolver resolver_; beast::tcp_stream stream_; - std::deque requests_; + boost::lockfree::spsc_queue requests_; + std::mutex event_mutex_; + net::deadline_timer flush_timer_; request_type build_request(std::size_t counter, launchdarkly::sse::event_data ev) { @@ -77,12 +82,47 @@ class stream_entity : public std::enable_shared_from_this { if (ec) return fail(ec, "write"); - requests_.pop_front(); + std::cout << "on_write: popping request; shutting down socket; setting timer\n"; + requests_.pop(); stream_.socket().shutdown(tcp::socket::shutdown_both, ec); // not_connected happens sometimes so don't bother reporting it. if (ec && ec != beast::errc::not_connected) return fail(ec, "shutdown"); + + if (!requests_.empty()) { + resolver_.async_resolve( + callback_host_, callback_port_, + beast::bind_front_handler(&stream_entity::on_resolve, + shared_from_this())); + } else { + flush_timer_.expires_from_now( + boost::posix_time::milliseconds{500}); + flush_timer_.async_wait(beast::bind_front_handler( + &stream_entity::on_flush, shared_from_this())); + } + + } + + void on_flush(boost::system::error_code const& ec) { + if (ec) { + return fail(ec, "flush"); + } + + std::cout << "on_flush\n"; + + if (!requests_.empty()) { + std::cout << "on_flush: queueing resolve\n"; + resolver_.async_resolve( + callback_host_, callback_port_, + beast::bind_front_handler(&stream_entity::on_resolve, + shared_from_this())); + } else { + flush_timer_.expires_from_now( + boost::posix_time::milliseconds{500}); + flush_timer_.async_wait(beast::bind_front_handler( + &stream_entity::on_flush, shared_from_this())); + } } public: @@ -95,7 +135,9 @@ class stream_entity : public std::enable_shared_from_this { executor_{executor}, resolver_{executor}, stream_{executor}, - requests_{} { + requests_{1024}, + flush_timer_{executor, boost::posix_time::milliseconds{50}}, + event_mutex_{} { auto builder = launchdarkly::sse::builder{executor, params.streamUrl}; if (params.headers) { for (auto h : *params.headers) { @@ -113,13 +155,21 @@ class stream_entity : public std::enable_shared_from_this { if (client_) { client_->on_event([this](launchdarkly::sse::event_data ev) { auto req = build_request(callback_counter_++, std::move(ev)); - requests_.push_back(req); - resolver_.async_resolve( - callback_host_, callback_port_, - beast::bind_front_handler(&stream_entity::on_resolve, - shared_from_this())); + requests_.push(req); }); + client_->read(); } } + + void close() { + flush_timer_.cancel(); + client_->close(); + } + + void run() { + flush_timer_.async_wait(beast::bind_front_handler( + &stream_entity::on_flush, + shared_from_this())); + } }; diff --git a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp index 28273d930..614c8703c 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp @@ -101,6 +101,7 @@ class client : public std::enable_shared_from_this { } virtual void read() = 0; + virtual void close() = 0; }; } // namespace launchdarkly::sse diff --git a/libs/server-sent-events/src/sse.cpp b/libs/server-sent-events/src/sse.cpp index 9fb36375d..0bc88580d 100644 --- a/libs/server-sent-events/src/sse.cpp +++ b/libs/server-sent-events/src/sse.cpp @@ -37,7 +37,7 @@ std::string const& event_data::get_data() { } void event_data::trim_trailing_newline() { - if (m_data[m_data.size()-1] == '\n') { + if (m_data[m_data.size() - 1] == '\n') { m_data.resize(m_data.size() - 1); } } @@ -269,6 +269,11 @@ class ssl_client : public client { beast::bind_front_handler(&ssl_client::on_read, shared())); } + void on_stop() { + beast::error_code ec; + beast::close_socket(beast::get_lowest_layer(stream_)); + } + public: ssl_client(net::any_io_executor ex, ssl::context& ctx, @@ -294,6 +299,14 @@ class ssl_client : public client { host_, port_, beast::bind_front_handler(&ssl_client::on_resolve, shared())); } + + void close() override { + net::post( + stream_.get_executor(), + beast::bind_front_handler( + &ssl_client::on_stop, + shared())); + } }; class plaintext_client : public client { @@ -356,6 +369,11 @@ class plaintext_client : public client { beast::bind_front_handler(&plaintext_client::on_read, shared())); } + void on_stop() { + beast::error_code ec; + beast::close_socket(beast::get_lowest_layer(stream_)); + } + public: plaintext_client(net::any_io_executor ex, ssl::context& ctx, @@ -373,6 +391,14 @@ class plaintext_client : public client { host_, port_, beast::bind_front_handler(&plaintext_client::on_resolve, shared())); } + + void close() override { + net::post( + stream_.get_executor(), + beast::bind_front_handler( + &plaintext_client::on_stop, + shared())); + } }; builder::builder(net::any_io_executor ctx, std::string url) From e14202d32f9e01bbed2344bf4b26ba82a29e9dbf Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 22 Mar 2023 10:50:20 -0700 Subject: [PATCH 12/53] pass CRLF tests --- libs/server-sent-events/src/sse.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/libs/server-sent-events/src/sse.cpp b/libs/server-sent-events/src/sse.cpp index 0bc88580d..1b36401f1 100644 --- a/libs/server-sent-events/src/sse.cpp +++ b/libs/server-sent-events/src/sse.cpp @@ -184,13 +184,16 @@ size_t client::parse_stream(std::uint64_t remain, size_t i = 0; while (i < body.length()) { i += this->append_up_to(body.substr(i, body.length() - i), "\r\n"); - if (body[i] == '\r') { + if (i == body.size()) { + continue; + } else if (body.at(i) == '\r') { if (this->begin_CR_) { - // todo: illegal token + assert(0 && "illegal carriage return (likely a bug in the parser)"); } else { this->begin_CR_ = true; + i++; } - } else if (body[i] == '\n') { + } else if (body.at(i) == '\n') { this->begin_CR_ = false; this->complete_line(); i++; From f3b15b3ce4762472b8168c0e34314de0427def4a Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 22 Mar 2023 11:14:34 -0700 Subject: [PATCH 13/53] pass all tests except for redirects and reconnects --- apps/sse-contract-tests/entity_manager.hpp | 2 +- apps/sse-contract-tests/stream_entity.hpp | 58 ++++++++-------------- libs/server-sent-events/src/sse.cpp | 30 ++++++----- 3 files changed, 36 insertions(+), 54 deletions(-) diff --git a/apps/sse-contract-tests/entity_manager.hpp b/apps/sse-contract-tests/entity_manager.hpp index b9999e19e..efc55be0a 100644 --- a/apps/sse-contract-tests/entity_manager.hpp +++ b/apps/sse-contract-tests/entity_manager.hpp @@ -34,7 +34,7 @@ class entity_manager { if (!client) { return std::nullopt; } - auto entity = std::make_shared(executor_, std::move(params)); + auto entity = std::make_shared(executor_, client, params.callbackUrl); entity->run(); entities_.emplace(id, entity); return id; diff --git a/apps/sse-contract-tests/stream_entity.hpp b/apps/sse-contract-tests/stream_entity.hpp index f0ec9171e..495c81a96 100644 --- a/apps/sse-contract-tests/stream_entity.hpp +++ b/apps/sse-contract-tests/stream_entity.hpp @@ -82,7 +82,6 @@ class stream_entity : public std::enable_shared_from_this { if (ec) return fail(ec, "write"); - std::cout << "on_write: popping request; shutting down socket; setting timer\n"; requests_.pop(); stream_.socket().shutdown(tcp::socket::shutdown_both, ec); @@ -96,12 +95,10 @@ class stream_entity : public std::enable_shared_from_this { beast::bind_front_handler(&stream_entity::on_resolve, shared_from_this())); } else { - flush_timer_.expires_from_now( - boost::posix_time::milliseconds{500}); + flush_timer_.expires_from_now(boost::posix_time::milliseconds{100}); flush_timer_.async_wait(beast::bind_front_handler( &stream_entity::on_flush, shared_from_this())); } - } void on_flush(boost::system::error_code const& ec) { @@ -109,57 +106,45 @@ class stream_entity : public std::enable_shared_from_this { return fail(ec, "flush"); } - std::cout << "on_flush\n"; - if (!requests_.empty()) { - std::cout << "on_flush: queueing resolve\n"; - resolver_.async_resolve( - callback_host_, callback_port_, - beast::bind_front_handler(&stream_entity::on_resolve, - shared_from_this())); + resolver_.async_resolve( + callback_host_, callback_port_, + beast::bind_front_handler(&stream_entity::on_resolve, + shared_from_this())); } else { - flush_timer_.expires_from_now( - boost::posix_time::milliseconds{500}); - flush_timer_.async_wait(beast::bind_front_handler( - &stream_entity::on_flush, shared_from_this())); + flush_timer_.expires_from_now(boost::posix_time::milliseconds{100}); + flush_timer_.async_wait(beast::bind_front_handler( + &stream_entity::on_flush, shared_from_this())); } } public: - stream_entity(net::any_io_executor executor, config_params params) - : callback_url_{params.callbackUrl}, + stream_entity(net::any_io_executor executor, + std::shared_ptr client, + std::string callback_url) + : callback_url_{std::move(callback_url)}, callback_host_{}, callback_port_{}, callback_counter_{0}, - client_{}, + client_{std::move(client)}, executor_{executor}, resolver_{executor}, stream_{executor}, requests_{1024}, - flush_timer_{executor, boost::posix_time::milliseconds{50}}, + flush_timer_{executor, boost::posix_time::milliseconds{0}}, event_mutex_{} { - auto builder = launchdarkly::sse::builder{executor, params.streamUrl}; - if (params.headers) { - for (auto h : *params.headers) { - builder.header(h.first, h.second); - } - } - boost::system::result uri_components = - boost::urls::parse_uri(params.callbackUrl); + boost::urls::parse_uri(callback_url_); callback_host_ = uri_components->host(); callback_port_ = uri_components->port(); - client_ = builder.build(); - if (client_) { - client_->on_event([this](launchdarkly::sse::event_data ev) { - auto req = build_request(callback_counter_++, std::move(ev)); - requests_.push(req); - }); + client_->on_event([this](launchdarkly::sse::event_data ev) { + auto req = build_request(callback_counter_++, std::move(ev)); + requests_.push(req); + }); - client_->read(); - } + client_->read(); } void close() { @@ -169,7 +154,6 @@ class stream_entity : public std::enable_shared_from_this { void run() { flush_timer_.async_wait(beast::bind_front_handler( - &stream_entity::on_flush, - shared_from_this())); + &stream_entity::on_flush, shared_from_this())); } }; diff --git a/libs/server-sent-events/src/sse.cpp b/libs/server-sent-events/src/sse.cpp index 1b36401f1..3347409b3 100644 --- a/libs/server-sent-events/src/sse.cpp +++ b/libs/server-sent-events/src/sse.cpp @@ -187,16 +187,19 @@ size_t client::parse_stream(std::uint64_t remain, if (i == body.size()) { continue; } else if (body.at(i) == '\r') { - if (this->begin_CR_) { - assert(0 && "illegal carriage return (likely a bug in the parser)"); + complete_line(); + begin_CR_ = true; + i++; + } else if (body.at(i) == '\n') { + if (begin_CR_) { + begin_CR_ = false; + i++; } else { - this->begin_CR_ = true; + complete_line(); i++; } - } else if (body.at(i) == '\n') { - this->begin_CR_ = false; - this->complete_line(); - i++; + } else { + begin_CR_ = false; } } return body.length(); @@ -304,11 +307,8 @@ class ssl_client : public client { } void close() override { - net::post( - stream_.get_executor(), - beast::bind_front_handler( - &ssl_client::on_stop, - shared())); + net::post(stream_.get_executor(), + beast::bind_front_handler(&ssl_client::on_stop, shared())); } }; @@ -361,7 +361,7 @@ class plaintext_client : public client { void on_read(beast::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); - if (ec) { + if (ec && ec != beast::errc::operation_canceled) { return fail(ec, "read"); } @@ -398,9 +398,7 @@ class plaintext_client : public client { void close() override { net::post( stream_.get_executor(), - beast::bind_front_handler( - &plaintext_client::on_stop, - shared())); + beast::bind_front_handler(&plaintext_client::on_stop, shared())); } }; From 928989d6c07e399f7673695d774ceae069b656b3 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 22 Mar 2023 11:30:43 -0700 Subject: [PATCH 14/53] rename read() to run() --- apps/hello-cpp/main.cpp | 2 +- apps/sse-contract-tests/session.hpp | 2 +- apps/sse-contract-tests/stream_entity.hpp | 5 ++--- libs/server-sent-events/include/launchdarkly/sse/sse.hpp | 2 +- libs/server-sent-events/src/sse.cpp | 4 ++-- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/hello-cpp/main.cpp b/apps/hello-cpp/main.cpp index 8a06545c5..6c09d799d 100644 --- a/apps/hello-cpp/main.cpp +++ b/apps/hello-cpp/main.cpp @@ -28,7 +28,7 @@ int main() { return 1; } - client->read(); + client->run(); // client->on_event([](launchdarkly::sse::event_data e){ diff --git a/apps/sse-contract-tests/session.hpp b/apps/sse-contract-tests/session.hpp index 18f78656a..38be6474b 100644 --- a/apps/sse-contract-tests/session.hpp +++ b/apps/sse-contract-tests/session.hpp @@ -179,7 +179,7 @@ class session : public std::enable_shared_from_this return do_close(); } if (ec) { - std::cout << "read failed\n"; + std::cout << "run failed\n"; return; } send_response(handle_request(std::move(request_))); diff --git a/apps/sse-contract-tests/stream_entity.hpp b/apps/sse-contract-tests/stream_entity.hpp index 495c81a96..3a9a5503d 100644 --- a/apps/sse-contract-tests/stream_entity.hpp +++ b/apps/sse-contract-tests/stream_entity.hpp @@ -140,11 +140,10 @@ class stream_entity : public std::enable_shared_from_this { callback_port_ = uri_components->port(); client_->on_event([this](launchdarkly::sse::event_data ev) { - auto req = build_request(callback_counter_++, std::move(ev)); - requests_.push(req); + requests_.push(build_request(callback_counter_++, std::move(ev))); }); - client_->read(); + client_->run(); } void close() { diff --git a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp index 614c8703c..20ff1a799 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp @@ -100,7 +100,7 @@ class client : public std::enable_shared_from_this { m_cb = event_cb; } - virtual void read() = 0; + virtual void run() = 0; virtual void close() = 0; }; diff --git a/libs/server-sent-events/src/sse.cpp b/libs/server-sent-events/src/sse.cpp index 3347409b3..04800c3cd 100644 --- a/libs/server-sent-events/src/sse.cpp +++ b/libs/server-sent-events/src/sse.cpp @@ -289,7 +289,7 @@ class ssl_client : public client { : client(ex, std::move(req), std::move(host), std::move(port)), stream_{ex, ctx} {} - void read() override { + void run() override { // Set SNI Hostname (many hosts need this to handshake successfully) if (!SSL_set_tlsext_host_name(stream_.native_handle(), host_.c_str())) { beast::error_code ec{static_cast(::ERR_get_error()), @@ -386,7 +386,7 @@ class plaintext_client : public client { : client(ex, std::move(req), std::move(host), std::move(port)), stream_{ex} {} - void read() override { + void run() override { beast::get_lowest_layer(stream_).expires_after( std::chrono::seconds(15)); From 805707071d3596032d687bbc7df307285c2f213e Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 22 Mar 2023 11:53:11 -0700 Subject: [PATCH 15/53] cleaning up stream_entity --- apps/sse-contract-tests/definitions.hpp | 4 +- apps/sse-contract-tests/entity_manager.hpp | 49 +++++--- apps/sse-contract-tests/server.hpp | 2 +- apps/sse-contract-tests/session.hpp | 7 +- apps/sse-contract-tests/stream_entity.hpp | 135 ++++++++++++--------- 5 files changed, 114 insertions(+), 83 deletions(-) diff --git a/apps/sse-contract-tests/definitions.hpp b/apps/sse-contract-tests/definitions.hpp index fb53b9da0..79f3b4df7 100644 --- a/apps/sse-contract-tests/definitions.hpp +++ b/apps/sse-contract-tests/definitions.hpp @@ -31,7 +31,7 @@ namespace nlohmann { } // namespace nlohmann -struct config_params { +struct ConfigParams { std::string streamUrl; std::string callbackUrl; std::string tag; @@ -43,7 +43,7 @@ struct config_params { std::optional body; }; -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(config_params, +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigParams, streamUrl, callbackUrl, tag, diff --git a/apps/sse-contract-tests/entity_manager.hpp b/apps/sse-contract-tests/entity_manager.hpp index efc55be0a..294735f75 100644 --- a/apps/sse-contract-tests/entity_manager.hpp +++ b/apps/sse-contract-tests/entity_manager.hpp @@ -1,42 +1,52 @@ #pragma once -#include "stream_entity.hpp" #include "definitions.hpp" +#include "stream_entity.hpp" -#include -#include #include +#include +#include -class entity_manager { +using namespace launchdarkly::sse; + +// Manages the individual SSE clients (called entities here) which are +// instantiated for each contract test. +class EntityManager { + // Maps the entity's ID to the entity. Shared pointer is necessary because + // these entities are doing async IO and must remain alive as long as that + // is happening. std::unordered_map> entities_; + // Incremented each time create() is called to instantiate a new entity. std::size_t counter_; + // Synchronizes access to create()/destroy(); std::mutex lock_; boost::asio::any_io_executor executor_; -public: - explicit entity_manager(boost::asio::any_io_executor executor): - entities_{}, - counter_{0}, - lock_{}, - executor_{std::move(executor)}{ - } - std::optional create(config_params params) { + public: + explicit EntityManager(boost::asio::any_io_executor executor) + : entities_{}, counter_{0}, lock_{}, executor_{std::move(executor)} {} + + std::optional create(ConfigParams params) { std::lock_guard guard{lock_}; - auto id = std::to_string(counter_++); + std::string id = std::to_string(counter_++); - auto builder = launchdarkly::sse::builder{executor_, params.streamUrl}; + auto client_builder = builder{executor_, params.streamUrl}; if (params.headers) { - for (auto h: *params.headers) { - builder.header(h.first, h.second); + for (auto h : *params.headers) { + client_builder.header(h.first, h.second); } } - auto client = builder.build(); + std::shared_ptr client = client_builder.build(); if (!client) { return std::nullopt; } - auto entity = std::make_shared(executor_, client, params.callbackUrl); + std::shared_ptr entity = std::make_shared( + executor_, client, params.callbackUrl); + + // Kicks off asynchronous operations. entity->run(); entities_.emplace(id, entity); + return id; } @@ -46,7 +56,8 @@ class entity_manager { if (it == entities_.end()) { return false; } - it->second->close(); + // Shuts down asynchronous operations. + it->second->stop(); entities_.erase(it); return true; } diff --git a/apps/sse-contract-tests/server.hpp b/apps/sse-contract-tests/server.hpp index e5e66b0ad..5da3d218d 100644 --- a/apps/sse-contract-tests/server.hpp +++ b/apps/sse-contract-tests/server.hpp @@ -76,6 +76,6 @@ class server tcp::acceptor acceptor_; boost::asio::signal_set signals_; bool stopped_; - entity_manager manager_; + EntityManager manager_; std::vector caps_; }; diff --git a/apps/sse-contract-tests/session.hpp b/apps/sse-contract-tests/session.hpp index 38be6474b..e5edc4e4b 100644 --- a/apps/sse-contract-tests/session.hpp +++ b/apps/sse-contract-tests/session.hpp @@ -38,7 +38,7 @@ class session : public std::enable_shared_from_this // The request message. http::request request_; - entity_manager& manager_; + EntityManager& manager_; std::vector capabilities_; @@ -131,7 +131,7 @@ class session : public std::enable_shared_from_this if (req.method() == http::verb::post && req.target() == "/") { try { auto json = nlohmann::json::parse(request_.body()); - auto params = json.get(); + auto params = json.get(); if (auto id = manager_.create(std::move(params))) { return create_entity_response(*id); } else { @@ -152,7 +152,8 @@ class session : public std::enable_shared_from_this return bad_request("unknown route"); } public: - explicit session(tcp::socket&& socket, entity_manager& manager, std::vector caps): + explicit session(tcp::socket&& socket, + EntityManager& manager, std::vector caps): stream_{std::move(socket)}, manager_{manager}, capabilities_{std::move(caps)} diff --git a/apps/sse-contract-tests/stream_entity.hpp b/apps/sse-contract-tests/stream_entity.hpp index 3a9a5503d..c10e82dcd 100644 --- a/apps/sse-contract-tests/stream_entity.hpp +++ b/apps/sse-contract-tests/stream_entity.hpp @@ -18,41 +18,97 @@ namespace http = beast::http; // from namespace net = boost::asio; // from using tcp = boost::asio::ip::tcp; // from -// Report a failure void fail(beast::error_code ec, char const* what) { std::cerr << what << ": " << ec.message() << "\n"; } +const auto kFlushInterval = boost::posix_time::milliseconds {100}; + class stream_entity : public std::enable_shared_from_this { + // Simple string body request is appropriate since the JSON + // returned to the test service is minimal. using request_type = http::request; + std::shared_ptr client_; + std::string callback_url_; std::string callback_port_; std::string callback_host_; size_t callback_counter_; - std::shared_ptr client_; + net::any_io_executor executor_; tcp::resolver resolver_; beast::tcp_stream stream_; - boost::lockfree::spsc_queue requests_; - std::mutex event_mutex_; + + // When events are received from the SSE client, they are pushed into + // this queue. + boost::lockfree::spsc_queue outbox_; + // Periodically, the events are flushed to the test harness. net::deadline_timer flush_timer_; + public: + stream_entity(net::any_io_executor executor, + std::shared_ptr client, + std::string callback_url) + : client_{std::move(client)}, + callback_url_{std::move(callback_url)}, + callback_port_{}, + callback_host_{}, + callback_counter_{0}, + executor_{executor}, + resolver_{executor}, + stream_{executor}, + outbox_{1024}, + flush_timer_{executor, boost::posix_time::milliseconds{0}} { + boost::system::result uri_components = + boost::urls::parse_uri(callback_url_); + + callback_host_ = uri_components->host(); + callback_port_ = uri_components->port(); + } + + void run() { + // Setup the SSE client to callback into the entity whenever it + // receives a comment/event. + client_->on_event( + [self = shared_from_this()](launchdarkly::sse::event_data ev) { + auto http_request = self->build_request( + self->callback_counter_++, std::move(ev)); + self->outbox_.push(http_request); + }); + + // Kickoff the SSE client's async operations. + client_->run(); + + // Immediately kickoff the flush "loop". + flush_timer_.async_wait(beast::bind_front_handler( + &stream_entity::on_flush_timer, shared_from_this())); + } + + void stop() { + flush_timer_.cancel(); + client_->close(); + } + + private: request_type build_request(std::size_t counter, launchdarkly::sse::event_data ev) { request_type req; + req.set(http::field::host, callback_host_); req.method(http::verb::get); req.target(callback_url_ + "/" + std::to_string(counter)); + + nlohmann::json json; + if (ev.get_type() == "comment") { - nlohmann::json json = comment_message{"comment", ev.get_data()}; - req.body() = json.dump(); + json = comment_message{"comment", ev.get_data()}; } else { - nlohmann::json json = event_message{ - "event", - event{ev.get_type(), ev.get_data(), ev.get_id().value_or("")}}; - req.body() = json.dump(); + json = event_message{"event", event{ev.get_type(), ev.get_data(), + ev.get_id().value_or("")}}; } + + req.body() = json.dump(); req.prepare_payload(); return req; } @@ -71,8 +127,7 @@ class stream_entity : public std::enable_shared_from_this { if (ec) return fail(ec, "connect"); - request_type& req = requests_.front(); - std::cout << "writing event: " << req.body() << '\n'; + request_type& req = outbox_.front(); http::async_write(stream_, req, beast::bind_front_handler(&stream_entity::on_write, shared_from_this())); @@ -82,77 +137,41 @@ class stream_entity : public std::enable_shared_from_this { if (ec) return fail(ec, "write"); - requests_.pop(); + outbox_.pop(); stream_.socket().shutdown(tcp::socket::shutdown_both, ec); // not_connected happens sometimes so don't bother reporting it. if (ec && ec != beast::errc::not_connected) return fail(ec, "shutdown"); - if (!requests_.empty()) { + // If there's more to do, queue up another resolve. Otherwise, + // wait a little while before trying again. + if (!outbox_.empty()) { resolver_.async_resolve( callback_host_, callback_port_, beast::bind_front_handler(&stream_entity::on_resolve, shared_from_this())); } else { - flush_timer_.expires_from_now(boost::posix_time::milliseconds{100}); + flush_timer_.expires_from_now(kFlushInterval); flush_timer_.async_wait(beast::bind_front_handler( - &stream_entity::on_flush, shared_from_this())); + &stream_entity::on_flush_timer, shared_from_this())); } } - void on_flush(boost::system::error_code const& ec) { + void on_flush_timer(boost::system::error_code const& ec) { if (ec) { return fail(ec, "flush"); } - if (!requests_.empty()) { + if (!outbox_.empty()) { resolver_.async_resolve( callback_host_, callback_port_, beast::bind_front_handler(&stream_entity::on_resolve, shared_from_this())); } else { - flush_timer_.expires_from_now(boost::posix_time::milliseconds{100}); + flush_timer_.expires_from_now(kFlushInterval); flush_timer_.async_wait(beast::bind_front_handler( - &stream_entity::on_flush, shared_from_this())); + &stream_entity::on_flush_timer, shared_from_this())); } } - - public: - stream_entity(net::any_io_executor executor, - std::shared_ptr client, - std::string callback_url) - : callback_url_{std::move(callback_url)}, - callback_host_{}, - callback_port_{}, - callback_counter_{0}, - client_{std::move(client)}, - executor_{executor}, - resolver_{executor}, - stream_{executor}, - requests_{1024}, - flush_timer_{executor, boost::posix_time::milliseconds{0}}, - event_mutex_{} { - boost::system::result uri_components = - boost::urls::parse_uri(callback_url_); - - callback_host_ = uri_components->host(); - callback_port_ = uri_components->port(); - - client_->on_event([this](launchdarkly::sse::event_data ev) { - requests_.push(build_request(callback_counter_++, std::move(ev))); - }); - - client_->run(); - } - - void close() { - flush_timer_.cancel(); - client_->close(); - } - - void run() { - flush_timer_.async_wait(beast::bind_front_handler( - &stream_entity::on_flush, shared_from_this())); - } }; From b53229f5d329f824c0110b48b077ca889debf7c7 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 22 Mar 2023 12:29:57 -0700 Subject: [PATCH 16/53] make tests execute fast --- apps/sse-contract-tests/stream_entity.hpp | 82 +++++++++++------------ 1 file changed, 40 insertions(+), 42 deletions(-) diff --git a/apps/sse-contract-tests/stream_entity.hpp b/apps/sse-contract-tests/stream_entity.hpp index c10e82dcd..99b91df0a 100644 --- a/apps/sse-contract-tests/stream_entity.hpp +++ b/apps/sse-contract-tests/stream_entity.hpp @@ -22,7 +22,9 @@ void fail(beast::error_code ec, char const* what) { std::cerr << what << ": " << ec.message() << "\n"; } -const auto kFlushInterval = boost::posix_time::milliseconds {100}; +// Periodically check the outbox (outgoing events to the test harness) +// at this interval. +auto const kFlushInterval = boost::posix_time::milliseconds{10}; class stream_entity : public std::enable_shared_from_this { // Simple string body request is appropriate since the JSON @@ -80,12 +82,17 @@ class stream_entity : public std::enable_shared_from_this { // Kickoff the SSE client's async operations. client_->run(); - // Immediately kickoff the flush "loop". - flush_timer_.async_wait(beast::bind_front_handler( - &stream_entity::on_flush_timer, shared_from_this())); + // Begin connecting to the test harness's event-posting service. + resolver_.async_resolve( + callback_host_, callback_port_, + beast::bind_front_handler(&stream_entity::on_resolve, + shared_from_this())); } void stop() { + beast::error_code ec; + stream_.socket().shutdown(tcp::socket::shutdown_both, ec); + flush_timer_.cancel(); client_->close(); } @@ -116,7 +123,8 @@ class stream_entity : public std::enable_shared_from_this { void on_resolve(beast::error_code ec, tcp::resolver::results_type results) { if (ec) return fail(ec, "resolve"); - // Make the connection on the IP address we get from a lookup + + // Make the connection on the IP address we get from a lookup. beast::get_lowest_layer(stream_).async_connect( results, beast::bind_front_handler(&stream_entity::on_connect, shared_from_this())); @@ -127,51 +135,41 @@ class stream_entity : public std::enable_shared_from_this { if (ec) return fail(ec, "connect"); - request_type& req = outbox_.front(); - http::async_write(stream_, req, - beast::bind_front_handler(&stream_entity::on_write, - shared_from_this())); + // Now that we're connected, kickoff the event flush "loop". + boost::system::error_code dummy; + net::post(executor_, + beast::bind_front_handler(&stream_entity::on_flush_timer, + shared_from_this(), dummy)); } - void on_write(beast::error_code ec, std::size_t) { - if (ec) - return fail(ec, "write"); - - outbox_.pop(); - stream_.socket().shutdown(tcp::socket::shutdown_both, ec); - - // not_connected happens sometimes so don't bother reporting it. - if (ec && ec != beast::errc::not_connected) - return fail(ec, "shutdown"); - - // If there's more to do, queue up another resolve. Otherwise, - // wait a little while before trying again. - if (!outbox_.empty()) { - resolver_.async_resolve( - callback_host_, callback_port_, - beast::bind_front_handler(&stream_entity::on_resolve, - shared_from_this())); - } else { - flush_timer_.expires_from_now(kFlushInterval); - flush_timer_.async_wait(beast::bind_front_handler( - &stream_entity::on_flush_timer, shared_from_this())); - } - } - - void on_flush_timer(boost::system::error_code const& ec) { + void on_flush_timer(boost::system::error_code ec) { if (ec) { return fail(ec, "flush"); } if (!outbox_.empty()) { - resolver_.async_resolve( - callback_host_, callback_port_, - beast::bind_front_handler(&stream_entity::on_resolve, + request_type& request = outbox_.front(); + + // Flip-flop between this function and on_write; pushing an event + // and then popping it. + http::async_write( + stream_, request, + beast::bind_front_handler(&stream_entity::on_write, shared_from_this())); - } else { - flush_timer_.expires_from_now(kFlushInterval); - flush_timer_.async_wait(beast::bind_front_handler( - &stream_entity::on_flush_timer, shared_from_this())); + return; } + + // If the outbox is empty, wait a bit before trying again. + flush_timer_.expires_from_now(kFlushInterval); + flush_timer_.async_wait(beast::bind_front_handler( + &stream_entity::on_flush_timer, shared_from_this())); + } + + void on_write(beast::error_code ec, std::size_t) { + if (ec) + return fail(ec, "write"); + + outbox_.pop(); + on_flush_timer(boost::system::error_code{}); } }; From 68e0da59960081f74f6cbaa702b02ecd08772d3b Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 22 Mar 2023 12:53:07 -0700 Subject: [PATCH 17/53] refactoring into src/include dirs --- apps/sse-contract-tests/CMakeLists.txt | 11 +- apps/sse-contract-tests/entity_manager.hpp | 64 ----- .../{ => include}/definitions.hpp | 2 +- .../include/entity_manager.hpp | 31 +++ apps/sse-contract-tests/include/server.hpp | 35 +++ apps/sse-contract-tests/include/session.hpp | 47 ++++ .../include/stream_entity.hpp | 55 +++++ apps/sse-contract-tests/server.hpp | 81 ------- apps/sse-contract-tests/session.hpp | 218 ------------------ .../sse-contract-tests/src/entity_manager.cpp | 43 ++++ apps/sse-contract-tests/{ => src}/main.cpp | 4 +- apps/sse-contract-tests/src/server.cpp | 48 ++++ apps/sse-contract-tests/src/session.cpp | 168 ++++++++++++++ apps/sse-contract-tests/src/stream_entity.cpp | 138 +++++++++++ apps/sse-contract-tests/stream_entity.hpp | 175 -------------- 15 files changed, 579 insertions(+), 541 deletions(-) delete mode 100644 apps/sse-contract-tests/entity_manager.hpp rename apps/sse-contract-tests/{ => include}/definitions.hpp (98%) create mode 100644 apps/sse-contract-tests/include/entity_manager.hpp create mode 100644 apps/sse-contract-tests/include/server.hpp create mode 100644 apps/sse-contract-tests/include/session.hpp create mode 100644 apps/sse-contract-tests/include/stream_entity.hpp delete mode 100644 apps/sse-contract-tests/server.hpp delete mode 100644 apps/sse-contract-tests/session.hpp create mode 100644 apps/sse-contract-tests/src/entity_manager.cpp rename apps/sse-contract-tests/{ => src}/main.cpp (92%) create mode 100644 apps/sse-contract-tests/src/server.cpp create mode 100644 apps/sse-contract-tests/src/session.cpp create mode 100644 apps/sse-contract-tests/src/stream_entity.cpp delete mode 100644 apps/sse-contract-tests/stream_entity.hpp diff --git a/apps/sse-contract-tests/CMakeLists.txt b/apps/sse-contract-tests/CMakeLists.txt index a333b0f54..878f04fe1 100644 --- a/apps/sse-contract-tests/CMakeLists.txt +++ b/apps/sse-contract-tests/CMakeLists.txt @@ -10,8 +10,17 @@ project( include(${CMAKE_FILES}/json.cmake) -add_executable(sse-tests main.cpp) +add_executable(sse-tests + src/main.cpp + src/server.cpp + src/entity_manager.cpp + src/session.cpp + src/stream_entity.cpp +) + target_link_libraries(sse-tests PRIVATE launchdarkly::sse nlohmann_json::nlohmann_json ) + +target_include_directories(sse-tests PUBLIC include) diff --git a/apps/sse-contract-tests/entity_manager.hpp b/apps/sse-contract-tests/entity_manager.hpp deleted file mode 100644 index 294735f75..000000000 --- a/apps/sse-contract-tests/entity_manager.hpp +++ /dev/null @@ -1,64 +0,0 @@ -#pragma once - -#include "definitions.hpp" -#include "stream_entity.hpp" - -#include -#include -#include - -using namespace launchdarkly::sse; - -// Manages the individual SSE clients (called entities here) which are -// instantiated for each contract test. -class EntityManager { - // Maps the entity's ID to the entity. Shared pointer is necessary because - // these entities are doing async IO and must remain alive as long as that - // is happening. - std::unordered_map> entities_; - // Incremented each time create() is called to instantiate a new entity. - std::size_t counter_; - // Synchronizes access to create()/destroy(); - std::mutex lock_; - boost::asio::any_io_executor executor_; - - public: - explicit EntityManager(boost::asio::any_io_executor executor) - : entities_{}, counter_{0}, lock_{}, executor_{std::move(executor)} {} - - std::optional create(ConfigParams params) { - std::lock_guard guard{lock_}; - std::string id = std::to_string(counter_++); - - auto client_builder = builder{executor_, params.streamUrl}; - if (params.headers) { - for (auto h : *params.headers) { - client_builder.header(h.first, h.second); - } - } - std::shared_ptr client = client_builder.build(); - if (!client) { - return std::nullopt; - } - std::shared_ptr entity = std::make_shared( - executor_, client, params.callbackUrl); - - // Kicks off asynchronous operations. - entity->run(); - entities_.emplace(id, entity); - - return id; - } - - bool destroy(std::string const& id) { - std::lock_guard guard{lock_}; - auto it = entities_.find(id); - if (it == entities_.end()) { - return false; - } - // Shuts down asynchronous operations. - it->second->stop(); - entities_.erase(it); - return true; - } -}; diff --git a/apps/sse-contract-tests/definitions.hpp b/apps/sse-contract-tests/include/definitions.hpp similarity index 98% rename from apps/sse-contract-tests/definitions.hpp rename to apps/sse-contract-tests/include/definitions.hpp index 79f3b4df7..a5e58b059 100644 --- a/apps/sse-contract-tests/definitions.hpp +++ b/apps/sse-contract-tests/include/definitions.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include "nlohmann/json.hpp" #include #include diff --git a/apps/sse-contract-tests/include/entity_manager.hpp b/apps/sse-contract-tests/include/entity_manager.hpp new file mode 100644 index 000000000..be0290906 --- /dev/null +++ b/apps/sse-contract-tests/include/entity_manager.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "definitions.hpp" +#include "stream_entity.hpp" + +#include + +#include +#include +#include +#include +#include + +// Manages the individual SSE clients (called entities here) which are +// instantiated for each contract test. +class EntityManager { + // Maps the entity's ID to the entity. Shared pointer is necessary because + // these entities are doing async IO and must remain alive as long as that + // is happening. + std::unordered_map> entities_; + // Incremented each time create() is called to instantiate a new entity. + std::size_t counter_; + // Synchronizes access to create()/destroy(); + std::mutex lock_; + boost::asio::any_io_executor executor_; + + public: + explicit EntityManager(boost::asio::any_io_executor executor); + std::optional create(ConfigParams params); + bool destroy(std::string const& id); +}; diff --git a/apps/sse-contract-tests/include/server.hpp b/apps/sse-contract-tests/include/server.hpp new file mode 100644 index 000000000..069918092 --- /dev/null +++ b/apps/sse-contract-tests/include/server.hpp @@ -0,0 +1,35 @@ +#pragma once + + +#include "entity_manager.hpp" + +#include +#include + +#include + + +namespace beast = boost::beast; // from +namespace http = beast::http; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from + +class server { + tcp::acceptor acceptor_; + bool stopped_; + boost::asio::signal_set signals_; + EntityManager manager_; + std::vector caps_; + + public: + server(net::any_io_executor executor, + boost::asio::ip::address const& address, + unsigned short port); + + void add_capability(std::string cap); + void start(); + void stop(); + + private: + void accept_connection(); +}; diff --git a/apps/sse-contract-tests/include/session.hpp b/apps/sse-contract-tests/include/session.hpp new file mode 100644 index 000000000..93ac2da53 --- /dev/null +++ b/apps/sse-contract-tests/include/session.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include "entity_manager.hpp" + +#include +#include +#include + +namespace beast = boost::beast; // from +namespace http = beast::http; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from + + +class session : public std::enable_shared_from_this { + // The socket for the currently connected client. + beast::tcp_stream stream_; + + // The buffer for performing reads. + beast::flat_buffer buffer_{8192}; + + // The request message. + http::request request_; + + EntityManager& manager_; + + std::vector capabilities_; + + public: + explicit session(tcp::socket&& socket, + EntityManager& manager, + std::vector caps); + void start(); + + void do_read(); + + void on_read(beast::error_code ec, std::size_t bytes_transferred); + void do_close(); + + void send_response(http::message_generator&& msg); + + void on_write(bool keep_alive, + beast::error_code ec, + std::size_t bytes_transferred); + private: + http::message_generator handle_request(http::request&& req); +}; diff --git a/apps/sse-contract-tests/include/stream_entity.hpp b/apps/sse-contract-tests/include/stream_entity.hpp new file mode 100644 index 000000000..f119e9fff --- /dev/null +++ b/apps/sse-contract-tests/include/stream_entity.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include +#include + +namespace beast = boost::beast; // from +namespace http = beast::http; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from + +class stream_entity : public std::enable_shared_from_this { + // Simple string body request is appropriate since the JSON + // returned to the test service is minimal. + using request_type = http::request; + + std::shared_ptr client_; + + std::string callback_url_; + std::string callback_port_; + std::string callback_host_; + size_t callback_counter_; + + net::any_io_executor executor_; + tcp::resolver resolver_; + beast::tcp_stream stream_; + + // When events are received from the SSE client, they are pushed into + // this queue. + boost::lockfree::spsc_queue outbox_; + // Periodically, the events are flushed to the test harness. + net::deadline_timer flush_timer_; + + public: + stream_entity(net::any_io_executor executor, + std::shared_ptr client, + std::string callback_url); + + void run(); + void stop(); + private: + request_type build_request(std::size_t counter, + launchdarkly::sse::event_data ev); + void on_resolve(beast::error_code ec, tcp::resolver::results_type results); + void on_connect(beast::error_code ec, + tcp::resolver::results_type::endpoint_type); + void on_flush_timer(boost::system::error_code ec); + void on_write(beast::error_code ec, std::size_t); +}; diff --git a/apps/sse-contract-tests/server.hpp b/apps/sse-contract-tests/server.hpp deleted file mode 100644 index 5da3d218d..000000000 --- a/apps/sse-contract-tests/server.hpp +++ /dev/null @@ -1,81 +0,0 @@ -#pragma once - -#include "stream_entity.hpp" -#include "session.hpp" - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace beast = boost::beast; // from -namespace http = beast::http; // from -namespace net = boost::asio; // from -using tcp = boost::asio::ip::tcp; // from - - -class server -{ -public: - server(net::any_io_executor executor, boost::asio::ip::address const& address, unsigned short port): - acceptor_(executor, {address, port}), - stopped_(false), - signals_{executor}, - manager_{executor} - { - signals_.add(SIGTERM); - signals_.add(SIGINT); - signals_.async_wait(boost::bind(&server::stop, this)); - acceptor_.set_option(tcp::acceptor::reuse_address(true)); - } - - void add_capability(std::string cap) { - caps_.push_back(cap); - } - - void start_accepting() - { - acceptor_.listen(); - accept_loop(); - } - - void stop() - { - boost::asio::post(acceptor_.get_executor(), [this]() - { - std::cout << "Stopping server\n"; - acceptor_.cancel(); - stopped_ = true; - std::cout << "Server stopped\n"; - }); - } - -private: - void accept_loop() - { - acceptor_.async_accept([this](beast::error_code ec, tcp::socket peer){ - if (!ec) { - if (!stopped_) { - std::make_shared(std::move(peer), manager_, caps_)->start(); - accept_loop(); - } - } - }); - } - - tcp::acceptor acceptor_; - boost::asio::signal_set signals_; - bool stopped_; - EntityManager manager_; - std::vector caps_; -}; diff --git a/apps/sse-contract-tests/session.hpp b/apps/sse-contract-tests/session.hpp deleted file mode 100644 index e5edc4e4b..000000000 --- a/apps/sse-contract-tests/session.hpp +++ /dev/null @@ -1,218 +0,0 @@ -#pragma once - -#include "entity_manager.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace beast = boost::beast; // from -namespace http = beast::http; // from -namespace net = boost::asio; // from -using tcp = boost::asio::ip::tcp; // from - - - - -const std::string ENTITY_PATH = "/entity/"; - -class session : public std::enable_shared_from_this -{ - // The socket for the currently connected client. - beast::tcp_stream stream_; - - // The buffer for performing reads. - beast::flat_buffer buffer_{8192}; - - // The request message. - http::request request_; - - EntityManager& manager_; - - std::vector capabilities_; - - - template - http::message_generator - handle_request(http::request>&& req) - { - - std::cout << "handling " << req.method() << " <" << req.target() << ">\n"; - // Returns a bad request response - auto const bad_request = - [&req](beast::string_view why) - { - http::response res{http::status::bad_request, req.version()}; - res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - res.set(http::field::content_type, "application/json"); - res.keep_alive(req.keep_alive()); - res.body() = nlohmann::json{"error", why}.dump(); - res.prepare_payload(); - return res; - }; - - // Returns a not found response - auto const not_found = - [&req](beast::string_view target) - { - http::response res{http::status::not_found, req.version()}; - res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - res.set(http::field::content_type, "text/html"); - res.keep_alive(req.keep_alive()); - res.body() = "The resource '" + std::string(target) + "' was not found."; - res.prepare_payload(); - return res; - }; - - // Returns a server error response - auto const server_error = - [&req](beast::string_view what) - { - http::response res{http::status::internal_server_error, req.version()}; - res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - res.set(http::field::content_type, "text/html"); - res.keep_alive(req.keep_alive()); - res.body() = "An error occurred: '" + std::string(what) + "'"; - res.prepare_payload(); - return res; - }; - - auto const capabilities_response = [&req](std::vector const& caps) { - http::response res{http::status::ok, req.version()}; - res.set(http::field::content_type, "application/json"); - res.keep_alive(req.keep_alive()); - res.body() = nlohmann::json{{"capabilities", caps}}.dump(); - res.prepare_payload(); - return res; - }; - - auto const create_entity_response = [&req](std::string id) { - http::response res{http::status::ok, req.version()}; - res.keep_alive(req.keep_alive()); - res.set("Location", ENTITY_PATH + id); - res.prepare_payload(); - return res; - }; - - auto const destroy_entity_response = [&req](bool erased) { - auto status = erased? http::status::ok : http::status::not_found; - http::response res{status, req.version()}; - res.keep_alive(req.keep_alive()); - res.prepare_payload(); - return res; - }; - - - if (req.method() == http::verb::get && req.target() == "/") { - return capabilities_response(capabilities_); - } - - if (req.method() == http::verb::head && req.target() == "/") { - http::response res{http::status::ok, req.version()}; - return res; - } - - if (req.method() == http::verb::delete_ && req.target() == "/") { - // not clean, but doesn't matter from the test-harness's perspective. - std::raise(SIGTERM); - } - - if (req.method() == http::verb::post && req.target() == "/") { - try { - auto json = nlohmann::json::parse(request_.body()); - auto params = json.get(); - if (auto id = manager_.create(std::move(params))) { - return create_entity_response(*id); - } else { - return server_error("couldn't create client entity"); - } - } catch(nlohmann::json::exception& e) { - return bad_request("unable to parse config JSON"); - } - } - - if (req.method() == http::verb::delete_ && req.target().starts_with(ENTITY_PATH)) { - std::string id = req.target(); - boost::erase_first(id, ENTITY_PATH); - bool erased = manager_.destroy(id); - return destroy_entity_response(erased); - } - - return bad_request("unknown route"); - } -public: - explicit session(tcp::socket&& socket, - EntityManager& manager, std::vector caps): - stream_{std::move(socket)}, - manager_{manager}, - capabilities_{std::move(caps)} - { - } - - void start() { - net::dispatch(stream_.get_executor(), beast::bind_front_handler(&session::do_read, shared_from_this())); - } - - - void do_read() { - request_ = {}; - http::async_read(stream_, buffer_, request_, - beast::bind_front_handler(&session::on_read, shared_from_this()) - ); - } - - void on_read(beast::error_code ec, std::size_t bytes_transferred) - { - boost::ignore_unused(bytes_transferred); - - if (ec == http::error::end_of_stream) { - return do_close(); - } - if (ec) { - std::cout << "run failed\n"; - return; - } - send_response(handle_request(std::move(request_))); - } - - void do_close() { - beast::error_code ec; - stream_.socket().shutdown(tcp::socket::shutdown_send, ec); - } - - void send_response(http::message_generator&& msg) - { - beast::async_write( - stream_, - std::move(msg), - beast::bind_front_handler(&session::on_write, shared_from_this(), request_.keep_alive())); - } - - void - on_write(bool keep_alive, beast::error_code ec, std::size_t bytes_transferred) - { - boost::ignore_unused(bytes_transferred); - - if (ec) { - std::cout << "write failed\n"; - return; - } - - if (!keep_alive) { - return do_close(); - } - - do_read(); - } -}; diff --git a/apps/sse-contract-tests/src/entity_manager.cpp b/apps/sse-contract-tests/src/entity_manager.cpp new file mode 100644 index 000000000..8ab19a78d --- /dev/null +++ b/apps/sse-contract-tests/src/entity_manager.cpp @@ -0,0 +1,43 @@ +#include "../include/entity_manager.hpp" + +#include "launchdarkly/sse/sse.hpp" + +EntityManager::EntityManager(boost::asio::any_io_executor executor) + : entities_{}, counter_{0}, lock_{}, executor_{std::move(executor)} {} + +std::optional EntityManager::create(ConfigParams params) { + std::lock_guard guard{lock_}; + std::string id = std::to_string(counter_++); + + auto client_builder = + launchdarkly::sse::builder{executor_, params.streamUrl}; + if (params.headers) { + for (auto h : *params.headers) { + client_builder.header(h.first, h.second); + } + } + std::shared_ptr client = client_builder.build(); + if (!client) { + return std::nullopt; + } + std::shared_ptr entity = + std::make_shared(executor_, client, params.callbackUrl); + + // Kicks off asynchronous operations. + entity->run(); + entities_.emplace(id, entity); + + return id; +} + +bool EntityManager::destroy(std::string const& id) { + std::lock_guard guard{lock_}; + auto it = entities_.find(id); + if (it == entities_.end()) { + return false; + } + // Shuts down asynchronous operations. + it->second->stop(); + entities_.erase(it); + return true; +} diff --git a/apps/sse-contract-tests/main.cpp b/apps/sse-contract-tests/src/main.cpp similarity index 92% rename from apps/sse-contract-tests/main.cpp rename to apps/sse-contract-tests/src/main.cpp index a2cb5577f..d325cd8f2 100644 --- a/apps/sse-contract-tests/main.cpp +++ b/apps/sse-contract-tests/src/main.cpp @@ -1,5 +1,7 @@ #include "server.hpp" +#include + int main(int argc, char* argv[]) { @@ -11,7 +13,7 @@ main(int argc, char* argv[]) server s{ioc.get_executor(), address, port}; s.add_capability("headers"); - s.start_accepting(); + s.start(); std::cout << "listening on " << address << ":" << port << std::endl; diff --git a/apps/sse-contract-tests/src/server.cpp b/apps/sse-contract-tests/src/server.cpp new file mode 100644 index 000000000..d9575e85e --- /dev/null +++ b/apps/sse-contract-tests/src/server.cpp @@ -0,0 +1,48 @@ +#include "server.hpp" +#include "session.hpp" + +#include +#include + + +server::server(net::any_io_executor executor, + boost::asio::ip::address const& address, + unsigned short port) + : acceptor_{executor, {address, port}}, + stopped_{false}, + signals_{executor}, + manager_{executor}, + caps_{} { + signals_.add(SIGTERM); + signals_.add(SIGINT); + signals_.async_wait(boost::bind(&server::stop, this)); + acceptor_.set_option(tcp::acceptor::reuse_address(true)); +} + +void server::add_capability(std::string cap) { + caps_.push_back(std::move(cap)); +} + +void server::start() { + acceptor_.listen(); + accept_connection(); +} + +void server::stop() { + boost::asio::post(acceptor_.get_executor(), [this]() { + std::cout << "stopping server\n"; + acceptor_.cancel(); + stopped_ = true; + std::cout << "server stopped\n"; + }); +} + +void server::accept_connection() { + acceptor_.async_accept([this](beast::error_code ec, tcp::socket peer) { + if (!ec && !stopped_) { + std::make_shared(std::move(peer), manager_, caps_) + ->start(); + accept_connection(); + } + }); +} diff --git a/apps/sse-contract-tests/src/session.cpp b/apps/sse-contract-tests/src/session.cpp new file mode 100644 index 000000000..0fa7bdb6e --- /dev/null +++ b/apps/sse-contract-tests/src/session.cpp @@ -0,0 +1,168 @@ +#include "session.hpp" +#include +#include + +const std::string ENTITY_PATH = "/entity/"; + +http::message_generator session::handle_request( + http::request&& req) { + auto const bad_request = [&req](beast::string_view why) { + http::response res{http::status::bad_request, + req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "application/json"); + res.keep_alive(req.keep_alive()); + res.body() = nlohmann::json{"error", why}.dump(); + res.prepare_payload(); + return res; + }; + + auto const not_found = [&req](beast::string_view target) { + http::response res{http::status::not_found, + req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "text/html"); + res.keep_alive(req.keep_alive()); + res.body() = + "The resource '" + std::string(target) + "' was not found."; + res.prepare_payload(); + return res; + }; + + auto const server_error = [&req](beast::string_view what) { + http::response res{ + http::status::internal_server_error, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "text/html"); + res.keep_alive(req.keep_alive()); + res.body() = "An error occurred: '" + std::string(what) + "'"; + res.prepare_payload(); + return res; + }; + + auto const capabilities_response = [&req](std::vector const& + caps) { + http::response res{http::status::ok, req.version()}; + res.set(http::field::content_type, "application/json"); + res.keep_alive(req.keep_alive()); + res.body() = nlohmann::json{{"capabilities", caps}}.dump(); + res.prepare_payload(); + return res; + }; + + auto const create_entity_response = [&req](std::string const& id) { + http::response res{http::status::ok, req.version()}; + res.keep_alive(req.keep_alive()); + res.set("Location", ENTITY_PATH + id); + res.prepare_payload(); + return res; + }; + + auto const destroy_entity_response = [&req](bool erased) { + auto status = erased ? http::status::ok : http::status::not_found; + http::response res{status, req.version()}; + res.keep_alive(req.keep_alive()); + res.prepare_payload(); + return res; + }; + + if (req.method() == http::verb::get && req.target() == "/") { + return capabilities_response(capabilities_); + } + + if (req.method() == http::verb::head && req.target() == "/") { + http::response res{http::status::ok, req.version()}; + return res; + } + + if (req.method() == http::verb::delete_ && req.target() == "/") { + // not clean, but doesn't matter from the test-harness's perspective. + std::raise(SIGTERM); + } + + if (req.method() == http::verb::post && req.target() == "/") { + try { + auto json = nlohmann::json::parse(request_.body()); + auto params = json.get(); + if (auto id = manager_.create(std::move(params))) { + return create_entity_response(*id); + } else { + return server_error("couldn't create client entity"); + } + } catch (nlohmann::json::exception& e) { + return bad_request("unable to parse config JSON"); + } + } + + if (req.method() == http::verb::delete_ && + req.target().starts_with(ENTITY_PATH)) { + std::string id = req.target(); + boost::erase_first(id, ENTITY_PATH); + bool erased = manager_.destroy(id); + return destroy_entity_response(erased); + } + + return bad_request("unknown route"); +} + +session::session(tcp::socket&& socket, + EntityManager& manager, + std::vector caps) + : stream_{std::move(socket)}, + manager_{manager}, + capabilities_{std::move(caps)} {} + +void session::start() { + net::dispatch( + stream_.get_executor(), + beast::bind_front_handler(&session::do_read, shared_from_this())); +} + +void session::do_read() { + request_ = {}; + http::async_read( + stream_, buffer_, request_, + beast::bind_front_handler(&session::on_read, shared_from_this())); +} + +void session::on_read(beast::error_code ec, std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + + if (ec == http::error::end_of_stream) { + return do_close(); + } + if (ec) { + std::cout << "run failed\n"; + return; + } + send_response(handle_request(std::move(request_))); +} + +void session::do_close() { + beast::error_code ec; + stream_.socket().shutdown(tcp::socket::shutdown_send, ec); +} + +void session::send_response(http::message_generator&& msg) { + beast::async_write( + stream_, std::move(msg), + beast::bind_front_handler(&session::on_write, shared_from_this(), + request_.keep_alive())); +} + +void session::on_write(bool keep_alive, + beast::error_code ec, + std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + + if (ec) { + std::cout << "write failed\n"; + return; + } + + if (!keep_alive) { + return do_close(); + } + + do_read(); +} diff --git a/apps/sse-contract-tests/src/stream_entity.cpp b/apps/sse-contract-tests/src/stream_entity.cpp new file mode 100644 index 000000000..93074871c --- /dev/null +++ b/apps/sse-contract-tests/src/stream_entity.cpp @@ -0,0 +1,138 @@ +#include "stream_entity.hpp" +#include "definitions.hpp" + +#include +#include +#include + +void fail(beast::error_code ec, char const* what) { + std::cerr << what << ": " << ec.message() << "\n"; +} + +// Periodically check the outbox (outgoing events to the test harness) +// at this interval. +auto const kFlushInterval = boost::posix_time::milliseconds{10}; + +stream_entity::stream_entity(net::any_io_executor executor, + std::shared_ptr client, + std::string callback_url) + : client_{std::move(client)}, + callback_url_{std::move(callback_url)}, + callback_port_{}, + callback_host_{}, + callback_counter_{0}, + executor_{executor}, + resolver_{executor}, + stream_{executor}, + outbox_{1024}, + flush_timer_{executor, boost::posix_time::milliseconds{0}} { + boost::system::result uri_components = + boost::urls::parse_uri(callback_url_); + + callback_host_ = uri_components->host(); + callback_port_ = uri_components->port(); +} + +void stream_entity::run() { + // Setup the SSE client to callback into the entity whenever it + // receives a comment/event. + client_->on_event( + [self = shared_from_this()](launchdarkly::sse::event_data ev) { + auto http_request = + self->build_request(self->callback_counter_++, std::move(ev)); + self->outbox_.push(http_request); + }); + + // Kickoff the SSE client's async operations. + client_->run(); + + // Begin connecting to the test harness's event-posting service. + resolver_.async_resolve( + callback_host_, callback_port_, + beast::bind_front_handler(&stream_entity::on_resolve, + shared_from_this())); +} + +void stream_entity::stop() { + beast::error_code ec; + stream_.socket().shutdown(tcp::socket::shutdown_both, ec); + + flush_timer_.cancel(); + client_->close(); +} + +stream_entity::request_type stream_entity::build_request( + std::size_t counter, + launchdarkly::sse::event_data ev) { + request_type req; + + req.set(http::field::host, callback_host_); + req.method(http::verb::get); + req.target(callback_url_ + "/" + std::to_string(counter)); + + nlohmann::json json; + + if (ev.get_type() == "comment") { + json = comment_message{"comment", ev.get_data()}; + } else { + json = event_message{"event", event{ev.get_type(), ev.get_data(), + ev.get_id().value_or("")}}; + } + + req.body() = json.dump(); + req.prepare_payload(); + return req; +} + +void stream_entity::on_resolve(beast::error_code ec, + tcp::resolver::results_type results) { + if (ec) + return fail(ec, "resolve"); + + // Make the connection on the IP address we get from a lookup. + beast::get_lowest_layer(stream_).async_connect( + results, beast::bind_front_handler(&stream_entity::on_connect, + shared_from_this())); +} + +void stream_entity::on_connect(beast::error_code ec, + tcp::resolver::results_type::endpoint_type) { + if (ec) + return fail(ec, "connect"); + + // Now that we're connected, kickoff the event flush "loop". + boost::system::error_code dummy; + net::post(executor_, + beast::bind_front_handler(&stream_entity::on_flush_timer, + shared_from_this(), dummy)); +} + +void stream_entity::on_flush_timer(boost::system::error_code ec) { + if (ec) { + return fail(ec, "flush"); + } + + if (!outbox_.empty()) { + request_type& request = outbox_.front(); + + // Flip-flop between this function and on_write; pushing an event + // and then popping it. + http::async_write(stream_, request, + beast::bind_front_handler(&stream_entity::on_write, + shared_from_this())); + return; + } + + // If the outbox is empty, wait a bit before trying again. + flush_timer_.expires_from_now(kFlushInterval); + flush_timer_.async_wait(beast::bind_front_handler( + &stream_entity::on_flush_timer, shared_from_this())); +} + +void stream_entity::on_write(beast::error_code ec, std::size_t) { + if (ec) + return fail(ec, "write"); + + outbox_.pop(); + on_flush_timer(boost::system::error_code{}); +} diff --git a/apps/sse-contract-tests/stream_entity.hpp b/apps/sse-contract-tests/stream_entity.hpp deleted file mode 100644 index 99b91df0a..000000000 --- a/apps/sse-contract-tests/stream_entity.hpp +++ /dev/null @@ -1,175 +0,0 @@ -#pragma once - -#include "definitions.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace beast = boost::beast; // from -namespace http = beast::http; // from -namespace net = boost::asio; // from -using tcp = boost::asio::ip::tcp; // from - -void fail(beast::error_code ec, char const* what) { - std::cerr << what << ": " << ec.message() << "\n"; -} - -// Periodically check the outbox (outgoing events to the test harness) -// at this interval. -auto const kFlushInterval = boost::posix_time::milliseconds{10}; - -class stream_entity : public std::enable_shared_from_this { - // Simple string body request is appropriate since the JSON - // returned to the test service is minimal. - using request_type = http::request; - - std::shared_ptr client_; - - std::string callback_url_; - std::string callback_port_; - std::string callback_host_; - size_t callback_counter_; - - net::any_io_executor executor_; - tcp::resolver resolver_; - beast::tcp_stream stream_; - - // When events are received from the SSE client, they are pushed into - // this queue. - boost::lockfree::spsc_queue outbox_; - // Periodically, the events are flushed to the test harness. - net::deadline_timer flush_timer_; - - public: - stream_entity(net::any_io_executor executor, - std::shared_ptr client, - std::string callback_url) - : client_{std::move(client)}, - callback_url_{std::move(callback_url)}, - callback_port_{}, - callback_host_{}, - callback_counter_{0}, - executor_{executor}, - resolver_{executor}, - stream_{executor}, - outbox_{1024}, - flush_timer_{executor, boost::posix_time::milliseconds{0}} { - boost::system::result uri_components = - boost::urls::parse_uri(callback_url_); - - callback_host_ = uri_components->host(); - callback_port_ = uri_components->port(); - } - - void run() { - // Setup the SSE client to callback into the entity whenever it - // receives a comment/event. - client_->on_event( - [self = shared_from_this()](launchdarkly::sse::event_data ev) { - auto http_request = self->build_request( - self->callback_counter_++, std::move(ev)); - self->outbox_.push(http_request); - }); - - // Kickoff the SSE client's async operations. - client_->run(); - - // Begin connecting to the test harness's event-posting service. - resolver_.async_resolve( - callback_host_, callback_port_, - beast::bind_front_handler(&stream_entity::on_resolve, - shared_from_this())); - } - - void stop() { - beast::error_code ec; - stream_.socket().shutdown(tcp::socket::shutdown_both, ec); - - flush_timer_.cancel(); - client_->close(); - } - - private: - request_type build_request(std::size_t counter, - launchdarkly::sse::event_data ev) { - request_type req; - - req.set(http::field::host, callback_host_); - req.method(http::verb::get); - req.target(callback_url_ + "/" + std::to_string(counter)); - - nlohmann::json json; - - if (ev.get_type() == "comment") { - json = comment_message{"comment", ev.get_data()}; - } else { - json = event_message{"event", event{ev.get_type(), ev.get_data(), - ev.get_id().value_or("")}}; - } - - req.body() = json.dump(); - req.prepare_payload(); - return req; - } - - void on_resolve(beast::error_code ec, tcp::resolver::results_type results) { - if (ec) - return fail(ec, "resolve"); - - // Make the connection on the IP address we get from a lookup. - beast::get_lowest_layer(stream_).async_connect( - results, beast::bind_front_handler(&stream_entity::on_connect, - shared_from_this())); - } - - void on_connect(beast::error_code ec, - tcp::resolver::results_type::endpoint_type) { - if (ec) - return fail(ec, "connect"); - - // Now that we're connected, kickoff the event flush "loop". - boost::system::error_code dummy; - net::post(executor_, - beast::bind_front_handler(&stream_entity::on_flush_timer, - shared_from_this(), dummy)); - } - - void on_flush_timer(boost::system::error_code ec) { - if (ec) { - return fail(ec, "flush"); - } - - if (!outbox_.empty()) { - request_type& request = outbox_.front(); - - // Flip-flop between this function and on_write; pushing an event - // and then popping it. - http::async_write( - stream_, request, - beast::bind_front_handler(&stream_entity::on_write, - shared_from_this())); - return; - } - - // If the outbox is empty, wait a bit before trying again. - flush_timer_.expires_from_now(kFlushInterval); - flush_timer_.async_wait(beast::bind_front_handler( - &stream_entity::on_flush_timer, shared_from_this())); - } - - void on_write(beast::error_code ec, std::size_t) { - if (ec) - return fail(ec, "write"); - - outbox_.pop(); - on_flush_timer(boost::system::error_code{}); - } -}; From 5f4b05bbafcf1cdbb474d1bc8b3ef3d89698f6cc Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 22 Mar 2023 13:05:40 -0700 Subject: [PATCH 18/53] follow style guide --- apps/sse-contract-tests/include/definitions.hpp | 14 +++++++------- apps/sse-contract-tests/include/server.hpp | 1 - apps/sse-contract-tests/src/entity_manager.cpp | 2 +- apps/sse-contract-tests/src/session.cpp | 8 ++++---- apps/sse-contract-tests/src/stream_entity.cpp | 4 ++-- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/apps/sse-contract-tests/include/definitions.hpp b/apps/sse-contract-tests/include/definitions.hpp index a5e58b059..88e63979a 100644 --- a/apps/sse-contract-tests/include/definitions.hpp +++ b/apps/sse-contract-tests/include/definitions.hpp @@ -55,24 +55,24 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigParams, body ); -struct event { +struct Event { std::string type; std::string data; std::string id; }; -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(event, type, data, id); +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(Event, type, data, id); -struct event_message { +struct EventMessage { std::string kind; - event event; + Event event; }; -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(event_message, kind, event); +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EventMessage, kind, event); -struct comment_message { +struct CommentMessage { std::string kind; std::string comment; }; -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(comment_message, kind, comment); +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(CommentMessage, kind, comment); diff --git a/apps/sse-contract-tests/include/server.hpp b/apps/sse-contract-tests/include/server.hpp index 069918092..e9d1a6943 100644 --- a/apps/sse-contract-tests/include/server.hpp +++ b/apps/sse-contract-tests/include/server.hpp @@ -1,6 +1,5 @@ #pragma once - #include "entity_manager.hpp" #include diff --git a/apps/sse-contract-tests/src/entity_manager.cpp b/apps/sse-contract-tests/src/entity_manager.cpp index 8ab19a78d..f03e83b4b 100644 --- a/apps/sse-contract-tests/src/entity_manager.cpp +++ b/apps/sse-contract-tests/src/entity_manager.cpp @@ -1,4 +1,4 @@ -#include "../include/entity_manager.hpp" +#include "entity_manager.hpp" #include "launchdarkly/sse/sse.hpp" diff --git a/apps/sse-contract-tests/src/session.cpp b/apps/sse-contract-tests/src/session.cpp index 0fa7bdb6e..379eff814 100644 --- a/apps/sse-contract-tests/src/session.cpp +++ b/apps/sse-contract-tests/src/session.cpp @@ -2,7 +2,7 @@ #include #include -const std::string ENTITY_PATH = "/entity/"; +const std::string kEntityPath = "/entity/"; http::message_generator session::handle_request( http::request&& req) { @@ -53,7 +53,7 @@ http::message_generator session::handle_request( auto const create_entity_response = [&req](std::string const& id) { http::response res{http::status::ok, req.version()}; res.keep_alive(req.keep_alive()); - res.set("Location", ENTITY_PATH + id); + res.set("Location", kEntityPath + id); res.prepare_payload(); return res; }; @@ -95,9 +95,9 @@ http::message_generator session::handle_request( } if (req.method() == http::verb::delete_ && - req.target().starts_with(ENTITY_PATH)) { + req.target().starts_with(kEntityPath)) { std::string id = req.target(); - boost::erase_first(id, ENTITY_PATH); + boost::erase_first(id, kEntityPath); bool erased = manager_.destroy(id); return destroy_entity_response(erased); } diff --git a/apps/sse-contract-tests/src/stream_entity.cpp b/apps/sse-contract-tests/src/stream_entity.cpp index 93074871c..a4299a10f 100644 --- a/apps/sse-contract-tests/src/stream_entity.cpp +++ b/apps/sse-contract-tests/src/stream_entity.cpp @@ -73,9 +73,9 @@ stream_entity::request_type stream_entity::build_request( nlohmann::json json; if (ev.get_type() == "comment") { - json = comment_message{"comment", ev.get_data()}; + json = CommentMessage{"comment", ev.get_data()}; } else { - json = event_message{"event", event{ev.get_type(), ev.get_data(), + json = EventMessage{"event", Event{ev.get_type(), ev.get_data(), ev.get_id().value_or("")}}; } From 30723f6e31ed677200cee9177adc50b5b6d2178a Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 22 Mar 2023 13:35:50 -0700 Subject: [PATCH 19/53] fix read errors on socket close --- apps/hello-cpp/main.cpp | 6 ------ apps/sse-contract-tests/src/stream_entity.cpp | 2 +- libs/server-sent-events/src/sse.cpp | 19 ++++++++++--------- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/apps/hello-cpp/main.cpp b/apps/hello-cpp/main.cpp index 6c09d799d..08095a3a8 100644 --- a/apps/hello-cpp/main.cpp +++ b/apps/hello-cpp/main.cpp @@ -29,11 +29,5 @@ int main() { } client->run(); - - -// client->on_event([](launchdarkly::sse::event_data e){ -// std::cout << "Got[" << e.get_type() << "] = <" << e.get_data() << ">\n"; -// }); - ioc.run(); } diff --git a/apps/sse-contract-tests/src/stream_entity.cpp b/apps/sse-contract-tests/src/stream_entity.cpp index a4299a10f..6d3fe9671 100644 --- a/apps/sse-contract-tests/src/stream_entity.cpp +++ b/apps/sse-contract-tests/src/stream_entity.cpp @@ -108,7 +108,7 @@ void stream_entity::on_connect(beast::error_code ec, } void stream_entity::on_flush_timer(boost::system::error_code ec) { - if (ec) { + if (ec && ec != net::error::operation_aborted) { return fail(ec, "flush"); } diff --git a/libs/server-sent-events/src/sse.cpp b/libs/server-sent-events/src/sse.cpp index 04800c3cd..c298a79d3 100644 --- a/libs/server-sent-events/src/sse.cpp +++ b/libs/server-sent-events/src/sse.cpp @@ -65,7 +65,9 @@ client::client(net::any_io_executor ex, m_cb{[](event_data e) { std::cout << "Event[" << e.get_type() << "] = <" << e.get_data() << ">\n"; - }} {} + }} { + parser_.body_limit(boost::none); +} // Report a failure void fail(beast::error_code ec, char const* what) { @@ -130,8 +132,7 @@ void client::parse_events() { m_event_data->append_data(field.second); } else if (field.first == "id") { if (field.second.find('\0') != std::string::npos) { - std::cout - << "Debug: ignoring event ID will null terminator\n"; + // IDs with null-terminators are acceptable, but ignored. continue; } last_event_id_ = field.second; @@ -265,7 +266,7 @@ class ssl_client : public client { void on_read(beast::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); if (ec) { - return fail(ec, "read"); + return fail(ec, "sse:read"); } beast::get_lowest_layer(stream_).expires_never(); @@ -276,8 +277,8 @@ class ssl_client : public client { } void on_stop() { - beast::error_code ec; - beast::close_socket(beast::get_lowest_layer(stream_)); + //beast::close_socket(beast::get_lowest_layer(stream_)); + beast::get_lowest_layer(stream_).cancel(); } public: @@ -362,7 +363,7 @@ class plaintext_client : public client { void on_read(beast::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); if (ec && ec != beast::errc::operation_canceled) { - return fail(ec, "read"); + return fail(ec, "sse:read"); } beast::get_lowest_layer(stream_).expires_never(); @@ -373,8 +374,8 @@ class plaintext_client : public client { } void on_stop() { - beast::error_code ec; - beast::close_socket(beast::get_lowest_layer(stream_)); + // beast::close_socket(beast::get_lowest_layer(stream_)); + stream_.cancel(); } public: From 13cd644d8ce9c80ece791e093d38452ccd7b249d Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 22 Mar 2023 13:59:41 -0700 Subject: [PATCH 20/53] rename stream_entity to StreamEntity --- .../include/entity_manager.hpp | 2 +- .../include/stream_entity.hpp | 4 +-- .../sse-contract-tests/src/entity_manager.cpp | 4 +-- apps/sse-contract-tests/src/stream_entity.cpp | 26 +++++++++---------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/apps/sse-contract-tests/include/entity_manager.hpp b/apps/sse-contract-tests/include/entity_manager.hpp index be0290906..cb6f62f1a 100644 --- a/apps/sse-contract-tests/include/entity_manager.hpp +++ b/apps/sse-contract-tests/include/entity_manager.hpp @@ -17,7 +17,7 @@ class EntityManager { // Maps the entity's ID to the entity. Shared pointer is necessary because // these entities are doing async IO and must remain alive as long as that // is happening. - std::unordered_map> entities_; + std::unordered_map> entities_; // Incremented each time create() is called to instantiate a new entity. std::size_t counter_; // Synchronizes access to create()/destroy(); diff --git a/apps/sse-contract-tests/include/stream_entity.hpp b/apps/sse-contract-tests/include/stream_entity.hpp index f119e9fff..9ffbdffc3 100644 --- a/apps/sse-contract-tests/include/stream_entity.hpp +++ b/apps/sse-contract-tests/include/stream_entity.hpp @@ -15,7 +15,7 @@ namespace http = beast::http; // from namespace net = boost::asio; // from using tcp = boost::asio::ip::tcp; // from -class stream_entity : public std::enable_shared_from_this { +class StreamEntity : public std::enable_shared_from_this { // Simple string body request is appropriate since the JSON // returned to the test service is minimal. using request_type = http::request; @@ -38,7 +38,7 @@ class stream_entity : public std::enable_shared_from_this { net::deadline_timer flush_timer_; public: - stream_entity(net::any_io_executor executor, + StreamEntity(net::any_io_executor executor, std::shared_ptr client, std::string callback_url); diff --git a/apps/sse-contract-tests/src/entity_manager.cpp b/apps/sse-contract-tests/src/entity_manager.cpp index f03e83b4b..bc9de662f 100644 --- a/apps/sse-contract-tests/src/entity_manager.cpp +++ b/apps/sse-contract-tests/src/entity_manager.cpp @@ -20,8 +20,8 @@ std::optional EntityManager::create(ConfigParams params) { if (!client) { return std::nullopt; } - std::shared_ptr entity = - std::make_shared(executor_, client, params.callbackUrl); + std::shared_ptr entity = + std::make_shared(executor_, client, params.callbackUrl); // Kicks off asynchronous operations. entity->run(); diff --git a/apps/sse-contract-tests/src/stream_entity.cpp b/apps/sse-contract-tests/src/stream_entity.cpp index 6d3fe9671..a35ec5fff 100644 --- a/apps/sse-contract-tests/src/stream_entity.cpp +++ b/apps/sse-contract-tests/src/stream_entity.cpp @@ -13,7 +13,7 @@ void fail(beast::error_code ec, char const* what) { // at this interval. auto const kFlushInterval = boost::posix_time::milliseconds{10}; -stream_entity::stream_entity(net::any_io_executor executor, +StreamEntity::StreamEntity(net::any_io_executor executor, std::shared_ptr client, std::string callback_url) : client_{std::move(client)}, @@ -33,7 +33,7 @@ stream_entity::stream_entity(net::any_io_executor executor, callback_port_ = uri_components->port(); } -void stream_entity::run() { +void StreamEntity::run() { // Setup the SSE client to callback into the entity whenever it // receives a comment/event. client_->on_event( @@ -49,11 +49,11 @@ void stream_entity::run() { // Begin connecting to the test harness's event-posting service. resolver_.async_resolve( callback_host_, callback_port_, - beast::bind_front_handler(&stream_entity::on_resolve, + beast::bind_front_handler(&StreamEntity::on_resolve, shared_from_this())); } -void stream_entity::stop() { +void StreamEntity::stop() { beast::error_code ec; stream_.socket().shutdown(tcp::socket::shutdown_both, ec); @@ -61,7 +61,7 @@ void stream_entity::stop() { client_->close(); } -stream_entity::request_type stream_entity::build_request( +StreamEntity::request_type StreamEntity::build_request( std::size_t counter, launchdarkly::sse::event_data ev) { request_type req; @@ -84,18 +84,18 @@ stream_entity::request_type stream_entity::build_request( return req; } -void stream_entity::on_resolve(beast::error_code ec, +void StreamEntity::on_resolve(beast::error_code ec, tcp::resolver::results_type results) { if (ec) return fail(ec, "resolve"); // Make the connection on the IP address we get from a lookup. beast::get_lowest_layer(stream_).async_connect( - results, beast::bind_front_handler(&stream_entity::on_connect, + results, beast::bind_front_handler(&StreamEntity::on_connect, shared_from_this())); } -void stream_entity::on_connect(beast::error_code ec, +void StreamEntity::on_connect(beast::error_code ec, tcp::resolver::results_type::endpoint_type) { if (ec) return fail(ec, "connect"); @@ -103,11 +103,11 @@ void stream_entity::on_connect(beast::error_code ec, // Now that we're connected, kickoff the event flush "loop". boost::system::error_code dummy; net::post(executor_, - beast::bind_front_handler(&stream_entity::on_flush_timer, + beast::bind_front_handler(&StreamEntity::on_flush_timer, shared_from_this(), dummy)); } -void stream_entity::on_flush_timer(boost::system::error_code ec) { +void StreamEntity::on_flush_timer(boost::system::error_code ec) { if (ec && ec != net::error::operation_aborted) { return fail(ec, "flush"); } @@ -118,7 +118,7 @@ void stream_entity::on_flush_timer(boost::system::error_code ec) { // Flip-flop between this function and on_write; pushing an event // and then popping it. http::async_write(stream_, request, - beast::bind_front_handler(&stream_entity::on_write, + beast::bind_front_handler(&StreamEntity::on_write, shared_from_this())); return; } @@ -126,10 +126,10 @@ void stream_entity::on_flush_timer(boost::system::error_code ec) { // If the outbox is empty, wait a bit before trying again. flush_timer_.expires_from_now(kFlushInterval); flush_timer_.async_wait(beast::bind_front_handler( - &stream_entity::on_flush_timer, shared_from_this())); + &StreamEntity::on_flush_timer, shared_from_this())); } -void stream_entity::on_write(beast::error_code ec, std::size_t) { +void StreamEntity::on_write(beast::error_code ec, std::size_t) { if (ec) return fail(ec, "write"); From 57c383452ca469521309b6aa5f6eee9bfa7cf8af Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 22 Mar 2023 14:05:47 -0700 Subject: [PATCH 21/53] forward declare StreamEntity in EntityManager --- apps/sse-contract-tests/include/entity_manager.hpp | 5 +++-- apps/sse-contract-tests/include/server.hpp | 5 +---- apps/sse-contract-tests/src/entity_manager.cpp | 2 +- apps/sse-contract-tests/src/main.cpp | 5 +++-- apps/sse-contract-tests/src/session.cpp | 3 +++ 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/sse-contract-tests/include/entity_manager.hpp b/apps/sse-contract-tests/include/entity_manager.hpp index cb6f62f1a..1d6617939 100644 --- a/apps/sse-contract-tests/include/entity_manager.hpp +++ b/apps/sse-contract-tests/include/entity_manager.hpp @@ -1,9 +1,8 @@ #pragma once #include "definitions.hpp" -#include "stream_entity.hpp" -#include +#include #include #include @@ -11,6 +10,8 @@ #include #include +class StreamEntity; + // Manages the individual SSE clients (called entities here) which are // instantiated for each contract test. class EntityManager { diff --git a/apps/sse-contract-tests/include/server.hpp b/apps/sse-contract-tests/include/server.hpp index e9d1a6943..94133c86c 100644 --- a/apps/sse-contract-tests/include/server.hpp +++ b/apps/sse-contract-tests/include/server.hpp @@ -8,9 +8,6 @@ #include -namespace beast = boost::beast; // from -namespace http = beast::http; // from -namespace net = boost::asio; // from using tcp = boost::asio::ip::tcp; // from class server { @@ -21,7 +18,7 @@ class server { std::vector caps_; public: - server(net::any_io_executor executor, + server(boost::asio::any_io_executor executor, boost::asio::ip::address const& address, unsigned short port); diff --git a/apps/sse-contract-tests/src/entity_manager.cpp b/apps/sse-contract-tests/src/entity_manager.cpp index bc9de662f..9311267a0 100644 --- a/apps/sse-contract-tests/src/entity_manager.cpp +++ b/apps/sse-contract-tests/src/entity_manager.cpp @@ -1,5 +1,5 @@ #include "entity_manager.hpp" - +#include "stream_entity.hpp" #include "launchdarkly/sse/sse.hpp" EntityManager::EntityManager(boost::asio::any_io_executor executor) diff --git a/apps/sse-contract-tests/src/main.cpp b/apps/sse-contract-tests/src/main.cpp index d325cd8f2..623b5105f 100644 --- a/apps/sse-contract-tests/src/main.cpp +++ b/apps/sse-contract-tests/src/main.cpp @@ -1,5 +1,6 @@ #include "server.hpp" +#include #include int @@ -7,9 +8,9 @@ main(int argc, char* argv[]) { try { - const auto address = net::ip::make_address("0.0.0.0"); + const auto address = boost::asio::ip::make_address("0.0.0.0"); unsigned short port = 8111; - net::io_context ioc; + boost::asio::io_context ioc; server s{ioc.get_executor(), address, port}; s.add_capability("headers"); diff --git a/apps/sse-contract-tests/src/session.cpp b/apps/sse-contract-tests/src/session.cpp index 379eff814..651591eeb 100644 --- a/apps/sse-contract-tests/src/session.cpp +++ b/apps/sse-contract-tests/src/session.cpp @@ -1,9 +1,12 @@ #include "session.hpp" #include +#include #include const std::string kEntityPath = "/entity/"; +namespace net = boost::asio; + http::message_generator session::handle_request( http::request&& req) { auto const bad_request = [&req](beast::string_view why) { From 609fc70126668855d5672a6628942a2adc180654 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 22 Mar 2023 16:15:54 -0700 Subject: [PATCH 22/53] various cleanups --- apps/sse-contract-tests/CMakeLists.txt | 2 + .../include/entity_manager.hpp | 4 +- apps/sse-contract-tests/include/session.hpp | 4 +- .../include/stream_entity.hpp | 13 +++- .../sse-contract-tests/src/entity_manager.cpp | 12 ++-- apps/sse-contract-tests/src/server.cpp | 2 +- apps/sse-contract-tests/src/session.cpp | 24 +++---- apps/sse-contract-tests/src/stream_entity.cpp | 68 ++++++++++--------- .../include/launchdarkly/sse/sse.hpp | 1 + libs/server-sent-events/src/sse.cpp | 5 +- 10 files changed, 77 insertions(+), 58 deletions(-) diff --git a/apps/sse-contract-tests/CMakeLists.txt b/apps/sse-contract-tests/CMakeLists.txt index 878f04fe1..8f87cf443 100644 --- a/apps/sse-contract-tests/CMakeLists.txt +++ b/apps/sse-contract-tests/CMakeLists.txt @@ -24,3 +24,5 @@ target_link_libraries(sse-tests PRIVATE ) target_include_directories(sse-tests PUBLIC include) + +#add_definitions(-DBOOST_ASIO_ENABLE_HANDLER_TRACKING) diff --git a/apps/sse-contract-tests/include/entity_manager.hpp b/apps/sse-contract-tests/include/entity_manager.hpp index 1d6617939..b36d80d4b 100644 --- a/apps/sse-contract-tests/include/entity_manager.hpp +++ b/apps/sse-contract-tests/include/entity_manager.hpp @@ -18,7 +18,7 @@ class EntityManager { // Maps the entity's ID to the entity. Shared pointer is necessary because // these entities are doing async IO and must remain alive as long as that // is happening. - std::unordered_map> entities_; + std::unordered_map> entities_; // Incremented each time create() is called to instantiate a new entity. std::size_t counter_; // Synchronizes access to create()/destroy(); @@ -29,4 +29,6 @@ class EntityManager { explicit EntityManager(boost::asio::any_io_executor executor); std::optional create(ConfigParams params); bool destroy(std::string const& id); + + friend class StreamEntity; }; diff --git a/apps/sse-contract-tests/include/session.hpp b/apps/sse-contract-tests/include/session.hpp index 93ac2da53..54ccb6e8e 100644 --- a/apps/sse-contract-tests/include/session.hpp +++ b/apps/sse-contract-tests/include/session.hpp @@ -12,7 +12,7 @@ namespace net = boost::asio; // from using tcp = boost::asio::ip::tcp; // from -class session : public std::enable_shared_from_this { +class Session : public std::enable_shared_from_this { // The socket for the currently connected client. beast::tcp_stream stream_; @@ -27,7 +27,7 @@ class session : public std::enable_shared_from_this { std::vector capabilities_; public: - explicit session(tcp::socket&& socket, + explicit Session(tcp::socket&& socket, EntityManager& manager, std::vector caps); void start(); diff --git a/apps/sse-contract-tests/include/stream_entity.hpp b/apps/sse-contract-tests/include/stream_entity.hpp index 9ffbdffc3..d4a594675 100644 --- a/apps/sse-contract-tests/include/stream_entity.hpp +++ b/apps/sse-contract-tests/include/stream_entity.hpp @@ -1,5 +1,7 @@ #pragma once +#include "entity_manager.hpp" + #include #include @@ -29,21 +31,24 @@ class StreamEntity : public std::enable_shared_from_this { net::any_io_executor executor_; tcp::resolver resolver_; - beast::tcp_stream stream_; + beast::tcp_stream event_stream_; // When events are received from the SSE client, they are pushed into // this queue. boost::lockfree::spsc_queue outbox_; // Periodically, the events are flushed to the test harness. net::deadline_timer flush_timer_; + std::string id_; public: StreamEntity(net::any_io_executor executor, - std::shared_ptr client, - std::string callback_url); + std::shared_ptr client, + std::string callback_url); + ~StreamEntity() = default; void run(); void stop(); + private: request_type build_request(std::size_t counter, launchdarkly::sse::event_data ev); @@ -52,4 +57,6 @@ class StreamEntity : public std::enable_shared_from_this { tcp::resolver::results_type::endpoint_type); void on_flush_timer(boost::system::error_code ec); void on_write(beast::error_code ec, std::size_t); + + void do_shutdown(beast::error_code ec, std::string what); }; diff --git a/apps/sse-contract-tests/src/entity_manager.cpp b/apps/sse-contract-tests/src/entity_manager.cpp index 9311267a0..9a95bfa74 100644 --- a/apps/sse-contract-tests/src/entity_manager.cpp +++ b/apps/sse-contract-tests/src/entity_manager.cpp @@ -21,12 +21,12 @@ std::optional EntityManager::create(ConfigParams params) { return std::nullopt; } std::shared_ptr entity = - std::make_shared(executor_, client, params.callbackUrl); + std::make_shared(executor_, client, params.callbackUrl); // Kicks off asynchronous operations. entity->run(); - entities_.emplace(id, entity); + entities_.emplace(id, entity); return id; } @@ -36,8 +36,10 @@ bool EntityManager::destroy(std::string const& id) { if (it == entities_.end()) { return false; } - // Shuts down asynchronous operations. - it->second->stop(); - entities_.erase(it); + + if (auto weak = it->second.lock()) { + weak->stop(); + } + return true; } diff --git a/apps/sse-contract-tests/src/server.cpp b/apps/sse-contract-tests/src/server.cpp index d9575e85e..584124f81 100644 --- a/apps/sse-contract-tests/src/server.cpp +++ b/apps/sse-contract-tests/src/server.cpp @@ -40,7 +40,7 @@ void server::stop() { void server::accept_connection() { acceptor_.async_accept([this](beast::error_code ec, tcp::socket peer) { if (!ec && !stopped_) { - std::make_shared(std::move(peer), manager_, caps_) + std::make_shared(std::move(peer), manager_, caps_) ->start(); accept_connection(); } diff --git a/apps/sse-contract-tests/src/session.cpp b/apps/sse-contract-tests/src/session.cpp index 651591eeb..972c8882b 100644 --- a/apps/sse-contract-tests/src/session.cpp +++ b/apps/sse-contract-tests/src/session.cpp @@ -7,7 +7,7 @@ const std::string kEntityPath = "/entity/"; namespace net = boost::asio; -http::message_generator session::handle_request( +http::message_generator Session::handle_request( http::request&& req) { auto const bad_request = [&req](beast::string_view why) { http::response res{http::status::bad_request, @@ -105,30 +105,30 @@ http::message_generator session::handle_request( return destroy_entity_response(erased); } - return bad_request("unknown route"); + return not_found(req.target()); } -session::session(tcp::socket&& socket, +Session::Session(tcp::socket&& socket, EntityManager& manager, std::vector caps) : stream_{std::move(socket)}, manager_{manager}, capabilities_{std::move(caps)} {} -void session::start() { +void Session::start() { net::dispatch( stream_.get_executor(), - beast::bind_front_handler(&session::do_read, shared_from_this())); + beast::bind_front_handler(&Session::do_read, shared_from_this())); } -void session::do_read() { +void Session::do_read() { request_ = {}; http::async_read( stream_, buffer_, request_, - beast::bind_front_handler(&session::on_read, shared_from_this())); + beast::bind_front_handler(&Session::on_read, shared_from_this())); } -void session::on_read(beast::error_code ec, std::size_t bytes_transferred) { +void Session::on_read(beast::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); if (ec == http::error::end_of_stream) { @@ -141,19 +141,19 @@ void session::on_read(beast::error_code ec, std::size_t bytes_transferred) { send_response(handle_request(std::move(request_))); } -void session::do_close() { +void Session::do_close() { beast::error_code ec; stream_.socket().shutdown(tcp::socket::shutdown_send, ec); } -void session::send_response(http::message_generator&& msg) { +void Session::send_response(http::message_generator&& msg) { beast::async_write( stream_, std::move(msg), - beast::bind_front_handler(&session::on_write, shared_from_this(), + beast::bind_front_handler(&Session::on_write, shared_from_this(), request_.keep_alive())); } -void session::on_write(bool keep_alive, +void Session::on_write(bool keep_alive, beast::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); diff --git a/apps/sse-contract-tests/src/stream_entity.cpp b/apps/sse-contract-tests/src/stream_entity.cpp index a35ec5fff..203848a4c 100644 --- a/apps/sse-contract-tests/src/stream_entity.cpp +++ b/apps/sse-contract-tests/src/stream_entity.cpp @@ -5,17 +5,13 @@ #include #include -void fail(beast::error_code ec, char const* what) { - std::cerr << what << ": " << ec.message() << "\n"; -} - // Periodically check the outbox (outgoing events to the test harness) // at this interval. auto const kFlushInterval = boost::posix_time::milliseconds{10}; StreamEntity::StreamEntity(net::any_io_executor executor, - std::shared_ptr client, - std::string callback_url) + std::shared_ptr client, + std::string callback_url) : client_{std::move(client)}, callback_url_{std::move(callback_url)}, callback_port_{}, @@ -23,7 +19,7 @@ StreamEntity::StreamEntity(net::any_io_executor executor, callback_counter_{0}, executor_{executor}, resolver_{executor}, - stream_{executor}, + event_stream_{executor}, outbox_{1024}, flush_timer_{executor, boost::posix_time::milliseconds{0}} { boost::system::result uri_components = @@ -33,6 +29,12 @@ StreamEntity::StreamEntity(net::any_io_executor executor, callback_port_ = uri_components->port(); } +void StreamEntity::do_shutdown(beast::error_code ec, std::string what) { + event_stream_.socket().shutdown(tcp::socket::shutdown_both, ec); + flush_timer_.cancel(); + client_->close(); +} + void StreamEntity::run() { // Setup the SSE client to callback into the entity whenever it // receives a comment/event. @@ -47,18 +49,17 @@ void StreamEntity::run() { client_->run(); // Begin connecting to the test harness's event-posting service. - resolver_.async_resolve( - callback_host_, callback_port_, - beast::bind_front_handler(&StreamEntity::on_resolve, - shared_from_this())); + resolver_.async_resolve(callback_host_, callback_port_, + beast::bind_front_handler(&StreamEntity::on_resolve, + shared_from_this())); } void StreamEntity::stop() { - beast::error_code ec; - stream_.socket().shutdown(tcp::socket::shutdown_both, ec); - - flush_timer_.cancel(); - client_->close(); + beast::error_code ec = net::error::basic_errors::operation_aborted; + std::string reason = "stop"; + net::post(executor_, + beast::bind_front_handler(&StreamEntity::do_shutdown, + shared_from_this(), ec, reason)); } StreamEntity::request_type StreamEntity::build_request( @@ -76,7 +77,7 @@ StreamEntity::request_type StreamEntity::build_request( json = CommentMessage{"comment", ev.get_data()}; } else { json = EventMessage{"event", Event{ev.get_type(), ev.get_data(), - ev.get_id().value_or("")}}; + ev.get_id().value_or("")}}; } req.body() = json.dump(); @@ -85,20 +86,23 @@ StreamEntity::request_type StreamEntity::build_request( } void StreamEntity::on_resolve(beast::error_code ec, - tcp::resolver::results_type results) { - if (ec) - return fail(ec, "resolve"); + tcp::resolver::results_type results) { + if (ec) { + return do_shutdown(ec, "resolve"); + } // Make the connection on the IP address we get from a lookup. - beast::get_lowest_layer(stream_).async_connect( - results, beast::bind_front_handler(&StreamEntity::on_connect, - shared_from_this())); + beast::get_lowest_layer(event_stream_) + .async_connect(results, + beast::bind_front_handler(&StreamEntity::on_connect, + shared_from_this())); } void StreamEntity::on_connect(beast::error_code ec, - tcp::resolver::results_type::endpoint_type) { - if (ec) - return fail(ec, "connect"); + tcp::resolver::results_type::endpoint_type) { + if (ec) { + return do_shutdown(ec, "connect"); + } // Now that we're connected, kickoff the event flush "loop". boost::system::error_code dummy; @@ -108,8 +112,8 @@ void StreamEntity::on_connect(beast::error_code ec, } void StreamEntity::on_flush_timer(boost::system::error_code ec) { - if (ec && ec != net::error::operation_aborted) { - return fail(ec, "flush"); + if (ec) { + return do_shutdown(ec, "flush"); } if (!outbox_.empty()) { @@ -117,7 +121,7 @@ void StreamEntity::on_flush_timer(boost::system::error_code ec) { // Flip-flop between this function and on_write; pushing an event // and then popping it. - http::async_write(stream_, request, + http::async_write(event_stream_, request, beast::bind_front_handler(&StreamEntity::on_write, shared_from_this())); return; @@ -130,9 +134,9 @@ void StreamEntity::on_flush_timer(boost::system::error_code ec) { } void StreamEntity::on_write(beast::error_code ec, std::size_t) { - if (ec) - return fail(ec, "write"); - + if (ec) { + return do_shutdown(ec, "write"); + } outbox_.pop(); on_flush_timer(boost::system::error_code{}); } diff --git a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp index 20ff1a799..095138776 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp @@ -94,6 +94,7 @@ class client : public std::enable_shared_from_this { http::request req, std::string host, std::string port); + ~client(); template void on_event(Callback event_cb) { diff --git a/libs/server-sent-events/src/sse.cpp b/libs/server-sent-events/src/sse.cpp index c298a79d3..8040db504 100644 --- a/libs/server-sent-events/src/sse.cpp +++ b/libs/server-sent-events/src/sse.cpp @@ -68,6 +68,9 @@ client::client(net::any_io_executor ex, }} { parser_.body_limit(boost::none); } +client::~client() { + std::cout << "~client\n"; +} // Report a failure void fail(beast::error_code ec, char const* what) { @@ -277,7 +280,6 @@ class ssl_client : public client { } void on_stop() { - //beast::close_socket(beast::get_lowest_layer(stream_)); beast::get_lowest_layer(stream_).cancel(); } @@ -374,7 +376,6 @@ class plaintext_client : public client { } void on_stop() { - // beast::close_socket(beast::get_lowest_layer(stream_)); stream_.cancel(); } From 33e697ece9ff32a1197858f639ea7edab44656f6 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 23 Mar 2023 10:55:03 -0700 Subject: [PATCH 23/53] Fix shutdown behavior --- .../include/entity_manager.hpp | 2 + apps/sse-contract-tests/include/server.hpp | 23 ++-- apps/sse-contract-tests/include/session.hpp | 21 +++- .../include/stream_entity.hpp | 2 +- .../sse-contract-tests/src/entity_manager.cpp | 14 ++- apps/sse-contract-tests/src/main.cpp | 30 ++--- apps/sse-contract-tests/src/server.cpp | 109 +++++++++++++----- apps/sse-contract-tests/src/session.cpp | 61 +++++++--- apps/sse-contract-tests/src/stream_entity.cpp | 4 + libs/server-sent-events/src/sse.cpp | 51 +++++++- 10 files changed, 239 insertions(+), 78 deletions(-) diff --git a/apps/sse-contract-tests/include/entity_manager.hpp b/apps/sse-contract-tests/include/entity_manager.hpp index b36d80d4b..34d1df8d9 100644 --- a/apps/sse-contract-tests/include/entity_manager.hpp +++ b/apps/sse-contract-tests/include/entity_manager.hpp @@ -30,5 +30,7 @@ class EntityManager { std::optional create(ConfigParams params); bool destroy(std::string const& id); + void destroy_all(); + friend class StreamEntity; }; diff --git a/apps/sse-contract-tests/include/server.hpp b/apps/sse-contract-tests/include/server.hpp index 94133c86c..08861ca35 100644 --- a/apps/sse-contract-tests/include/server.hpp +++ b/apps/sse-contract-tests/include/server.hpp @@ -1,31 +1,32 @@ #pragma once #include "entity_manager.hpp" - #include #include +#include -#include +namespace net = boost::asio; // from +#include using tcp = boost::asio::ip::tcp; // from -class server { +class server : public std::enable_shared_from_this { + net::io_context& ioc_; tcp::acceptor acceptor_; bool stopped_; - boost::asio::signal_set signals_; - EntityManager manager_; + EntityManager entity_manager_; std::vector caps_; public: - server(boost::asio::any_io_executor executor, - boost::asio::ip::address const& address, - unsigned short port); - + server(net::io_context& ioc, std::string const& address, std::string const& port); void add_capability(std::string cap); - void start(); + void run(); void stop(); private: - void accept_connection(); + void do_accept(); + void on_accept(const boost::system::error_code& ec, tcp::socket socket); + + void do_stop(); }; diff --git a/apps/sse-contract-tests/include/session.hpp b/apps/sse-contract-tests/include/session.hpp index 54ccb6e8e..73314c425 100644 --- a/apps/sse-contract-tests/include/session.hpp +++ b/apps/sse-contract-tests/include/session.hpp @@ -1,7 +1,6 @@ #pragma once #include "entity_manager.hpp" - #include #include #include @@ -26,14 +25,30 @@ class Session : public std::enable_shared_from_this { std::vector capabilities_; + std::function on_shutdown_cb_; + + bool shutdown_requested_; + public: explicit Session(tcp::socket&& socket, EntityManager& manager, std::vector caps); + + ~Session(); + template + void on_shutdown(Callback cb) { + on_shutdown_cb_ = cb; + } + void start(); + void stop(); + + private: + http::message_generator handle_request(http::request&& req); void do_read(); + void do_stop(); void on_read(beast::error_code ec, std::size_t bytes_transferred); void do_close(); @@ -42,6 +57,6 @@ class Session : public std::enable_shared_from_this { void on_write(bool keep_alive, beast::error_code ec, std::size_t bytes_transferred); - private: - http::message_generator handle_request(http::request&& req); }; + +using SessionPtr = std::shared_ptr; diff --git a/apps/sse-contract-tests/include/stream_entity.hpp b/apps/sse-contract-tests/include/stream_entity.hpp index d4a594675..b13bc94fc 100644 --- a/apps/sse-contract-tests/include/stream_entity.hpp +++ b/apps/sse-contract-tests/include/stream_entity.hpp @@ -45,7 +45,7 @@ class StreamEntity : public std::enable_shared_from_this { std::shared_ptr client, std::string callback_url); - ~StreamEntity() = default; + ~StreamEntity(); void run(); void stop(); diff --git a/apps/sse-contract-tests/src/entity_manager.cpp b/apps/sse-contract-tests/src/entity_manager.cpp index 9a95bfa74..ea410cd7e 100644 --- a/apps/sse-contract-tests/src/entity_manager.cpp +++ b/apps/sse-contract-tests/src/entity_manager.cpp @@ -1,6 +1,6 @@ #include "entity_manager.hpp" -#include "stream_entity.hpp" #include "launchdarkly/sse/sse.hpp" +#include "stream_entity.hpp" EntityManager::EntityManager(boost::asio::any_io_executor executor) : entities_{}, counter_{0}, lock_{}, executor_{std::move(executor)} {} @@ -21,7 +21,7 @@ std::optional EntityManager::create(ConfigParams params) { return std::nullopt; } std::shared_ptr entity = - std::make_shared(executor_, client, params.callbackUrl); + std::make_shared(executor_, client, params.callbackUrl); // Kicks off asynchronous operations. entity->run(); @@ -30,6 +30,14 @@ std::optional EntityManager::create(ConfigParams params) { return id; } +void EntityManager::destroy_all() { + for (auto& entity : entities_) { + if (auto weak = entity.second.lock()) { + weak->stop(); + } + } + entities_.clear(); +} bool EntityManager::destroy(std::string const& id) { std::lock_guard guard{lock_}; auto it = entities_.find(id); @@ -41,5 +49,7 @@ bool EntityManager::destroy(std::string const& id) { weak->stop(); } + entities_.erase(it); + return true; } diff --git a/apps/sse-contract-tests/src/main.cpp b/apps/sse-contract-tests/src/main.cpp index 623b5105f..725ab61e0 100644 --- a/apps/sse-contract-tests/src/main.cpp +++ b/apps/sse-contract-tests/src/main.cpp @@ -1,27 +1,27 @@ #include "server.hpp" #include +#include +#include #include +#include -int -main(int argc, char* argv[]) -{ - try - { - const auto address = boost::asio::ip::make_address("0.0.0.0"); - unsigned short port = 8111; - boost::asio::io_context ioc; +namespace net = boost::asio; +namespace beast = boost::beast; - server s{ioc.get_executor(), address, port}; - s.add_capability("headers"); - s.start(); +int main(int argc, char* argv[]) { + try { + net::io_context ioc{1}; - std::cout << "listening on " << address << ":" << port << std::endl; + auto s = std::make_shared(ioc, "0.0.0.0", "8111"); + s->add_capability("headers"); + s->run(); + + net::signal_set signals{ioc, SIGINT, SIGTERM}; + signals.async_wait([&](beast::error_code const&, int) { ioc.stop(); }); ioc.run(); - } - catch(std::exception const& e) - { + } catch (std::exception const& e) { std::cerr << "Error: " << e.what() << std::endl; return EXIT_FAILURE; } diff --git a/apps/sse-contract-tests/src/server.cpp b/apps/sse-contract-tests/src/server.cpp index 584124f81..e16a94df0 100644 --- a/apps/sse-contract-tests/src/server.cpp +++ b/apps/sse-contract-tests/src/server.cpp @@ -1,48 +1,99 @@ #include "server.hpp" #include "session.hpp" +#include +#include +#include #include #include +void fail(beast::error_code ec, char const* what) { + std::cerr << what << ": " << ec.message() << "\n"; +} -server::server(net::any_io_executor executor, - boost::asio::ip::address const& address, - unsigned short port) - : acceptor_{executor, {address, port}}, +server::server(net::io_context& ioc, + std::string const& address, + std::string const& port) + : ioc_{ioc}, stopped_{false}, - signals_{executor}, - manager_{executor}, + acceptor_{ioc}, + entity_manager_{ioc.get_executor()}, caps_{} { - signals_.add(SIGTERM); - signals_.add(SIGINT); - signals_.async_wait(boost::bind(&server::stop, this)); - acceptor_.set_option(tcp::acceptor::reuse_address(true)); + beast::error_code ec; + + tcp::resolver resolver{ioc_}; + tcp::endpoint endpoint = *resolver.resolve(address, port, ec).begin(); + if (ec) { + fail(ec, "resolve"); + return; + } + acceptor_.open(endpoint.protocol(), ec); + if (ec) { + fail(ec, "open"); + return; + } + acceptor_.set_option(tcp::acceptor::reuse_address(true), ec); + if (ec) { + fail(ec, "set_option"); + return; + } + acceptor_.bind(endpoint, ec); + if (ec) { + fail(ec, "bind"); + return; + } + acceptor_.listen(net::socket_base::max_listen_connections, ec); + if (ec) { + fail(ec, "listen"); + return; + } + + std::cout << "listening on " << address << ":" << port << std::endl; } void server::add_capability(std::string cap) { caps_.push_back(std::move(cap)); } -void server::start() { - acceptor_.listen(); - accept_connection(); +void server::run() { + net::dispatch( + acceptor_.get_executor(), + beast::bind_front_handler(&server::do_accept, shared_from_this())); } void server::stop() { - boost::asio::post(acceptor_.get_executor(), [this]() { - std::cout << "stopping server\n"; - acceptor_.cancel(); - stopped_ = true; - std::cout << "server stopped\n"; - }); -} - -void server::accept_connection() { - acceptor_.async_accept([this](beast::error_code ec, tcp::socket peer) { - if (!ec && !stopped_) { - std::make_shared(std::move(peer), manager_, caps_) - ->start(); - accept_connection(); - } - }); + std::cout << "server: stop\n"; + net::dispatch( + acceptor_.get_executor(), + beast::bind_front_handler(&server::do_stop, shared_from_this())); +} + +void server::do_accept() { + acceptor_.async_accept( + net::make_strand(ioc_), + beast::bind_front_handler(&server::on_accept, shared_from_this())); +} + +void server::do_stop() { + std::cout << "server: do_stop\n"; + entity_manager_.destroy_all(); +} + +void server::on_accept(boost::system::error_code const& ec, + tcp::socket socket) { + if (!acceptor_.is_open()) { + return; + } + if (ec) { + fail(ec, "accept"); + return; + } + auto session = + std::make_shared(std::move(socket), entity_manager_, caps_); + + session->on_shutdown([this]() { ioc_.stop(); }); + + session->start(); + + do_accept(); } diff --git a/apps/sse-contract-tests/src/session.cpp b/apps/sse-contract-tests/src/session.cpp index 972c8882b..afb01fe18 100644 --- a/apps/sse-contract-tests/src/session.cpp +++ b/apps/sse-contract-tests/src/session.cpp @@ -69,18 +69,25 @@ http::message_generator Session::handle_request( return res; }; + auto const shutdown_server_response = [&req]() { + http::response res{http::status::ok, req.version()}; + res.keep_alive(false); + res.prepare_payload(); + return res; + }; + if (req.method() == http::verb::get && req.target() == "/") { return capabilities_response(capabilities_); } if (req.method() == http::verb::head && req.target() == "/") { - http::response res{http::status::ok, req.version()}; - return res; + return http::response{http::status::ok, + req.version()}; } if (req.method() == http::verb::delete_ && req.target() == "/") { - // not clean, but doesn't matter from the test-harness's perspective. - std::raise(SIGTERM); + shutdown_requested_ = true; + return shutdown_server_response(); } if (req.method() == http::verb::post && req.target() == "/") { @@ -113,7 +120,15 @@ Session::Session(tcp::socket&& socket, std::vector caps) : stream_{std::move(socket)}, manager_{manager}, - capabilities_{std::move(caps)} {} + capabilities_{std::move(caps)}, + on_shutdown_cb_{}, + shutdown_requested_{false} { + std::cout << "session created\n"; +} + +Session::~Session() { + std::cout << "~session\n"; +} void Session::start() { net::dispatch( @@ -121,8 +136,20 @@ void Session::start() { beast::bind_front_handler(&Session::do_read, shared_from_this())); } +void Session::stop() { + net::dispatch( + stream_.get_executor(), + beast::bind_front_handler(&Session::do_stop, shared_from_this())); +} + +void Session::do_stop() { + stream_.close(); +} + void Session::do_read() { request_ = {}; + + std::cout << "waiting for request\n"; http::async_read( stream_, buffer_, request_, beast::bind_front_handler(&Session::on_read, shared_from_this())); @@ -132,18 +159,16 @@ void Session::on_read(beast::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); if (ec == http::error::end_of_stream) { - return do_close(); + std::cout << "end of stream" << std::endl; + return do_stop(); } + if (ec) { - std::cout << "run failed\n"; - return; + std::cout << "read failed" << ec << std::endl; + return do_stop(); } - send_response(handle_request(std::move(request_))); -} -void Session::do_close() { - beast::error_code ec; - stream_.socket().shutdown(tcp::socket::shutdown_send, ec); + send_response(handle_request(std::move(request_))); } void Session::send_response(http::message_generator&& msg) { @@ -158,13 +183,19 @@ void Session::on_write(bool keep_alive, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); + if (shutdown_requested_ && on_shutdown_cb_) { + on_shutdown_cb_(); + shutdown_requested_ = false; + } + if (ec) { std::cout << "write failed\n"; - return; + return do_stop(); } if (!keep_alive) { - return do_close(); + std::cout << "client requested to drop the connection\n"; + return do_stop(); } do_read(); diff --git a/apps/sse-contract-tests/src/stream_entity.cpp b/apps/sse-contract-tests/src/stream_entity.cpp index 203848a4c..2c625a5f9 100644 --- a/apps/sse-contract-tests/src/stream_entity.cpp +++ b/apps/sse-contract-tests/src/stream_entity.cpp @@ -29,6 +29,10 @@ StreamEntity::StreamEntity(net::any_io_executor executor, callback_port_ = uri_components->port(); } +StreamEntity::~StreamEntity() { + std::cout << "~StreamEntity\n"; +} + void StreamEntity::do_shutdown(beast::error_code ec, std::string what) { event_stream_.socket().shutdown(tcp::socket::shutdown_both, ec); flush_timer_.cancel(); diff --git a/libs/server-sent-events/src/sse.cpp b/libs/server-sent-events/src/sse.cpp index 8040db504..9435168f9 100644 --- a/libs/server-sent-events/src/sse.cpp +++ b/libs/server-sent-events/src/sse.cpp @@ -55,6 +55,7 @@ client::client(net::any_io_executor ex, host_{std::move(host)}, port_{std::move(port)}, m_request{std::move(req)}, + m_response{}, buffered_line_{}, complete_lines_{}, begin_CR_{false}, @@ -317,6 +318,7 @@ class ssl_client : public client { class plaintext_client : public client { beast::tcp_stream stream_; + ssl::context& ctx_; std::shared_ptr shared() { return std::static_pointer_cast(shared_from_this()); @@ -357,6 +359,48 @@ class plaintext_client : public client { parser_.on_chunk_body(*this->on_chunk_body_trampoline_); + http::async_read( + stream_, m_buffer, parser_, + beast::bind_front_handler(&plaintext_client::on_got_headers, shared())); + } + + void on_got_headers(beast::error_code ec, std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + + if (ec) { + return fail(ec, "sse:headers"); + } + + if (parser_.is_header_done()) { + auto status = parser_.get().result(); + if (status == http::status::moved_permanently) { + if (auto it = parser_.get().find("Location"); + it != parser_.get().end()) { + + boost::system::result uri_components = + boost::urls::parse_uri(it->value()); + if (!uri_components) { + return fail(boost::asio::error::host_unreachable, "invalid 301 redirect"); + } + + host_ = uri_components->host(); + port_ = uri_components->has_port() ? uri_components->port() : "80"; + m_request.set(http::field::host, uri_components->host()); + m_request.target(uri_components->path()); + + auto client = std::make_shared(stream_.get_executor(), + ctx_, m_request, + host_, port_); + client->m_cb = m_cb; + client->run(); + return; + } + } + + } + + beast::get_lowest_layer(stream_).expires_never(); + http::async_read_some( stream_, m_buffer, parser_, beast::bind_front_handler(&plaintext_client::on_read, shared())); @@ -368,7 +412,7 @@ class plaintext_client : public client { return fail(ec, "sse:read"); } - beast::get_lowest_layer(stream_).expires_never(); + http::async_read_some( stream_, m_buffer, parser_, @@ -386,7 +430,10 @@ class plaintext_client : public client { std::string host, std::string port) : client(ex, std::move(req), std::move(host), std::move(port)), - stream_{ex} {} + stream_{ex}, ctx_{ctx} { + + std::cout << "construct client: " << host_ << port_ << req.target() << '\n'; + } void run() override { beast::get_lowest_layer(stream_).expires_after( From bd4cd8169a8e2cd54bf370c66c7ab657647e95d8 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 23 Mar 2023 11:46:02 -0700 Subject: [PATCH 24/53] integrate common logger --- apps/sse-contract-tests/CMakeLists.txt | 1 + .../include/definitions.hpp | 34 +++++++++---------- .../include/entity_manager.hpp | 2 +- apps/sse-contract-tests/include/server.hpp | 19 +++++++---- apps/sse-contract-tests/include/session.hpp | 10 +++--- apps/sse-contract-tests/src/main.cpp | 22 +++++++++--- apps/sse-contract-tests/src/server.cpp | 19 +++++++---- 7 files changed, 65 insertions(+), 42 deletions(-) diff --git a/apps/sse-contract-tests/CMakeLists.txt b/apps/sse-contract-tests/CMakeLists.txt index 8f87cf443..73000339c 100644 --- a/apps/sse-contract-tests/CMakeLists.txt +++ b/apps/sse-contract-tests/CMakeLists.txt @@ -20,6 +20,7 @@ add_executable(sse-tests target_link_libraries(sse-tests PRIVATE launchdarkly::sse + launchdarkly::common nlohmann_json::nlohmann_json ) diff --git a/apps/sse-contract-tests/include/definitions.hpp b/apps/sse-contract-tests/include/definitions.hpp index 88e63979a..b75887f44 100644 --- a/apps/sse-contract-tests/include/definitions.hpp +++ b/apps/sse-contract-tests/include/definitions.hpp @@ -8,28 +8,27 @@ namespace nlohmann { - template - struct adl_serializer> { - static void to_json(json& j, const std::optional& opt) { +template +struct adl_serializer> { + static void to_json(json& j, std::optional const& opt) { if (opt == std::nullopt) { j = nullptr; } else { - j = *opt; // this will call adl_serializer::to_json which will + j = *opt; // this will call adl_serializer::to_json which will // find the free function to_json in T's namespace! } } - static void from_json(const json& j, std::optional& opt) { + static void from_json(json const& j, std::optional& opt) { if (j.is_null()) { opt = std::nullopt; } else { - opt = j.get(); // same as above, but with + opt = j.get(); // same as above, but with // adl_serializer::from_json } } }; -} // namespace nlohmann - +} // namespace nlohmann struct ConfigParams { std::string streamUrl; @@ -44,16 +43,15 @@ struct ConfigParams { }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigParams, - streamUrl, - callbackUrl, - tag, - initialDelayMs, - readTimeoutMs, - lastEventId, - headers, - method, - body -); + streamUrl, + callbackUrl, + tag, + initialDelayMs, + readTimeoutMs, + lastEventId, + headers, + method, + body); struct Event { std::string type; diff --git a/apps/sse-contract-tests/include/entity_manager.hpp b/apps/sse-contract-tests/include/entity_manager.hpp index 34d1df8d9..6abfb4e30 100644 --- a/apps/sse-contract-tests/include/entity_manager.hpp +++ b/apps/sse-contract-tests/include/entity_manager.hpp @@ -6,9 +6,9 @@ #include #include -#include #include #include +#include class StreamEntity; diff --git a/apps/sse-contract-tests/include/server.hpp b/apps/sse-contract-tests/include/server.hpp index 08861ca35..5873b6346 100644 --- a/apps/sse-contract-tests/include/server.hpp +++ b/apps/sse-contract-tests/include/server.hpp @@ -1,11 +1,13 @@ #pragma once -#include "entity_manager.hpp" +#include #include #include -#include +#include +#include "entity_manager.hpp" +#include "logger.hpp" -namespace net = boost::asio; // from +namespace net = boost::asio; // from #include @@ -14,19 +16,22 @@ using tcp = boost::asio::ip::tcp; // from class server : public std::enable_shared_from_this { net::io_context& ioc_; tcp::acceptor acceptor_; - bool stopped_; EntityManager entity_manager_; std::vector caps_; + launchdarkly::Logger& logger_; public: - server(net::io_context& ioc, std::string const& address, std::string const& port); + server(net::io_context& ioc, + std::string const& address, + std::string const& port, + launchdarkly::Logger& logger); void add_capability(std::string cap); void run(); void stop(); private: void do_accept(); - void on_accept(const boost::system::error_code& ec, tcp::socket socket); - + void on_accept(boost::system::error_code const& ec, tcp::socket socket); + void fail(boost::beast::error_code ec, char const* what); void do_stop(); }; diff --git a/apps/sse-contract-tests/include/session.hpp b/apps/sse-contract-tests/include/session.hpp index 73314c425..9635956d4 100644 --- a/apps/sse-contract-tests/include/session.hpp +++ b/apps/sse-contract-tests/include/session.hpp @@ -1,16 +1,15 @@ #pragma once -#include "entity_manager.hpp" -#include #include +#include #include +#include "entity_manager.hpp" namespace beast = boost::beast; // from namespace http = beast::http; // from namespace net = boost::asio; // from using tcp = boost::asio::ip::tcp; // from - class Session : public std::enable_shared_from_this { // The socket for the currently connected client. beast::tcp_stream stream_; @@ -35,7 +34,7 @@ class Session : public std::enable_shared_from_this { std::vector caps); ~Session(); - template + template void on_shutdown(Callback cb) { on_shutdown_cb_ = cb; } @@ -45,7 +44,8 @@ class Session : public std::enable_shared_from_this { void stop(); private: - http::message_generator handle_request(http::request&& req); + http::message_generator handle_request( + http::request&& req); void do_read(); void do_stop(); diff --git a/apps/sse-contract-tests/src/main.cpp b/apps/sse-contract-tests/src/main.cpp index 725ab61e0..13bc2e26a 100644 --- a/apps/sse-contract-tests/src/main.cpp +++ b/apps/sse-contract-tests/src/main.cpp @@ -1,28 +1,42 @@ #include "server.hpp" +#include "console_backend.hpp" +#include "logger.hpp" + #include #include #include #include -#include +#include namespace net = boost::asio; namespace beast = boost::beast; +using launchdarkly::ConsoleBackend; + +using launchdarkly::LogLevel; + int main(int argc, char* argv[]) { + launchdarkly::Logger logger{std::make_unique( + LogLevel::kInfo, "sse-contract-tests")}; + try { net::io_context ioc{1}; - auto s = std::make_shared(ioc, "0.0.0.0", "8111"); + auto s = std::make_shared(ioc, "0.0.0.0", "8111", logger); s->add_capability("headers"); s->run(); net::signal_set signals{ioc, SIGINT, SIGTERM}; - signals.async_wait([&](beast::error_code const&, int) { ioc.stop(); }); + signals.async_wait([&](beast::error_code const&, int) { + LD_LOG(logger, LogLevel::kInfo) << "shutting down.."; + ioc.stop(); + LD_LOG(logger, LogLevel::kInfo) << "bye!"; + }); ioc.run(); } catch (std::exception const& e) { - std::cerr << "Error: " << e.what() << std::endl; + LD_LOG(logger, LogLevel::kError) << e.what(); return EXIT_FAILURE; } } diff --git a/apps/sse-contract-tests/src/server.cpp b/apps/sse-contract-tests/src/server.cpp index e16a94df0..d79fedf27 100644 --- a/apps/sse-contract-tests/src/server.cpp +++ b/apps/sse-contract-tests/src/server.cpp @@ -7,18 +7,17 @@ #include #include -void fail(beast::error_code ec, char const* what) { - std::cerr << what << ": " << ec.message() << "\n"; -} +using launchdarkly::LogLevel; server::server(net::io_context& ioc, std::string const& address, - std::string const& port) + std::string const& port, + launchdarkly::Logger& logger) : ioc_{ioc}, - stopped_{false}, acceptor_{ioc}, entity_manager_{ioc.get_executor()}, - caps_{} { + caps_{}, + logger_{logger} { beast::error_code ec; tcp::resolver resolver{ioc_}; @@ -48,10 +47,16 @@ server::server(net::io_context& ioc, return; } - std::cout << "listening on " << address << ":" << port << std::endl; + LD_LOG(logger_, LogLevel::kInfo) + << "listening on " << address << ":" << port; +} + +void server::fail(beast::error_code ec, char const* what) { + LD_LOG(logger_, LogLevel::kError) << what << ": " << ec.message(); } void server::add_capability(std::string cap) { + LD_LOG(logger_, LogLevel::kDebug) << "test capability: <" << cap << ">"; caps_.push_back(std::move(cap)); } From ff72dddec8650a9fd45a306558c686e88bc7216c Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 23 Mar 2023 12:13:03 -0700 Subject: [PATCH 25/53] more logging goodness --- apps/sse-contract-tests/include/server.hpp | 2 - apps/sse-contract-tests/include/session.hpp | 11 +- apps/sse-contract-tests/src/main.cpp | 5 +- apps/sse-contract-tests/src/server.cpp | 29 ++-- apps/sse-contract-tests/src/session.cpp | 175 ++++++++++---------- 5 files changed, 112 insertions(+), 110 deletions(-) diff --git a/apps/sse-contract-tests/include/server.hpp b/apps/sse-contract-tests/include/server.hpp index 5873b6346..ba871f8de 100644 --- a/apps/sse-contract-tests/include/server.hpp +++ b/apps/sse-contract-tests/include/server.hpp @@ -27,11 +27,9 @@ class server : public std::enable_shared_from_this { launchdarkly::Logger& logger); void add_capability(std::string cap); void run(); - void stop(); private: void do_accept(); void on_accept(boost::system::error_code const& ec, tcp::socket socket); void fail(boost::beast::error_code ec, char const* what); - void do_stop(); }; diff --git a/apps/sse-contract-tests/include/session.hpp b/apps/sse-contract-tests/include/session.hpp index 9635956d4..4b1989953 100644 --- a/apps/sse-contract-tests/include/session.hpp +++ b/apps/sse-contract-tests/include/session.hpp @@ -1,9 +1,11 @@ #pragma once +#include "entity_manager.hpp" +#include "logger.hpp" + #include #include #include -#include "entity_manager.hpp" namespace beast = boost::beast; // from namespace http = beast::http; // from @@ -28,10 +30,13 @@ class Session : public std::enable_shared_from_this { bool shutdown_requested_; + launchdarkly::Logger& logger_; + public: explicit Session(tcp::socket&& socket, EntityManager& manager, - std::vector caps); + std::vector caps, + launchdarkly::Logger& logger); ~Session(); template @@ -48,7 +53,7 @@ class Session : public std::enable_shared_from_this { http::request&& req); void do_read(); - void do_stop(); + void do_stop(char const* reason); void on_read(beast::error_code ec, std::size_t bytes_transferred); void do_close(); diff --git a/apps/sse-contract-tests/src/main.cpp b/apps/sse-contract-tests/src/main.cpp index 13bc2e26a..897548b16 100644 --- a/apps/sse-contract-tests/src/main.cpp +++ b/apps/sse-contract-tests/src/main.cpp @@ -18,7 +18,7 @@ using launchdarkly::LogLevel; int main(int argc, char* argv[]) { launchdarkly::Logger logger{std::make_unique( - LogLevel::kInfo, "sse-contract-tests")}; + LogLevel::kDebug,"sse-contract-tests")}; try { net::io_context ioc{1}; @@ -31,10 +31,11 @@ int main(int argc, char* argv[]) { signals.async_wait([&](beast::error_code const&, int) { LD_LOG(logger, LogLevel::kInfo) << "shutting down.."; ioc.stop(); - LD_LOG(logger, LogLevel::kInfo) << "bye!"; }); ioc.run(); + LD_LOG(logger, LogLevel::kInfo) << "bye!"; + } catch (std::exception const& e) { LD_LOG(logger, LogLevel::kError) << e.what(); return EXIT_FAILURE; diff --git a/apps/sse-contract-tests/src/server.cpp b/apps/sse-contract-tests/src/server.cpp index d79fedf27..844bb1264 100644 --- a/apps/sse-contract-tests/src/server.cpp +++ b/apps/sse-contract-tests/src/server.cpp @@ -48,42 +48,32 @@ server::server(net::io_context& ioc, } LD_LOG(logger_, LogLevel::kInfo) - << "listening on " << address << ":" << port; + << "server: listening on " << address << ":" << port; } void server::fail(beast::error_code ec, char const* what) { - LD_LOG(logger_, LogLevel::kError) << what << ": " << ec.message(); + LD_LOG(logger_, LogLevel::kError) << "server: " << what << ": " << ec.message(); } void server::add_capability(std::string cap) { - LD_LOG(logger_, LogLevel::kDebug) << "test capability: <" << cap << ">"; + LD_LOG(logger_, LogLevel::kDebug) << "server: test capability: <" << cap << ">"; caps_.push_back(std::move(cap)); } void server::run() { + LD_LOG(logger_, LogLevel::kDebug) << "server: run requested"; net::dispatch( acceptor_.get_executor(), beast::bind_front_handler(&server::do_accept, shared_from_this())); } -void server::stop() { - std::cout << "server: stop\n"; - net::dispatch( - acceptor_.get_executor(), - beast::bind_front_handler(&server::do_stop, shared_from_this())); -} - void server::do_accept() { + LD_LOG(logger_, LogLevel::kDebug) << "server: waiting for connection"; acceptor_.async_accept( net::make_strand(ioc_), beast::bind_front_handler(&server::on_accept, shared_from_this())); } -void server::do_stop() { - std::cout << "server: do_stop\n"; - entity_manager_.destroy_all(); -} - void server::on_accept(boost::system::error_code const& ec, tcp::socket socket) { if (!acceptor_.is_open()) { @@ -93,10 +83,15 @@ void server::on_accept(boost::system::error_code const& ec, fail(ec, "accept"); return; } + + auto session = - std::make_shared(std::move(socket), entity_manager_, caps_); + std::make_shared(std::move(socket), entity_manager_, caps_, logger_); - session->on_shutdown([this]() { ioc_.stop(); }); + session->on_shutdown([this]() { + LD_LOG(logger_, LogLevel::kDebug) << "server: terminating"; + ioc_.stop(); + }); session->start(); diff --git a/apps/sse-contract-tests/src/session.cpp b/apps/sse-contract-tests/src/session.cpp index afb01fe18..a5f37f76e 100644 --- a/apps/sse-contract-tests/src/session.cpp +++ b/apps/sse-contract-tests/src/session.cpp @@ -7,6 +7,95 @@ const std::string kEntityPath = "/entity/"; namespace net = boost::asio; +using launchdarkly::LogLevel; + +Session::Session(tcp::socket&& socket, + EntityManager& manager, + std::vector caps, + launchdarkly::Logger& logger) + : stream_{std::move(socket)}, + manager_{manager}, + capabilities_{std::move(caps)}, + on_shutdown_cb_{}, + shutdown_requested_{false}, + logger_{logger} { + LD_LOG(logger_, LogLevel::kDebug) << "session: created"; +} + +Session::~Session() { + LD_LOG(logger_, LogLevel::kDebug) << "session: destroyed"; +} + +void Session::start() { + LD_LOG(logger_, LogLevel::kDebug) << "session: start"; + net::dispatch( + stream_.get_executor(), + beast::bind_front_handler(&Session::do_read, shared_from_this())); +} + +void Session::stop() { + LD_LOG(logger_, LogLevel::kDebug) << "session: stop"; + net::dispatch( + stream_.get_executor(), + beast::bind_front_handler(&Session::do_stop, shared_from_this(), "stop requested")); +} + +void Session::do_stop(char const* reason) { + LD_LOG(logger_, LogLevel::kDebug) << "session: closing socket (" << reason << ")"; + stream_.close(); +} + +void Session::do_read() { + request_ = {}; + + LD_LOG(logger_, LogLevel::kDebug) << "session: awaiting request"; + http::async_read( + stream_, buffer_, request_, + beast::bind_front_handler(&Session::on_read, shared_from_this())); +} + +void Session::on_read(beast::error_code ec, std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + + if (ec == http::error::end_of_stream) { + return do_stop("end of stream"); + } + + if (ec) { + return do_stop("read failed"); + } + + send_response(handle_request(std::move(request_))); +} + +void Session::send_response(http::message_generator&& msg) { + beast::async_write( + stream_, std::move(msg), + beast::bind_front_handler(&Session::on_write, shared_from_this(), + request_.keep_alive())); +} + +void Session::on_write(bool keep_alive, + beast::error_code ec, + std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + + if (shutdown_requested_ && on_shutdown_cb_) { + LD_LOG(logger_, LogLevel::kDebug) << "session: client requested server termination"; + on_shutdown_cb_(); + } + + if (ec) { + return do_stop("write failed"); + } + + if (!keep_alive) { + return do_stop("client dropped connection"); + } + + do_read(); +} + http::message_generator Session::handle_request( http::request&& req) { auto const bad_request = [&req](beast::string_view why) { @@ -114,89 +203,3 @@ http::message_generator Session::handle_request( return not_found(req.target()); } - -Session::Session(tcp::socket&& socket, - EntityManager& manager, - std::vector caps) - : stream_{std::move(socket)}, - manager_{manager}, - capabilities_{std::move(caps)}, - on_shutdown_cb_{}, - shutdown_requested_{false} { - std::cout << "session created\n"; -} - -Session::~Session() { - std::cout << "~session\n"; -} - -void Session::start() { - net::dispatch( - stream_.get_executor(), - beast::bind_front_handler(&Session::do_read, shared_from_this())); -} - -void Session::stop() { - net::dispatch( - stream_.get_executor(), - beast::bind_front_handler(&Session::do_stop, shared_from_this())); -} - -void Session::do_stop() { - stream_.close(); -} - -void Session::do_read() { - request_ = {}; - - std::cout << "waiting for request\n"; - http::async_read( - stream_, buffer_, request_, - beast::bind_front_handler(&Session::on_read, shared_from_this())); -} - -void Session::on_read(beast::error_code ec, std::size_t bytes_transferred) { - boost::ignore_unused(bytes_transferred); - - if (ec == http::error::end_of_stream) { - std::cout << "end of stream" << std::endl; - return do_stop(); - } - - if (ec) { - std::cout << "read failed" << ec << std::endl; - return do_stop(); - } - - send_response(handle_request(std::move(request_))); -} - -void Session::send_response(http::message_generator&& msg) { - beast::async_write( - stream_, std::move(msg), - beast::bind_front_handler(&Session::on_write, shared_from_this(), - request_.keep_alive())); -} - -void Session::on_write(bool keep_alive, - beast::error_code ec, - std::size_t bytes_transferred) { - boost::ignore_unused(bytes_transferred); - - if (shutdown_requested_ && on_shutdown_cb_) { - on_shutdown_cb_(); - shutdown_requested_ = false; - } - - if (ec) { - std::cout << "write failed\n"; - return do_stop(); - } - - if (!keep_alive) { - std::cout << "client requested to drop the connection\n"; - return do_stop(); - } - - do_read(); -} From 3ed8a790faebb05e12b665bc3a13ac0ca8519f33 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 23 Mar 2023 16:33:11 -0700 Subject: [PATCH 26/53] plumb a nasty logging callback into sse clients --- .../include/entity_manager.hpp | 5 +- .../sse-contract-tests/src/entity_manager.cpp | 20 +- apps/sse-contract-tests/src/server.cpp | 2 +- .../include/launchdarkly/sse/sse.hpp | 47 ++-- libs/server-sent-events/src/sse.cpp | 223 +++++++++--------- 5 files changed, 165 insertions(+), 132 deletions(-) diff --git a/apps/sse-contract-tests/include/entity_manager.hpp b/apps/sse-contract-tests/include/entity_manager.hpp index 6abfb4e30..e3d5a41c5 100644 --- a/apps/sse-contract-tests/include/entity_manager.hpp +++ b/apps/sse-contract-tests/include/entity_manager.hpp @@ -1,6 +1,7 @@ #pragma once #include "definitions.hpp" +#include "logger.hpp" #include @@ -25,8 +26,10 @@ class EntityManager { std::mutex lock_; boost::asio::any_io_executor executor_; + launchdarkly::Logger& logger_; + public: - explicit EntityManager(boost::asio::any_io_executor executor); + EntityManager(boost::asio::any_io_executor executor, launchdarkly::Logger& logger); std::optional create(ConfigParams params); bool destroy(std::string const& id); diff --git a/apps/sse-contract-tests/src/entity_manager.cpp b/apps/sse-contract-tests/src/entity_manager.cpp index ea410cd7e..83c15a28e 100644 --- a/apps/sse-contract-tests/src/entity_manager.cpp +++ b/apps/sse-contract-tests/src/entity_manager.cpp @@ -2,8 +2,15 @@ #include "launchdarkly/sse/sse.hpp" #include "stream_entity.hpp" -EntityManager::EntityManager(boost::asio::any_io_executor executor) - : entities_{}, counter_{0}, lock_{}, executor_{std::move(executor)} {} +using launchdarkly::LogLevel; + +EntityManager::EntityManager(boost::asio::any_io_executor executor, + launchdarkly::Logger& logger) + : entities_{}, + counter_{0}, + lock_{}, + executor_{std::move(executor)}, + logger_{logger} {} std::optional EntityManager::create(ConfigParams params) { std::lock_guard guard{lock_}; @@ -16,14 +23,20 @@ std::optional EntityManager::create(ConfigParams params) { client_builder.header(h.first, h.second); } } + + client_builder.logging([this](std::string msg){ + LD_LOG(logger_, LogLevel::kDebug) << std::move(msg); + }); + std::shared_ptr client = client_builder.build(); if (!client) { + LD_LOG(logger_, LogLevel::kWarn) + << "entity_manager: couldn't build sse client"; return std::nullopt; } std::shared_ptr entity = std::make_shared(executor_, client, params.callbackUrl); - // Kicks off asynchronous operations. entity->run(); entities_.emplace(id, entity); @@ -38,6 +51,7 @@ void EntityManager::destroy_all() { } entities_.clear(); } + bool EntityManager::destroy(std::string const& id) { std::lock_guard guard{lock_}; auto it = entities_.find(id); diff --git a/apps/sse-contract-tests/src/server.cpp b/apps/sse-contract-tests/src/server.cpp index 844bb1264..c670ba3bc 100644 --- a/apps/sse-contract-tests/src/server.cpp +++ b/apps/sse-contract-tests/src/server.cpp @@ -15,7 +15,7 @@ server::server(net::io_context& ioc, launchdarkly::Logger& logger) : ioc_{ioc}, acceptor_{ioc}, - entity_manager_{ioc.get_executor()}, + entity_manager_{ioc.get_executor(), logger}, caps_{}, logger_{logger} { beast::error_code ec; diff --git a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp index 095138776..ae1745b64 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/sse.hpp @@ -30,14 +30,16 @@ class builder { builder& header(std::string const& name, std::string const& value); builder& method(http::verb verb); builder& tls(ssl::context_base::method); + builder& logging(std::function callback); std::shared_ptr build(); private: - std::string m_url; - net::any_io_executor m_executor; - ssl::context m_ssl_ctx; - http::request m_request; + std::string url_; + net::any_io_executor executor_; + ssl::context ssl_context_; + http::request request_; std::optional tls_version_; + std::function logging_cb_; }; class event_data { @@ -62,6 +64,24 @@ using sse_comment = std::string; using event = std::variant; class client : public std::enable_shared_from_this { + public: + using logger = std::function; + + client(boost::asio::any_io_executor ex, + http::request req, + std::string host, + std::string port, + logger logger, std::string log_tag); + ~client(); + + template + void on_event(Callback event_cb) { + m_cb = event_cb; + } + + virtual void run() = 0; + virtual void close() = 0; + protected: using parser = http::response_parser; tcp::resolver m_resolver; @@ -78,31 +98,24 @@ class client : public std::enable_shared_from_this { bool begin_CR_; std::optional m_event_data; std::function m_cb; + logger logging_cb_; + std::string log_tag_; + void complete_line(); + void parse_events(); size_t append_up_to(boost::string_view body, std::string const& search); std::size_t parse_stream(std::uint64_t remain, boost::string_view body, beast::error_code& ec); - void parse_events(); std::optional> on_chunk_body_trampoline_; - public: - client(boost::asio::any_io_executor ex, - http::request req, - std::string host, - std::string port); - ~client(); + void log(std::string); + void fail(beast::error_code ec, char const* what); - template - void on_event(Callback event_cb) { - m_cb = event_cb; - } - virtual void run() = 0; - virtual void close() = 0; }; } // namespace launchdarkly::sse diff --git a/libs/server-sent-events/src/sse.cpp b/libs/server-sent-events/src/sse.cpp index 9435168f9..99f7f5c9b 100644 --- a/libs/server-sent-events/src/sse.cpp +++ b/libs/server-sent-events/src/sse.cpp @@ -49,8 +49,10 @@ std::optional const& event_data::get_id() { client::client(net::any_io_executor ex, http::request req, std::string host, - std::string port) - : m_resolver{ex}, + std::string port, + std::function logging_cb, + std::string log_tag) + : m_resolver{std::move(ex)}, parser_{}, host_{std::move(host)}, port_{std::move(port)}, @@ -62,20 +64,29 @@ client::client(net::any_io_executor ex, last_event_id_{}, m_event_data{}, m_events{}, + logging_cb_{std::move(logging_cb)}, on_chunk_body_trampoline_{}, - m_cb{[](event_data e) { - std::cout << "Event[" << e.get_type() << "] = <" << e.get_data() - << ">\n"; + log_tag_{std::move(log_tag)}, + m_cb{[this](event_data e) { + log("got event: (" + e.get_type() + ", " + e.get_data() + ")"); }} { parser_.body_limit(boost::none); + log("create"); } + client::~client() { - std::cout << "~client\n"; + log("destroy"); +} + +void client::log(std::string what) { + if (logging_cb_) { + logging_cb_(log_tag_ + ": " + std::move(what)); + } } // Report a failure -void fail(beast::error_code ec, char const* what) { - std::cerr << what << ": " << ec.message() << "\n"; +void client::fail(beast::error_code ec, char const* what) { + log(std::string(what) + ":" + ec.message()); } std::pair parse_field(std::string field) { @@ -142,7 +153,7 @@ void client::parse_events() { last_event_id_ = field.second; m_event_data->set_id(last_event_id_); } else if (field.first == "retry") { - std::cout << "Got RETRY field\n"; + log("got unhandled 'retry' field"); } } @@ -211,6 +222,44 @@ size_t client::parse_stream(std::uint64_t remain, } class ssl_client : public client { + public: + ssl_client(net::any_io_executor ex, + ssl::context& ctx, + http::request req, + std::string host, + std::string port, + logger logging_cb) + : client(ex, + std::move(req), + std::move(host), + std::move(port), + std::move(logging_cb), + "sse-tls"), + stream_{ex, ctx} {} + + void run() override { + // Set SNI Hostname (many hosts need this to handshake successfully) + if (!SSL_set_tlsext_host_name(stream_.native_handle(), host_.c_str())) { + beast::error_code ec{static_cast(::ERR_get_error()), + net::error::get_ssl_category()}; + log("failed to set TLS host name extension: " + ec.message()); + return; + } + + beast::get_lowest_layer(stream_).expires_after( + std::chrono::seconds(15)); + + m_resolver.async_resolve( + host_, port_, + beast::bind_front_handler(&ssl_client::on_resolve, shared())); + } + + void close() override { + net::post(stream_.get_executor(), + beast::bind_front_handler(&ssl_client::on_stop, shared())); + } + + private: beast::ssl_stream stream_; std::shared_ptr shared() { @@ -270,7 +319,7 @@ class ssl_client : public client { void on_read(beast::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); if (ec) { - return fail(ec, "sse:read"); + return fail(ec, "read"); } beast::get_lowest_layer(stream_).expires_never(); @@ -280,43 +329,42 @@ class ssl_client : public client { beast::bind_front_handler(&ssl_client::on_read, shared())); } - void on_stop() { - beast::get_lowest_layer(stream_).cancel(); - } + void on_stop() { beast::get_lowest_layer(stream_).cancel(); } +}; +class plaintext_client : public client { public: - ssl_client(net::any_io_executor ex, - ssl::context& ctx, - http::request req, - std::string host, - std::string port) - : client(ex, std::move(req), std::move(host), std::move(port)), - stream_{ex, ctx} {} + plaintext_client(net::any_io_executor ex, + ssl::context& ctx, + http::request req, + std::string host, + std::string port, + logger logger) + : client(ex, + std::move(req), + std::move(host), + std::move(port), + std::move(logger), + "sse-plaintext"), + stream_{ex}, + ctx_{ctx} {} void run() override { - // Set SNI Hostname (many hosts need this to handshake successfully) - if (!SSL_set_tlsext_host_name(stream_.native_handle(), host_.c_str())) { - beast::error_code ec{static_cast(::ERR_get_error()), - net::error::get_ssl_category()}; - std::cerr << ec.message() << "\n"; - return; - } - beast::get_lowest_layer(stream_).expires_after( std::chrono::seconds(15)); m_resolver.async_resolve( host_, port_, - beast::bind_front_handler(&ssl_client::on_resolve, shared())); + beast::bind_front_handler(&plaintext_client::on_resolve, shared())); } void close() override { - net::post(stream_.get_executor(), - beast::bind_front_handler(&ssl_client::on_stop, shared())); + net::post( + stream_.get_executor(), + beast::bind_front_handler(&plaintext_client::on_stop, shared())); } -}; -class plaintext_client : public client { + private: beast::tcp_stream stream_; ssl::context& ctx_; @@ -359,16 +407,16 @@ class plaintext_client : public client { parser_.on_chunk_body(*this->on_chunk_body_trampoline_); - http::async_read( - stream_, m_buffer, parser_, - beast::bind_front_handler(&plaintext_client::on_got_headers, shared())); + http::async_read(stream_, m_buffer, parser_, + beast::bind_front_handler( + &plaintext_client::on_got_headers, shared())); } void on_got_headers(beast::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); if (ec) { - return fail(ec, "sse:headers"); + return fail(ec, "headers"); } if (parser_.is_header_done()) { @@ -376,27 +424,8 @@ class plaintext_client : public client { if (status == http::status::moved_permanently) { if (auto it = parser_.get().find("Location"); it != parser_.get().end()) { - - boost::system::result uri_components = - boost::urls::parse_uri(it->value()); - if (!uri_components) { - return fail(boost::asio::error::host_unreachable, "invalid 301 redirect"); - } - - host_ = uri_components->host(); - port_ = uri_components->has_port() ? uri_components->port() : "80"; - m_request.set(http::field::host, uri_components->host()); - m_request.target(uri_components->path()); - - auto client = std::make_shared(stream_.get_executor(), - ctx_, m_request, - host_, port_); - client->m_cb = m_cb; - client->run(); - return; } } - } beast::get_lowest_layer(stream_).expires_never(); @@ -409,102 +438,76 @@ class plaintext_client : public client { void on_read(beast::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); if (ec && ec != beast::errc::operation_canceled) { - return fail(ec, "sse:read"); + return fail(ec, "read"); } - - http::async_read_some( stream_, m_buffer, parser_, beast::bind_front_handler(&plaintext_client::on_read, shared())); } - void on_stop() { - stream_.cancel(); - } - - public: - plaintext_client(net::any_io_executor ex, - ssl::context& ctx, - http::request req, - std::string host, - std::string port) - : client(ex, std::move(req), std::move(host), std::move(port)), - stream_{ex}, ctx_{ctx} { - - std::cout << "construct client: " << host_ << port_ << req.target() << '\n'; - } - - void run() override { - beast::get_lowest_layer(stream_).expires_after( - std::chrono::seconds(15)); - - m_resolver.async_resolve( - host_, port_, - beast::bind_front_handler(&plaintext_client::on_resolve, shared())); - } - - void close() override { - net::post( - stream_.get_executor(), - beast::bind_front_handler(&plaintext_client::on_stop, shared())); - } + void on_stop() { stream_.cancel(); } }; builder::builder(net::any_io_executor ctx, std::string url) - : m_url{std::move(url)}, - m_ssl_ctx{ssl::context::tlsv12_client}, - m_executor{std::move(ctx)}, + : url_{std::move(url)}, + ssl_context_{ssl::context::tlsv12_client}, + executor_{std::move(ctx)}, tls_version_{12} { // This needs to be verify_peer in production!! - m_ssl_ctx.set_verify_mode(ssl::verify_none); + ssl_context_.set_verify_mode(ssl::verify_none); - m_request.version(11); - m_request.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); - m_request.method(http::verb::get); - m_request.set("Accept", "text/event-stream"); - m_request.set("Cache-Control", "no-cache"); + request_.version(11); + request_.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); + request_.method(http::verb::get); + request_.set("Accept", "text/event-stream"); + request_.set("Cache-Control", "no-cache"); } builder& builder::header(std::string const& name, std::string const& value) { - m_request.set(name, value); + request_.set(name, value); return *this; } builder& builder::method(http::verb verb) { - m_request.method(verb); + request_.method(verb); return *this; } builder& builder::tls(ssl::context_base::method ctx) { - m_ssl_ctx = ssl::context{ctx}; + ssl_context_ = ssl::context{ctx}; + return *this; +} + +builder& builder::logging(std::function cb) { + logging_cb_ = std::move(cb); return *this; } std::shared_ptr builder::build() { boost::system::result uri_components = - boost::urls::parse_uri(m_url); + boost::urls::parse_uri(url_); if (!uri_components) { return nullptr; } - m_request.set(http::field::host, uri_components->host()); - m_request.target(uri_components->path()); + request_.set(http::field::host, uri_components->host()); + request_.target(uri_components->path()); if (uri_components->scheme_id() == boost::urls::scheme::https) { std::string port = uri_components->has_port() ? uri_components->port() : "443"; - return std::make_shared(net::make_strand(m_executor), - m_ssl_ctx, m_request, - uri_components->host(), port); + return std::make_shared( + net::make_strand(executor_), ssl_context_, request_, + uri_components->host(), port, logging_cb_); } else { std::string port = uri_components->has_port() ? uri_components->port() : "80"; - return std::make_shared(net::make_strand(m_executor), - m_ssl_ctx, m_request, - uri_components->host(), port); + return std::make_shared( + net::make_strand(executor_), ssl_context_, request_, + uri_components->host(), port, logging_cb_); } } From 5d8b85fa2e46f49526a6b00e474ec15ce3fc01cf Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 23 Mar 2023 16:46:41 -0700 Subject: [PATCH 27/53] use env logger --- apps/sse-contract-tests/src/main.cpp | 3 +-- apps/sse-contract-tests/src/stream_entity.cpp | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sse-contract-tests/src/main.cpp b/apps/sse-contract-tests/src/main.cpp index 897548b16..7ee4b6921 100644 --- a/apps/sse-contract-tests/src/main.cpp +++ b/apps/sse-contract-tests/src/main.cpp @@ -17,8 +17,7 @@ using launchdarkly::ConsoleBackend; using launchdarkly::LogLevel; int main(int argc, char* argv[]) { - launchdarkly::Logger logger{std::make_unique( - LogLevel::kDebug,"sse-contract-tests")}; + launchdarkly::Logger logger{std::make_unique("sse-contract-tests")}; try { net::io_context ioc{1}; diff --git a/apps/sse-contract-tests/src/stream_entity.cpp b/apps/sse-contract-tests/src/stream_entity.cpp index 2c625a5f9..852243721 100644 --- a/apps/sse-contract-tests/src/stream_entity.cpp +++ b/apps/sse-contract-tests/src/stream_entity.cpp @@ -125,6 +125,7 @@ void StreamEntity::on_flush_timer(boost::system::error_code ec) { // Flip-flop between this function and on_write; pushing an event // and then popping it. + http::async_write(event_stream_, request, beast::bind_front_handler(&StreamEntity::on_write, shared_from_this())); From b159858f9293c955a9324ed7741061d7be475a96 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 23 Mar 2023 16:55:35 -0700 Subject: [PATCH 28/53] cleanup some includes --- apps/hello-cpp/main.cpp | 8 ++++--- .../include/stream_entity.hpp | 2 +- .../sse-contract-tests/src/entity_manager.cpp | 2 +- apps/sse-contract-tests/src/main.cpp | 3 +-- .../launchdarkly/sse/{sse.hpp => client.hpp} | 21 +++++++++++-------- libs/server-sent-events/src/CMakeLists.txt | 2 +- .../src/{sse.cpp => client.cpp} | 4 +++- 7 files changed, 24 insertions(+), 18 deletions(-) rename libs/server-sent-events/include/launchdarkly/sse/{sse.hpp => client.hpp} (91%) rename libs/server-sent-events/src/{sse.cpp => client.cpp} (99%) diff --git a/apps/hello-cpp/main.cpp b/apps/hello-cpp/main.cpp index 24e0e6e9c..3b0e7ee2e 100644 --- a/apps/hello-cpp/main.cpp +++ b/apps/hello-cpp/main.cpp @@ -1,10 +1,12 @@ -#include #include -#include -#include +#include + #include "console_backend.hpp" #include "logger.hpp" +#include +#include + namespace net = boost::asio; // from using launchdarkly::ConsoleBackend; diff --git a/apps/sse-contract-tests/include/stream_entity.hpp b/apps/sse-contract-tests/include/stream_entity.hpp index b13bc94fc..4f7400f93 100644 --- a/apps/sse-contract-tests/include/stream_entity.hpp +++ b/apps/sse-contract-tests/include/stream_entity.hpp @@ -2,7 +2,7 @@ #include "entity_manager.hpp" -#include +#include "launchdarkly/sse/client.hpp" #include #include diff --git a/apps/sse-contract-tests/src/entity_manager.cpp b/apps/sse-contract-tests/src/entity_manager.cpp index 83c15a28e..556c605a5 100644 --- a/apps/sse-contract-tests/src/entity_manager.cpp +++ b/apps/sse-contract-tests/src/entity_manager.cpp @@ -1,5 +1,5 @@ #include "entity_manager.hpp" -#include "launchdarkly/sse/sse.hpp" +#include "launchdarkly/sse/client.hpp" #include "stream_entity.hpp" using launchdarkly::LogLevel; diff --git a/apps/sse-contract-tests/src/main.cpp b/apps/sse-contract-tests/src/main.cpp index 7ee4b6921..0a5f20061 100644 --- a/apps/sse-contract-tests/src/main.cpp +++ b/apps/sse-contract-tests/src/main.cpp @@ -1,12 +1,11 @@ #include "server.hpp" #include "console_backend.hpp" -#include "logger.hpp" #include #include #include -#include + #include namespace net = boost::asio; diff --git a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp b/libs/server-sent-events/include/launchdarkly/sse/client.hpp similarity index 91% rename from libs/server-sent-events/include/launchdarkly/sse/sse.hpp rename to libs/server-sent-events/include/launchdarkly/sse/client.hpp index ae1745b64..d8edb2629 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/sse.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/client.hpp @@ -1,17 +1,20 @@ #pragma once -#include -#include +#include +#include +#include + #include -#include -#include -#include -#include +#include +#include +#include +#include + +#include #include #include -#include -#include -#include +#include +#include #include namespace launchdarkly::sse { diff --git a/libs/server-sent-events/src/CMakeLists.txt b/libs/server-sent-events/src/CMakeLists.txt index 9f839b815..dbdcb93ae 100644 --- a/libs/server-sent-events/src/CMakeLists.txt +++ b/libs/server-sent-events/src/CMakeLists.txt @@ -2,7 +2,7 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${LaunchDarklySSEClient_SOURCE_DIR}/include/launchdarkly/*.hpp") # Automatic library: static or dynamic based on user config. -add_library(${LIBNAME} sse.cpp boost-url.cpp ${HEADER_LIST}) +add_library(${LIBNAME} client.cpp boost-url.cpp ${HEADER_LIST}) target_link_libraries(${LIBNAME} PUBLIC OpenSSL::SSL Boost::headers) add_library(launchdarkly::sse ALIAS ${LIBNAME}) diff --git a/libs/server-sent-events/src/sse.cpp b/libs/server-sent-events/src/client.cpp similarity index 99% rename from libs/server-sent-events/src/sse.cpp rename to libs/server-sent-events/src/client.cpp index 99f7f5c9b..5e0d23892 100644 --- a/libs/server-sent-events/src/sse.cpp +++ b/libs/server-sent-events/src/client.cpp @@ -1,9 +1,11 @@ +#include + +#include #include #include #include #include #include -#include #include #include From c728a1f0346f2c32328aacb71d8763535cd6a936 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 23 Mar 2023 17:13:08 -0700 Subject: [PATCH 29/53] renamings in client.hpp for style --- .../include/launchdarkly/sse/client.hpp | 17 +++--- libs/server-sent-events/src/client.cpp | 59 +++++++++---------- 2 files changed, 37 insertions(+), 39 deletions(-) diff --git a/libs/server-sent-events/include/launchdarkly/sse/client.hpp b/libs/server-sent-events/include/launchdarkly/sse/client.hpp index d8edb2629..3695452bc 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/client.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/client.hpp @@ -41,7 +41,6 @@ class builder { net::any_io_executor executor_; ssl::context ssl_context_; http::request request_; - std::optional tls_version_; std::function logging_cb_; }; @@ -79,7 +78,7 @@ class client : public std::enable_shared_from_this { template void on_event(Callback event_cb) { - m_cb = event_cb; + event_callback_ = event_cb; } virtual void run() = 0; @@ -87,20 +86,20 @@ class client : public std::enable_shared_from_this { protected: using parser = http::response_parser; - tcp::resolver m_resolver; - beast::flat_buffer m_buffer; - http::request m_request; - http::response m_response; + tcp::resolver resolver_; + beast::flat_buffer buffer_; + http::request request_; + http::response response_; parser parser_; std::string host_; std::string port_; std::optional buffered_line_; std::deque complete_lines_; - std::vector m_events; + std::vector events_; std::optional last_event_id_; bool begin_CR_; - std::optional m_event_data; - std::function m_cb; + std::optional event_buffer_; + std::function event_callback_; logger logging_cb_; std::string log_tag_; diff --git a/libs/server-sent-events/src/client.cpp b/libs/server-sent-events/src/client.cpp index 5e0d23892..90d13b2aa 100644 --- a/libs/server-sent-events/src/client.cpp +++ b/libs/server-sent-events/src/client.cpp @@ -1,7 +1,7 @@ #include -#include #include +#include #include #include #include @@ -54,22 +54,22 @@ client::client(net::any_io_executor ex, std::string port, std::function logging_cb, std::string log_tag) - : m_resolver{std::move(ex)}, + : resolver_{std::move(ex)}, parser_{}, host_{std::move(host)}, port_{std::move(port)}, - m_request{std::move(req)}, - m_response{}, + request_{std::move(req)}, + response_{}, buffered_line_{}, complete_lines_{}, begin_CR_{false}, last_event_id_{}, - m_event_data{}, - m_events{}, + event_buffer_{}, + events_{}, logging_cb_{std::move(logging_cb)}, on_chunk_body_trampoline_{}, log_tag_{std::move(log_tag)}, - m_cb{[this](event_data e) { + event_callback_{[this](event_data e) { log("got event: (" + e.get_type() + ", " + e.get_data() + ")"); }} { parser_.body_limit(boost::none); @@ -122,7 +122,7 @@ void client::parse_events() { complete_lines_.pop_front(); if (line.empty()) { - if (m_event_data.has_value()) { + if (event_buffer_.has_value()) { seen_empty_line = true; break; } @@ -134,38 +134,38 @@ void client::parse_events() { event_data e; e.set_type("comment"); e.append_data(field.second); - m_cb(std::move(e)); + event_callback_(std::move(e)); continue; } - if (!m_event_data.has_value()) { - m_event_data.emplace(event_data{}); - m_event_data->set_id(last_event_id_); + if (!event_buffer_.has_value()) { + event_buffer_.emplace(event_data{}); + event_buffer_->set_id(last_event_id_); } if (field.first == "event") { - m_event_data->set_type(field.second); + event_buffer_->set_type(field.second); } else if (field.first == "data") { - m_event_data->append_data(field.second); + event_buffer_->append_data(field.second); } else if (field.first == "id") { if (field.second.find('\0') != std::string::npos) { // IDs with null-terminators are acceptable, but ignored. continue; } last_event_id_ = field.second; - m_event_data->set_id(last_event_id_); + event_buffer_->set_id(last_event_id_); } else if (field.first == "retry") { log("got unhandled 'retry' field"); } } if (seen_empty_line) { - std::optional data = m_event_data; - m_event_data = std::nullopt; + std::optional data = event_buffer_; + event_buffer_ = std::nullopt; if (data.has_value()) { data->trim_trailing_newline(); - m_cb(std::move(*data)); + event_callback_(std::move(*data)); } continue; @@ -251,7 +251,7 @@ class ssl_client : public client { beast::get_lowest_layer(stream_).expires_after( std::chrono::seconds(15)); - m_resolver.async_resolve( + resolver_.async_resolve( host_, port_, beast::bind_front_handler(&ssl_client::on_resolve, shared())); } @@ -293,7 +293,7 @@ class ssl_client : public client { // Send the HTTP request to the remote host http::async_write( - stream_, m_request, + stream_, request_, beast::bind_front_handler(&ssl_client::on_write, shared())); } @@ -314,7 +314,7 @@ class ssl_client : public client { parser_.on_chunk_body(*this->on_chunk_body_trampoline_); http::async_read_some( - stream_, m_buffer, parser_, + stream_, buffer_, parser_, beast::bind_front_handler(&ssl_client::on_read, shared())); } @@ -327,7 +327,7 @@ class ssl_client : public client { beast::get_lowest_layer(stream_).expires_never(); http::async_read_some( - stream_, m_buffer, parser_, + stream_, buffer_, parser_, beast::bind_front_handler(&ssl_client::on_read, shared())); } @@ -355,7 +355,7 @@ class plaintext_client : public client { beast::get_lowest_layer(stream_).expires_after( std::chrono::seconds(15)); - m_resolver.async_resolve( + resolver_.async_resolve( host_, port_, beast::bind_front_handler(&plaintext_client::on_resolve, shared())); } @@ -389,7 +389,7 @@ class plaintext_client : public client { return fail(ec, "connect"); http::async_write( - stream_, m_request, + stream_, request_, beast::bind_front_handler(&plaintext_client::on_write, shared())); } @@ -409,7 +409,7 @@ class plaintext_client : public client { parser_.on_chunk_body(*this->on_chunk_body_trampoline_); - http::async_read(stream_, m_buffer, parser_, + http::async_read(stream_, buffer_, parser_, beast::bind_front_handler( &plaintext_client::on_got_headers, shared())); } @@ -433,7 +433,7 @@ class plaintext_client : public client { beast::get_lowest_layer(stream_).expires_never(); http::async_read_some( - stream_, m_buffer, parser_, + stream_, buffer_, parser_, beast::bind_front_handler(&plaintext_client::on_read, shared())); } @@ -444,7 +444,7 @@ class plaintext_client : public client { } http::async_read_some( - stream_, m_buffer, parser_, + stream_, buffer_, parser_, beast::bind_front_handler(&plaintext_client::on_read, shared())); } @@ -454,9 +454,8 @@ class plaintext_client : public client { builder::builder(net::any_io_executor ctx, std::string url) : url_{std::move(url)}, ssl_context_{ssl::context::tlsv12_client}, - executor_{std::move(ctx)}, - tls_version_{12} { - // This needs to be verify_peer in production!! + executor_{std::move(ctx)} { + // TODO: This needs to be verify_peer in production!! ssl_context_.set_verify_mode(ssl::verify_none); request_.version(11); From c3f3341f0cec0075ccb722db0f1a6d2117060221 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 23 Mar 2023 18:28:20 -0700 Subject: [PATCH 30/53] The custom parser compiles --- .../include/launchdarkly/sse/client.hpp | 4 +- .../include/launchdarkly/sse/parser.hpp | 107 ++++++++++++++++++ libs/server-sent-events/src/CMakeLists.txt | 2 +- libs/server-sent-events/src/client.cpp | 29 ++--- libs/server-sent-events/src/parser.cpp | 1 + 5 files changed, 128 insertions(+), 15 deletions(-) create mode 100644 libs/server-sent-events/include/launchdarkly/sse/parser.hpp create mode 100644 libs/server-sent-events/src/parser.cpp diff --git a/libs/server-sent-events/include/launchdarkly/sse/client.hpp b/libs/server-sent-events/include/launchdarkly/sse/client.hpp index 3695452bc..c0d482e7b 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/client.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/client.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -85,7 +87,7 @@ class client : public std::enable_shared_from_this { virtual void close() = 0; protected: - using parser = http::response_parser; + using parser = launchdarkly::sse::parser; tcp::resolver resolver_; beast::flat_buffer buffer_; http::request request_; diff --git a/libs/server-sent-events/include/launchdarkly/sse/parser.hpp b/libs/server-sent-events/include/launchdarkly/sse/parser.hpp new file mode 100644 index 000000000..304887009 --- /dev/null +++ b/libs/server-sent-events/include/launchdarkly/sse/parser.hpp @@ -0,0 +1,107 @@ +#pragma once + +#include +#include +#include + +#include + +namespace launchdarkly::sse { + +using namespace boost::beast; + +class parser : public http::basic_parser { + private: + void on_request_impl(http::verb method, + string_view method_str, + string_view target, + int version, + error_code& ec) override { +// try { +// m_.target(target); +// if (method != http::verb::unknown) +// m_.method(method); +// else +// m_.method_string(method_str); +// ec.assign(0, ec.category()); +// } catch (std::bad_alloc const&) { +// ec = http::error::bad_alloc; +// } +// m_.version(version); +std::cout << "on_request\n"; + } + void on_response_impl(int code, + string_view reason, + int version, + error_code& ec) override { + // m_.result(code); + // m_.version(version); + // try { + // m_.reason(reason); + // ec.assign(0, ec.category()); + // } catch (std::bad_alloc const&) { + // ec = http::error::bad_alloc; + // } +std::cout << "on_response\n"; + + } + void on_field_impl(http::field name, + string_view name_string, + string_view value, + error_code& ec) override { + // try { + // m_.insert(name, name_string, value); + // ec.assign(0, ec.category()); + // } catch (std::bad_alloc const&) { + // ec = http::error::bad_alloc; + // } +std::cout << "on_field\n"; + + } + void on_header_impl(error_code& ec) override { + // ec.assign(0, ec.category()); +std::cout << "on_header\n"; + + } + + void on_body_init_impl(boost::optional const& content_length, + error_code& ec) override { + // rd_.emplace(m_, content_length, ec); +std::cout << "on_body_init\n"; + + } + std::size_t on_body_impl(string_view body, error_code& ec) override { + // return rd_->put(boost::asio::buffer(body.data(), body.size()), ec); +std::cout << "on_body_impl" << body << '\n'; + + return body.length(); + } + + void on_chunk_header_impl(std::uint64_t size, + string_view extensions, + error_code& ec) override { + // ec.assign(0, ec.category()); + std::cout << "on_chunk_header_impl\n"; + + } + std::size_t on_chunk_body_impl(std::uint64_t remain, + string_view body, + error_code& ec) override { + // ec.assign(0, ec.category()); + std::cout << "on_chunk_body_impl " << body << '\n'; + + return body.length(); + } + + void on_finish_impl(error_code& ec) override { + // if (rd_) + // rd_->finish(ec); + // else + // ec.assign(0, ec.category()); + std::cout << "on_finish_impl\n"; + } + + public: + parser() = default; +}; +} // namespace launchdarkly::sse diff --git a/libs/server-sent-events/src/CMakeLists.txt b/libs/server-sent-events/src/CMakeLists.txt index dbdcb93ae..27843fb60 100644 --- a/libs/server-sent-events/src/CMakeLists.txt +++ b/libs/server-sent-events/src/CMakeLists.txt @@ -2,7 +2,7 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${LaunchDarklySSEClient_SOURCE_DIR}/include/launchdarkly/*.hpp") # Automatic library: static or dynamic based on user config. -add_library(${LIBNAME} client.cpp boost-url.cpp ${HEADER_LIST}) +add_library(${LIBNAME} client.cpp parser.cpp boost-url.cpp ${HEADER_LIST}) target_link_libraries(${LIBNAME} PUBLIC OpenSSL::SSL Boost::headers) add_library(launchdarkly::sse ALIAS ${LIBNAME}) diff --git a/libs/server-sent-events/src/client.cpp b/libs/server-sent-events/src/client.cpp index 90d13b2aa..8429401ec 100644 --- a/libs/server-sent-events/src/client.cpp +++ b/libs/server-sent-events/src/client.cpp @@ -54,7 +54,8 @@ client::client(net::any_io_executor ex, std::string port, std::function logging_cb, std::string log_tag) - : resolver_{std::move(ex)}, + : resolver_{ex}, + buffer_{}, parser_{}, host_{std::move(host)}, port_{std::move(port)}, @@ -72,7 +73,9 @@ client::client(net::any_io_executor ex, event_callback_{[this](event_data e) { log("got event: (" + e.get_type() + ", " + e.get_data() + ")"); }} { - parser_.body_limit(boost::none); + + // parser_.body_limit(boost::none); + log("create"); } @@ -311,7 +314,7 @@ class ssl_client : public client { return consumed; }); - parser_.on_chunk_body(*this->on_chunk_body_trampoline_); + // parser_.on_chunk_body(*this->on_chunk_body_trampoline_); http::async_read_some( stream_, buffer_, parser_, @@ -407,9 +410,9 @@ class plaintext_client : public client { return consumed; }); - parser_.on_chunk_body(*this->on_chunk_body_trampoline_); + // parser_.on_chunk_body(*this->on_chunk_body_trampoline_); - http::async_read(stream_, buffer_, parser_, + http::async_read_some(stream_, buffer_, parser_, beast::bind_front_handler( &plaintext_client::on_got_headers, shared())); } @@ -421,14 +424,14 @@ class plaintext_client : public client { return fail(ec, "headers"); } - if (parser_.is_header_done()) { - auto status = parser_.get().result(); - if (status == http::status::moved_permanently) { - if (auto it = parser_.get().find("Location"); - it != parser_.get().end()) { - } - } - } +// if (parser_.is_header_done()) { +// auto status = parser_.; +// if (status == http::status::moved_permanently) { +// if (auto it = parser_.get().find("Location"); +// it != parser_.get().end()) { +// } +// } +// } beast::get_lowest_layer(stream_).expires_never(); diff --git a/libs/server-sent-events/src/parser.cpp b/libs/server-sent-events/src/parser.cpp new file mode 100644 index 000000000..00afeb08c --- /dev/null +++ b/libs/server-sent-events/src/parser.cpp @@ -0,0 +1 @@ +#include From 698546bff1d07ab77f065af3c2b856df014440ca Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 23 Mar 2023 18:33:43 -0700 Subject: [PATCH 31/53] it works --- .../include/launchdarkly/sse/parser.hpp | 101 +++++++++++++----- 1 file changed, 73 insertions(+), 28 deletions(-) diff --git a/libs/server-sent-events/include/launchdarkly/sse/parser.hpp b/libs/server-sent-events/include/launchdarkly/sse/parser.hpp index 304887009..e10c3bc05 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/parser.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/parser.hpp @@ -5,6 +5,9 @@ #include #include +#include +#include +#include namespace launchdarkly::sse { @@ -12,23 +15,26 @@ using namespace boost::beast; class parser : public http::basic_parser { private: + std::optional buffered_line_; + bool begin_CR_; + std::vector complete_lines_; void on_request_impl(http::verb method, string_view method_str, string_view target, int version, error_code& ec) override { -// try { -// m_.target(target); -// if (method != http::verb::unknown) -// m_.method(method); -// else -// m_.method_string(method_str); -// ec.assign(0, ec.category()); -// } catch (std::bad_alloc const&) { -// ec = http::error::bad_alloc; -// } -// m_.version(version); -std::cout << "on_request\n"; + // try { + // m_.target(target); + // if (method != http::verb::unknown) + // m_.method(method); + // else + // m_.method_string(method_str); + // ec.assign(0, ec.category()); + // } catch (std::bad_alloc const&) { + // ec = http::error::bad_alloc; + // } + // m_.version(version); + std::cout << "on_request\n"; } void on_response_impl(int code, string_view reason, @@ -42,8 +48,7 @@ std::cout << "on_request\n"; // } catch (std::bad_alloc const&) { // ec = http::error::bad_alloc; // } -std::cout << "on_response\n"; - + std::cout << "on_response\n"; } void on_field_impl(http::field name, string_view name_string, @@ -55,26 +60,23 @@ std::cout << "on_response\n"; // } catch (std::bad_alloc const&) { // ec = http::error::bad_alloc; // } -std::cout << "on_field\n"; - + std::cout << "on_field\n"; } void on_header_impl(error_code& ec) override { // ec.assign(0, ec.category()); -std::cout << "on_header\n"; - + std::cout << "on_header\n"; } void on_body_init_impl(boost::optional const& content_length, error_code& ec) override { // rd_.emplace(m_, content_length, ec); -std::cout << "on_body_init\n"; - + std::cout << "on_body_init\n"; } std::size_t on_body_impl(string_view body, error_code& ec) override { // return rd_->put(boost::asio::buffer(body.data(), body.size()), ec); -std::cout << "on_body_impl" << body << '\n'; + std::cout << "on_body_impl" << body << '\n'; - return body.length(); + return parse_stream(0, body, ec); } void on_chunk_header_impl(std::uint64_t size, @@ -82,15 +84,11 @@ std::cout << "on_body_impl" << body << '\n'; error_code& ec) override { // ec.assign(0, ec.category()); std::cout << "on_chunk_header_impl\n"; - } std::size_t on_chunk_body_impl(std::uint64_t remain, string_view body, error_code& ec) override { - // ec.assign(0, ec.category()); - std::cout << "on_chunk_body_impl " << body << '\n'; - - return body.length(); + return parse_stream(remain, body, ec); } void on_finish_impl(error_code& ec) override { @@ -101,7 +99,54 @@ std::cout << "on_body_impl" << body << '\n'; std::cout << "on_finish_impl\n"; } + void complete_line() { + if (buffered_line_.has_value()) { + complete_lines_.push_back(buffered_line_.value()); + buffered_line_.reset(); + } + } + + size_t append_up_to(boost::string_view body, std::string const& search) { + std::size_t index = body.find_first_of(search); + if (index != std::string::npos) { + body.remove_suffix(body.size() - index); + } + if (buffered_line_.has_value()) { + buffered_line_->append(body.to_string()); + } else { + buffered_line_ = std::string{body}; + } + return index == std::string::npos ? body.size() : index; + } + + size_t parse_stream(std::uint64_t remain, + boost::string_view body, + boost::beast::error_code& ec) { + size_t i = 0; + while (i < body.length()) { + i += this->append_up_to(body.substr(i, body.length() - i), "\r\n"); + if (i == body.size()) { + continue; + } else if (body.at(i) == '\r') { + complete_line(); + begin_CR_ = true; + i++; + } else if (body.at(i) == '\n') { + if (begin_CR_) { + begin_CR_ = false; + i++; + } else { + complete_line(); + i++; + } + } else { + begin_CR_ = false; + } + } + return body.length(); + } + public: - parser() = default; + parser() : buffered_line_(), complete_lines_(), begin_CR_(false) {} }; } // namespace launchdarkly::sse From 74e3ad198e5b5665dc8a575370a72eac03b4dc7a Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 24 Mar 2023 08:13:09 -0700 Subject: [PATCH 32/53] use a custom http body --- .../include/launchdarkly/sse/client.hpp | 54 +--- .../include/launchdarkly/sse/parser.hpp | 264 ++++++++++++------ libs/server-sent-events/src/client.cpp | 226 +-------------- libs/server-sent-events/src/parser.cpp | 33 +++ 4 files changed, 240 insertions(+), 337 deletions(-) diff --git a/libs/server-sent-events/include/launchdarkly/sse/client.hpp b/libs/server-sent-events/include/launchdarkly/sse/client.hpp index c0d482e7b..f38453afd 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/client.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/client.hpp @@ -2,21 +2,21 @@ #include -#include #include +#include #include +#include +#include #include #include #include -#include -#include -#include +#include #include #include #include -#include +#include #include namespace launchdarkly::sse { @@ -46,22 +46,6 @@ class builder { std::function logging_cb_; }; -class event_data { - std::string m_type; - std::string m_data; - std::optional m_id; - - public: - explicit event_data(); - void set_type(std::string); - void set_id(std::optional); - void append_data(std::string const&); - void trim_trailing_newline(); - std::string const& get_type(); - std::string const& get_data(); - std::optional const& get_id(); -}; - using sse_event = event_data; using sse_comment = std::string; @@ -75,7 +59,8 @@ class client : public std::enable_shared_from_this { http::request req, std::string host, std::string port, - logger logger, std::string log_tag); + logger logger, + std::string log_tag); ~client(); template @@ -87,39 +72,22 @@ class client : public std::enable_shared_from_this { virtual void close() = 0; protected: - using parser = launchdarkly::sse::parser; + using body = launchdarkly::sse::EventBody>; + using parser = http::response_parser; tcp::resolver resolver_; beast::flat_buffer buffer_; http::request request_; - http::response response_; + http::response response_; parser parser_; std::string host_; std::string port_; - std::optional buffered_line_; - std::deque complete_lines_; - std::vector events_; - std::optional last_event_id_; - bool begin_CR_; - std::optional event_buffer_; + std::function event_callback_; logger logging_cb_; std::string log_tag_; - void complete_line(); - void parse_events(); - size_t append_up_to(boost::string_view body, std::string const& search); - std::size_t parse_stream(std::uint64_t remain, - boost::string_view body, - beast::error_code& ec); - - std::optional> - on_chunk_body_trampoline_; - void log(std::string); void fail(beast::error_code ec, char const* what); - - }; } // namespace launchdarkly::sse diff --git a/libs/server-sent-events/include/launchdarkly/sse/parser.hpp b/libs/server-sent-events/include/launchdarkly/sse/parser.hpp index e10c3bc05..bce35db18 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/parser.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/parser.hpp @@ -1,9 +1,12 @@ #pragma once +#include #include #include +#include #include +#include #include #include #include @@ -13,92 +16,106 @@ namespace launchdarkly::sse { using namespace boost::beast; -class parser : public http::basic_parser { - private: +class event_data { + std::string m_type; + std::string m_data; + std::optional m_id; + + public: + explicit event_data(); + void set_type(std::string); + void set_id(std::optional); + void append_data(std::string const&); + void trim_trailing_newline(); + std::string const& get_type(); + std::string const& get_data(); + std::optional const& get_id(); +}; + +template +struct EventBody { + using event_type = EventContainer; + class reader; + class value_type; + static std::uint64_t size(value_type const& body); +}; + +template +class EventBody::value_type { + friend class reader; + friend struct EventBody; + + EventContainer events_; + + public: + EventContainer& events() { return events_; } +}; + +template +struct EventBody::reader { + value_type& body_; + std::optional buffered_line_; + std::deque complete_lines_; bool begin_CR_; - std::vector complete_lines_; - void on_request_impl(http::verb method, - string_view method_str, - string_view target, - int version, - error_code& ec) override { - // try { - // m_.target(target); - // if (method != http::verb::unknown) - // m_.method(method); - // else - // m_.method_string(method_str); - // ec.assign(0, ec.category()); - // } catch (std::bad_alloc const&) { - // ec = http::error::bad_alloc; - // } - // m_.version(version); - std::cout << "on_request\n"; - } - void on_response_impl(int code, - string_view reason, - int version, - error_code& ec) override { - // m_.result(code); - // m_.version(version); - // try { - // m_.reason(reason); - // ec.assign(0, ec.category()); - // } catch (std::bad_alloc const&) { - // ec = http::error::bad_alloc; - // } - std::cout << "on_response\n"; - } - void on_field_impl(http::field name, - string_view name_string, - string_view value, - error_code& ec) override { - // try { - // m_.insert(name, name_string, value); - // ec.assign(0, ec.category()); - // } catch (std::bad_alloc const&) { - // ec = http::error::bad_alloc; - // } - std::cout << "on_field\n"; - } - void on_header_impl(error_code& ec) override { - // ec.assign(0, ec.category()); - std::cout << "on_header\n"; - } + std::optional last_event_id_; - void on_body_init_impl(boost::optional const& content_length, - error_code& ec) override { - // rd_.emplace(m_, content_length, ec); - std::cout << "on_body_init\n"; - } - std::size_t on_body_impl(string_view body, error_code& ec) override { - // return rd_->put(boost::asio::buffer(body.data(), body.size()), ec); - std::cout << "on_body_impl" << body << '\n'; + std::optional event_; - return parse_stream(0, body, ec); + public: + template + reader(http::header& h, value_type& body) + : body_(body), + buffered_line_(), + complete_lines_(), + begin_CR_(false), + last_event_id_(), + event_() { + boost::ignore_unused(h); } - void on_chunk_header_impl(std::uint64_t size, - string_view extensions, - error_code& ec) override { - // ec.assign(0, ec.category()); - std::cout << "on_chunk_header_impl\n"; + /** Initialize the reader. + + This is called after construction and before the first + call to `put`. The message is valid and complete upon + entry.@param ec Set to the error, if any occurred. + */ + void init(boost::optional const& content_length, + error_code& ec) { + boost::ignore_unused(content_length); + + // The specification requires this to indicate "no error" + ec = {}; } - std::size_t on_chunk_body_impl(std::uint64_t remain, - string_view body, - error_code& ec) override { - return parse_stream(remain, body, ec); + + /** Store buffers. + This is called zero or more times with parsed body octets. + + @param buffers The constant buffer sequence to store. + + @param ec Set to the error, if any occurred. + + @return The number of bytes transferred from the input buffers. + */ + template + std::size_t put(ConstBufferSequence const& buffers, error_code& ec) { + // The specification requires this to indicate "no error" + ec = {}; + parse_stream(buffers_to_string(buffers)); + parse_events(); + return buffer_bytes(buffers); } - void on_finish_impl(error_code& ec) override { - // if (rd_) - // rd_->finish(ec); - // else - // ec.assign(0, ec.category()); - std::cout << "on_finish_impl\n"; + /** Called when the body is complete. + + @param ec Set to the error, if any occurred. + */ + void finish(error_code& ec) { + // The specification requires this to indicate "no error" + ec = {}; } + private: void complete_line() { if (buffered_line_.has_value()) { complete_lines_.push_back(buffered_line_.value()); @@ -119,9 +136,7 @@ class parser : public http::basic_parser { return index == std::string::npos ? body.size() : index; } - size_t parse_stream(std::uint64_t remain, - boost::string_view body, - boost::beast::error_code& ec) { + void parse_stream(boost::string_view body) { size_t i = 0; while (i < body.length()) { i += this->append_up_to(body.substr(i, body.length() - i), "\r\n"); @@ -143,10 +158,97 @@ class parser : public http::basic_parser { begin_CR_ = false; } } - return body.length(); } - public: - parser() : buffered_line_(), complete_lines_(), begin_CR_(false) {} + static std::pair parse_field(std::string field) { + if (field.empty()) { + assert(0 && "should never parse an empty line"); + } + + size_t colon_index = field.find(':'); + switch (colon_index) { + case 0: + field.erase(0, 1); + return std::make_pair(std::string{"comment"}, std::move(field)); + case std::string::npos: + return std::make_pair(std::move(field), std::string{}); + default: + auto key = field.substr(0, colon_index); + field.erase(0, colon_index + 1); + if (field.find(' ') == 0) { + field.erase(0, 1); + } + return std::make_pair(std::move(key), std::move(field)); + } + } + + void parse_events() { + while (true) { + bool seen_empty_line = false; + + while (!complete_lines_.empty()) { + std::string line = std::move(complete_lines_.front()); + complete_lines_.pop_front(); + + if (line.empty()) { + if (event_.has_value()) { + seen_empty_line = true; + break; + } + continue; + } + + auto field = parse_field(std::move(line)); + if (field.first == "comment") { + event_data e; + e.set_type("comment"); + e.append_data(field.second); + body_.events_.push_back(std::move(e)); + continue; + } + + if (!event_.has_value()) { + event_.emplace(event_data{}); + event_->set_id(last_event_id_); + } + + if (field.first == "event") { + event_->set_type(field.second); + } else if (field.first == "data") { + event_->append_data(field.second); + } else if (field.first == "id") { + if (field.second.find('\0') != std::string::npos) { + // IDs with null-terminators are acceptable, but + // ignored. + continue; + } + last_event_id_ = field.second; + event_->set_id(last_event_id_); + } else if (field.first == "retry") { + } + } + + if (seen_empty_line) { + std::optional data = event_; + event_ = std::nullopt; + + if (data.has_value()) { + data->trim_trailing_newline(); + body_.events_.push_back(std::move(*data)); + } + + continue; + } + + break; + } + } }; + +template +std::ostream& operator<<( + std::ostream&, + http::message, Fields> const&) = + delete; + } // namespace launchdarkly::sse diff --git a/libs/server-sent-events/src/client.cpp b/libs/server-sent-events/src/client.cpp index 8429401ec..d25a3f459 100644 --- a/libs/server-sent-events/src/client.cpp +++ b/libs/server-sent-events/src/client.cpp @@ -17,37 +17,6 @@ namespace net = boost::asio; // from namespace ssl = boost::asio::ssl; // from using tcp = boost::asio::ip::tcp; // from -event_data::event_data() : m_type{}, m_data{}, m_id{} {} - -void event_data::set_type(std::string type) { - m_type = std::move(type); -} -void event_data::append_data(std::string const& data) { - m_data.append(data); - m_data.append("\n"); -} - -void event_data::set_id(std::optional id) { - m_id = std::move(id); -} - -std::string const& event_data::get_type() { - return m_type; -} -std::string const& event_data::get_data() { - return m_data; -} - -void event_data::trim_trailing_newline() { - if (m_data[m_data.size() - 1] == '\n') { - m_data.resize(m_data.size() - 1); - } -} - -std::optional const& event_data::get_id() { - return m_id; -} - client::client(net::any_io_executor ex, http::request req, std::string host, @@ -61,20 +30,12 @@ client::client(net::any_io_executor ex, port_{std::move(port)}, request_{std::move(req)}, response_{}, - buffered_line_{}, - complete_lines_{}, - begin_CR_{false}, - last_event_id_{}, - event_buffer_{}, - events_{}, logging_cb_{std::move(logging_cb)}, - on_chunk_body_trampoline_{}, log_tag_{std::move(log_tag)}, event_callback_{[this](event_data e) { log("got event: (" + e.get_type() + ", " + e.get_data() + ")"); }} { - - // parser_.body_limit(boost::none); + // parser_.body_limit(boost::none); log("create"); } @@ -94,138 +55,6 @@ void client::fail(beast::error_code ec, char const* what) { log(std::string(what) + ":" + ec.message()); } -std::pair parse_field(std::string field) { - if (field.empty()) { - assert(0 && "should never parse an empty line"); - } - - size_t colon_index = field.find(':'); - switch (colon_index) { - case 0: - field.erase(0, 1); - return std::make_pair(std::string{"comment"}, std::move(field)); - case std::string::npos: - return std::make_pair(std::move(field), std::string{}); - default: - auto key = field.substr(0, colon_index); - field.erase(0, colon_index + 1); - if (field.find(' ') == 0) { - field.erase(0, 1); - } - return std::make_pair(std::move(key), std::move(field)); - } -} - -void client::parse_events() { - while (true) { - bool seen_empty_line = false; - - while (!complete_lines_.empty()) { - std::string line = std::move(complete_lines_.front()); - complete_lines_.pop_front(); - - if (line.empty()) { - if (event_buffer_.has_value()) { - seen_empty_line = true; - break; - } - continue; - } - - auto field = parse_field(std::move(line)); - if (field.first == "comment") { - event_data e; - e.set_type("comment"); - e.append_data(field.second); - event_callback_(std::move(e)); - continue; - } - - if (!event_buffer_.has_value()) { - event_buffer_.emplace(event_data{}); - event_buffer_->set_id(last_event_id_); - } - - if (field.first == "event") { - event_buffer_->set_type(field.second); - } else if (field.first == "data") { - event_buffer_->append_data(field.second); - } else if (field.first == "id") { - if (field.second.find('\0') != std::string::npos) { - // IDs with null-terminators are acceptable, but ignored. - continue; - } - last_event_id_ = field.second; - event_buffer_->set_id(last_event_id_); - } else if (field.first == "retry") { - log("got unhandled 'retry' field"); - } - } - - if (seen_empty_line) { - std::optional data = event_buffer_; - event_buffer_ = std::nullopt; - - if (data.has_value()) { - data->trim_trailing_newline(); - event_callback_(std::move(*data)); - } - - continue; - } - - break; - } -} - -void client::complete_line() { - if (buffered_line_.has_value()) { - complete_lines_.push_back(buffered_line_.value()); - buffered_line_.reset(); - } -} - -size_t client::append_up_to(boost::string_view body, - std::string const& search) { - std::size_t index = body.find_first_of(search); - if (index != std::string::npos) { - body.remove_suffix(body.size() - index); - } - if (buffered_line_.has_value()) { - buffered_line_->append(body.to_string()); - } else { - buffered_line_ = std::string{body}; - } - return index == std::string::npos ? body.size() : index; -} - -size_t client::parse_stream(std::uint64_t remain, - boost::string_view body, - beast::error_code& ec) { - size_t i = 0; - while (i < body.length()) { - i += this->append_up_to(body.substr(i, body.length() - i), "\r\n"); - if (i == body.size()) { - continue; - } else if (body.at(i) == '\r') { - complete_line(); - begin_CR_ = true; - i++; - } else if (body.at(i) == '\n') { - if (begin_CR_) { - begin_CR_ = false; - i++; - } else { - complete_line(); - i++; - } - } else { - begin_CR_ = false; - } - } - return body.length(); -} - class ssl_client : public client { public: ssl_client(net::any_io_executor ex, @@ -307,15 +136,6 @@ class ssl_client : public client { beast::get_lowest_layer(stream_).expires_after( std::chrono::seconds(10)); - on_chunk_body_trampoline_.emplace( - [self = shared()](auto remain, auto body, auto ec) { - auto consumed = self->parse_stream(remain, body, ec); - self->parse_events(); - return consumed; - }); - - // parser_.on_chunk_body(*this->on_chunk_body_trampoline_); - http::async_read_some( stream_, buffer_, parser_, beast::bind_front_handler(&ssl_client::on_read, shared())); @@ -327,6 +147,12 @@ class ssl_client : public client { return fail(ec, "read"); } + std::vector& events = parser_.get().body().events(); + for (auto e : events) { + event_callback_(std::move(e)); + } + events.clear(); + beast::get_lowest_layer(stream_).expires_never(); http::async_read_some( @@ -403,38 +229,6 @@ class plaintext_client : public client { beast::get_lowest_layer(stream_).expires_after( std::chrono::seconds(10)); - on_chunk_body_trampoline_.emplace( - [self = shared()](auto remain, auto body, auto ec) { - auto consumed = self->parse_stream(remain, body, ec); - self->parse_events(); - return consumed; - }); - - // parser_.on_chunk_body(*this->on_chunk_body_trampoline_); - - http::async_read_some(stream_, buffer_, parser_, - beast::bind_front_handler( - &plaintext_client::on_got_headers, shared())); - } - - void on_got_headers(beast::error_code ec, std::size_t bytes_transferred) { - boost::ignore_unused(bytes_transferred); - - if (ec) { - return fail(ec, "headers"); - } - -// if (parser_.is_header_done()) { -// auto status = parser_.; -// if (status == http::status::moved_permanently) { -// if (auto it = parser_.get().find("Location"); -// it != parser_.get().end()) { -// } -// } -// } - - beast::get_lowest_layer(stream_).expires_never(); - http::async_read_some( stream_, buffer_, parser_, beast::bind_front_handler(&plaintext_client::on_read, shared())); @@ -446,6 +240,12 @@ class plaintext_client : public client { return fail(ec, "read"); } + std::vector& events = parser_.get().body().events(); + for (auto e : events) { + event_callback_(std::move(e)); + } + events.clear(); + http::async_read_some( stream_, buffer_, parser_, beast::bind_front_handler(&plaintext_client::on_read, shared())); diff --git a/libs/server-sent-events/src/parser.cpp b/libs/server-sent-events/src/parser.cpp index 00afeb08c..aba4ece1d 100644 --- a/libs/server-sent-events/src/parser.cpp +++ b/libs/server-sent-events/src/parser.cpp @@ -1 +1,34 @@ #include + +namespace launchdarkly::sse { +event_data::event_data() : m_type{}, m_data{}, m_id{} {} + +void event_data::set_type(std::string type) { + m_type = std::move(type); +} +void event_data::append_data(std::string const& data) { + m_data.append(data); + m_data.append("\n"); +} + +void event_data::set_id(std::optional id) { + m_id = std::move(id); +} + +std::string const& event_data::get_type() { + return m_type; +} +std::string const& event_data::get_data() { + return m_data; +} + +void event_data::trim_trailing_newline() { + if (m_data[m_data.size() - 1] == '\n') { + m_data.resize(m_data.size() - 1); + } +} + +std::optional const& event_data::get_id() { + return m_id; +} +} // namespace launchdarkly::sse From 81b09433dfd54af40c7526f8b965731cf97bb166 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 24 Mar 2023 09:41:10 -0700 Subject: [PATCH 33/53] Make an official Event class --- .../include/entity_manager.hpp | 3 +- .../include/stream_entity.hpp | 2 +- .../sse-contract-tests/src/entity_manager.cpp | 2 +- apps/sse-contract-tests/src/main.cpp | 3 +- apps/sse-contract-tests/src/server.cpp | 11 +-- apps/sse-contract-tests/src/session.cpp | 12 ++-- apps/sse-contract-tests/src/stream_entity.cpp | 21 +++--- .../include/launchdarkly/sse/client.hpp | 22 ++++-- .../launchdarkly/sse/{ => detail}/parser.hpp | 70 +++++++++---------- .../include/launchdarkly/sse/event.hpp | 24 +++++++ libs/server-sent-events/src/CMakeLists.txt | 2 +- libs/server-sent-events/src/client.cpp | 23 ++---- libs/server-sent-events/src/event.cpp | 30 ++++++++ libs/server-sent-events/src/parser.cpp | 36 +++------- 14 files changed, 147 insertions(+), 114 deletions(-) rename libs/server-sent-events/include/launchdarkly/sse/{ => detail}/parser.hpp (80%) create mode 100644 libs/server-sent-events/include/launchdarkly/sse/event.hpp create mode 100644 libs/server-sent-events/src/event.cpp diff --git a/apps/sse-contract-tests/include/entity_manager.hpp b/apps/sse-contract-tests/include/entity_manager.hpp index e3d5a41c5..d0ea9f948 100644 --- a/apps/sse-contract-tests/include/entity_manager.hpp +++ b/apps/sse-contract-tests/include/entity_manager.hpp @@ -29,7 +29,8 @@ class EntityManager { launchdarkly::Logger& logger_; public: - EntityManager(boost::asio::any_io_executor executor, launchdarkly::Logger& logger); + EntityManager(boost::asio::any_io_executor executor, + launchdarkly::Logger& logger); std::optional create(ConfigParams params); bool destroy(std::string const& id); diff --git a/apps/sse-contract-tests/include/stream_entity.hpp b/apps/sse-contract-tests/include/stream_entity.hpp index 4f7400f93..0674bd0cf 100644 --- a/apps/sse-contract-tests/include/stream_entity.hpp +++ b/apps/sse-contract-tests/include/stream_entity.hpp @@ -51,7 +51,7 @@ class StreamEntity : public std::enable_shared_from_this { private: request_type build_request(std::size_t counter, - launchdarkly::sse::event_data ev); + launchdarkly::sse::Event ev); void on_resolve(beast::error_code ec, tcp::resolver::results_type results); void on_connect(beast::error_code ec, tcp::resolver::results_type::endpoint_type); diff --git a/apps/sse-contract-tests/src/entity_manager.cpp b/apps/sse-contract-tests/src/entity_manager.cpp index 556c605a5..7959b8740 100644 --- a/apps/sse-contract-tests/src/entity_manager.cpp +++ b/apps/sse-contract-tests/src/entity_manager.cpp @@ -24,7 +24,7 @@ std::optional EntityManager::create(ConfigParams params) { } } - client_builder.logging([this](std::string msg){ + client_builder.logging([this](std::string msg) { LD_LOG(logger_, LogLevel::kDebug) << std::move(msg); }); diff --git a/apps/sse-contract-tests/src/main.cpp b/apps/sse-contract-tests/src/main.cpp index 0a5f20061..32d44a657 100644 --- a/apps/sse-contract-tests/src/main.cpp +++ b/apps/sse-contract-tests/src/main.cpp @@ -16,7 +16,8 @@ using launchdarkly::ConsoleBackend; using launchdarkly::LogLevel; int main(int argc, char* argv[]) { - launchdarkly::Logger logger{std::make_unique("sse-contract-tests")}; + launchdarkly::Logger logger{ + std::make_unique("sse-contract-tests")}; try { net::io_context ioc{1}; diff --git a/apps/sse-contract-tests/src/server.cpp b/apps/sse-contract-tests/src/server.cpp index c670ba3bc..52c58caed 100644 --- a/apps/sse-contract-tests/src/server.cpp +++ b/apps/sse-contract-tests/src/server.cpp @@ -52,11 +52,13 @@ server::server(net::io_context& ioc, } void server::fail(beast::error_code ec, char const* what) { - LD_LOG(logger_, LogLevel::kError) << "server: " << what << ": " << ec.message(); + LD_LOG(logger_, LogLevel::kError) + << "server: " << what << ": " << ec.message(); } void server::add_capability(std::string cap) { - LD_LOG(logger_, LogLevel::kDebug) << "server: test capability: <" << cap << ">"; + LD_LOG(logger_, LogLevel::kDebug) + << "server: test capability: <" << cap << ">"; caps_.push_back(std::move(cap)); } @@ -84,9 +86,8 @@ void server::on_accept(boost::system::error_code const& ec, return; } - - auto session = - std::make_shared(std::move(socket), entity_manager_, caps_, logger_); + auto session = std::make_shared(std::move(socket), entity_manager_, + caps_, logger_); session->on_shutdown([this]() { LD_LOG(logger_, LogLevel::kDebug) << "server: terminating"; diff --git a/apps/sse-contract-tests/src/session.cpp b/apps/sse-contract-tests/src/session.cpp index a5f37f76e..7237095e8 100644 --- a/apps/sse-contract-tests/src/session.cpp +++ b/apps/sse-contract-tests/src/session.cpp @@ -35,13 +35,14 @@ void Session::start() { void Session::stop() { LD_LOG(logger_, LogLevel::kDebug) << "session: stop"; - net::dispatch( - stream_.get_executor(), - beast::bind_front_handler(&Session::do_stop, shared_from_this(), "stop requested")); + net::dispatch(stream_.get_executor(), + beast::bind_front_handler( + &Session::do_stop, shared_from_this(), "stop requested")); } void Session::do_stop(char const* reason) { - LD_LOG(logger_, LogLevel::kDebug) << "session: closing socket (" << reason << ")"; + LD_LOG(logger_, LogLevel::kDebug) + << "session: closing socket (" << reason << ")"; stream_.close(); } @@ -81,7 +82,8 @@ void Session::on_write(bool keep_alive, boost::ignore_unused(bytes_transferred); if (shutdown_requested_ && on_shutdown_cb_) { - LD_LOG(logger_, LogLevel::kDebug) << "session: client requested server termination"; + LD_LOG(logger_, LogLevel::kDebug) + << "session: client requested server termination"; on_shutdown_cb_(); } diff --git a/apps/sse-contract-tests/src/stream_entity.cpp b/apps/sse-contract-tests/src/stream_entity.cpp index 852243721..666e08a5a 100644 --- a/apps/sse-contract-tests/src/stream_entity.cpp +++ b/apps/sse-contract-tests/src/stream_entity.cpp @@ -42,12 +42,11 @@ void StreamEntity::do_shutdown(beast::error_code ec, std::string what) { void StreamEntity::run() { // Setup the SSE client to callback into the entity whenever it // receives a comment/event. - client_->on_event( - [self = shared_from_this()](launchdarkly::sse::event_data ev) { - auto http_request = - self->build_request(self->callback_counter_++, std::move(ev)); - self->outbox_.push(http_request); - }); + client_->on_event([self = shared_from_this()](launchdarkly::sse::Event ev) { + auto http_request = + self->build_request(self->callback_counter_++, std::move(ev)); + self->outbox_.push(http_request); + }); // Kickoff the SSE client's async operations. client_->run(); @@ -68,7 +67,7 @@ void StreamEntity::stop() { StreamEntity::request_type StreamEntity::build_request( std::size_t counter, - launchdarkly::sse::event_data ev) { + launchdarkly::sse::Event ev) { request_type req; req.set(http::field::host, callback_host_); @@ -77,11 +76,11 @@ StreamEntity::request_type StreamEntity::build_request( nlohmann::json json; - if (ev.get_type() == "comment") { - json = CommentMessage{"comment", ev.get_data()}; + if (ev.type() == "comment") { + json = CommentMessage{"comment", ev.take()}; } else { - json = EventMessage{"event", Event{ev.get_type(), ev.get_data(), - ev.get_id().value_or("")}}; + json = EventMessage{"event", + Event{ev.type(), ev.take(), ev.id().value_or("")}}; } req.body() = json.dump(); diff --git a/libs/server-sent-events/include/launchdarkly/sse/client.hpp b/libs/server-sent-events/include/launchdarkly/sse/client.hpp index f38453afd..6a713e984 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/client.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/client.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include "launchdarkly/sse/detail/parser.hpp" #include #include @@ -46,15 +46,26 @@ class builder { std::function logging_cb_; }; -using sse_event = event_data; +using sse_event = Event; using sse_comment = std::string; using event = std::variant; +// struct event_consumer_callback { +// std::function cb; +// event_consumer_callback() = default; +// ~event_consumer_callback() = default; +// void operator()(event_data data) { +// if (cb) { +// cb(std::move(data)); +// } +// } +// }; + class client : public std::enable_shared_from_this { public: using logger = std::function; - + using events = std::function; client(boost::asio::any_io_executor ex, http::request req, std::string host, @@ -65,14 +76,14 @@ class client : public std::enable_shared_from_this { template void on_event(Callback event_cb) { - event_callback_ = event_cb; + parser_.get().body().on_event(event_cb); } virtual void run() = 0; virtual void close() = 0; protected: - using body = launchdarkly::sse::EventBody>; + using body = launchdarkly::sse::detail::EventBody; using parser = http::response_parser; tcp::resolver resolver_; beast::flat_buffer buffer_; @@ -82,7 +93,6 @@ class client : public std::enable_shared_from_this { std::string host_; std::string port_; - std::function event_callback_; logger logging_cb_; std::string log_tag_; diff --git a/libs/server-sent-events/include/launchdarkly/sse/parser.hpp b/libs/server-sent-events/include/launchdarkly/sse/detail/parser.hpp similarity index 80% rename from libs/server-sent-events/include/launchdarkly/sse/parser.hpp rename to libs/server-sent-events/include/launchdarkly/sse/detail/parser.hpp index bce35db18..9c01e13c2 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/parser.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/detail/parser.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -12,55 +14,48 @@ #include #include -namespace launchdarkly::sse { +namespace launchdarkly::sse::detail { -using namespace boost::beast; +struct Event { + std::string type; + std::string data; + std::optional id; -class event_data { - std::string m_type; - std::string m_data; - std::optional m_id; + Event() = default; - public: - explicit event_data(); - void set_type(std::string); - void set_id(std::optional); void append_data(std::string const&); void trim_trailing_newline(); - std::string const& get_type(); - std::string const& get_data(); - std::optional const& get_id(); }; -template +using namespace boost::beast; + +template struct EventBody { - using event_type = EventContainer; + using event_type = EventReceiver; class reader; class value_type; - static std::uint64_t size(value_type const& body); }; -template -class EventBody::value_type { +template +class EventBody::value_type { friend class reader; friend struct EventBody; - EventContainer events_; + EventReceiver events_; public: - EventContainer& events() { return events_; } + void on_event(EventReceiver&& receiver) { events_ = std::move(receiver); } }; -template -struct EventBody::reader { +template +struct EventBody::reader { value_type& body_; std::optional buffered_line_; std::deque complete_lines_; bool begin_CR_; std::optional last_event_id_; - - std::optional event_; + std::optional event_; public: template @@ -200,20 +195,18 @@ struct EventBody::reader { auto field = parse_field(std::move(line)); if (field.first == "comment") { - event_data e; - e.set_type("comment"); - e.append_data(field.second); - body_.events_.push_back(std::move(e)); + body_.events_( + launchdarkly::sse::Event("comment", field.second)); continue; } if (!event_.has_value()) { - event_.emplace(event_data{}); - event_->set_id(last_event_id_); + event_.emplace(Event{}); + event_->id = last_event_id_; } if (field.first == "event") { - event_->set_type(field.second); + event_->type = field.second; } else if (field.first == "data") { event_->append_data(field.second); } else if (field.first == "id") { @@ -223,18 +216,20 @@ struct EventBody::reader { continue; } last_event_id_ = field.second; - event_->set_id(last_event_id_); + event_->id = last_event_id_; } else if (field.first == "retry") { } } if (seen_empty_line) { - std::optional data = event_; + std::optional data = event_; event_ = std::nullopt; if (data.has_value()) { data->trim_trailing_newline(); - body_.events_.push_back(std::move(*data)); + body_.events_(launchdarkly::sse::Event( + data->type, data->data, data->id)); + data.reset(); } continue; @@ -245,10 +240,9 @@ struct EventBody::reader { } }; -template +template std::ostream& operator<<( std::ostream&, - http::message, Fields> const&) = - delete; + http::message, Fields> const&) = delete; -} // namespace launchdarkly::sse +} // namespace launchdarkly::sse::detail diff --git a/libs/server-sent-events/include/launchdarkly/sse/event.hpp b/libs/server-sent-events/include/launchdarkly/sse/event.hpp new file mode 100644 index 000000000..1487fdede --- /dev/null +++ b/libs/server-sent-events/include/launchdarkly/sse/event.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +namespace launchdarkly::sse { + +class Event { + public: + Event(std::string type, std::string data); + Event(std::string type, std::string data, std::string id); + Event(std::string type, std::string data, std::optional id); + std::string const& type() const; + std::string const& data() const; + std::optional const& id() const; + std::string&& take(); + + private: + std::string type_; + std::string data_; + std::optional id_; +}; + +} // namespace launchdarkly::sse diff --git a/libs/server-sent-events/src/CMakeLists.txt b/libs/server-sent-events/src/CMakeLists.txt index 27843fb60..74de52018 100644 --- a/libs/server-sent-events/src/CMakeLists.txt +++ b/libs/server-sent-events/src/CMakeLists.txt @@ -2,7 +2,7 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${LaunchDarklySSEClient_SOURCE_DIR}/include/launchdarkly/*.hpp") # Automatic library: static or dynamic based on user config. -add_library(${LIBNAME} client.cpp parser.cpp boost-url.cpp ${HEADER_LIST}) +add_library(${LIBNAME} client.cpp parser.cpp event.cpp boost-url.cpp ${HEADER_LIST}) target_link_libraries(${LIBNAME} PUBLIC OpenSSL::SSL Boost::headers) add_library(launchdarkly::sse ALIAS ${LIBNAME}) diff --git a/libs/server-sent-events/src/client.cpp b/libs/server-sent-events/src/client.cpp index d25a3f459..0ae221af8 100644 --- a/libs/server-sent-events/src/client.cpp +++ b/libs/server-sent-events/src/client.cpp @@ -30,12 +30,13 @@ client::client(net::any_io_executor ex, port_{std::move(port)}, request_{std::move(req)}, response_{}, - logging_cb_{std::move(logging_cb)}, log_tag_{std::move(log_tag)}, - event_callback_{[this](event_data e) { - log("got event: (" + e.get_type() + ", " + e.get_data() + ")"); - }} { - // parser_.body_limit(boost::none); + logging_cb_{std::move(logging_cb)} { + parser_.body_limit(boost::none); + + on_event([this](Event e) { + log("got event: (" + e.type() + ", " + e.data() + ")"); + }); log("create"); } @@ -147,12 +148,6 @@ class ssl_client : public client { return fail(ec, "read"); } - std::vector& events = parser_.get().body().events(); - for (auto e : events) { - event_callback_(std::move(e)); - } - events.clear(); - beast::get_lowest_layer(stream_).expires_never(); http::async_read_some( @@ -240,12 +235,6 @@ class plaintext_client : public client { return fail(ec, "read"); } - std::vector& events = parser_.get().body().events(); - for (auto e : events) { - event_callback_(std::move(e)); - } - events.clear(); - http::async_read_some( stream_, buffer_, parser_, beast::bind_front_handler(&plaintext_client::on_read, shared())); diff --git a/libs/server-sent-events/src/event.cpp b/libs/server-sent-events/src/event.cpp new file mode 100644 index 000000000..80fbaf69a --- /dev/null +++ b/libs/server-sent-events/src/event.cpp @@ -0,0 +1,30 @@ +#include + +namespace launchdarkly::sse { + +Event::Event(std::string type, std::string data) + : Event(std::move(type), std::move(data), std::nullopt) {} + +Event::Event(std::string type, std::string data, std::string id) + : Event(std::move(type), + std::move(data), + std::optional{std::move(id)}) {} + +Event::Event(std::string type, std::string data, std::optional id) + : type_(std::move(type)), data_(std::move(data)), id_(std::move(id)) {} + +std::string const& Event::type() const { + return type_; +} +std::string const& Event::data() const { + return data_; +} +std::optional const& Event::id() const { + return id_; +} + +std::string&& Event::take() { + return std::move(data_); +}; + +} // namespace launchdarkly::sse diff --git a/libs/server-sent-events/src/parser.cpp b/libs/server-sent-events/src/parser.cpp index aba4ece1d..dc498c5dd 100644 --- a/libs/server-sent-events/src/parser.cpp +++ b/libs/server-sent-events/src/parser.cpp @@ -1,34 +1,16 @@ -#include +#include -namespace launchdarkly::sse { -event_data::event_data() : m_type{}, m_data{}, m_id{} {} +namespace launchdarkly::sse::detail { -void event_data::set_type(std::string type) { - m_type = std::move(type); -} -void event_data::append_data(std::string const& data) { - m_data.append(data); - m_data.append("\n"); -} - -void event_data::set_id(std::optional id) { - m_id = std::move(id); +void Event::append_data(std::string const& input) { + data.append(input); + data.append("\n"); } -std::string const& event_data::get_type() { - return m_type; -} -std::string const& event_data::get_data() { - return m_data; -} - -void event_data::trim_trailing_newline() { - if (m_data[m_data.size() - 1] == '\n') { - m_data.resize(m_data.size() - 1); +void Event::trim_trailing_newline() { + if (data[data.size() - 1] == '\n') { + data.resize(data.size() - 1); } } -std::optional const& event_data::get_id() { - return m_id; -} -} // namespace launchdarkly::sse +} // namespace launchdarkly::sse::detail From 2b6da695bdfe9847def9cba2670d595b74913e3f Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 24 Mar 2023 09:55:15 -0700 Subject: [PATCH 34/53] Make construction of JSON EventMessage easier --- apps/sse-contract-tests/include/definitions.hpp | 11 ++++++++--- apps/sse-contract-tests/src/stream_entity.cpp | 5 ++--- .../include/launchdarkly/sse/detail/parser.hpp | 4 +++- .../include/launchdarkly/sse/event.hpp | 2 +- libs/server-sent-events/src/event.cpp | 2 +- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/apps/sse-contract-tests/include/definitions.hpp b/apps/sse-contract-tests/include/definitions.hpp index b75887f44..cc75b4eba 100644 --- a/apps/sse-contract-tests/include/definitions.hpp +++ b/apps/sse-contract-tests/include/definitions.hpp @@ -1,10 +1,10 @@ #pragma once -#include "nlohmann/json.hpp" - +#include #include #include #include +#include "nlohmann/json.hpp" namespace nlohmann { @@ -55,8 +55,13 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigParams, struct Event { std::string type; - std::string data; std::string id; + std::string data; + Event() = default; + explicit Event(launchdarkly::sse::Event event) + : type(event.type()), + id(event.id().value_or("")), + data(std::move(event).take()) {} }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(Event, type, data, id); diff --git a/apps/sse-contract-tests/src/stream_entity.cpp b/apps/sse-contract-tests/src/stream_entity.cpp index 666e08a5a..6f3499587 100644 --- a/apps/sse-contract-tests/src/stream_entity.cpp +++ b/apps/sse-contract-tests/src/stream_entity.cpp @@ -77,10 +77,9 @@ StreamEntity::request_type StreamEntity::build_request( nlohmann::json json; if (ev.type() == "comment") { - json = CommentMessage{"comment", ev.take()}; + json = CommentMessage{"comment", std::move(ev).take()}; } else { - json = EventMessage{"event", - Event{ev.type(), ev.take(), ev.id().value_or("")}}; + json = EventMessage{"event", Event{ev}}; } req.body() = json.dump(); diff --git a/libs/server-sent-events/include/launchdarkly/sse/detail/parser.hpp b/libs/server-sent-events/include/launchdarkly/sse/detail/parser.hpp index 9c01e13c2..80a8a89d9 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/detail/parser.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/detail/parser.hpp @@ -16,6 +16,9 @@ namespace launchdarkly::sse::detail { +using namespace boost::beast; + + struct Event { std::string type; std::string data; @@ -27,7 +30,6 @@ struct Event { void trim_trailing_newline(); }; -using namespace boost::beast; template struct EventBody { diff --git a/libs/server-sent-events/include/launchdarkly/sse/event.hpp b/libs/server-sent-events/include/launchdarkly/sse/event.hpp index 1487fdede..fb01c6edc 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/event.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/event.hpp @@ -13,7 +13,7 @@ class Event { std::string const& type() const; std::string const& data() const; std::optional const& id() const; - std::string&& take(); + std::string&& take() &&; private: std::string type_; diff --git a/libs/server-sent-events/src/event.cpp b/libs/server-sent-events/src/event.cpp index 80fbaf69a..7ee25c652 100644 --- a/libs/server-sent-events/src/event.cpp +++ b/libs/server-sent-events/src/event.cpp @@ -23,7 +23,7 @@ std::optional const& Event::id() const { return id_; } -std::string&& Event::take() { +std::string&& Event::take() && { return std::move(data_); }; From bd224f229f50feb0c873eb9aa79e930c231e04a5 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 24 Mar 2023 14:57:02 -0700 Subject: [PATCH 35/53] Bunch of renamings --- apps/hello-cpp/main.cpp | 12 +- apps/sse-contract-tests/README.md | 3 + .../include/definitions.hpp | 5 + .../include/entity_manager.hpp | 20 +- .../include/stream_entity.hpp | 15 +- .../sse-contract-tests/src/entity_manager.cpp | 37 +- apps/sse-contract-tests/src/stream_entity.cpp | 51 +-- .../include/launchdarkly/sse/client.hpp | 83 +--- libs/server-sent-events/src/client.cpp | 354 +++++++++--------- 9 files changed, 257 insertions(+), 323 deletions(-) create mode 100644 apps/sse-contract-tests/README.md diff --git a/apps/hello-cpp/main.cpp b/apps/hello-cpp/main.cpp index 3b0e7ee2e..b8a3bea82 100644 --- a/apps/hello-cpp/main.cpp +++ b/apps/hello-cpp/main.cpp @@ -24,13 +24,19 @@ int main() { net::io_context ioc; - const char* key = std::getenv("STG_SDK_KEY"); - if (!key){ + char const* key = std::getenv("STG_SDK_KEY"); + if (!key) { std::cout << "Set environment variable STG_SDK_KEY to the sdk key\n"; return 1; } - auto client = launchdarkly::sse::builder(ioc.get_executor(), "https://stream-stg.launchdarkly.com/all") + auto client = + launchdarkly::sse::Builder(ioc.get_executor(), + "https://stream-stg.launchdarkly.com/all") .header("Authorization", key) + .receiver([&](launchdarkly::sse::Event ev) { + LD_LOG(logger, LogLevel::kInfo) << "event: " << ev.type(); + LD_LOG(logger, LogLevel::kInfo) << "data: " << std::move(ev).take(); + }) .build(); if (!client) { diff --git a/apps/sse-contract-tests/README.md b/apps/sse-contract-tests/README.md new file mode 100644 index 000000000..92f007e5a --- /dev/null +++ b/apps/sse-contract-tests/README.md @@ -0,0 +1,3 @@ +## SSE contract tests + +### Conceptual Model diff --git a/apps/sse-contract-tests/include/definitions.hpp b/apps/sse-contract-tests/include/definitions.hpp index cc75b4eba..34539a279 100644 --- a/apps/sse-contract-tests/include/definitions.hpp +++ b/apps/sse-contract-tests/include/definitions.hpp @@ -30,6 +30,7 @@ struct adl_serializer> { }; } // namespace nlohmann +// Represents the initial JSON configuration sent by the test harness. struct ConfigParams { std::string streamUrl; std::string callbackUrl; @@ -53,6 +54,10 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigParams, method, body); +// Represents an event payload that this service posts back +// to the test harness. The events are originally received by this server +// via the SSE stream; they are posted back so the test harness can verify +// that we parsed and dispatched them successfully. struct Event { std::string type; std::string id; diff --git a/apps/sse-contract-tests/include/entity_manager.hpp b/apps/sse-contract-tests/include/entity_manager.hpp index d0ea9f948..95698eb02 100644 --- a/apps/sse-contract-tests/include/entity_manager.hpp +++ b/apps/sse-contract-tests/include/entity_manager.hpp @@ -11,19 +11,19 @@ #include #include -class StreamEntity; +#include -// Manages the individual SSE clients (called entities here) which are -// instantiated for each contract test. +class EventOutbox; + +// class EntityManager { - // Maps the entity's ID to the entity. Shared pointer is necessary because - // these entities are doing async IO and must remain alive as long as that - // is happening. - std::unordered_map> entities_; - // Incremented each time create() is called to instantiate a new entity. + using Inbox = std::shared_ptr; + using Outbox = std::shared_ptr; + using Entity = std::pair; + + std::unordered_map entities_; + std::size_t counter_; - // Synchronizes access to create()/destroy(); - std::mutex lock_; boost::asio::any_io_executor executor_; launchdarkly::Logger& logger_; diff --git a/apps/sse-contract-tests/include/stream_entity.hpp b/apps/sse-contract-tests/include/stream_entity.hpp index 0674bd0cf..fff134b08 100644 --- a/apps/sse-contract-tests/include/stream_entity.hpp +++ b/apps/sse-contract-tests/include/stream_entity.hpp @@ -2,11 +2,14 @@ #include "entity_manager.hpp" -#include "launchdarkly/sse/client.hpp" +#include #include #include + #include +#include + #include #include @@ -17,13 +20,11 @@ namespace http = beast::http; // from namespace net = boost::asio; // from using tcp = boost::asio::ip::tcp; // from -class StreamEntity : public std::enable_shared_from_this { +class EventOutbox : public std::enable_shared_from_this { // Simple string body request is appropriate since the JSON // returned to the test service is minimal. using request_type = http::request; - std::shared_ptr client_; - std::string callback_url_; std::string callback_port_; std::string callback_host_; @@ -41,11 +42,11 @@ class StreamEntity : public std::enable_shared_from_this { std::string id_; public: - StreamEntity(net::any_io_executor executor, - std::shared_ptr client, + EventOutbox(net::any_io_executor executor, std::string callback_url); - ~StreamEntity(); + void deliver_event(launchdarkly::sse::Event event); + void run(); void stop(); diff --git a/apps/sse-contract-tests/src/entity_manager.cpp b/apps/sse-contract-tests/src/entity_manager.cpp index 7959b8740..d9d5e9e81 100644 --- a/apps/sse-contract-tests/src/entity_manager.cpp +++ b/apps/sse-contract-tests/src/entity_manager.cpp @@ -6,20 +6,22 @@ using launchdarkly::LogLevel; EntityManager::EntityManager(boost::asio::any_io_executor executor, launchdarkly::Logger& logger) - : entities_{}, + : entities_(), counter_{0}, - lock_{}, executor_{std::move(executor)}, logger_{logger} {} std::optional EntityManager::create(ConfigParams params) { - std::lock_guard guard{lock_}; std::string id = std::to_string(counter_++); + auto poster = std::make_shared(executor_, params.callbackUrl); + poster->run(); + auto client_builder = - launchdarkly::sse::builder{executor_, params.streamUrl}; + launchdarkly::sse::Builder(executor_, params.streamUrl); + if (params.headers) { - for (auto h : *params.headers) { + for (auto const& h : *params.headers) { client_builder.header(h.first, h.second); } } @@ -28,40 +30,39 @@ std::optional EntityManager::create(ConfigParams params) { LD_LOG(logger_, LogLevel::kDebug) << std::move(msg); }); - std::shared_ptr client = client_builder.build(); + client_builder.receiver([copy = poster](launchdarkly::sse::Event e) { + copy->deliver_event(std::move(e)); + }); + + auto client = client_builder.build(); if (!client) { LD_LOG(logger_, LogLevel::kWarn) << "entity_manager: couldn't build sse client"; return std::nullopt; } - std::shared_ptr entity = - std::make_shared(executor_, client, params.callbackUrl); - entity->run(); + client->run(); - entities_.emplace(id, entity); + entities_.emplace(id, std::make_pair(client, poster)); return id; } void EntityManager::destroy_all() { - for (auto& entity : entities_) { - if (auto weak = entity.second.lock()) { - weak->stop(); - } + for (auto& kv : entities_) { + auto& entities = kv.second; + // todo: entities.first.stop() + entities.second->stop(); } entities_.clear(); } bool EntityManager::destroy(std::string const& id) { - std::lock_guard guard{lock_}; auto it = entities_.find(id); if (it == entities_.end()) { return false; } - if (auto weak = it->second.lock()) { - weak->stop(); - } + it->second.second->stop(); entities_.erase(it); diff --git a/apps/sse-contract-tests/src/stream_entity.cpp b/apps/sse-contract-tests/src/stream_entity.cpp index 6f3499587..d5600bab4 100644 --- a/apps/sse-contract-tests/src/stream_entity.cpp +++ b/apps/sse-contract-tests/src/stream_entity.cpp @@ -9,10 +9,9 @@ // at this interval. auto const kFlushInterval = boost::posix_time::milliseconds{10}; -StreamEntity::StreamEntity(net::any_io_executor executor, - std::shared_ptr client, +EventOutbox::EventOutbox(net::any_io_executor executor, std::string callback_url) - : client_{std::move(client)}, + : callback_url_{std::move(callback_url)}, callback_port_{}, callback_host_{}, @@ -29,43 +28,33 @@ StreamEntity::StreamEntity(net::any_io_executor executor, callback_port_ = uri_components->port(); } -StreamEntity::~StreamEntity() { - std::cout << "~StreamEntity\n"; -} - -void StreamEntity::do_shutdown(beast::error_code ec, std::string what) { +void EventOutbox::do_shutdown(beast::error_code ec, std::string what) { event_stream_.socket().shutdown(tcp::socket::shutdown_both, ec); flush_timer_.cancel(); - client_->close(); } -void StreamEntity::run() { - // Setup the SSE client to callback into the entity whenever it - // receives a comment/event. - client_->on_event([self = shared_from_this()](launchdarkly::sse::Event ev) { - auto http_request = - self->build_request(self->callback_counter_++, std::move(ev)); - self->outbox_.push(http_request); - }); +void EventOutbox::deliver_event(launchdarkly::sse::Event event) { + auto http_request = build_request(callback_counter_++, std::move(event)); + outbox_.push(http_request); +} - // Kickoff the SSE client's async operations. - client_->run(); +void EventOutbox::run() { // Begin connecting to the test harness's event-posting service. resolver_.async_resolve(callback_host_, callback_port_, - beast::bind_front_handler(&StreamEntity::on_resolve, + beast::bind_front_handler(&EventOutbox::on_resolve, shared_from_this())); } -void StreamEntity::stop() { +void EventOutbox::stop() { beast::error_code ec = net::error::basic_errors::operation_aborted; std::string reason = "stop"; net::post(executor_, - beast::bind_front_handler(&StreamEntity::do_shutdown, + beast::bind_front_handler(&EventOutbox::do_shutdown, shared_from_this(), ec, reason)); } -StreamEntity::request_type StreamEntity::build_request( +EventOutbox::request_type EventOutbox::build_request( std::size_t counter, launchdarkly::sse::Event ev) { request_type req; @@ -87,7 +76,7 @@ StreamEntity::request_type StreamEntity::build_request( return req; } -void StreamEntity::on_resolve(beast::error_code ec, +void EventOutbox::on_resolve(beast::error_code ec, tcp::resolver::results_type results) { if (ec) { return do_shutdown(ec, "resolve"); @@ -96,11 +85,11 @@ void StreamEntity::on_resolve(beast::error_code ec, // Make the connection on the IP address we get from a lookup. beast::get_lowest_layer(event_stream_) .async_connect(results, - beast::bind_front_handler(&StreamEntity::on_connect, + beast::bind_front_handler(&EventOutbox::on_connect, shared_from_this())); } -void StreamEntity::on_connect(beast::error_code ec, +void EventOutbox::on_connect(beast::error_code ec, tcp::resolver::results_type::endpoint_type) { if (ec) { return do_shutdown(ec, "connect"); @@ -109,11 +98,11 @@ void StreamEntity::on_connect(beast::error_code ec, // Now that we're connected, kickoff the event flush "loop". boost::system::error_code dummy; net::post(executor_, - beast::bind_front_handler(&StreamEntity::on_flush_timer, + beast::bind_front_handler(&EventOutbox::on_flush_timer, shared_from_this(), dummy)); } -void StreamEntity::on_flush_timer(boost::system::error_code ec) { +void EventOutbox::on_flush_timer(boost::system::error_code ec) { if (ec) { return do_shutdown(ec, "flush"); } @@ -125,7 +114,7 @@ void StreamEntity::on_flush_timer(boost::system::error_code ec) { // and then popping it. http::async_write(event_stream_, request, - beast::bind_front_handler(&StreamEntity::on_write, + beast::bind_front_handler(&EventOutbox::on_write, shared_from_this())); return; } @@ -133,10 +122,10 @@ void StreamEntity::on_flush_timer(boost::system::error_code ec) { // If the outbox is empty, wait a bit before trying again. flush_timer_.expires_from_now(kFlushInterval); flush_timer_.async_wait(beast::bind_front_handler( - &StreamEntity::on_flush_timer, shared_from_this())); + &EventOutbox::on_flush_timer, shared_from_this())); } -void StreamEntity::on_write(beast::error_code ec, std::size_t) { +void EventOutbox::on_write(beast::error_code ec, std::size_t) { if (ec) { return do_shutdown(ec, "write"); } diff --git a/libs/server-sent-events/include/launchdarkly/sse/client.hpp b/libs/server-sent-events/include/launchdarkly/sse/client.hpp index 6a713e984..e96a590b7 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/client.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/client.hpp @@ -1,16 +1,11 @@ #pragma once -#include "launchdarkly/sse/detail/parser.hpp" +#include #include -#include -#include -#include -#include #include #include -#include #include #include @@ -24,80 +19,34 @@ namespace launchdarkly::sse { namespace beast = boost::beast; // from namespace http = beast::http; // from namespace net = boost::asio; // from -namespace ssl = boost::asio::ssl; // from -using tcp = boost::asio::ip::tcp; // from -class client; +class Client; -class builder { +class Builder { public: - builder(net::any_io_executor ioc, std::string url); - builder& header(std::string const& name, std::string const& value); - builder& method(http::verb verb); - builder& tls(ssl::context_base::method); - builder& logging(std::function callback); - std::shared_ptr build(); + using EventReceiver = std::function; + using LogCallback = std::function; + + Builder(net::any_io_executor ioc, std::string url); + Builder& header(std::string const& name, std::string const& value); + Builder& method(http::verb verb); + Builder& receiver(EventReceiver); + Builder& logging(LogCallback callback); + std::shared_ptr build(); private: std::string url_; net::any_io_executor executor_; - ssl::context ssl_context_; http::request request_; - std::function logging_cb_; + LogCallback logging_cb_; + EventReceiver receiver_; }; -using sse_event = Event; -using sse_comment = std::string; - -using event = std::variant; - -// struct event_consumer_callback { -// std::function cb; -// event_consumer_callback() = default; -// ~event_consumer_callback() = default; -// void operator()(event_data data) { -// if (cb) { -// cb(std::move(data)); -// } -// } -// }; - -class client : public std::enable_shared_from_this { +class Client { public: - using logger = std::function; - using events = std::function; - client(boost::asio::any_io_executor ex, - http::request req, - std::string host, - std::string port, - logger logger, - std::string log_tag); - ~client(); - - template - void on_event(Callback event_cb) { - parser_.get().body().on_event(event_cb); - } - + virtual ~Client() = default; virtual void run() = 0; virtual void close() = 0; - - protected: - using body = launchdarkly::sse::detail::EventBody; - using parser = http::response_parser; - tcp::resolver resolver_; - beast::flat_buffer buffer_; - http::request request_; - http::response response_; - parser parser_; - std::string host_; - std::string port_; - - logger logging_cb_; - std::string log_tag_; - - void log(std::string); - void fail(beast::error_code ec, char const* what); }; } // namespace launchdarkly::sse diff --git a/libs/server-sent-events/src/client.cpp b/libs/server-sent-events/src/client.cpp index 0ae221af8..3d4447eca 100644 --- a/libs/server-sent-events/src/client.cpp +++ b/libs/server-sent-events/src/client.cpp @@ -1,10 +1,17 @@ #include +#include "launchdarkly/sse/detail/parser.hpp" +#include #include #include -#include -#include + +#include +#include +#include + #include + +#include #include #include #include @@ -17,129 +24,87 @@ namespace net = boost::asio; // from namespace ssl = boost::asio::ssl; // from using tcp = boost::asio::ip::tcp; // from -client::client(net::any_io_executor ex, - http::request req, - std::string host, - std::string port, - std::function logging_cb, - std::string log_tag) - : resolver_{ex}, - buffer_{}, - parser_{}, - host_{std::move(host)}, - port_{std::move(port)}, - request_{std::move(req)}, - response_{}, - log_tag_{std::move(log_tag)}, - logging_cb_{std::move(logging_cb)} { - parser_.body_limit(boost::none); - - on_event([this](Event e) { - log("got event: (" + e.type() + ", " + e.data() + ")"); - }); - - log("create"); -} +char const* kUserAgent = "CPPClient/0.0.0"; -client::~client() { - log("destroy"); -} +template +class Session { + private: + Derived& derived() { return static_cast(*this); } + http::request req_; + std::chrono::seconds connect_timeout_; + std::chrono::seconds write_timeout_; -void client::log(std::string what) { - if (logging_cb_) { - logging_cb_(log_tag_ + ": " + std::move(what)); - } -} + protected: + beast::flat_buffer buffer_; + std::string host_; + std::string port_; + tcp::resolver resolver_; -// Report a failure -void client::fail(beast::error_code ec, char const* what) { - log(std::string(what) + ":" + ec.message()); -} + using cb = std::function; + using body = launchdarkly::sse::detail::EventBody; + http::response_parser parser_; -class ssl_client : public client { public: - ssl_client(net::any_io_executor ex, - ssl::context& ctx, - http::request req, - std::string host, - std::string port, - logger logging_cb) - : client(ex, - std::move(req), - std::move(host), - std::move(port), - std::move(logging_cb), - "sse-tls"), - stream_{ex, ctx} {} - - void run() override { - // Set SNI Hostname (many hosts need this to handshake successfully) - if (!SSL_set_tlsext_host_name(stream_.native_handle(), host_.c_str())) { - beast::error_code ec{static_cast(::ERR_get_error()), - net::error::get_ssl_category()}; - log("failed to set TLS host name extension: " + ec.message()); - return; - } - - beast::get_lowest_layer(stream_).expires_after( - std::chrono::seconds(15)); - - resolver_.async_resolve( - host_, port_, - beast::bind_front_handler(&ssl_client::on_resolve, shared())); + Session(net::any_io_executor exec, + std::string host, + std::string port, + http::request r, + std::chrono::seconds connect_timeout, + Builder::EventReceiver receiver) + : req_(std::move(r)), + resolver_(std::move(exec)), + connect_timeout_(connect_timeout), + write_timeout_(std::chrono::seconds(10)), + host_(std::move(host)), + port_(std::move(port)), + parser_() { + parser_.get().body().on_event(std::move(receiver)); } - void close() override { - net::post(stream_.get_executor(), - beast::bind_front_handler(&ssl_client::on_stop, shared())); + void do_write() { + http::async_write( + derived().stream(), req_, + beast::bind_front_handler(&Session::on_write, + derived().shared_from_this())); } - private: - beast::ssl_stream stream_; - - std::shared_ptr shared() { - return std::static_pointer_cast(shared_from_this()); + void do_resolve() { + resolver_.async_resolve( + host_, port_, + beast::bind_front_handler(&Session::on_resolve, + derived().shared_from_this())); } - void on_resolve(beast::error_code ec, tcp::resolver::results_type results) { - if (ec) - return fail(ec, "resolve"); - // Make the connection on the IP address we get from a lookup - beast::get_lowest_layer(stream_).async_connect( - results, - beast::bind_front_handler(&ssl_client::on_connect, shared())); + void fail(beast::error_code ec, char const* what) { + // log(std::string(what) + ":" + ec.message()); } void on_connect(beast::error_code ec, - tcp::resolver::results_type::endpoint_type) { - if (ec) + tcp::resolver::results_type::endpoint_type eps) { + if (ec) { return fail(ec, "connect"); - - stream_.async_handshake( - ssl::stream_base::client, - beast::bind_front_handler(&ssl_client::on_handshake, shared())); + } + derived().do_handshake(); } void on_handshake(beast::error_code ec) { if (ec) return fail(ec, "handshake"); - // Send the HTTP request to the remote host - http::async_write( - stream_, request_, - beast::bind_front_handler(&ssl_client::on_write, shared())); + do_write(); } void on_write(beast::error_code ec, std::size_t) { if (ec) return fail(ec, "write"); - beast::get_lowest_layer(stream_).expires_after( - std::chrono::seconds(10)); + beast::get_lowest_layer(derived().stream()) + .expires_after(write_timeout_); http::async_read_some( - stream_, buffer_, parser_, - beast::bind_front_handler(&ssl_client::on_read, shared())); + derived().stream(), buffer_, parser_, + beast::bind_front_handler(&Session::on_read, + derived().shared_from_this())); } void on_read(beast::error_code ec, std::size_t bytes_transferred) { @@ -148,159 +113,174 @@ class ssl_client : public client { return fail(ec, "read"); } - beast::get_lowest_layer(stream_).expires_never(); + beast::get_lowest_layer(derived().stream()).expires_never(); http::async_read_some( - stream_, buffer_, parser_, - beast::bind_front_handler(&ssl_client::on_read, shared())); + derived().stream(), buffer_, parser_, + beast::bind_front_handler(&Session::on_read, + derived().shared_from_this())); } - void on_stop() { beast::get_lowest_layer(stream_).cancel(); } -}; + void on_resolve(beast::error_code ec, tcp::resolver::results_type results) { + if (ec) + return fail(ec, "resolve"); -class plaintext_client : public client { - public: - plaintext_client(net::any_io_executor ex, - ssl::context& ctx, - http::request req, - std::string host, - std::string port, - logger logger) - : client(ex, - std::move(req), - std::move(host), - std::move(port), - std::move(logger), - "sse-plaintext"), - stream_{ex}, - ctx_{ctx} {} - - void run() override { - beast::get_lowest_layer(stream_).expires_after( - std::chrono::seconds(15)); + beast::get_lowest_layer(derived().stream()) + .expires_after(connect_timeout_); - resolver_.async_resolve( - host_, port_, - beast::bind_front_handler(&plaintext_client::on_resolve, shared())); + beast::get_lowest_layer(derived().stream()) + .async_connect(results, beast::bind_front_handler( + &Session::on_connect, + derived().shared_from_this())); } - void close() override { - net::post( - stream_.get_executor(), - beast::bind_front_handler(&plaintext_client::on_stop, shared())); - } - - private: - beast::tcp_stream stream_; - ssl::context& ctx_; + void do_close() { beast::get_lowest_layer(derived().stream()).cancel(); } +}; - std::shared_ptr shared() { - return std::static_pointer_cast(shared_from_this()); - } +class SecureClient : public Client, + public Session, + public std::enable_shared_from_this { + public: + SecureClient(net::any_io_executor ex, + ssl::context ctx, + http::request req, + std::string host, + std::string port, + Builder::EventReceiver receiver) + : Session(ex, + host, + port, + req, + std::chrono::seconds(5), + std::move(receiver)), + ssl_ctx_(std::move(ctx)), + stream_{ex, ssl_ctx_} {} + + virtual void run() override { + // Set SNI Hostname (many hosts need this to handshake successfully) + if (!SSL_set_tlsext_host_name(stream_.native_handle(), host_.c_str())) { + beast::error_code ec{static_cast(::ERR_get_error()), + net::error::get_ssl_category()}; + // log("failed to set TLS host name extension: " + + // ec.message()); + return; + } - void on_resolve(beast::error_code ec, tcp::resolver::results_type results) { - if (ec) - return fail(ec, "resolve"); - // Make the connection on the IP address we get from a lookup - beast::get_lowest_layer(stream_).async_connect( - results, - beast::bind_front_handler(&plaintext_client::on_connect, shared())); + do_resolve(); } - void on_connect(beast::error_code ec, - tcp::resolver::results_type::endpoint_type) { - if (ec) - return fail(ec, "connect"); + virtual void close() override { do_close(); } - http::async_write( - stream_, request_, - beast::bind_front_handler(&plaintext_client::on_write, shared())); + void do_handshake() { + stream_.async_handshake( + ssl::stream_base::client, + beast::bind_front_handler(&SecureClient::on_handshake, shared())); } - void on_write(beast::error_code ec, std::size_t) { - if (ec) - return fail(ec, "write"); + beast::ssl_stream& stream() { return stream_; } - beast::get_lowest_layer(stream_).expires_after( - std::chrono::seconds(10)); + private: + ssl::context ssl_ctx_; + beast::ssl_stream stream_; - http::async_read_some( - stream_, buffer_, parser_, - beast::bind_front_handler(&plaintext_client::on_read, shared())); + std::shared_ptr shared() { + return std::static_pointer_cast(shared_from_this()); } +}; - void on_read(beast::error_code ec, std::size_t bytes_transferred) { - boost::ignore_unused(bytes_transferred); - if (ec && ec != beast::errc::operation_canceled) { - return fail(ec, "read"); - } - - http::async_read_some( - stream_, buffer_, parser_, - beast::bind_front_handler(&plaintext_client::on_read, shared())); +class PlaintextClient : public Client, + public Session, + public std::enable_shared_from_this { + public: + PlaintextClient(net::any_io_executor ex, + http::request req, + std::string host, + std::string port, + Builder::EventReceiver receiver) + : Session(ex, + host, + port, + req, + std::chrono::seconds(5), + std::move(receiver)), + stream_{ex} {} + + virtual void run() override { do_resolve(); } + + void do_handshake() { + // No handshake for plaintext; immediately send the request instead. + do_write(); } - void on_stop() { stream_.cancel(); } + virtual void close() override { do_close(); } + + beast::tcp_stream& stream() { return stream_; } + + private: + beast::tcp_stream stream_; }; -builder::builder(net::any_io_executor ctx, std::string url) - : url_{std::move(url)}, - ssl_context_{ssl::context::tlsv12_client}, - executor_{std::move(ctx)} { +Builder::Builder(net::any_io_executor ctx, std::string url) + : url_{std::move(url)}, executor_{std::move(ctx)} { // TODO: This needs to be verify_peer in production!! - ssl_context_.set_verify_mode(ssl::verify_none); + + receiver_ = [](launchdarkly::sse::Event) {}; request_.version(11); - request_.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); + request_.set(http::field::user_agent, kUserAgent); request_.method(http::verb::get); request_.set("Accept", "text/event-stream"); request_.set("Cache-Control", "no-cache"); } -builder& builder::header(std::string const& name, std::string const& value) { +Builder& Builder::header(std::string const& name, std::string const& value) { request_.set(name, value); return *this; } -builder& builder::method(http::verb verb) { +Builder& Builder::method(http::verb verb) { request_.method(verb); return *this; } -builder& builder::tls(ssl::context_base::method ctx) { - ssl_context_ = ssl::context{ctx}; +Builder& Builder::receiver(EventReceiver receiver) { + receiver_ = std::move(receiver); return *this; } -builder& builder::logging(std::function cb) { +Builder& Builder::logging(std::function cb) { logging_cb_ = std::move(cb); return *this; } -std::shared_ptr builder::build() { +std::shared_ptr Builder::build() { boost::system::result uri_components = boost::urls::parse_uri(url_); if (!uri_components) { return nullptr; } - request_.set(http::field::host, uri_components->host()); + std::string host = uri_components->host(); + + request_.set(http::field::host, host); request_.target(uri_components->path()); if (uri_components->scheme_id() == boost::urls::scheme::https) { std::string port = uri_components->has_port() ? uri_components->port() : "443"; - return std::make_shared( - net::make_strand(executor_), ssl_context_, request_, - uri_components->host(), port, logging_cb_); + ssl::context ssl_ctx{ssl::context::tlsv12_client}; + ssl_ctx.set_verify_mode(ssl::verify_none); + + return std::make_shared(net::make_strand(executor_), + std::move(ssl_ctx), request_, + host, port, receiver_); } else { std::string port = uri_components->has_port() ? uri_components->port() : "80"; - return std::make_shared( - net::make_strand(executor_), ssl_context_, request_, - uri_components->host(), port, logging_cb_); + return std::make_shared( + net::make_strand(executor_), request_, host, port, receiver_); } } From 038ac98d6293c9069a8f9f2c9bfdbdf993e9a996 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 24 Mar 2023 15:03:18 -0700 Subject: [PATCH 36/53] clang-format --- .../include/stream_entity.hpp | 7 ++----- apps/sse-contract-tests/src/entity_manager.cpp | 1 - apps/sse-contract-tests/src/stream_entity.cpp | 18 ++++++------------ .../include/launchdarkly/sse/client.hpp | 12 ++++-------- .../include/launchdarkly/sse/detail/parser.hpp | 2 -- 5 files changed, 12 insertions(+), 28 deletions(-) diff --git a/apps/sse-contract-tests/include/stream_entity.hpp b/apps/sse-contract-tests/include/stream_entity.hpp index fff134b08..6c395196e 100644 --- a/apps/sse-contract-tests/include/stream_entity.hpp +++ b/apps/sse-contract-tests/include/stream_entity.hpp @@ -6,10 +6,8 @@ #include #include - -#include #include - +#include #include #include @@ -42,8 +40,7 @@ class EventOutbox : public std::enable_shared_from_this { std::string id_; public: - EventOutbox(net::any_io_executor executor, - std::string callback_url); + EventOutbox(net::any_io_executor executor, std::string callback_url); void deliver_event(launchdarkly::sse::Event event); diff --git a/apps/sse-contract-tests/src/entity_manager.cpp b/apps/sse-contract-tests/src/entity_manager.cpp index d9d5e9e81..07d6af894 100644 --- a/apps/sse-contract-tests/src/entity_manager.cpp +++ b/apps/sse-contract-tests/src/entity_manager.cpp @@ -1,5 +1,4 @@ #include "entity_manager.hpp" -#include "launchdarkly/sse/client.hpp" #include "stream_entity.hpp" using launchdarkly::LogLevel; diff --git a/apps/sse-contract-tests/src/stream_entity.cpp b/apps/sse-contract-tests/src/stream_entity.cpp index d5600bab4..4c07d7598 100644 --- a/apps/sse-contract-tests/src/stream_entity.cpp +++ b/apps/sse-contract-tests/src/stream_entity.cpp @@ -10,9 +10,8 @@ auto const kFlushInterval = boost::posix_time::milliseconds{10}; EventOutbox::EventOutbox(net::any_io_executor executor, - std::string callback_url) - : - callback_url_{std::move(callback_url)}, + std::string callback_url) + : callback_url_{std::move(callback_url)}, callback_port_{}, callback_host_{}, callback_counter_{0}, @@ -39,8 +38,6 @@ void EventOutbox::deliver_event(launchdarkly::sse::Event event) { } void EventOutbox::run() { - - // Begin connecting to the test harness's event-posting service. resolver_.async_resolve(callback_host_, callback_port_, beast::bind_front_handler(&EventOutbox::on_resolve, shared_from_this())); @@ -77,12 +74,11 @@ EventOutbox::request_type EventOutbox::build_request( } void EventOutbox::on_resolve(beast::error_code ec, - tcp::resolver::results_type results) { + tcp::resolver::results_type results) { if (ec) { return do_shutdown(ec, "resolve"); } - // Make the connection on the IP address we get from a lookup. beast::get_lowest_layer(event_stream_) .async_connect(results, beast::bind_front_handler(&EventOutbox::on_connect, @@ -90,16 +86,14 @@ void EventOutbox::on_resolve(beast::error_code ec, } void EventOutbox::on_connect(beast::error_code ec, - tcp::resolver::results_type::endpoint_type) { + tcp::resolver::results_type::endpoint_type) { if (ec) { return do_shutdown(ec, "connect"); } - // Now that we're connected, kickoff the event flush "loop". boost::system::error_code dummy; - net::post(executor_, - beast::bind_front_handler(&EventOutbox::on_flush_timer, - shared_from_this(), dummy)); + net::post(executor_, beast::bind_front_handler(&EventOutbox::on_flush_timer, + shared_from_this(), dummy)); } void EventOutbox::on_flush_timer(boost::system::error_code ec) { diff --git a/libs/server-sent-events/include/launchdarkly/sse/client.hpp b/libs/server-sent-events/include/launchdarkly/sse/client.hpp index e96a590b7..d94d8b4f6 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/client.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/client.hpp @@ -3,22 +3,18 @@ #include #include - -#include +#include #include -#include #include #include -#include #include -#include namespace launchdarkly::sse { -namespace beast = boost::beast; // from -namespace http = beast::http; // from -namespace net = boost::asio; // from +namespace beast = boost::beast; +namespace http = beast::http; +namespace net = boost::asio; class Client; diff --git a/libs/server-sent-events/include/launchdarkly/sse/detail/parser.hpp b/libs/server-sent-events/include/launchdarkly/sse/detail/parser.hpp index 80a8a89d9..874644ce9 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/detail/parser.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/detail/parser.hpp @@ -18,7 +18,6 @@ namespace launchdarkly::sse::detail { using namespace boost::beast; - struct Event { std::string type; std::string data; @@ -30,7 +29,6 @@ struct Event { void trim_trailing_newline(); }; - template struct EventBody { using event_type = EventReceiver; From fa719d171a9ea7b83192989485d543b352a47729 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 24 Mar 2023 15:25:57 -0700 Subject: [PATCH 37/53] Add readme, update hello-cpp --- apps/hello-cpp/main.cpp | 3 +- apps/sse-contract-tests/CMakeLists.txt | 2 +- apps/sse-contract-tests/README.md | 43 ++++++++++++++++++- .../{stream_entity.hpp => event_outbox.hpp} | 0 .../sse-contract-tests/src/entity_manager.cpp | 2 +- .../{stream_entity.cpp => event_outbox.cpp} | 2 +- libs/server-sent-events/src/client.cpp | 6 ++- 7 files changed, 51 insertions(+), 7 deletions(-) rename apps/sse-contract-tests/include/{stream_entity.hpp => event_outbox.hpp} (100%) rename apps/sse-contract-tests/src/{stream_entity.cpp => event_outbox.cpp} (99%) diff --git a/apps/hello-cpp/main.cpp b/apps/hello-cpp/main.cpp index b8a3bea82..e420c901a 100644 --- a/apps/hello-cpp/main.cpp +++ b/apps/hello-cpp/main.cpp @@ -1,11 +1,12 @@ #include #include +#include + #include "console_backend.hpp" #include "logger.hpp" #include -#include namespace net = boost::asio; // from diff --git a/apps/sse-contract-tests/CMakeLists.txt b/apps/sse-contract-tests/CMakeLists.txt index 73000339c..06a807b7e 100644 --- a/apps/sse-contract-tests/CMakeLists.txt +++ b/apps/sse-contract-tests/CMakeLists.txt @@ -15,7 +15,7 @@ add_executable(sse-tests src/server.cpp src/entity_manager.cpp src/session.cpp - src/stream_entity.cpp + src/event_outbox.cpp ) target_link_libraries(sse-tests PRIVATE diff --git a/apps/sse-contract-tests/README.md b/apps/sse-contract-tests/README.md index 92f007e5a..7c1ae95b2 100644 --- a/apps/sse-contract-tests/README.md +++ b/apps/sse-contract-tests/README.md @@ -1,3 +1,44 @@ ## SSE contract tests -### Conceptual Model + +Contract tests have a "test service" on one side, and the "test harness" on +the other. + +This project implements the test service for the C++ EventSource client. + + + + +**session (session.hpp)** + +This provides a simple REST API for creating/destroying +test entities. Examples: + +`GET /` - returns the capabilities of this service. + +`DELETE /` - shutdown the service. + +`POST /` - create a new test entity, and return its ID. + +`DELETE /entity/1` - delete the an entity identified by `1`. + +**entity manager (entity_manager.hpp)** + +This manages "entities" - the combination of an SSE client, and an outbox that posts events _received_ from the stream +_back to_ the test harness. + +The point is to allow the test harness to assert that events were parsed and dispatched as expected. + +**event outbox (event_outbox.hpp)** + +The 2nd half of an "entity". It receives events from the SSE client, pushes them into a queue, +and then periodically flushes the queue out to the test harness. + + +**definitions (definitions.hpp)** + +Contains JSON definitions that are used to communicate with the test harness. + +**server (server.hpp)** + +Glues everything together, mainly providing the TCP acceptor that spawns new sessions. diff --git a/apps/sse-contract-tests/include/stream_entity.hpp b/apps/sse-contract-tests/include/event_outbox.hpp similarity index 100% rename from apps/sse-contract-tests/include/stream_entity.hpp rename to apps/sse-contract-tests/include/event_outbox.hpp diff --git a/apps/sse-contract-tests/src/entity_manager.cpp b/apps/sse-contract-tests/src/entity_manager.cpp index 07d6af894..165d5c2ad 100644 --- a/apps/sse-contract-tests/src/entity_manager.cpp +++ b/apps/sse-contract-tests/src/entity_manager.cpp @@ -1,5 +1,5 @@ #include "entity_manager.hpp" -#include "stream_entity.hpp" +#include "event_outbox.hpp" using launchdarkly::LogLevel; diff --git a/apps/sse-contract-tests/src/stream_entity.cpp b/apps/sse-contract-tests/src/event_outbox.cpp similarity index 99% rename from apps/sse-contract-tests/src/stream_entity.cpp rename to apps/sse-contract-tests/src/event_outbox.cpp index 4c07d7598..875257a62 100644 --- a/apps/sse-contract-tests/src/stream_entity.cpp +++ b/apps/sse-contract-tests/src/event_outbox.cpp @@ -1,4 +1,4 @@ -#include "stream_entity.hpp" +#include "event_outbox.hpp" #include "definitions.hpp" #include diff --git a/libs/server-sent-events/src/client.cpp b/libs/server-sent-events/src/client.cpp index 3d4447eca..27172d24e 100644 --- a/libs/server-sent-events/src/client.cpp +++ b/libs/server-sent-events/src/client.cpp @@ -1,12 +1,14 @@ #include -#include "launchdarkly/sse/detail/parser.hpp" +#include #include -#include #include #include #include +#include +#include +#include #include #include From 8c8deb02d003abbd708295c09f679986ed376036 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 24 Mar 2023 16:08:52 -0700 Subject: [PATCH 38/53] add more docs --- .../include/entity_manager.hpp | 26 ++++++++---- .../include/event_outbox.hpp | 42 ++++++++++++------- apps/sse-contract-tests/include/server.hpp | 16 ++++++- apps/sse-contract-tests/include/session.hpp | 32 +++++++------- .../sse-contract-tests/src/entity_manager.cpp | 10 +---- apps/sse-contract-tests/src/event_outbox.cpp | 8 ++-- 6 files changed, 82 insertions(+), 52 deletions(-) diff --git a/apps/sse-contract-tests/include/entity_manager.hpp b/apps/sse-contract-tests/include/entity_manager.hpp index 95698eb02..a4d3f1db2 100644 --- a/apps/sse-contract-tests/include/entity_manager.hpp +++ b/apps/sse-contract-tests/include/entity_manager.hpp @@ -3,6 +3,8 @@ #include "definitions.hpp" #include "logger.hpp" +#include + #include #include @@ -11,11 +13,8 @@ #include #include -#include - class EventOutbox; -// class EntityManager { using Inbox = std::shared_ptr; using Outbox = std::shared_ptr; @@ -29,12 +28,25 @@ class EntityManager { launchdarkly::Logger& logger_; public: + /** + * Create an entity manager, which can be used to create and destroy + * entities (SSE clients + event channel back to test harness). + * @param executor Executor. + * @param logger Logger. + */ EntityManager(boost::asio::any_io_executor executor, launchdarkly::Logger& logger); + /** + * Create an entity with the given configuration. + * @param params Config of the entity. + * @return An ID representing the entity, or none if the entity couldn't + * be created. + */ std::optional create(ConfigParams params); + /** + * Destroy an entity with the given ID. + * @param id ID of the entity. + * @return True if the entity was found and destroyed. + */ bool destroy(std::string const& id); - - void destroy_all(); - - friend class StreamEntity; }; diff --git a/apps/sse-contract-tests/include/event_outbox.hpp b/apps/sse-contract-tests/include/event_outbox.hpp index 6c395196e..4176e0923 100644 --- a/apps/sse-contract-tests/include/event_outbox.hpp +++ b/apps/sse-contract-tests/include/event_outbox.hpp @@ -13,15 +13,14 @@ #include #include -namespace beast = boost::beast; // from -namespace http = beast::http; // from -namespace net = boost::asio; // from -using tcp = boost::asio::ip::tcp; // from +namespace beast = boost::beast; +namespace http = beast::http; +namespace net = boost::asio; +using tcp = boost::asio::ip::tcp; class EventOutbox : public std::enable_shared_from_this { - // Simple string body request is appropriate since the JSON - // returned to the test service is minimal. - using request_type = http::request; + + using RequestType = http::request; std::string callback_url_; std::string callback_port_; @@ -32,29 +31,40 @@ class EventOutbox : public std::enable_shared_from_this { tcp::resolver resolver_; beast::tcp_stream event_stream_; - // When events are received from the SSE client, they are pushed into - // this queue. - boost::lockfree::spsc_queue outbox_; - // Periodically, the events are flushed to the test harness. + boost::lockfree::spsc_queue outbox_; + net::deadline_timer flush_timer_; std::string id_; public: + /** + * Instantiate an outbox; events will be posted to the given URL. + * @param executor Executor. + * @param callback_url Target URL. + */ EventOutbox(net::any_io_executor executor, std::string callback_url); - - void deliver_event(launchdarkly::sse::Event event); - + /** + * Enqueues an event, which will be posted to the server + * later. + * @param event Event to post. + */ + void post_event(launchdarkly::sse::Event event); + /** + * Begins an async operation to connect to the server. + */ void run(); + /** + * Begins an async operation to disconnect from the server. + */ void stop(); private: - request_type build_request(std::size_t counter, + RequestType build_request(std::size_t counter, launchdarkly::sse::Event ev); void on_resolve(beast::error_code ec, tcp::resolver::results_type results); void on_connect(beast::error_code ec, tcp::resolver::results_type::endpoint_type); void on_flush_timer(boost::system::error_code ec); void on_write(beast::error_code ec, std::size_t); - void do_shutdown(beast::error_code ec, std::string what); }; diff --git a/apps/sse-contract-tests/include/server.hpp b/apps/sse-contract-tests/include/server.hpp index ba871f8de..4a5e85fb4 100644 --- a/apps/sse-contract-tests/include/server.hpp +++ b/apps/sse-contract-tests/include/server.hpp @@ -21,13 +21,27 @@ class server : public std::enable_shared_from_this { launchdarkly::Logger& logger_; public: + /** + * Constructs a server, which stands up a REST API at the given + * port and address. + * @param ioc IO context. + * @param address Address to bind. + * @param port Port to bind. + * @param logger Logger. + */ server(net::io_context& ioc, std::string const& address, std::string const& port, launchdarkly::Logger& logger); + /** + * Advertise an optional test-harness capability, such as "comments". + * @param cap + */ void add_capability(std::string cap); + /** + * Begins an async operation to start accepting requests. + */ void run(); - private: void do_accept(); void on_accept(boost::system::error_code const& ec, tcp::socket socket); diff --git a/apps/sse-contract-tests/include/session.hpp b/apps/sse-contract-tests/include/session.hpp index 4b1989953..d0140a85e 100644 --- a/apps/sse-contract-tests/include/session.hpp +++ b/apps/sse-contract-tests/include/session.hpp @@ -13,39 +13,44 @@ namespace net = boost::asio; // from using tcp = boost::asio::ip::tcp; // from class Session : public std::enable_shared_from_this { - // The socket for the currently connected client. beast::tcp_stream stream_; - - // The buffer for performing reads. beast::flat_buffer buffer_{8192}; - - // The request message. http::request request_; - EntityManager& manager_; - std::vector capabilities_; - std::function on_shutdown_cb_; - bool shutdown_requested_; - launchdarkly::Logger& logger_; public: + /** + * Constructs a session, which provides a REST API. + * @param socket Connected socket. + * @param manager Manager through which entities can be created/destroyed. + * @param caps Test service capabilities to advertise. + * @param logger Logger. + */ explicit Session(tcp::socket&& socket, EntityManager& manager, std::vector caps, launchdarkly::Logger& logger); ~Session(); + + /** + * Set a callback to be invoked when a REST client requests shutdown. + */ template void on_shutdown(Callback cb) { on_shutdown_cb_ = cb; } - + /** + * Begin waiting for requests. + */ void start(); - + /** + * Stop waiting for requests and close the session. + */ void stop(); private: @@ -55,7 +60,6 @@ class Session : public std::enable_shared_from_this { void do_stop(char const* reason); void on_read(beast::error_code ec, std::size_t bytes_transferred); - void do_close(); void send_response(http::message_generator&& msg); @@ -63,5 +67,3 @@ class Session : public std::enable_shared_from_this { beast::error_code ec, std::size_t bytes_transferred); }; - -using SessionPtr = std::shared_ptr; diff --git a/apps/sse-contract-tests/src/entity_manager.cpp b/apps/sse-contract-tests/src/entity_manager.cpp index 165d5c2ad..ac0a4237f 100644 --- a/apps/sse-contract-tests/src/entity_manager.cpp +++ b/apps/sse-contract-tests/src/entity_manager.cpp @@ -30,7 +30,7 @@ std::optional EntityManager::create(ConfigParams params) { }); client_builder.receiver([copy = poster](launchdarkly::sse::Event e) { - copy->deliver_event(std::move(e)); + copy->post_event(std::move(e)); }); auto client = client_builder.build(); @@ -46,14 +46,6 @@ std::optional EntityManager::create(ConfigParams params) { return id; } -void EntityManager::destroy_all() { - for (auto& kv : entities_) { - auto& entities = kv.second; - // todo: entities.first.stop() - entities.second->stop(); - } - entities_.clear(); -} bool EntityManager::destroy(std::string const& id) { auto it = entities_.find(id); diff --git a/apps/sse-contract-tests/src/event_outbox.cpp b/apps/sse-contract-tests/src/event_outbox.cpp index 875257a62..9a6856f86 100644 --- a/apps/sse-contract-tests/src/event_outbox.cpp +++ b/apps/sse-contract-tests/src/event_outbox.cpp @@ -32,7 +32,7 @@ void EventOutbox::do_shutdown(beast::error_code ec, std::string what) { flush_timer_.cancel(); } -void EventOutbox::deliver_event(launchdarkly::sse::Event event) { +void EventOutbox::post_event(launchdarkly::sse::Event event) { auto http_request = build_request(callback_counter_++, std::move(event)); outbox_.push(http_request); } @@ -51,10 +51,10 @@ void EventOutbox::stop() { shared_from_this(), ec, reason)); } -EventOutbox::request_type EventOutbox::build_request( +EventOutbox::RequestType EventOutbox::build_request( std::size_t counter, launchdarkly::sse::Event ev) { - request_type req; + RequestType req; req.set(http::field::host, callback_host_); req.method(http::verb::get); @@ -102,7 +102,7 @@ void EventOutbox::on_flush_timer(boost::system::error_code ec) { } if (!outbox_.empty()) { - request_type& request = outbox_.front(); + RequestType& request = outbox_.front(); // Flip-flop between this function and on_write; pushing an event // and then popping it. From 78e9194b5dc1f6871a2294f3f9606b4f0d5ae735 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 24 Mar 2023 16:20:50 -0700 Subject: [PATCH 39/53] pass comments tests --- apps/sse-contract-tests/src/main.cpp | 1 + .../include/launchdarkly/sse/detail/parser.hpp | 2 +- libs/server-sent-events/src/parser.cpp | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/sse-contract-tests/src/main.cpp b/apps/sse-contract-tests/src/main.cpp index 32d44a657..5b9e49489 100644 --- a/apps/sse-contract-tests/src/main.cpp +++ b/apps/sse-contract-tests/src/main.cpp @@ -24,6 +24,7 @@ int main(int argc, char* argv[]) { auto s = std::make_shared(ioc, "0.0.0.0", "8111", logger); s->add_capability("headers"); + s->add_capability("comments"); s->run(); net::signal_set signals{ioc, SIGINT, SIGTERM}; diff --git a/libs/server-sent-events/include/launchdarkly/sse/detail/parser.hpp b/libs/server-sent-events/include/launchdarkly/sse/detail/parser.hpp index 874644ce9..0665661b2 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/detail/parser.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/detail/parser.hpp @@ -23,7 +23,7 @@ struct Event { std::string data; std::optional id; - Event() = default; + Event(); void append_data(std::string const&); void trim_trailing_newline(); diff --git a/libs/server-sent-events/src/parser.cpp b/libs/server-sent-events/src/parser.cpp index dc498c5dd..2fc243c02 100644 --- a/libs/server-sent-events/src/parser.cpp +++ b/libs/server-sent-events/src/parser.cpp @@ -2,6 +2,8 @@ namespace launchdarkly::sse::detail { +Event::Event() : type("message"), data(), id() {} + void Event::append_data(std::string const& input) { data.append(input); data.append("\n"); From e33947cb1bd96bc40ea384c322cff928f681ca8b Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 24 Mar 2023 16:41:48 -0700 Subject: [PATCH 40/53] more docs --- .../include/event_outbox.hpp | 2 ++ apps/sse-contract-tests/src/event_outbox.cpp | 21 +++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/apps/sse-contract-tests/include/event_outbox.hpp b/apps/sse-contract-tests/include/event_outbox.hpp index 4176e0923..3372f495c 100644 --- a/apps/sse-contract-tests/include/event_outbox.hpp +++ b/apps/sse-contract-tests/include/event_outbox.hpp @@ -36,6 +36,8 @@ class EventOutbox : public std::enable_shared_from_this { net::deadline_timer flush_timer_; std::string id_; + bool shutdown_; + public: /** * Instantiate an outbox; events will be posted to the given URL. diff --git a/apps/sse-contract-tests/src/event_outbox.cpp b/apps/sse-contract-tests/src/event_outbox.cpp index 9a6856f86..79b8b36b8 100644 --- a/apps/sse-contract-tests/src/event_outbox.cpp +++ b/apps/sse-contract-tests/src/event_outbox.cpp @@ -5,9 +5,10 @@ #include #include -// Periodically check the outbox (outgoing events to the test harness) -// at this interval. -auto const kFlushInterval = boost::posix_time::milliseconds{10}; +// Check the outbox at this interval. Normally a flush is triggered for +// every event; this is just a failsafe in case a flush is happening concurrently. +auto const kFlushInterval = boost::posix_time::milliseconds{500}; +auto const kOutboxCapacity = 1023; EventOutbox::EventOutbox(net::any_io_executor executor, std::string callback_url) @@ -18,10 +19,10 @@ EventOutbox::EventOutbox(net::any_io_executor executor, executor_{executor}, resolver_{executor}, event_stream_{executor}, - outbox_{1024}, - flush_timer_{executor, boost::posix_time::milliseconds{0}} { - boost::system::result uri_components = - boost::urls::parse_uri(callback_url_); + outbox_{kOutboxCapacity}, + flush_timer_{executor}, + shutdown_(false) { + auto uri_components = boost::urls::parse_uri(callback_url_); callback_host_ = uri_components->host(); callback_port_ = uri_components->port(); @@ -35,6 +36,7 @@ void EventOutbox::do_shutdown(beast::error_code ec, std::string what) { void EventOutbox::post_event(launchdarkly::sse::Event event) { auto http_request = build_request(callback_counter_++, std::move(event)); outbox_.push(http_request); + flush_timer_.expires_from_now(boost::posix_time::milliseconds(0)); } void EventOutbox::run() { @@ -46,6 +48,7 @@ void EventOutbox::run() { void EventOutbox::stop() { beast::error_code ec = net::error::basic_errors::operation_aborted; std::string reason = "stop"; + shutdown_ = true; net::post(executor_, beast::bind_front_handler(&EventOutbox::do_shutdown, shared_from_this(), ec, reason)); @@ -97,14 +100,14 @@ void EventOutbox::on_connect(beast::error_code ec, } void EventOutbox::on_flush_timer(boost::system::error_code ec) { - if (ec) { + if (ec && shutdown_) { return do_shutdown(ec, "flush"); } if (!outbox_.empty()) { RequestType& request = outbox_.front(); - // Flip-flop between this function and on_write; pushing an event + // Flip-flop between this function and on_write; peeking an event // and then popping it. http::async_write(event_stream_, request, From dd74e0266c3f17b81c31e7a6cb99d15a0422aebf Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 24 Mar 2023 16:47:05 -0700 Subject: [PATCH 41/53] fix some confusing code --- .../launchdarkly/sse/detail/parser.hpp | 20 +++++++++---------- libs/server-sent-events/src/client.cpp | 7 +++---- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/libs/server-sent-events/include/launchdarkly/sse/detail/parser.hpp b/libs/server-sent-events/include/launchdarkly/sse/detail/parser.hpp index 0665661b2..45b857177 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/detail/parser.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/detail/parser.hpp @@ -133,11 +133,12 @@ struct EventBody::reader { void parse_stream(boost::string_view body) { size_t i = 0; - while (i < body.length()) { + while (i < body.size()) { i += this->append_up_to(body.substr(i, body.length() - i), "\r\n"); if (i == body.size()) { - continue; - } else if (body.at(i) == '\r') { + break; + } + if (body.at(i) == '\r') { complete_line(); begin_CR_ = true; i++; @@ -218,20 +219,17 @@ struct EventBody::reader { last_event_id_ = field.second; event_->id = last_event_id_; } else if (field.first == "retry") { + // todo: implement } } if (seen_empty_line) { - std::optional data = event_; - event_ = std::nullopt; - - if (data.has_value()) { - data->trim_trailing_newline(); + if (event_.has_value()) { + event_->trim_trailing_newline(); body_.events_(launchdarkly::sse::Event( - data->type, data->data, data->id)); - data.reset(); + event_->type, event_->data, event_->id)); + event_.reset(); } - continue; } diff --git a/libs/server-sent-events/src/client.cpp b/libs/server-sent-events/src/client.cpp index 27172d24e..47cd86cb3 100644 --- a/libs/server-sent-events/src/client.cpp +++ b/libs/server-sent-events/src/client.cpp @@ -224,8 +224,6 @@ class PlaintextClient : public Client, Builder::Builder(net::any_io_executor ctx, std::string url) : url_{std::move(url)}, executor_{std::move(ctx)} { - // TODO: This needs to be verify_peer in production!! - receiver_ = [](launchdarkly::sse::Event) {}; request_.version(11); @@ -256,8 +254,7 @@ Builder& Builder::logging(std::function cb) { } std::shared_ptr Builder::build() { - boost::system::result uri_components = - boost::urls::parse_uri(url_); + auto uri_components = boost::urls::parse_uri(url_); if (!uri_components) { return nullptr; } @@ -272,6 +269,8 @@ std::shared_ptr Builder::build() { uri_components->has_port() ? uri_components->port() : "443"; ssl::context ssl_ctx{ssl::context::tlsv12_client}; + // TODO: This needs to be verify_peer in production, + // and we need to provide a certificate store! ssl_ctx.set_verify_mode(ssl::verify_none); return std::make_shared(net::make_strand(executor_), From 064872400d5d5cc2b389e90bf2f8eb54f01b14ed Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 24 Mar 2023 17:26:09 -0700 Subject: [PATCH 42/53] fmt --- apps/hello-cpp/main.cpp | 3 ++- apps/sse-contract-tests/include/event_outbox.hpp | 4 +--- apps/sse-contract-tests/include/server.hpp | 1 + apps/sse-contract-tests/src/entity_manager.cpp | 1 - apps/sse-contract-tests/src/event_outbox.cpp | 3 ++- libs/client-sdk/src/api.cpp | 5 ++++- libs/common/src/log_level.cpp | 12 +++++++----- libs/server-sent-events/src/client.cpp | 2 +- 8 files changed, 18 insertions(+), 13 deletions(-) diff --git a/apps/hello-cpp/main.cpp b/apps/hello-cpp/main.cpp index e420c901a..185e66750 100644 --- a/apps/hello-cpp/main.cpp +++ b/apps/hello-cpp/main.cpp @@ -36,7 +36,8 @@ int main() { .header("Authorization", key) .receiver([&](launchdarkly::sse::Event ev) { LD_LOG(logger, LogLevel::kInfo) << "event: " << ev.type(); - LD_LOG(logger, LogLevel::kInfo) << "data: " << std::move(ev).take(); + LD_LOG(logger, LogLevel::kInfo) + << "data: " << std::move(ev).take(); }) .build(); diff --git a/apps/sse-contract-tests/include/event_outbox.hpp b/apps/sse-contract-tests/include/event_outbox.hpp index 3372f495c..a8598bc44 100644 --- a/apps/sse-contract-tests/include/event_outbox.hpp +++ b/apps/sse-contract-tests/include/event_outbox.hpp @@ -19,7 +19,6 @@ namespace net = boost::asio; using tcp = boost::asio::ip::tcp; class EventOutbox : public std::enable_shared_from_this { - using RequestType = http::request; std::string callback_url_; @@ -61,8 +60,7 @@ class EventOutbox : public std::enable_shared_from_this { void stop(); private: - RequestType build_request(std::size_t counter, - launchdarkly::sse::Event ev); + RequestType build_request(std::size_t counter, launchdarkly::sse::Event ev); void on_resolve(beast::error_code ec, tcp::resolver::results_type results); void on_connect(beast::error_code ec, tcp::resolver::results_type::endpoint_type); diff --git a/apps/sse-contract-tests/include/server.hpp b/apps/sse-contract-tests/include/server.hpp index 4a5e85fb4..dbae43a5c 100644 --- a/apps/sse-contract-tests/include/server.hpp +++ b/apps/sse-contract-tests/include/server.hpp @@ -42,6 +42,7 @@ class server : public std::enable_shared_from_this { * Begins an async operation to start accepting requests. */ void run(); + private: void do_accept(); void on_accept(boost::system::error_code const& ec, tcp::socket socket); diff --git a/apps/sse-contract-tests/src/entity_manager.cpp b/apps/sse-contract-tests/src/entity_manager.cpp index ac0a4237f..8b5a69073 100644 --- a/apps/sse-contract-tests/src/entity_manager.cpp +++ b/apps/sse-contract-tests/src/entity_manager.cpp @@ -46,7 +46,6 @@ std::optional EntityManager::create(ConfigParams params) { return id; } - bool EntityManager::destroy(std::string const& id) { auto it = entities_.find(id); if (it == entities_.end()) { diff --git a/apps/sse-contract-tests/src/event_outbox.cpp b/apps/sse-contract-tests/src/event_outbox.cpp index 79b8b36b8..aac1901c6 100644 --- a/apps/sse-contract-tests/src/event_outbox.cpp +++ b/apps/sse-contract-tests/src/event_outbox.cpp @@ -6,7 +6,8 @@ #include // Check the outbox at this interval. Normally a flush is triggered for -// every event; this is just a failsafe in case a flush is happening concurrently. +// every event; this is just a failsafe in case a flush is happening +// concurrently. auto const kFlushInterval = boost::posix_time::milliseconds{500}; auto const kOutboxCapacity = 1023; diff --git a/libs/client-sdk/src/api.cpp b/libs/client-sdk/src/api.cpp index df0d26d09..48c153fda 100644 --- a/libs/client-sdk/src/api.cpp +++ b/libs/client-sdk/src/api.cpp @@ -4,7 +4,10 @@ #include namespace launchdarkly { + +auto const kAnswerToLifeTheUniverseAndEverything = 42; + std::optional foo() { - return 42; + return kAnswerToLifeTheUniverseAndEverything; } } // namespace launchdarkly diff --git a/libs/common/src/log_level.cpp b/libs/common/src/log_level.cpp index 5453ca49e..67d7f1e36 100644 --- a/libs/common/src/log_level.cpp +++ b/libs/common/src/log_level.cpp @@ -31,15 +31,17 @@ LogLevel GetLogLevelEnum(char const* level, LogLevel default_) { if (lowercase == "debug") { return LogLevel::kDebug; - } else if (lowercase == "info") { + } + if (lowercase == "info") { return LogLevel::kInfo; - } else if (lowercase == "warn") { + } + if (lowercase == "warn") { return LogLevel::kWarn; - } else if (lowercase == "error") { + } + if (lowercase == "error") { return LogLevel::kError; - } else { - return default_; } + return default_; } } // namespace launchdarkly diff --git a/libs/server-sent-events/src/client.cpp b/libs/server-sent-events/src/client.cpp index 47cd86cb3..73e8a2ab9 100644 --- a/libs/server-sent-events/src/client.cpp +++ b/libs/server-sent-events/src/client.cpp @@ -26,7 +26,7 @@ namespace net = boost::asio; // from namespace ssl = boost::asio::ssl; // from using tcp = boost::asio::ip::tcp; // from -char const* kUserAgent = "CPPClient/0.0.0"; +const auto kUserAgent = "CPPClient/0.0.0"; template class Session { From 8cd4d54b5cd87d9178679a5228dca9305726c119 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 27 Mar 2023 11:41:53 -0700 Subject: [PATCH 43/53] use default beast user-agent if unspecified --- libs/server-sent-events/src/client.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/libs/server-sent-events/src/client.cpp b/libs/server-sent-events/src/client.cpp index 73e8a2ab9..512231765 100644 --- a/libs/server-sent-events/src/client.cpp +++ b/libs/server-sent-events/src/client.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -26,7 +27,7 @@ namespace net = boost::asio; // from namespace ssl = boost::asio::ssl; // from using tcp = boost::asio::ip::tcp; // from -const auto kUserAgent = "CPPClient/0.0.0"; +const auto kDefaultUserAgent = BOOST_BEAST_VERSION_STRING; template class Session { @@ -224,13 +225,13 @@ class PlaintextClient : public Client, Builder::Builder(net::any_io_executor ctx, std::string url) : url_{std::move(url)}, executor_{std::move(ctx)} { - receiver_ = [](launchdarkly::sse::Event) {}; + receiver_ = [](const launchdarkly::sse::Event&) {}; request_.version(11); - request_.set(http::field::user_agent, kUserAgent); + request_.set(http::field::user_agent, kDefaultUserAgent); request_.method(http::verb::get); - request_.set("Accept", "text/event-stream"); - request_.set("Cache-Control", "no-cache"); + request_.set(http::field::accept, "text/event-stream"); + request_.set(http::field::cache_control, "no-cache"); } Builder& Builder::header(std::string const& name, std::string const& value) { From 82947e6ab3fe3469285076bba04ad0e7088348f8 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 27 Mar 2023 13:41:08 -0700 Subject: [PATCH 44/53] add tls certification verification support via certify library --- cmake/certify.cmake | 10 ++++++++++ libs/server-sent-events/CMakeLists.txt | 3 +++ libs/server-sent-events/src/CMakeLists.txt | 5 ++++- libs/server-sent-events/src/client.cpp | 11 ++++++++--- 4 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 cmake/certify.cmake diff --git a/cmake/certify.cmake b/cmake/certify.cmake new file mode 100644 index 000000000..e6a87e47a --- /dev/null +++ b/cmake/certify.cmake @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.11) + +include(FetchContent) + +FetchContent_Declare(certify + GIT_REPOSITORY https://github.com/djarek/certify.git + GIT_TAG 97f5eebfd99a5d6e99d07e4820240994e4e59787 +) + +FetchContent_MakeAvailable(certify) diff --git a/libs/server-sent-events/CMakeLists.txt b/libs/server-sent-events/CMakeLists.txt index 9a1238171..743ac09e6 100644 --- a/libs/server-sent-events/CMakeLists.txt +++ b/libs/server-sent-events/CMakeLists.txt @@ -40,5 +40,8 @@ set(Boost_USE_STATIC_RUNTIME OFF) find_package(Boost 1.80 REQUIRED) message(STATUS "LaunchDarkly: using Boost v${Boost_VERSION}") +include(${CMAKE_FILES}/certify.cmake) + + add_subdirectory(src) diff --git a/libs/server-sent-events/src/CMakeLists.txt b/libs/server-sent-events/src/CMakeLists.txt index 74de52018..767c1b544 100644 --- a/libs/server-sent-events/src/CMakeLists.txt +++ b/libs/server-sent-events/src/CMakeLists.txt @@ -3,7 +3,10 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${LaunchDarklySSEClient_SOURCE_DIR}/inc # Automatic library: static or dynamic based on user config. add_library(${LIBNAME} client.cpp parser.cpp event.cpp boost-url.cpp ${HEADER_LIST}) -target_link_libraries(${LIBNAME} PUBLIC OpenSSL::SSL Boost::headers) +target_link_libraries(${LIBNAME} + PUBLIC OpenSSL::SSL Boost::headers + PRIVATE certify::core +) add_library(launchdarkly::sse ALIAS ${LIBNAME}) diff --git a/libs/server-sent-events/src/client.cpp b/libs/server-sent-events/src/client.cpp index 512231765..c8f917dfd 100644 --- a/libs/server-sent-events/src/client.cpp +++ b/libs/server-sent-events/src/client.cpp @@ -3,6 +3,10 @@ #include #include +#include + +#include +#include #include #include @@ -79,6 +83,7 @@ class Session { } void fail(beast::error_code ec, char const* what) { + std::cout << what << ": " << ec.message() << '\n'; // log(std::string(what) + ":" + ec.message()); } @@ -270,9 +275,9 @@ std::shared_ptr Builder::build() { uri_components->has_port() ? uri_components->port() : "443"; ssl::context ssl_ctx{ssl::context::tlsv12_client}; - // TODO: This needs to be verify_peer in production, - // and we need to provide a certificate store! - ssl_ctx.set_verify_mode(ssl::verify_none); + + ssl_ctx.set_verify_mode(ssl::verify_peer | ssl::verify_fail_if_no_peer_cert); + boost::certify::enable_native_https_server_verification(ssl_ctx); return std::make_shared(net::make_strand(executor_), std::move(ssl_ctx), request_, From 4ba7b7a6401e9385c26121d13e73e4495776f399 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 27 Mar 2023 14:18:48 -0700 Subject: [PATCH 45/53] rename SecureClient -> EncryptedClient --- libs/server-sent-events/src/client.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/libs/server-sent-events/src/client.cpp b/libs/server-sent-events/src/client.cpp index c8f917dfd..e80b9ddb4 100644 --- a/libs/server-sent-events/src/client.cpp +++ b/libs/server-sent-events/src/client.cpp @@ -145,17 +145,17 @@ class Session { void do_close() { beast::get_lowest_layer(derived().stream()).cancel(); } }; -class SecureClient : public Client, - public Session, - public std::enable_shared_from_this { +class EncryptedClient : public Client, + public Session, + public std::enable_shared_from_this { public: - SecureClient(net::any_io_executor ex, + EncryptedClient(net::any_io_executor ex, ssl::context ctx, http::request req, std::string host, std::string port, Builder::EventReceiver receiver) - : Session(ex, + : Session(ex, host, port, req, @@ -182,7 +182,7 @@ class SecureClient : public Client, void do_handshake() { stream_.async_handshake( ssl::stream_base::client, - beast::bind_front_handler(&SecureClient::on_handshake, shared())); + beast::bind_front_handler(&EncryptedClient::on_handshake, shared())); } beast::ssl_stream& stream() { return stream_; } @@ -191,8 +191,8 @@ class SecureClient : public Client, ssl::context ssl_ctx_; beast::ssl_stream stream_; - std::shared_ptr shared() { - return std::static_pointer_cast(shared_from_this()); + std::shared_ptr shared() { + return std::static_pointer_cast(shared_from_this()); } }; @@ -279,7 +279,7 @@ std::shared_ptr Builder::build() { ssl_ctx.set_verify_mode(ssl::verify_peer | ssl::verify_fail_if_no_peer_cert); boost::certify::enable_native_https_server_verification(ssl_ctx); - return std::make_shared(net::make_strand(executor_), + return std::make_shared(net::make_strand(executor_), std::move(ssl_ctx), request_, host, port, receiver_); } else { From 49d966fee752a0b30119373657f929a98e4f80a6 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 27 Mar 2023 15:16:41 -0700 Subject: [PATCH 46/53] plumb logging back into Session --- apps/hello-cpp/main.cpp | 4 + .../sse-contract-tests/src/entity_manager.cpp | 2 +- libs/common/include/log_backend.hpp | 2 +- libs/common/include/logger.hpp | 8 +- .../include/launchdarkly/sse/client.hpp | 68 +++++++- libs/server-sent-events/src/client.cpp | 156 +++++++++++------- 6 files changed, 170 insertions(+), 70 deletions(-) diff --git a/apps/hello-cpp/main.cpp b/apps/hello-cpp/main.cpp index 185e66750..d0f72e70c 100644 --- a/apps/hello-cpp/main.cpp +++ b/apps/hello-cpp/main.cpp @@ -7,6 +7,7 @@ #include "logger.hpp" #include +#include namespace net = boost::asio; // from @@ -39,6 +40,9 @@ int main() { LD_LOG(logger, LogLevel::kInfo) << "data: " << std::move(ev).take(); }) + .logger([&](std::string msg) { + LD_LOG(logger, LogLevel::kDebug) << std::move(msg); + }) .build(); if (!client) { diff --git a/apps/sse-contract-tests/src/entity_manager.cpp b/apps/sse-contract-tests/src/entity_manager.cpp index 8b5a69073..cbf943414 100644 --- a/apps/sse-contract-tests/src/entity_manager.cpp +++ b/apps/sse-contract-tests/src/entity_manager.cpp @@ -25,7 +25,7 @@ std::optional EntityManager::create(ConfigParams params) { } } - client_builder.logging([this](std::string msg) { + client_builder.logger([this](std::string msg) { LD_LOG(logger_, LogLevel::kDebug) << std::move(msg); }); diff --git a/libs/common/include/log_backend.hpp b/libs/common/include/log_backend.hpp index 2a8eba8a6..58ed1dfba 100644 --- a/libs/common/include/log_backend.hpp +++ b/libs/common/include/log_backend.hpp @@ -7,7 +7,7 @@ #include "log_level.hpp" namespace launchdarkly { /** - * Interface for logging back-ends. + * Interface for logger back-ends. * * @example ../src/ConsoleBackend.hpp */ diff --git a/libs/common/include/logger.hpp b/libs/common/include/logger.hpp index 03e35a506..c1e7c92f9 100644 --- a/libs/common/include/logger.hpp +++ b/libs/common/include/logger.hpp @@ -15,7 +15,7 @@ namespace launchdarkly { * Logger logger(std::make_unique(LogLevel::kInfo, * "Example")); * - * // Use log macro for logging. + * // Use log macro for logger. * LD_LOG(logger, LogLevel::kInfo) << "this is a log"; * ``` */ @@ -40,7 +40,7 @@ class Logger { }; /** - * Class which allows for ostream based logging. + * Class which allows for ostream based logger. * * Generally this should be used via the macro, but can be used directly. * If used directly the log level should be checked first. @@ -93,7 +93,7 @@ class Logger { Logger(std::unique_ptr backend); /** - * Open a logging record. + * Open a logger record. * * @param level The level for the record. * @return A new record, or null-opt if there are no sinks for the severity @@ -110,7 +110,7 @@ class Logger { /** * Check if a given log level is enabled. This is useful if you want to do - * some expensive operation only when logging is enabled. + * some expensive operation only when logger is enabled. * * @param level The level to check. * @return True if the level is enabled. diff --git a/libs/server-sent-events/include/launchdarkly/sse/client.hpp b/libs/server-sent-events/include/launchdarkly/sse/client.hpp index d94d8b4f6..eb3818afc 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/client.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/client.hpp @@ -18,16 +18,69 @@ namespace net = boost::asio; class Client; +/** + * Builder can be used to create an instance of Client. Minimal example: + * @code + * auto client = launchdarkly::sse::Builder(executor, + * "https://example.com").build(); + * @endcode + */ class Builder { public: using EventReceiver = std::function; using LogCallback = std::function; + /** + * Create a builder for the given URL. If the port is omitted, 443 is + * assumed for https scheme while 80 is assumed for http scheme. + * + * Example: https://example.com:8123/endpoint + * + * @param ioc Executor for the Client. + * @param url Server-Sent-Events server URL. + */ Builder(net::any_io_executor ioc, std::string url); + /** + * Add a custom header to the initial request. The following headers + * are added by default and can be overridden: + * + * User-Agent: the default Boost.Beast user agent. + * Accept: text/event-stream + * Cache-Control: no-cache + * + * @param name Header name. + * @param value Header value. + * @return Reference to this builder. + */ Builder& header(std::string const& name, std::string const& value); + /** + * Specify the method for the initial request. The default method is GET. + * @param verb The HTTP method. + * @return Reference to this builder. + */ Builder& method(http::verb verb); + /** + * Specify a receiver of events generated by the Client. For example: + * @code + * builder.receiver([](launchdarkly::sse::Event event) -> void { + * std::cout << event.type() << ": " << event.data() << std::endl; + * }); + * @endcode + * + * @return + */ Builder& receiver(EventReceiver); - Builder& logging(LogCallback callback); + /** + * Specify a logging callback for the Client. + * @param callback Callback to receive a string from the Client. + * @return Reference to this builder. + */ + Builder& logger(LogCallback callback); + /** + * Builds a Client. The shared pointer is necessary to extend the lifetime + * of the Client to encompass each asynchronous operation that it performs. + * @return + */ std::shared_ptr build(); private: @@ -38,10 +91,23 @@ class Builder { EventReceiver receiver_; }; +/** + * Client is a long-lived Server-Sent-Events (EventSource) client which + * reads from an event stream and dispatches events to a user-specified + * receiver. + */ class Client { public: virtual ~Client() = default; + /** + * Kicks off a connection to the server and begins reading the event stream. + * The provided event receiver and logging callbacks will be invoked from + * the thread that is servicing the Client's executor. + */ virtual void run() = 0; + /** + * Closes the stream. + */ virtual void close() = 0; }; diff --git a/libs/server-sent-events/src/client.cpp b/libs/server-sent-events/src/client.cpp index e80b9ddb4..7b9b75950 100644 --- a/libs/server-sent-events/src/client.cpp +++ b/libs/server-sent-events/src/client.cpp @@ -2,19 +2,19 @@ #include #include -#include #include +#include #include #include #include #include -#include #include #include #include #include +#include #include @@ -31,7 +31,14 @@ namespace net = boost::asio; // from namespace ssl = boost::asio::ssl; // from using tcp = boost::asio::ip::tcp; // from -const auto kDefaultUserAgent = BOOST_BEAST_VERSION_STRING; +auto const kDefaultUserAgent = BOOST_BEAST_VERSION_STRING; + +// The allowed amount of time to connect the socket and perform +// any TLS handshake, if necessary. +auto const kDefaultConnectTimeout = std::chrono::seconds(15); +// Once connected, the amount of time to send a request and receive the first +// batch of bytes back. +auto const kDefaultResponseTimeout = std::chrono::seconds(15); template class Session { @@ -39,52 +46,65 @@ class Session { Derived& derived() { return static_cast(*this); } http::request req_; std::chrono::seconds connect_timeout_; - std::chrono::seconds write_timeout_; + std::chrono::seconds response_timeout_; protected: beast::flat_buffer buffer_; std::string host_; std::string port_; tcp::resolver resolver_; + Builder::LogCallback logger_; using cb = std::function; using body = launchdarkly::sse::detail::EventBody; http::response_parser parser_; public: - Session(net::any_io_executor exec, + Session(net::any_io_executor const& exec, std::string host, std::string port, http::request r, std::chrono::seconds connect_timeout, - Builder::EventReceiver receiver) + std::chrono::seconds response_timeout, + Builder::EventReceiver receiver, + Builder::LogCallback logger) : req_(std::move(r)), - resolver_(std::move(exec)), + resolver_(exec), connect_timeout_(connect_timeout), - write_timeout_(std::chrono::seconds(10)), + response_timeout_(response_timeout), host_(std::move(host)), port_(std::move(port)), + logger_(std::move(logger)), parser_() { parser_.get().body().on_event(std::move(receiver)); } - void do_write() { - http::async_write( - derived().stream(), req_, - beast::bind_front_handler(&Session::on_write, - derived().shared_from_this())); + void fail(beast::error_code ec, char const* what) { + logger_(std::string(what) + ": " + ec.message()); } void do_resolve() { + logger_("resolving " + host_ + ":" + port_); resolver_.async_resolve( host_, port_, beast::bind_front_handler(&Session::on_resolve, derived().shared_from_this())); } - void fail(beast::error_code ec, char const* what) { - std::cout << what << ": " << ec.message() << '\n'; - // log(std::string(what) + ":" + ec.message()); + void on_resolve(beast::error_code ec, tcp::resolver::results_type results) { + if (ec) + return fail(ec, "resolve"); + + logger_("connecting (" + std::to_string(connect_timeout_.count()) + + " sec timeout)"); + + beast::get_lowest_layer(derived().stream()) + .expires_after(connect_timeout_); + + beast::get_lowest_layer(derived().stream()) + .async_connect(results, beast::bind_front_handler( + &Session::on_connect, + derived().shared_from_this())); } void on_connect(beast::error_code ec, @@ -92,6 +112,7 @@ class Session { if (ec) { return fail(ec, "connect"); } + derived().do_handshake(); } @@ -102,12 +123,24 @@ class Session { do_write(); } + void do_write() { + logger_("making request (" + std::to_string(response_timeout_.count()) + + " sec timeout)"); + + beast::get_lowest_layer(derived().stream()) + .expires_after(response_timeout_); + + http::async_write( + derived().stream(), req_, + beast::bind_front_handler(&Session::on_write, + derived().shared_from_this())); + } + void on_write(beast::error_code ec, std::size_t) { if (ec) return fail(ec, "write"); - beast::get_lowest_layer(derived().stream()) - .expires_after(write_timeout_); + logger_("reading response"); http::async_read_some( derived().stream(), buffer_, parser_, @@ -129,38 +162,31 @@ class Session { derived().shared_from_this())); } - void on_resolve(beast::error_code ec, tcp::resolver::results_type results) { - if (ec) - return fail(ec, "resolve"); - - beast::get_lowest_layer(derived().stream()) - .expires_after(connect_timeout_); - - beast::get_lowest_layer(derived().stream()) - .async_connect(results, beast::bind_front_handler( - &Session::on_connect, - derived().shared_from_this())); + void do_close() { + logger_("closing"); + beast::get_lowest_layer(derived().stream()).cancel(); } - - void do_close() { beast::get_lowest_layer(derived().stream()).cancel(); } }; class EncryptedClient : public Client, - public Session, - public std::enable_shared_from_this { + public Session, + public std::enable_shared_from_this { public: EncryptedClient(net::any_io_executor ex, - ssl::context ctx, - http::request req, - std::string host, - std::string port, - Builder::EventReceiver receiver) + ssl::context ctx, + http::request req, + std::string host, + std::string port, + Builder::EventReceiver receiver, + Builder::LogCallback logger) : Session(ex, - host, - port, - req, - std::chrono::seconds(5), - std::move(receiver)), + std::move(host), + std::move(port), + std::move(req), + kDefaultConnectTimeout, + kDefaultResponseTimeout, + std::move(receiver), + std::move(logger)), ssl_ctx_(std::move(ctx)), stream_{ex, ssl_ctx_} {} @@ -169,8 +195,7 @@ class EncryptedClient : public Client, if (!SSL_set_tlsext_host_name(stream_.native_handle(), host_.c_str())) { beast::error_code ec{static_cast(::ERR_get_error()), net::error::get_ssl_category()}; - // log("failed to set TLS host name extension: " + - // ec.message()); + logger_("failed to set TLS host name extension: " + ec.message()); return; } @@ -180,9 +205,9 @@ class EncryptedClient : public Client, virtual void close() override { do_close(); } void do_handshake() { - stream_.async_handshake( - ssl::stream_base::client, - beast::bind_front_handler(&EncryptedClient::on_handshake, shared())); + stream_.async_handshake(ssl::stream_base::client, + beast::bind_front_handler( + &EncryptedClient::on_handshake, shared())); } beast::ssl_stream& stream() { return stream_; } @@ -204,13 +229,16 @@ class PlaintextClient : public Client, http::request req, std::string host, std::string port, - Builder::EventReceiver receiver) + Builder::EventReceiver receiver, + Builder::LogCallback logger) : Session(ex, - host, - port, - req, - std::chrono::seconds(5), - std::move(receiver)), + std::move(host), + std::move(port), + std::move(req), + kDefaultConnectTimeout, + kDefaultResponseTimeout, + std::move(receiver), + std::move(logger)), stream_{ex} {} virtual void run() override { do_resolve(); } @@ -230,7 +258,7 @@ class PlaintextClient : public Client, Builder::Builder(net::any_io_executor ctx, std::string url) : url_{std::move(url)}, executor_{std::move(ctx)} { - receiver_ = [](const launchdarkly::sse::Event&) {}; + receiver_ = [](launchdarkly::sse::Event const&) {}; request_.version(11); request_.set(http::field::user_agent, kDefaultUserAgent); @@ -254,8 +282,8 @@ Builder& Builder::receiver(EventReceiver receiver) { return *this; } -Builder& Builder::logging(std::function cb) { - logging_cb_ = std::move(cb); +Builder& Builder::logger(std::function callback) { + logging_cb_ = std::move(callback); return *this; } @@ -276,18 +304,20 @@ std::shared_ptr Builder::build() { ssl::context ssl_ctx{ssl::context::tlsv12_client}; - ssl_ctx.set_verify_mode(ssl::verify_peer | ssl::verify_fail_if_no_peer_cert); + ssl_ctx.set_verify_mode(ssl::verify_peer | + ssl::verify_fail_if_no_peer_cert); boost::certify::enable_native_https_server_verification(ssl_ctx); - return std::make_shared(net::make_strand(executor_), - std::move(ssl_ctx), request_, - host, port, receiver_); + return std::make_shared( + net::make_strand(executor_), std::move(ssl_ctx), request_, host, + port, receiver_, logging_cb_); } else { std::string port = uri_components->has_port() ? uri_components->port() : "80"; - return std::make_shared( - net::make_strand(executor_), request_, host, port, receiver_); + return std::make_shared(net::make_strand(executor_), + request_, host, port, + receiver_, logging_cb_); } } From 12d70179769e54cf947f62619dcc5f033dc8d177 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 28 Mar 2023 07:25:02 -0700 Subject: [PATCH 47/53] implement POST and REPORT capabilities --- .../sse-contract-tests/src/entity_manager.cpp | 8 +++++ apps/sse-contract-tests/src/main.cpp | 2 ++ .../include/launchdarkly/sse/client.hpp | 10 ++++++- libs/server-sent-events/src/client.cpp | 29 +++++++++++++++---- 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/apps/sse-contract-tests/src/entity_manager.cpp b/apps/sse-contract-tests/src/entity_manager.cpp index cbf943414..b516c5a73 100644 --- a/apps/sse-contract-tests/src/entity_manager.cpp +++ b/apps/sse-contract-tests/src/entity_manager.cpp @@ -25,6 +25,14 @@ std::optional EntityManager::create(ConfigParams params) { } } + if (params.method) { + client_builder.method(http::string_to_verb(*params.method)); + } + + if (params.body) { + client_builder.body(std::move(*params.body)); + } + client_builder.logger([this](std::string msg) { LD_LOG(logger_, LogLevel::kDebug) << std::move(msg); }); diff --git a/apps/sse-contract-tests/src/main.cpp b/apps/sse-contract-tests/src/main.cpp index 5b9e49489..4e3872585 100644 --- a/apps/sse-contract-tests/src/main.cpp +++ b/apps/sse-contract-tests/src/main.cpp @@ -25,6 +25,8 @@ int main(int argc, char* argv[]) { auto s = std::make_shared(ioc, "0.0.0.0", "8111", logger); s->add_capability("headers"); s->add_capability("comments"); + s->add_capability("report"); + s->add_capability("post"); s->run(); net::signal_set signals{ioc, SIGINT, SIGTERM}; diff --git a/libs/server-sent-events/include/launchdarkly/sse/client.hpp b/libs/server-sent-events/include/launchdarkly/sse/client.hpp index eb3818afc..919b187e1 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/client.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/client.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -48,11 +49,18 @@ class Builder { * Accept: text/event-stream * Cache-Control: no-cache * + * Note that Content-Type and + * * @param name Header name. * @param value Header value. * @return Reference to this builder. */ Builder& header(std::string const& name, std::string const& value); + /** + * Specifies a request body. The body is sent when the method is POST or REPORt + * @return + */ + Builder& body(std::string); /** * Specify the method for the initial request. The default method is GET. * @param verb The HTTP method. @@ -86,7 +94,7 @@ class Builder { private: std::string url_; net::any_io_executor executor_; - http::request request_; + http::request request_; LogCallback logging_cb_; EventReceiver receiver_; }; diff --git a/libs/server-sent-events/src/client.cpp b/libs/server-sent-events/src/client.cpp index 7b9b75950..f64a0e838 100644 --- a/libs/server-sent-events/src/client.cpp +++ b/libs/server-sent-events/src/client.cpp @@ -44,7 +44,7 @@ template class Session { private: Derived& derived() { return static_cast(*this); } - http::request req_; + http::request req_; std::chrono::seconds connect_timeout_; std::chrono::seconds response_timeout_; @@ -63,12 +63,12 @@ class Session { Session(net::any_io_executor const& exec, std::string host, std::string port, - http::request r, + http::request req, std::chrono::seconds connect_timeout, std::chrono::seconds response_timeout, Builder::EventReceiver receiver, Builder::LogCallback logger) - : req_(std::move(r)), + : req_(std::move(req)), resolver_(exec), connect_timeout_(connect_timeout), response_timeout_(response_timeout), @@ -174,7 +174,7 @@ class EncryptedClient : public Client, public: EncryptedClient(net::any_io_executor ex, ssl::context ctx, - http::request req, + http::request req, std::string host, std::string port, Builder::EventReceiver receiver, @@ -226,7 +226,7 @@ class PlaintextClient : public Client, public std::enable_shared_from_this { public: PlaintextClient(net::any_io_executor ex, - http::request req, + http::request req, std::string host, std::string port, Builder::EventReceiver receiver, @@ -272,6 +272,11 @@ Builder& Builder::header(std::string const& name, std::string const& value) { return *this; } +Builder& Builder::body(std::string data) { + request_.body() = std::move(data); + return *this; +} + Builder& Builder::method(http::verb verb) { request_.method(verb); return *this; @@ -293,6 +298,20 @@ std::shared_ptr Builder::build() { return nullptr; } + // Don't send a body unless the method is POST or REPORT + if (!(request_.method() == http::verb::post || + request_.method() == http::verb::report)) { + request_.body() = ""; + } else { + // If it is, then setup Content-Type, only if one wasn't + // specified. + if (auto it = request_.find(http::field::content_type); it == request_.end()) { + request_.set(http::field::content_type, "text/plain"); + } + } + + request_.prepare_payload(); + std::string host = uri_components->host(); request_.set(http::field::host, host); From 3633ef981faffd19a1b811a28653f139c46addbf Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 28 Mar 2023 10:11:22 -0700 Subject: [PATCH 48/53] add read-timeout capability --- .../sse-contract-tests/src/entity_manager.cpp | 5 ++ apps/sse-contract-tests/src/main.cpp | 1 + .../include/launchdarkly/sse/client.hpp | 10 +++- libs/server-sent-events/src/client.cpp | 48 ++++++++++++++----- 4 files changed, 50 insertions(+), 14 deletions(-) diff --git a/apps/sse-contract-tests/src/entity_manager.cpp b/apps/sse-contract-tests/src/entity_manager.cpp index b516c5a73..574b23b0d 100644 --- a/apps/sse-contract-tests/src/entity_manager.cpp +++ b/apps/sse-contract-tests/src/entity_manager.cpp @@ -33,6 +33,11 @@ std::optional EntityManager::create(ConfigParams params) { client_builder.body(std::move(*params.body)); } + if (params.readTimeoutMs) { + client_builder.read_timeout( + std::chrono::milliseconds(*params.readTimeoutMs)); + } + client_builder.logger([this](std::string msg) { LD_LOG(logger_, LogLevel::kDebug) << std::move(msg); }); diff --git a/apps/sse-contract-tests/src/main.cpp b/apps/sse-contract-tests/src/main.cpp index 4e3872585..5b61f2f97 100644 --- a/apps/sse-contract-tests/src/main.cpp +++ b/apps/sse-contract-tests/src/main.cpp @@ -27,6 +27,7 @@ int main(int argc, char* argv[]) { s->add_capability("comments"); s->add_capability("report"); s->add_capability("post"); + s->add_capability("read-timeout"); s->run(); net::signal_set signals{ioc, SIGINT, SIGTERM}; diff --git a/libs/server-sent-events/include/launchdarkly/sse/client.hpp b/libs/server-sent-events/include/launchdarkly/sse/client.hpp index 919b187e1..93cebd5a8 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/client.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/client.hpp @@ -58,9 +58,16 @@ class Builder { Builder& header(std::string const& name, std::string const& value); /** * Specifies a request body. The body is sent when the method is POST or REPORt - * @return + * @return Reference to this builder. */ Builder& body(std::string); + /** + * Specifies the maximum time duration between subsequent reads from the stream. + * A read counts as receiving any amount of bytes. + * @param timeout + * @return Reference to this builder. + */ + Builder& read_timeout(std::chrono::milliseconds timeout); /** * Specify the method for the initial request. The default method is GET. * @param verb The HTTP method. @@ -95,6 +102,7 @@ class Builder { std::string url_; net::any_io_executor executor_; http::request request_; + std::optional read_timeout_; LogCallback logging_cb_; EventReceiver receiver_; }; diff --git a/libs/server-sent-events/src/client.cpp b/libs/server-sent-events/src/client.cpp index f64a0e838..6fbf703fe 100644 --- a/libs/server-sent-events/src/client.cpp +++ b/libs/server-sent-events/src/client.cpp @@ -35,18 +35,21 @@ auto const kDefaultUserAgent = BOOST_BEAST_VERSION_STRING; // The allowed amount of time to connect the socket and perform // any TLS handshake, if necessary. -auto const kDefaultConnectTimeout = std::chrono::seconds(15); +const std::chrono::milliseconds kDefaultConnectTimeout = + std::chrono::seconds(15); // Once connected, the amount of time to send a request and receive the first // batch of bytes back. -auto const kDefaultResponseTimeout = std::chrono::seconds(15); +const std::chrono::milliseconds kDefaultResponseTimeout = + std::chrono::seconds(15); template class Session { private: Derived& derived() { return static_cast(*this); } http::request req_; - std::chrono::seconds connect_timeout_; - std::chrono::seconds response_timeout_; + std::chrono::milliseconds connect_timeout_; + std::chrono::milliseconds response_timeout_; + std::optional read_timeout_; protected: beast::flat_buffer buffer_; @@ -64,14 +67,16 @@ class Session { std::string host, std::string port, http::request req, - std::chrono::seconds connect_timeout, - std::chrono::seconds response_timeout, + std::chrono::milliseconds connect_timeout, + std::chrono::milliseconds response_timeout, + std::optional read_timeout, Builder::EventReceiver receiver, Builder::LogCallback logger) : req_(std::move(req)), resolver_(exec), connect_timeout_(connect_timeout), response_timeout_(response_timeout), + read_timeout_(std::move(read_timeout)), host_(std::move(host)), port_(std::move(port)), logger_(std::move(logger)), @@ -154,7 +159,12 @@ class Session { return fail(ec, "read"); } - beast::get_lowest_layer(derived().stream()).expires_never(); + if (read_timeout_) { + beast::get_lowest_layer(derived().stream()) + .expires_after(*read_timeout_); + } else { + beast::get_lowest_layer(derived().stream()).expires_never(); + }; http::async_read_some( derived().stream(), buffer_, parser_, @@ -177,6 +187,7 @@ class EncryptedClient : public Client, http::request req, std::string host, std::string port, + std::optional read_timeout, Builder::EventReceiver receiver, Builder::LogCallback logger) : Session(ex, @@ -185,6 +196,7 @@ class EncryptedClient : public Client, std::move(req), kDefaultConnectTimeout, kDefaultResponseTimeout, + std::move(read_timeout), std::move(receiver), std::move(logger)), ssl_ctx_(std::move(ctx)), @@ -229,6 +241,7 @@ class PlaintextClient : public Client, http::request req, std::string host, std::string port, + std::optional read_timeout, Builder::EventReceiver receiver, Builder::LogCallback logger) : Session(ex, @@ -237,6 +250,7 @@ class PlaintextClient : public Client, std::move(req), kDefaultConnectTimeout, kDefaultResponseTimeout, + read_timeout, std::move(receiver), std::move(logger)), stream_{ex} {} @@ -257,7 +271,9 @@ class PlaintextClient : public Client, }; Builder::Builder(net::any_io_executor ctx, std::string url) - : url_{std::move(url)}, executor_{std::move(ctx)} { + : url_{std::move(url)}, + executor_{std::move(ctx)}, + read_timeout_{std::nullopt} { receiver_ = [](launchdarkly::sse::Event const&) {}; request_.version(11); @@ -277,6 +293,11 @@ Builder& Builder::body(std::string data) { return *this; } +Builder& Builder::read_timeout(std::chrono::milliseconds timeout) { + read_timeout_ = timeout; + return *this; +} + Builder& Builder::method(http::verb verb) { request_.method(verb); return *this; @@ -305,7 +326,8 @@ std::shared_ptr Builder::build() { } else { // If it is, then setup Content-Type, only if one wasn't // specified. - if (auto it = request_.find(http::field::content_type); it == request_.end()) { + if (auto it = request_.find(http::field::content_type); + it == request_.end()) { request_.set(http::field::content_type, "text/plain"); } } @@ -329,14 +351,14 @@ std::shared_ptr Builder::build() { return std::make_shared( net::make_strand(executor_), std::move(ssl_ctx), request_, host, - port, receiver_, logging_cb_); + port, read_timeout_, receiver_, logging_cb_); } else { std::string port = uri_components->has_port() ? uri_components->port() : "80"; - return std::make_shared(net::make_strand(executor_), - request_, host, port, - receiver_, logging_cb_); + return std::make_shared( + net::make_strand(executor_), request_, host, port, read_timeout_, + receiver_, logging_cb_); } } From 3b45e0c6471adbc691692303b8320d7e2ac4ff79 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 28 Mar 2023 12:07:12 -0700 Subject: [PATCH 49/53] address PR feedback --- apps/sse-contract-tests/CMakeLists.txt | 4 +- apps/sse-contract-tests/README.md | 17 +++----- libs/client-sdk/CMakeLists.txt | 4 +- libs/client-sdk/tests/CMakeLists.txt | 2 +- libs/common/include/log_backend.hpp | 2 +- libs/common/include/logger.hpp | 8 ++-- libs/common/tests/CMakeLists.txt | 2 +- .../include/launchdarkly/sse/client.hpp | 22 ++++++---- .../launchdarkly/sse/detail/parser.hpp | 42 ++++++++++--------- libs/server-sent-events/src/CMakeLists.txt | 2 +- libs/server-sent-events/tests/CMakeLists.txt | 2 +- 11 files changed, 57 insertions(+), 50 deletions(-) diff --git a/apps/sse-contract-tests/CMakeLists.txt b/apps/sse-contract-tests/CMakeLists.txt index 06a807b7e..68b97dcc2 100644 --- a/apps/sse-contract-tests/CMakeLists.txt +++ b/apps/sse-contract-tests/CMakeLists.txt @@ -16,13 +16,13 @@ add_executable(sse-tests src/entity_manager.cpp src/session.cpp src/event_outbox.cpp -) + ) target_link_libraries(sse-tests PRIVATE launchdarkly::sse launchdarkly::common nlohmann_json::nlohmann_json -) + ) target_include_directories(sse-tests PUBLIC include) diff --git a/apps/sse-contract-tests/README.md b/apps/sse-contract-tests/README.md index 7c1ae95b2..7533c42a8 100644 --- a/apps/sse-contract-tests/README.md +++ b/apps/sse-contract-tests/README.md @@ -1,17 +1,13 @@ ## SSE contract tests - -Contract tests have a "test service" on one side, and the "test harness" on -the other. +Contract tests have a "test service" on one side, and the "test harness" on +the other. This project implements the test service for the C++ EventSource client. - - - **session (session.hpp)** -This provides a simple REST API for creating/destroying +This provides a simple REST API for creating/destroying test entities. Examples: `GET /` - returns the capabilities of this service. @@ -20,12 +16,12 @@ test entities. Examples: `POST /` - create a new test entity, and return its ID. -`DELETE /entity/1` - delete the an entity identified by `1`. +`DELETE /entity/1` - delete the an entity identified by `1`. -**entity manager (entity_manager.hpp)** +**entity manager (entity_manager.hpp)** This manages "entities" - the combination of an SSE client, and an outbox that posts events _received_ from the stream -_back to_ the test harness. +_back to_ the test harness. The point is to allow the test harness to assert that events were parsed and dispatched as expected. @@ -34,7 +30,6 @@ The point is to allow the test harness to assert that events were parsed and dis The 2nd half of an "entity". It receives events from the SSE client, pushes them into a queue, and then periodically flushes the queue out to the test harness. - **definitions (definitions.hpp)** Contains JSON definitions that are used to communicate with the test harness. diff --git a/libs/client-sdk/CMakeLists.txt b/libs/client-sdk/CMakeLists.txt index 09f85c66b..6233cf41f 100644 --- a/libs/client-sdk/CMakeLists.txt +++ b/libs/client-sdk/CMakeLists.txt @@ -30,6 +30,6 @@ include(FetchContent) # Add main SDK sources. add_subdirectory(src) -if(BUILD_TESTING) +if (BUILD_TESTING) add_subdirectory(tests) -endif() +endif () diff --git a/libs/client-sdk/tests/CMakeLists.txt b/libs/client-sdk/tests/CMakeLists.txt index 12990c2cf..4d959d69d 100644 --- a/libs/client-sdk/tests/CMakeLists.txt +++ b/libs/client-sdk/tests/CMakeLists.txt @@ -5,7 +5,7 @@ include_directories("${PROJECT_SOURCE_DIR}/include") file(GLOB tests "${PROJECT_SOURCE_DIR}/tests/*.cpp") -set (CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) add_executable(gtest_${LIBNAME} ${tests}) diff --git a/libs/common/include/log_backend.hpp b/libs/common/include/log_backend.hpp index 58ed1dfba..2a8eba8a6 100644 --- a/libs/common/include/log_backend.hpp +++ b/libs/common/include/log_backend.hpp @@ -7,7 +7,7 @@ #include "log_level.hpp" namespace launchdarkly { /** - * Interface for logger back-ends. + * Interface for logging back-ends. * * @example ../src/ConsoleBackend.hpp */ diff --git a/libs/common/include/logger.hpp b/libs/common/include/logger.hpp index c1e7c92f9..888482304 100644 --- a/libs/common/include/logger.hpp +++ b/libs/common/include/logger.hpp @@ -15,7 +15,7 @@ namespace launchdarkly { * Logger logger(std::make_unique(LogLevel::kInfo, * "Example")); * - * // Use log macro for logger. + * // Use the macro for logging. * LD_LOG(logger, LogLevel::kInfo) << "this is a log"; * ``` */ @@ -40,7 +40,7 @@ class Logger { }; /** - * Class which allows for ostream based logger. + * Class which allows for ostream based logging. * * Generally this should be used via the macro, but can be used directly. * If used directly the log level should be checked first. @@ -93,7 +93,7 @@ class Logger { Logger(std::unique_ptr backend); /** - * Open a logger record. + * Open a logging record. * * @param level The level for the record. * @return A new record, or null-opt if there are no sinks for the severity @@ -110,7 +110,7 @@ class Logger { /** * Check if a given log level is enabled. This is useful if you want to do - * some expensive operation only when logger is enabled. + * some expensive operation only when logging is enabled. * * @param level The level to check. * @return True if the level is enabled. diff --git a/libs/common/tests/CMakeLists.txt b/libs/common/tests/CMakeLists.txt index a9f4277a1..e83e84c70 100644 --- a/libs/common/tests/CMakeLists.txt +++ b/libs/common/tests/CMakeLists.txt @@ -5,7 +5,7 @@ include_directories("${PROJECT_SOURCE_DIR}/include") file(GLOB tests "${PROJECT_SOURCE_DIR}/tests/*.cpp") -set (CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) add_executable(gtest_${LIBNAME} ${tests}) diff --git a/libs/server-sent-events/include/launchdarkly/sse/client.hpp b/libs/server-sent-events/include/launchdarkly/sse/client.hpp index 93cebd5a8..b5d7dad13 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/client.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/client.hpp @@ -4,8 +4,8 @@ #include #include -#include #include +#include #include #include @@ -41,6 +41,7 @@ class Builder { * @param url Server-Sent-Events server URL. */ Builder(net::any_io_executor ioc, std::string url); + /** * Add a custom header to the initial request. The following headers * are added by default and can be overridden: @@ -56,24 +57,29 @@ class Builder { * @return Reference to this builder. */ Builder& header(std::string const& name, std::string const& value); + /** - * Specifies a request body. The body is sent when the method is POST or REPORt + * Specifies a request body. The body is sent when the method is POST or + * REPORT. * @return Reference to this builder. */ Builder& body(std::string); + /** - * Specifies the maximum time duration between subsequent reads from the stream. - * A read counts as receiving any amount of bytes. + * Specifies the maximum time duration between subsequent reads from the + * stream. A read counts as receiving any amount of bytes. * @param timeout * @return Reference to this builder. */ Builder& read_timeout(std::chrono::milliseconds timeout); + /** * Specify the method for the initial request. The default method is GET. * @param verb The HTTP method. * @return Reference to this builder. */ Builder& method(http::verb verb); + /** * Specify a receiver of events generated by the Client. For example: * @code @@ -82,19 +88,21 @@ class Builder { * }); * @endcode * - * @return + * @return Reference to this builder. */ Builder& receiver(EventReceiver); + /** * Specify a logging callback for the Client. * @param callback Callback to receive a string from the Client. - * @return Reference to this builder. + * @return Reference to this builder. */ Builder& logger(LogCallback callback); + /** * Builds a Client. The shared pointer is necessary to extend the lifetime * of the Client to encompass each asynchronous operation that it performs. - * @return + * @return New client; call run() to kickoff the connection process and begin reading. */ std::shared_ptr build(); diff --git a/libs/server-sent-events/include/launchdarkly/sse/detail/parser.hpp b/libs/server-sent-events/include/launchdarkly/sse/detail/parser.hpp index 45b857177..f0e774789 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/detail/parser.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/detail/parser.hpp @@ -69,12 +69,14 @@ struct EventBody::reader { boost::ignore_unused(h); } - /** Initialize the reader. - - This is called after construction and before the first - call to `put`. The message is valid and complete upon - entry.@param ec Set to the error, if any occurred. - */ + /** + * Initialize the reader. + * This is called after construction and before the first + * call to `put`. The message is valid and complete upon + * entry. + * @param content_length + * @param ec Set to the error, if any occurred. + */ void init(boost::optional const& content_length, error_code& ec) { boost::ignore_unused(content_length); @@ -83,15 +85,14 @@ struct EventBody::reader { ec = {}; } - /** Store buffers. - This is called zero or more times with parsed body octets. - - @param buffers The constant buffer sequence to store. - - @param ec Set to the error, if any occurred. - - @return The number of bytes transferred from the input buffers. - */ + /** + * Store buffers. + * This is called zero or more times with parsed body octets. + * @tparam ConstBufferSequence + * @param buffers The constant buffer sequence to store. + * @param ec et to the error, if any occurred. + * @return The number of bytes transferred from the input buffers. + */ template std::size_t put(ConstBufferSequence const& buffers, error_code& ec) { // The specification requires this to indicate "no error" @@ -101,10 +102,10 @@ struct EventBody::reader { return buffer_bytes(buffers); } - /** Called when the body is complete. - - @param ec Set to the error, if any occurred. - */ + /** + * Called when the body is complete. + * @param ec Set to the error, if any occurred. + */ void finish(error_code& ec) { // The specification requires this to indicate "no error" ec = {}; @@ -118,6 +119,9 @@ struct EventBody::reader { } } + // Appends the body to the buffered line until reaching any of the + // characters specified within the search parameter. The search parameter is + // treated as an array of search characters, not as a single token. size_t append_up_to(boost::string_view body, std::string const& search) { std::size_t index = body.find_first_of(search); if (index != std::string::npos) { diff --git a/libs/server-sent-events/src/CMakeLists.txt b/libs/server-sent-events/src/CMakeLists.txt index 767c1b544..f7a2411a2 100644 --- a/libs/server-sent-events/src/CMakeLists.txt +++ b/libs/server-sent-events/src/CMakeLists.txt @@ -6,7 +6,7 @@ add_library(${LIBNAME} client.cpp parser.cpp event.cpp boost-url.cpp ${HEADER_LI target_link_libraries(${LIBNAME} PUBLIC OpenSSL::SSL Boost::headers PRIVATE certify::core -) + ) add_library(launchdarkly::sse ALIAS ${LIBNAME}) diff --git a/libs/server-sent-events/tests/CMakeLists.txt b/libs/server-sent-events/tests/CMakeLists.txt index 4390c6f5a..d1baf87ea 100644 --- a/libs/server-sent-events/tests/CMakeLists.txt +++ b/libs/server-sent-events/tests/CMakeLists.txt @@ -5,7 +5,7 @@ include_directories("${PROJECT_SOURCE_DIR}/include") file(GLOB tests "${PROJECT_SOURCE_DIR}/tests/*.cpp") -set (CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) add_executable(gtest_${LIBNAME} ${tests}) From 5aabeccd2862309f3ecb541b7790790534b757fd Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 28 Mar 2023 13:35:21 -0700 Subject: [PATCH 50/53] clang-fmt --- libs/server-sent-events/include/launchdarkly/sse/client.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/server-sent-events/include/launchdarkly/sse/client.hpp b/libs/server-sent-events/include/launchdarkly/sse/client.hpp index b5d7dad13..17400b761 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/client.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/client.hpp @@ -102,7 +102,8 @@ class Builder { /** * Builds a Client. The shared pointer is necessary to extend the lifetime * of the Client to encompass each asynchronous operation that it performs. - * @return New client; call run() to kickoff the connection process and begin reading. + * @return New client; call run() to kickoff the connection process and + * begin reading. */ std::shared_ptr build(); From 6651ee26fc24fd3a5188287b17a693a7df248ff1 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Mar 2023 09:05:20 -0700 Subject: [PATCH 51/53] attempt to suppress 'misc-non-private-member-variables-in-classes' --- libs/server-sent-events/src/client.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/server-sent-events/src/client.cpp b/libs/server-sent-events/src/client.cpp index 6fbf703fe..6589b1b16 100644 --- a/libs/server-sent-events/src/client.cpp +++ b/libs/server-sent-events/src/client.cpp @@ -42,6 +42,7 @@ const std::chrono::milliseconds kDefaultConnectTimeout = const std::chrono::milliseconds kDefaultResponseTimeout = std::chrono::seconds(15); +// NOLINTBEGIN(misc-non-private-member-variables-in-classes) template class Session { private: @@ -177,6 +178,7 @@ class Session { beast::get_lowest_layer(derived().stream()).cancel(); } }; +// NOLINTEND(misc-non-private-member-variables-in-classes) class EncryptedClient : public Client, public Session, From e1e548dc78b4d0ec97ce7baddbc8688881bc39e0 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Mar 2023 12:51:41 -0700 Subject: [PATCH 52/53] trigger github action --- CMakeLists.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a1b078969..9cd988916 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,16 +14,17 @@ project( if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.24") # Affects robustness of timestamp checking on FetchContent dependencies. cmake_policy(SET CMP0135 NEW) -endif() +endif () # All projects in this repo should share the same version of 3rd party depends. # It's the only way to remain sane. set(CMAKE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/cmake") set(CMAKE_CXX_STANDARD 17) + option(BUILD_TESTING "Enable C++ unit tests." ON) -if(BUILD_TESTING) +if (BUILD_TESTING) include(FetchContent) FetchContent_Declare( googletest @@ -34,7 +35,7 @@ if(BUILD_TESTING) FetchContent_MakeAvailable(googletest) enable_testing() -endif() +endif () add_subdirectory(libs/common) From 37a0ca0d79c5c0e0669fab58a604c3b6f12cecc0 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Mar 2023 13:17:38 -0700 Subject: [PATCH 53/53] Try another nolint --- libs/server-sent-events/src/client.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libs/server-sent-events/src/client.cpp b/libs/server-sent-events/src/client.cpp index 6589b1b16..d157bafe1 100644 --- a/libs/server-sent-events/src/client.cpp +++ b/libs/server-sent-events/src/client.cpp @@ -42,9 +42,9 @@ const std::chrono::milliseconds kDefaultConnectTimeout = const std::chrono::milliseconds kDefaultResponseTimeout = std::chrono::seconds(15); -// NOLINTBEGIN(misc-non-private-member-variables-in-classes) template -class Session { +class + Session { // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes) private: Derived& derived() { return static_cast(*this); } http::request req_; @@ -178,7 +178,6 @@ class Session { beast::get_lowest_layer(derived().stream()).cancel(); } }; -// NOLINTEND(misc-non-private-member-variables-in-classes) class EncryptedClient : public Client, public Session,