From 6dd17310b230f4a39e66dc89865dd30aecd97f3f Mon Sep 17 00:00:00 2001 From: Igor Baranov Date: Thu, 1 Oct 2026 02:22:16 -0700 Subject: [PATCH] Allow IS-07 and IS-12 WebSockets to share a port --- Development/cmake/NmosCppTest.cmake | 1 + Development/nmos/api_utils.cpp | 27 +++++++ Development/nmos/api_utils.h | 3 + Development/nmos/control_protocol_ws_api.cpp | 9 ++- Development/nmos/node_server.cpp | 20 +++-- Development/nmos/server.h | 1 + Development/nmos/test/node_server_test.cpp | 80 ++++++++++++++++++++ 7 files changed, 133 insertions(+), 8 deletions(-) create mode 100644 Development/nmos/test/node_server_test.cpp diff --git a/Development/cmake/NmosCppTest.cmake b/Development/cmake/NmosCppTest.cmake index 2df116932..a6d712190 100644 --- a/Development/cmake/NmosCppTest.cmake +++ b/Development/cmake/NmosCppTest.cmake @@ -61,6 +61,7 @@ set(NMOS_CPP_TEST_NMOS_TEST_SOURCES nmos/test/log_gate_test.cpp nmos/test/mdns_test.cpp nmos/test/node_interfaces_test.cpp + nmos/test/node_server_test.cpp nmos/test/paging_utils_test.cpp nmos/test/query_api_test.cpp nmos/test/resources_test.cpp diff --git a/Development/nmos/api_utils.cpp b/Development/nmos/api_utils.cpp index a4fa44be0..09d232ecb 100644 --- a/Development/nmos/api_utils.cpp +++ b/Development/nmos/api_utils.cpp @@ -1,5 +1,6 @@ #include "nmos/api_utils.h" +#include #include #include #include @@ -764,6 +765,32 @@ namespace nmos return ws_listener; } + web::websockets::experimental::listener::websocket_listener_handlers make_ws_api_router(std::map handlers) + { + const auto find_handlers = [handlers](const web::uri& uri) -> const web::websockets::experimental::listener::websocket_listener_handlers* + { + const auto& path = uri.path(); + const web::websockets::experimental::listener::websocket_listener_handlers* result{}; + std::size_t longest_path{}; + for (const auto& handler : handlers) + { + if (boost::starts_with(path, handler.first) && longest_path < handler.first.size()) + { + result = &handler.second; + longest_path = handler.first.size(); + } + } + return result; + }; + + return { + [find_handlers](web::http::http_request req) { const auto handlers = find_handlers(req.request_uri()); return handlers && handlers->validate_handler(req); }, + [find_handlers](const web::uri& uri, const web::websockets::experimental::listener::connection_id& id) { if (const auto handlers = find_handlers(uri)) handlers->open_handler(uri, id); }, + [find_handlers](const web::uri& uri, const web::websockets::experimental::listener::connection_id& id, web::websockets::websocket_close_status status, const utility::string_t& reason) { if (const auto handlers = find_handlers(uri)) handlers->close_handler(uri, id, status, reason); }, + [find_handlers](const web::uri& uri, const web::websockets::experimental::listener::connection_id& id, const web::websockets::websocket_incoming_message& message) { if (const auto handlers = find_handlers(uri)) handlers->message_handler(uri, id, message); } + }; + } + // returns "http" or "https" depending on settings utility::string_t http_scheme(const nmos::settings& settings) { diff --git a/Development/nmos/api_utils.h b/Development/nmos/api_utils.h index 1445d672d..f50ee80b6 100644 --- a/Development/nmos/api_utils.h +++ b/Development/nmos/api_utils.h @@ -163,6 +163,9 @@ namespace nmos // construct a websocket_listener on the specified address and port - captures handlers by reference! web::websockets::experimental::listener::websocket_listener make_ws_api_listener(bool secure, const utility::string_t& host_address, int port, const web::websockets::experimental::listener::websocket_listener_handlers& handlers, web::websockets::experimental::listener::websocket_listener_config config, slog::base_gate& gate); + // combine WebSocket API handlers for one listener, selecting an API by the leading path component + web::websockets::experimental::listener::websocket_listener_handlers make_ws_api_router(std::map handlers); + // returns "http" or "https" depending on settings utility::string_t http_scheme(const nmos::settings& settings); diff --git a/Development/nmos/control_protocol_ws_api.cpp b/Development/nmos/control_protocol_ws_api.cpp index 855d09026..7bbac9697 100644 --- a/Development/nmos/control_protocol_ws_api.cpp +++ b/Development/nmos/control_protocol_ws_api.cpp @@ -1,5 +1,6 @@ #include "nmos/control_protocol_ws_api.h" +#include #include #include #include "cpprest/json_validator.h" @@ -65,11 +66,13 @@ namespace nmos if (!ws_validate_authorization(req, nmos::experimental::scopes::ncp)) { return false; } } - // For now just return true const auto& ws_ncp_path = req.request_uri().path(); slog::log(gate, SLOG_FLF) << "Validating websocket connection to: " << ws_ncp_path; - - return true; + const auto is12_versions = nmos::is12_versions::from_settings(model.settings); + return std::any_of(is12_versions.begin(), is12_versions.end(), [&ws_ncp_path](const nmos::api_version& version) + { + return ws_ncp_path == U("/x-nmos/ncp/") + nmos::make_api_version(version); + }); }; } diff --git a/Development/nmos/node_server.cpp b/Development/nmos/node_server.cpp index 66b7c23ca..3702f10c6 100644 --- a/Development/nmos/node_server.cpp +++ b/Development/nmos/node_server.cpp @@ -94,9 +94,19 @@ namespace nmos const auto& control_protocol_ws_port = nmos::fields::control_protocol_ws_port(node_model.settings); if (control_protocol_enabled) { - if (control_protocol_ws_port == events_ws_port) throw std::runtime_error("Same port used for events and control protocol websockets are not supported"); - auto& control_protocol_ws_api = node_server.ws_handlers[{ {}, control_protocol_ws_port }]; - control_protocol_ws_api.first = nmos::make_control_protocol_ws_api(node_model, control_protocol_ws_api.second, node_implementation.ws_validate_authorization, node_implementation.get_control_protocol_class_descriptor, node_implementation.get_control_protocol_datatype_descriptor, node_implementation.get_control_protocol_method_descriptor, node_implementation.control_protocol_property_changed, gate); + if (control_protocol_ws_port == events_ws_port) + { + auto control_protocol_ws_api = nmos::make_control_protocol_ws_api(node_model, node_server.control_protocol_websockets, node_implementation.ws_validate_authorization, node_implementation.get_control_protocol_class_descriptor, node_implementation.get_control_protocol_datatype_descriptor, node_implementation.get_control_protocol_method_descriptor, node_implementation.control_protocol_property_changed, gate); + events_ws_api.first = nmos::make_ws_api_router({ + { U("/x-nmos/events/"), events_ws_api.first }, + { U("/x-nmos/ncp/"), control_protocol_ws_api } + }); + } + else + { + auto& control_protocol_ws_api = node_server.ws_handlers[{ {}, control_protocol_ws_port }]; + control_protocol_ws_api.first = nmos::make_control_protocol_ws_api(node_model, control_protocol_ws_api.second, node_implementation.ws_validate_authorization, node_implementation.get_control_protocol_class_descriptor, node_implementation.get_control_protocol_datatype_descriptor, node_implementation.get_control_protocol_method_descriptor, node_implementation.control_protocol_property_changed, gate); + } } // Set up the listeners for each HTTP API port @@ -171,8 +181,8 @@ namespace nmos if (control_protocol_enabled) { auto& control_protocol_ws_listener = node_server.ws_listeners.at(control_protocol_ws_pos); - auto& control_protocol_ws_api = node_server.ws_handlers.at({ {}, control_protocol_ws_port }); - node_server.thread_functions.push_back([&] { nmos::send_control_protocol_ws_messages_thread(control_protocol_ws_listener, node_model, control_protocol_ws_api.second, gate); }); + auto& control_protocol_websockets = control_protocol_ws_port == events_ws_port ? node_server.control_protocol_websockets : node_server.ws_handlers.at({ {}, control_protocol_ws_port }).second; + node_server.thread_functions.push_back([&] { nmos::send_control_protocol_ws_messages_thread(control_protocol_ws_listener, node_model, control_protocol_websockets, gate); }); } return node_server; diff --git a/Development/nmos/server.h b/Development/nmos/server.h index edf823c59..6e1a0b66f 100644 --- a/Development/nmos/server.h +++ b/Development/nmos/server.h @@ -27,6 +27,7 @@ namespace nmos // WebSocket APIs std::map> ws_handlers; + nmos::websockets control_protocol_websockets; std::vector ws_listeners; // Server behaviours diff --git a/Development/nmos/test/node_server_test.cpp b/Development/nmos/test/node_server_test.cpp new file mode 100644 index 000000000..fff91e619 --- /dev/null +++ b/Development/nmos/test/node_server_test.cpp @@ -0,0 +1,80 @@ +// The first "test" is of course whether the header compiles standalone +#include "nmos/node_server.h" + +#include +#include +#include "boost/iostreams/device/null.hpp" +#include "boost/iostreams/stream.hpp" +#include "bst/test/test.h" +#include "nmos/control_protocol_ws_api.h" +#include "nmos/log_gate.h" +#include "nmos/model.h" +#include "nmos/server.h" +#include "nmos/settings.h" + +namespace +{ + std::pair make_test_node_server(int events_ws_port, int control_protocol_ws_port) + { + nmos::node_model model; + model.settings[U("events_ws_port")] = web::json::value::number(events_ws_port); + model.settings[U("control_protocol_ws_port")] = web::json::value::number(control_protocol_ws_port); + nmos::insert_node_default_settings(model.settings); + + boost::iostreams::stream null_ostream((boost::iostreams::null_sink())); + nmos::experimental::log_model log_model; + nmos::experimental::log_gate gate(null_ostream, null_ostream, log_model); + + auto server = nmos::experimental::make_node_server(model, nmos::experimental::node_implementation(), log_model, gate); + return { server.ws_handlers.size(), server.ws_listeners.size() }; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testNodeServerWebSocketPorts) +{ + // IS-07 and IS-12 may use one listener because their endpoint paths do not overlap. + { + const auto listener_counts = make_test_node_server(3217, 3217); + BST_REQUIRE_EQUAL(1, listener_counts.first); + BST_REQUIRE_EQUAL(1, listener_counts.second); + } + + // Existing distinct-port configurations still create independent listeners. + { + const auto listener_counts = make_test_node_server(3217, 3218); + BST_REQUIRE_EQUAL(2, listener_counts.first); + BST_REQUIRE_EQUAL(2, listener_counts.second); + } + + // IS-07 remains independently usable when IS-12 is disabled. + { + const auto listener_counts = make_test_node_server(3217, -1); + BST_REQUIRE_EQUAL(1, listener_counts.first); + BST_REQUIRE_EQUAL(1, listener_counts.second); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testControlProtocolWebSocketRoutes) +{ + nmos::node_model model; + nmos::insert_node_default_settings(model.settings); + + boost::iostreams::stream null_ostream((boost::iostreams::null_sink())); + nmos::experimental::log_model log_model; + nmos::experimental::log_gate gate(null_ostream, null_ostream, log_model); + + const auto validate = nmos::make_control_protocol_ws_validate_handler(model, nullptr, gate); + web::http::http_request valid_request; + valid_request.set_request_uri(web::uri(U("/x-nmos/ncp/v1.0"))); + BST_REQUIRE(validate(valid_request)); + + web::http::http_request invalid_request; + invalid_request.set_request_uri(web::uri(U("/x-nmos/ncp/not-a-version"))); + BST_REQUIRE(!validate(invalid_request)); + + web::http::http_request conflicting_request; + conflicting_request.set_request_uri(web::uri(U("/x-nmos/events/v1.0/devices/not-an-is12-route"))); + BST_REQUIRE(!validate(conflicting_request)); +}