Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Development/cmake/NmosCppTest.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions Development/nmos/api_utils.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "nmos/api_utils.h"

#include <cstddef>
#include <boost/algorithm/cxx11/any_of.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/algorithm/string/trim.hpp>
Expand Down Expand Up @@ -764,6 +765,32 @@ namespace nmos
return ws_listener;
}

web::websockets::experimental::listener::websocket_listener_handlers make_ws_api_router(std::map<utility::string_t, web::websockets::experimental::listener::websocket_listener_handlers> 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)
{
Expand Down
3 changes: 3 additions & 0 deletions Development/nmos/api_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<utility::string_t, web::websockets::experimental::listener::websocket_listener_handlers> handlers);

// returns "http" or "https" depending on settings
utility::string_t http_scheme(const nmos::settings& settings);

Expand Down
9 changes: 6 additions & 3 deletions Development/nmos/control_protocol_ws_api.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "nmos/control_protocol_ws_api.h"

#include <algorithm>
#include <boost/algorithm/string/join.hpp>
#include <boost/range/join.hpp>
#include "cpprest/json_validator.h"
Expand Down Expand Up @@ -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<slog::severities::more_info>(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);
});
};
}

Expand Down
20 changes: 15 additions & 5 deletions Development/nmos/node_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions Development/nmos/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ namespace nmos

// WebSocket APIs
std::map<host_port, std::pair<web::websockets::experimental::listener::websocket_listener_handlers, nmos::websockets>> ws_handlers;
nmos::websockets control_protocol_websockets;
std::vector<web::websockets::experimental::listener::websocket_listener> ws_listeners;

// Server behaviours
Expand Down
80 changes: 80 additions & 0 deletions Development/nmos/test/node_server_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// The first "test" is of course whether the header compiles standalone
#include "nmos/node_server.h"

#include <cstddef>
#include <utility>
#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<std::size_t, std::size_t> 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<boost::iostreams::null_sink> 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<boost::iostreams::null_sink> 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));
}