diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 306362bb2..cdf5fa7f4 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -93,7 +93,7 @@ jobs: - name: install conan if: matrix.use_conan == true run: | - pip install conan + pip install conan~=1.47 conan config set general.revisions_enabled=1 - name: install cmake @@ -577,6 +577,8 @@ jobs: apt-get install -y software-properties-common apt-get --allow-unauthenticated update -q apt-get --allow-unauthenticated install -y curl g++ git make patch zlib1g-dev libssl-dev bsdmainutils dnsutils unzip + # ubuntu-14.04 ca-certificates are out of date + git config --global http.sslVerify false curl -sS https://www.python.org/ftp/python/3.6.9/Python-3.6.9.tar.xz | tar -xJ cd Python-3.6.9 ./configure @@ -600,7 +602,7 @@ jobs: - name: install conan if: matrix.use_conan == true run: | - pip install conan + pip install conan~=1.47 conan config set general.revisions_enabled=1 - name: install cmake @@ -1073,4 +1075,4 @@ jobs: git config --global user.name 'test-results-uploader' git config --global user.email 'test-results-uploader@nmos-cpp.iam.gserviceaccount.com' git commit -qm "Badges for README at ${{ env.GITHUB_COMMIT }}" - git push -f `git remote` badges-${{ env.GITHUB_COMMIT }}:badges \ No newline at end of file + git push -f `git remote` badges-${{ env.GITHUB_COMMIT }}:badges diff --git a/.github/workflows/src/build-setup.yml b/.github/workflows/src/build-setup.yml index 0580afcfc..2559b3092 100644 --- a/.github/workflows/src/build-setup.yml +++ b/.github/workflows/src/build-setup.yml @@ -1,7 +1,7 @@ - name: install conan if: matrix.use_conan == true run: | - pip install conan + pip install conan~=1.47 conan config set general.revisions_enabled=1 - name: install cmake diff --git a/.github/workflows/src/build-test.yml b/.github/workflows/src/build-test.yml index 451146c35..a4c5dd74e 100644 --- a/.github/workflows/src/build-test.yml +++ b/.github/workflows/src/build-test.yml @@ -127,6 +127,8 @@ jobs: apt-get install -y software-properties-common apt-get --allow-unauthenticated update -q apt-get --allow-unauthenticated install -y curl g++ git make patch zlib1g-dev libssl-dev bsdmainutils dnsutils unzip + # ubuntu-14.04 ca-certificates are out of date + git config --global http.sslVerify false curl -sS https://www.python.org/ftp/python/3.6.9/Python-3.6.9.tar.xz | tar -xJ cd Python-3.6.9 ./configure diff --git a/Development/cmake/NmosCppConan.cmake b/Development/cmake/NmosCppConan.cmake index 977c035ca..e88f61a2a 100644 --- a/Development/cmake/NmosCppConan.cmake +++ b/Development/cmake/NmosCppConan.cmake @@ -17,7 +17,7 @@ include(${CMAKE_CURRENT_BINARY_DIR}/conan.cmake) # it would be nice to output a message if its a more recent version than tested, like: # "Found Conan version 99.99 that is higher than the current tested version: " ${CONAN_VERSION_CUR}) set(CONAN_VERSION_MIN "1.47.0") -set(CONAN_VERSION_CUR "1.53.0") +set(CONAN_VERSION_CUR "1.59.0") conan_check(VERSION ${CONAN_VERSION_MIN} REQUIRED) set(NMOS_CPP_CONAN_BUILD_LIBS "missing" CACHE STRING "Semicolon separated list of libraries to build rather than download") diff --git a/Development/cmake/NmosCppLibraries.cmake b/Development/cmake/NmosCppLibraries.cmake index b6d6d972a..944eb9902 100644 --- a/Development/cmake/NmosCppLibraries.cmake +++ b/Development/cmake/NmosCppLibraries.cmake @@ -811,6 +811,7 @@ set(NMOS_CPP_NMOS_HEADERS nmos/api_downgrade.h nmos/api_utils.h nmos/api_version.h + nmos/asset.h nmos/capabilities.h nmos/certificate_handlers.h nmos/certificate_settings.h diff --git a/Development/cpprest/host_utils.cpp b/Development/cpprest/host_utils.cpp index 2d631a151..9c9b0b7e0 100644 --- a/Development/cpprest/host_utils.cpp +++ b/Development/cpprest/host_utils.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include "cpprest/asyncrt_utils.h" // for utility::conversions #if defined(_WIN32) @@ -328,6 +330,16 @@ namespace web } return addresses; // empty if host_name cannot be resolved } + + // get the associated network interface name from an IP address + utility::string_t get_interface_name(const utility::string_t& address, const std::vector& host_interfaces) + { + const auto interface = boost::range::find_if(host_interfaces, [&](const web::hosts::experimental::host_interface& interface) + { + return interface.addresses.end() != boost::range::find(interface.addresses, address); + }); + return host_interfaces.end() != interface ? interface->name : utility::string_t{}; + } } } } diff --git a/Development/cpprest/host_utils.h b/Development/cpprest/host_utils.h index f9dc6ad95..40ed32e2c 100644 --- a/Development/cpprest/host_utils.h +++ b/Development/cpprest/host_utils.h @@ -30,6 +30,9 @@ namespace web std::vector host_names(const utility::string_t& address); std::vector host_addresses(const utility::string_t& host_name); + + // get the associated network interface name from an IP address + utility::string_t get_interface_name(const utility::string_t& address, const std::vector& host_interfaces = web::hosts::experimental::host_interfaces()); } } } diff --git a/Development/cpprest/uri_schemes.h b/Development/cpprest/uri_schemes.h index 8963e5543..76e63e02b 100644 --- a/Development/cpprest/uri_schemes.h +++ b/Development/cpprest/uri_schemes.h @@ -22,6 +22,11 @@ namespace web inline utility::string_t http_scheme(bool secure) { return secure ? uri_schemes::https : uri_schemes::http; } inline utility::string_t ws_scheme(bool secure) { return secure ? uri_schemes::wss : uri_schemes::ws; } + + inline bool is_secure_uri_scheme(const utility::string_t& scheme) + { + return uri_schemes::https == scheme || uri_schemes::wss == scheme; + } } #endif diff --git a/Development/cpprest/ws_listener_impl.cpp b/Development/cpprest/ws_listener_impl.cpp index 712611f3b..101a54524 100644 --- a/Development/cpprest/ws_listener_impl.cpp +++ b/Development/cpprest/ws_listener_impl.cpp @@ -313,6 +313,7 @@ namespace web if (!init) { server.init_asio(); + server.set_reuse_addr(true); init = true; } else diff --git a/Development/nmos-cpp-node/config.json b/Development/nmos-cpp-node/config.json index ce83f8ee5..5bf63a9ff 100644 --- a/Development/nmos-cpp-node/config.json +++ b/Development/nmos-cpp-node/config.json @@ -4,6 +4,16 @@ { // Custom settings for the example node implementation + // node_tags, device_tags: used in resource tags fields + // "Each tag has a single key, but MAY have multiple values." + // See https://specs.amwa.tv/is-04/releases/v1.3.2/docs/APIs_-_Common_Keys.html#tags + // { + // "tag_1": [ "tag_1_value_1", "tag_1_value_2" ], + // "tag_2": [ "tag_2_value_1" ] + // } + //"node_tags": {}, + //"device_tags": {}, + // how_many: provides for very basic testing of a node with many sub-resources of each type //"how_many": 4, @@ -181,11 +191,20 @@ //"settings_port": 3209, //"logging_port": 5106, - // addresses [registry, node]: addresses on which to listen for each API, or empty string for the wildcard address + // addresses [registry, node]: IP addresses on which to listen for each API, or empty string for the wildcard address + + // server_address [registry, node]: if specified, this becomes the default address on which to listen for each API instead of the wildcard address + //"server_address": "", + + // addresses [registry, node]: IP addresses on which to listen for specific APIs //"settings_address": "127.0.0.1", //"logging_address": "", + // client_address [registry, node]: IP address of the network interface to bind client connections + // for now, only supporting HTTP/HTTPS client connections on Linux + //"client_address": "", + // logging_limit [registry, node]: maximum number of log events cached for the Logging API //"logging_limit": 1234, diff --git a/Development/nmos-cpp-node/node_implementation.cpp b/Development/nmos-cpp-node/node_implementation.cpp index 850549fe2..dc3ee9d26 100644 --- a/Development/nmos-cpp-node/node_implementation.cpp +++ b/Development/nmos-cpp-node/node_implementation.cpp @@ -56,6 +56,16 @@ namespace impl // custom settings for the example node implementation namespace fields { + // node_tags, device_tags: used in resource tags fields + // "Each tag has a single key, but MAY have multiple values." + // See https://specs.amwa.tv/is-04/releases/v1.3.2/docs/APIs_-_Common_Keys.html#tags + // { + // "tag_1": [ "tag_1_value_1", "tag_1_value_2" ], + // "tag_2": [ "tag_2_value_1" ] + // } + const web::json::field_as_value_or node_tags{ U("node_tags"), web::json::value::object() }; + const web::json::field_as_value_or device_tags{ U("device_tags"), web::json::value::object() }; + // how_many: provides for very basic testing of a node with many sub-resources of each type const web::json::field_as_integer_or how_many{ U("how_many"), 1 }; @@ -295,6 +305,7 @@ void node_implementation_init(nmos::node_model& model, slog::base_gate& gate) // example node { auto node = nmos::make_node(node_id, clocks, nmos::make_node_interfaces(interfaces), model.settings); + node.data[nmos::fields::tags] = impl::fields::node_tags(model.settings); if (!insert_resource_after(delay_millis, model.node_resources, std::move(node), gate)) throw node_implementation_init_exception(); } @@ -338,7 +349,9 @@ void node_implementation_init(nmos::node_model& model, slog::base_gate& gate) auto sender_ids = impl::make_ids(seed_id, nmos::types::sender, rtp_sender_ports, how_many); if (0 <= nmos::fields::events_port(model.settings)) boost::range::push_back(sender_ids, impl::make_ids(seed_id, nmos::types::sender, ws_sender_ports, how_many)); auto receiver_ids = impl::make_ids(seed_id, nmos::types::receiver, receiver_ports, how_many); - if (!insert_resource_after(delay_millis, model.node_resources, nmos::make_device(device_id, node_id, sender_ids, receiver_ids, model.settings), gate)) throw node_implementation_init_exception(); + auto device = nmos::make_device(device_id, node_id, sender_ids, receiver_ids, model.settings); + device.data[nmos::fields::tags] = impl::fields::device_tags(model.settings); + if (!insert_resource_after(delay_millis, model.node_resources, std::move(device), gate)) throw node_implementation_init_exception(); } // example sources, flows and senders diff --git a/Development/nmos-cpp-registry/config.json b/Development/nmos-cpp-registry/config.json index 48312edeb..940eb0645 100644 --- a/Development/nmos-cpp-registry/config.json +++ b/Development/nmos-cpp-registry/config.json @@ -109,17 +109,26 @@ //"mdns_port": 3208, //"schemas_port": 3208, - // addresses [registry, node]: addresses on which to listen for each API, or empty string for the wildcard address + // addresses [registry, node]: IP addresses on which to listen for each API, or empty string for the wildcard address + + // server_address [registry, node]: if specified, this becomes the default address on which to listen for each API instead of the wildcard address + //"server_address": "", + + // addresses [registry, node]: IP addresses on which to listen for specific APIs //"settings_address": "127.0.0.1", //"logging_address": "", - // addresses [registry]: addresses on which to listen for each API, or empty string for the wildcard address + // addresses [registry]: IP addresses on which to listen for specific APIs //"admin_address": "", //"mdns_address": "", //"schemas_address": "", + // client_address [registry, node]: IP address of the network interface to bind client connections + // for now, only supporting HTTP/HTTPS client connections on Linux + //"client_address": "", + // query_ws_paging_default/query_ws_paging_limit [registry]: default/maximum number of events per message when using the Query WebSocket API (a client may request a lower limit) //"query_ws_paging_default": 10, //"query_ws_paging_limit": 100, diff --git a/Development/nmos/api_utils.cpp b/Development/nmos/api_utils.cpp index 33c385d6a..bf352c77c 100644 --- a/Development/nmos/api_utils.cpp +++ b/Development/nmos/api_utils.cpp @@ -467,9 +467,6 @@ namespace nmos }; } - static const utility::string_t received_time{ U("X-Received-Time") }; - static const utility::string_t actual_method{ U("X-Actual-Method") }; - // make handler to set appropriate response headers, and error response body if indicated web::http::experimental::listener::route_handler make_api_finally_handler(slog::base_gate& gate) { diff --git a/Development/nmos/api_utils.h b/Development/nmos/api_utils.h index 2dda425a6..0db8898bb 100644 --- a/Development/nmos/api_utils.h +++ b/Development/nmos/api_utils.h @@ -161,6 +161,9 @@ namespace nmos namespace details { + const utility::string_t received_time{ U("X-Received-Time") }; + const utility::string_t actual_method{ U("X-Actual-Method") }; + // exception to skip other route handlers and then send the response (see add_api_finally_handler) struct to_api_finally_handler {}; diff --git a/Development/nmos/asset.h b/Development/nmos/asset.h new file mode 100644 index 000000000..fafd2c834 --- /dev/null +++ b/Development/nmos/asset.h @@ -0,0 +1,20 @@ +#ifndef NMOS_ASSET_H +#define NMOS_ASSET_H + +#include "cpprest/json_utils.h" + +// Asset Distinguishing Information +// See https://specs.amwa.tv/bcp-002-02/ +// and https://specs.amwa.tv/nmos-parameter-registers/branches/main/tags/ +namespace nmos +{ + namespace fields + { + const web::json::field_as_value_or asset_manufacturer{ U("urn:x-nmos:tag:asset:manufacturer/v1.0"), web::json::value::array() }; + const web::json::field_as_value_or asset_product_name{ U("urn:x-nmos:tag:asset:product/v1.0"), web::json::value::array() }; + const web::json::field_as_value_or asset_instance_id{ U("urn:x-nmos:tag:asset:instance-id/v1.0"), web::json::value::array() }; + const web::json::field_as_value_or asset_function{ U("urn:x-nmos:tag:asset:function/v1.0"), web::json::value::array() }; + } +} + +#endif diff --git a/Development/nmos/channels.cpp b/Development/nmos/channels.cpp index 611e22f3d..e9f596099 100644 --- a/Development/nmos/channels.cpp +++ b/Development/nmos/channels.cpp @@ -155,4 +155,52 @@ namespace nmos return channel_order.str(); } + + // See SMPTE ST 2110-30:2017 Section 6.2.2 Channel Order Convention + std::vector parse_fmtp_channel_order(const utility::string_t& channel_order) + { + std::vector channels; + + const auto first = channel_order.data(); + const auto last = first + channel_order.size(); + auto it = first; + + // check prefix + + static const auto prefix = U("SMPTE2110.("); + auto pit = &prefix[0]; + while (it != last && *pit != U('\0') && *it == *pit) ++it, ++pit; + if (*pit != U('\0')) return {}; + + // parse comma-separated channel group symbols + + while (true) + { + const auto git = it; + while (it != last && *it != U(')') && *it != U(',')) ++it; + if (it == last) return {}; + + const channel_group_symbol symbol(utility::string_t(git, it)); + + // hm, does not handle 22.2 Surround ('222'), SDI audio group ('SGRP') or Undefined ('U01' to 'U64') + auto group = std::find_if(details::channel_groups.begin(), details::channel_groups.end(), + [&](const std::pair, channel_group_symbol>& group) + { + return symbol == group.second; + }); + if (details::channel_groups.end() == group) return {}; + channels.insert(channels.end(), group->first.begin(), group->first.end()); + + if (*it == U(')')) break; + ++it; + } + + // check suffix + + if (it == last) return {}; + ++it; + if (it != last) return {}; + + return channels; + } } diff --git a/Development/nmos/channels.h b/Development/nmos/channels.h index 77e12a945..df0522b12 100644 --- a/Development/nmos/channels.h +++ b/Development/nmos/channels.h @@ -113,6 +113,7 @@ namespace nmos // See SMPTE ST 2110-30:2017 Section 6.2.2 Channel Order Convention utility::string_t make_fmtp_channel_order(const std::vector& channels); + std::vector parse_fmtp_channel_order(const utility::string_t& channel_order); } #endif diff --git a/Development/nmos/client_utils.cpp b/Development/nmos/client_utils.cpp index b2b902379..784a5183b 100644 --- a/Development/nmos/client_utils.cpp +++ b/Development/nmos/client_utils.cpp @@ -1,8 +1,12 @@ #include "nmos/client_utils.h" -// cf. preprocessor conditions in nmos::details::make_client_ssl_context_callback +// cf. preprocessor conditions in nmos::details::make_client_ssl_context_callback and nmos::details::make_client_nativehandle_options #if !defined(_WIN32) || !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) +#if defined(__linux__) +#include +#endif #include "boost/asio/ssl/set_cipher_list.hpp" +#include "cpprest/host_utils.h" #endif #include "cpprest/basic_utils.h" #include "cpprest/details/system_error.h" @@ -55,39 +59,149 @@ namespace nmos } }; } +#endif + +#if !defined(_WIN32) || !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) + // bind socket to a specific network interface + // for now, only supporting Linux because SO_BINDTODEVICE is not defined on Windows and Mac + inline void bind_to_device(const utility::string_t& interface_name, bool secure, void* native_handle) + { +#if defined(__linux__) + int socket_fd; + // hmm, frustrating that native_handle type has been erased so we need secure flag + if (secure) + { + auto socket = (boost::asio::ssl::stream*)native_handle; + if (!socket->lowest_layer().is_open()) + { + // for now, limited to IPv4 + socket->lowest_layer().open(boost::asio::ip::tcp::v4()); + } + socket_fd = socket->lowest_layer().native_handle(); + } + else + { + auto socket = (boost::asio::ip::tcp::socket*)native_handle; + if (!socket->is_open()) + { + // for now, limited to IPv4 + socket->open(boost::asio::ip::tcp::v4()); + } + socket_fd = socket->lowest_layer().native_handle(); + } + const auto interface_name_ = utility::us2s(interface_name); + if (0 != setsockopt(socket_fd, SOL_SOCKET, SO_BINDTODEVICE, interface_name_.data(), interface_name_.length())) + { + char error[1024]; + throw std::runtime_error(strerror_r(errno, error, sizeof(error))); + } +#else + throw std::logic_error("unsupported"); +#endif + } + + inline std::function make_client_nativehandle_options(bool secure, const utility::string_t& client_address, slog::base_gate& gate) + { + if (client_address.empty()) return {}; + // get the associated network interface name from IP address + const auto interface_name = web::hosts::experimental::get_interface_name(client_address); + if (interface_name.empty()) + { + slog::log(gate, SLOG_FLF) << "No network interface found for " << client_address << " to bind for the HTTP client connection"; + return {}; + } + + return [interface_name, secure, &gate](web::http::client::native_handle native_handle) + { + try + { + bind_to_device(interface_name, secure, native_handle); + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Unable to bind HTTP client connection to " << interface_name << ": " << e.what(); + } + }; + } + +#ifdef CPPRESTSDK_ENABLE_BIND_WEBSOCKET_CLIENT + // The current version of the C++ REST SDK 2.10.18 does not provide the callback to enable the custom websocket setting + inline std::function make_ws_client_nativehandle_options(bool secure, const utility::string_t& client_address, slog::base_gate& gate) + { + if (client_address.empty()) return {}; + // get the associated network interface name from IP address + const auto interface_name = web::hosts::experimental::get_interface_name(client_address); + if (interface_name.empty()) + { + slog::log(gate, SLOG_FLF) << "No network interface found for " << client_address << " to bind for the websocket client connection"; + return {}; + } + + return [interface_name, secure, &gate](web::websockets::client::native_handle native_handle) + { + try + { + bind_to_device(interface_name, secure, native_handle); + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Unable to bind websocket client connection to " << interface_name << ": " << e.what(); + } + }; + } +#endif + #endif } - // construct client config based on settings, e.g. using the specified proxy + // construct client config based on specified secure flag and settings, e.g. using the specified proxy and OCSP config // with the remaining options defaulted, e.g. request timeout - web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + web::http::client::http_client_config make_http_client_config(bool secure, const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) { web::http::client::http_client_config config; const auto proxy = proxy_uri(settings); if (!proxy.is_empty()) config.set_proxy(proxy); - config.set_validate_certificates(nmos::experimental::fields::validate_certificates(settings)); + if (secure) config.set_validate_certificates(nmos::experimental::fields::validate_certificates(settings)); #if !defined(_WIN32) && !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) - config.set_ssl_context_callback(details::make_client_ssl_context_callback(settings, load_ca_certificates, gate)); + if (secure) config.set_ssl_context_callback(details::make_client_ssl_context_callback(settings, load_ca_certificates, gate)); + config.set_nativehandle_options(details::make_client_nativehandle_options(secure, nmos::experimental::fields::client_address(settings), gate)); #endif return config; } - // construct client config based on settings, e.g. using the specified proxy + // construct client config based on settings, e.g. using the specified proxy and OCSP config + // with the remaining options defaulted, e.g. request timeout + web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + { + return make_http_client_config(nmos::experimental::fields::client_secure(settings), settings, load_ca_certificates, gate); + } + + // construct client config based on specified secure flag and settings, e.g. using the specified proxy // with the remaining options defaulted - web::websockets::client::websocket_client_config make_websocket_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + web::websockets::client::websocket_client_config make_websocket_client_config(bool secure, const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) { web::websockets::client::websocket_client_config config; const auto proxy = proxy_uri(settings); if (!proxy.is_empty()) config.set_proxy(proxy); - config.set_validate_certificates(nmos::experimental::fields::validate_certificates(settings)); + if (secure) config.set_validate_certificates(nmos::experimental::fields::validate_certificates(settings)); #if !defined(_WIN32) || !defined(__cplusplus_winrt) - config.set_ssl_context_callback(details::make_client_ssl_context_callback(settings, load_ca_certificates, gate)); + if (secure) config.set_ssl_context_callback(details::make_client_ssl_context_callback(settings, load_ca_certificates, gate)); +#ifdef CPPRESTSDK_ENABLE_BIND_WEBSOCKET_CLIENT + config.set_nativehandle_options(details::make_ws_client_nativehandle_options(secure, nmos::experimental::fields::client_address(settings), gate)); +#endif #endif return config; } + // construct client config based on settings, e.g. using the specified proxy + // with the remaining options defaulted + web::websockets::client::websocket_client_config make_websocket_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + { + return make_websocket_client_config(nmos::experimental::fields::client_secure(settings), settings, load_ca_certificates, gate); + } + // make a request with logging pplx::task api_request(web::http::client::http_client client, web::http::http_request request, slog::base_gate& gate, const pplx::cancellation_token& token) { diff --git a/Development/nmos/client_utils.h b/Development/nmos/client_utils.h index 1e76e8bc8..826d9e23c 100644 --- a/Development/nmos/client_utils.h +++ b/Development/nmos/client_utils.h @@ -11,10 +11,16 @@ namespace slog { class base_gate; } // Utility types, constants and functions for implementing NMOS REST API clients namespace nmos { - // construct client config based on settings, e.g. using the specified proxy + // construct client config based on specified secure flag and settings, e.g. using the specified proxy and OCSP config + // with the remaining options defaulted, e.g. request timeout + web::http::client::http_client_config make_http_client_config(bool secure, const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); + // construct client config based on settings, e.g. using the specified proxy and OCSP config // with the remaining options defaulted, e.g. request timeout web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); + // construct client config based on specified secure flag and settings, e.g. using the specified proxy + // with the remaining options defaulted + web::websockets::client::websocket_client_config make_websocket_client_config(bool secure, const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); // construct client config based on settings, e.g. using the specified proxy // with the remaining options defaulted web::websockets::client::websocket_client_config make_websocket_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); diff --git a/Development/nmos/node_server.cpp b/Development/nmos/node_server.cpp index 2e4469788..ecc75c461 100644 --- a/Development/nmos/node_server.cpp +++ b/Development/nmos/node_server.cpp @@ -35,6 +35,8 @@ namespace nmos const auto hsts = nmos::experimental::get_hsts(node_model.settings); + const auto server_address = nmos::experimental::fields::server_address(node_model.settings); + // Configure the Settings API const host_port settings_address(nmos::experimental::fields::settings_address(node_model.settings), nmos::experimental::fields::settings_port(node_model.settings)); @@ -70,8 +72,8 @@ namespace nmos for (auto& api_router : node_server.api_routers) { - // default empty string means the wildcard address - const auto& host = !api_router.first.first.empty() ? api_router.first.first : web::http::experimental::listener::host_wildcard; + // if IP address isn't specified for this router, use default server address or wildcard address + const auto& host = !api_router.first.first.empty() ? api_router.first.first : !server_address.empty() ? server_address : web::http::experimental::listener::host_wildcard; // map the configured client port to the server port on which to listen // hmm, this should probably also take account of the address node_server.http_listeners.push_back(nmos::make_api_listener(server_secure, host, nmos::experimental::server_port(api_router.first.second, node_model.settings), api_router.second, http_config, hsts, gate)); @@ -84,8 +86,8 @@ namespace nmos for (auto& ws_handler : node_server.ws_handlers) { - // default empty string means the wildcard address - const auto& host = !ws_handler.first.first.empty() ? ws_handler.first.first : web::websockets::experimental::listener::host_wildcard; + // if IP address isn't specified for this router, use default server address or wildcard address + const auto& host = !ws_handler.first.first.empty() ? ws_handler.first.first : !server_address.empty() ? server_address : web::websockets::experimental::listener::host_wildcard; // map the configured client port to the server port on which to listen // hmm, this should probably also take account of the address node_server.ws_listeners.push_back(nmos::make_ws_api_listener(server_secure, host, nmos::experimental::server_port(ws_handler.first.second, node_model.settings), ws_handler.second.first, websocket_config, gate)); diff --git a/Development/nmos/ocsp_behaviour.cpp b/Development/nmos/ocsp_behaviour.cpp index cd759b880..f16140be6 100644 --- a/Development/nmos/ocsp_behaviour.cpp +++ b/Development/nmos/ocsp_behaviour.cpp @@ -1,6 +1,7 @@ #include "nmos/ocsp_behaviour.h" #include "pplx/pplx_utils.h" // for pplx::complete_at +#include "cpprest/uri_schemes.h" #include "nmos/client_utils.h" #include "nmos/model.h" #include "nmos/ocsp_state.h" @@ -141,9 +142,9 @@ namespace nmos namespace details { - web::http::client::http_client_config make_ocsp_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + web::http::client::http_client_config make_ocsp_client_config(bool secure, const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) { - auto config = nmos::make_http_client_config(settings, std::move(load_ca_certificates), gate); + auto config = nmos::make_http_client_config(secure, settings, std::move(load_ca_certificates), gate); config.set_timeout(std::chrono::seconds(nmos::experimental::fields::ocsp_request_max(settings))); return config; } @@ -345,7 +346,8 @@ namespace nmos if (!state.client) { const auto ocsp_uri = ocsp_uris.front(); - state.client.reset(new web::http::client::http_client(ocsp_uri, make_ocsp_client_config(model.settings, state.load_ca_certificates, gate))); + const auto secure = web::is_secure_uri_scheme(ocsp_uri.scheme()); + state.client.reset(new web::http::client::http_client(ocsp_uri, make_ocsp_client_config(secure, model.settings, state.load_ca_certificates, gate))); } auto token = cancellation_source.get_token(); diff --git a/Development/nmos/registration_api.cpp b/Development/nmos/registration_api.cpp index 9d174eb75..1f7f704ab 100644 --- a/Development/nmos/registration_api.cpp +++ b/Development/nmos/registration_api.cpp @@ -282,7 +282,15 @@ namespace nmos const bool valid_version = creating || unchanged || nmos::fields::version(data) > nmos::fields::version(resource->data); valid = valid && valid_version; - if (!valid_type) + // check received request isn't being processed out of order + const auto received_time = req.headers().find(details::received_time); + const auto received = req.headers().end() != received_time ? nmos::parse_version(received_time->second) : nmos::tai{}; + const bool valid_received = creating || received == nmos::tai{} || received > resource->received; + valid = valid && valid_received; + + if (!valid_received) + slog::log(gate, SLOG_FLF) << "Registration requested for " << id_type << " at " << nmos::make_version(resource->received) << " processed before request received at " << nmos::make_version(received); + else if (!valid_type) slog::log(gate, SLOG_FLF) << "Registration requested for " << id_type << " would modify type from " << resource->type.name; else if (!valid_api_version) slog::log(gate, SLOG_FLF) << "Registration requested for " << id_type << " would modify API version from " << nmos::make_api_version(resource->version); @@ -399,6 +407,7 @@ namespace nmos if (creating) { nmos::resource created_resource{ version, type, data, false }; + created_resource.received = received; set_reply(res, status_codes::Created, data); res.headers().add(web::http::header_names::location, make_registration_api_resource_location(created_resource)); @@ -410,8 +419,9 @@ namespace nmos set_reply(res, status_codes::OK, data); res.headers().add(web::http::header_names::location, make_registration_api_resource_location(*resource)); - modify_resource(resources, id, [&data](nmos::resource& resource) + modify_resource(resources, id, [&received, &data](nmos::resource& resource) { + resource.received = received; resource.data = data; }); } @@ -424,6 +434,10 @@ namespace nmos slog::log(gate, SLOG_FLF) << "Notifying query websockets thread"; // and anyone else who cares... model.notify(); } + else if (!valid_received) + { + set_reply(res, status_codes::InternalError); + } else if (!valid_api_version) { // experimental extension, proposed for v1.3, using a more specific status code to distinguish conflicts from validation errors @@ -585,10 +599,19 @@ namespace nmos const string_t resourceType = parameters.at(nmos::patterns::resourceType.name); const string_t resourceId = parameters.at(nmos::patterns::resourceId.name); - auto resource = find_resource(resources, { resourceId, nmos::type_from_resourceType(resourceType) }); + const std::pair id_type{ resourceId, nmos::type_from_resourceType(resourceType) }; + auto resource = find_resource(resources, id_type); if (resources.end() != resource) { - if (resource->version == version) + // check received request isn't being processed out of order + const auto received_time = req.headers().find(details::received_time); + const auto received = req.headers().end() != received_time ? nmos::parse_version(received_time->second) : nmos::tai{}; + if (received != nmos::tai{} && received < resource->received) + { + slog::log(gate, SLOG_FLF) << "Registration deletion requested for " << id_type << " at " << nmos::make_version(resource->received) << " processed before request received at " << nmos::make_version(received); + set_reply(res, status_codes::InternalError); + } + else if (resource->version == version) { slog::log(gate, SLOG_FLF) << "Deleting resource: " << resourceId; diff --git a/Development/nmos/registry_server.cpp b/Development/nmos/registry_server.cpp index 6fc2f7314..bcd3d024a 100644 --- a/Development/nmos/registry_server.cpp +++ b/Development/nmos/registry_server.cpp @@ -45,6 +45,8 @@ namespace nmos const auto hsts = nmos::experimental::get_hsts(registry_model.settings); + const auto server_address = nmos::experimental::fields::server_address(registry_model.settings); + // Configure the DNS-SD Browsing API const host_port mdns_address(nmos::experimental::fields::mdns_address(registry_model.settings), nmos::experimental::fields::mdns_port(registry_model.settings)); @@ -111,8 +113,8 @@ namespace nmos for (auto& api_router : registry_server.api_routers) { - // default empty string means the wildcard address - const auto& host = !api_router.first.first.empty() ? api_router.first.first : web::http::experimental::listener::host_wildcard; + // if IP address isn't specified for this router, use default server address or wildcard address + const auto& host = !api_router.first.first.empty() ? api_router.first.first : !server_address.empty() ? server_address : web::http::experimental::listener::host_wildcard; // map the configured client port to the server port on which to listen // hmm, this should probably also take account of the address registry_server.http_listeners.push_back(nmos::make_api_listener(server_secure, host, nmos::experimental::server_port(api_router.first.second, registry_model.settings), api_router.second, http_config, hsts, gate)); @@ -125,8 +127,8 @@ namespace nmos for (auto& ws_handler : registry_server.ws_handlers) { - // default empty string means the wildcard address - const auto& host = !ws_handler.first.first.empty() ? ws_handler.first.first : web::websockets::experimental::listener::host_wildcard; + // if IP address isn't specified for this router, use default server address or wildcard address + const auto& host = !ws_handler.first.first.empty() ? ws_handler.first.first : !server_address.empty() ? server_address : web::websockets::experimental::listener::host_wildcard; // map the configured client port to the server port on which to listen // hmm, this should probably also take account of the address registry_server.ws_listeners.push_back(nmos::make_ws_api_listener(server_secure, host, nmos::experimental::server_port(ws_handler.first.second, registry_model.settings), ws_handler.second.first, websocket_config, gate)); diff --git a/Development/nmos/resource.h b/Development/nmos/resource.h index 81eb0f558..fedfde3a4 100644 --- a/Development/nmos/resource.h +++ b/Development/nmos/resource.h @@ -66,6 +66,8 @@ namespace nmos // see https://specs.amwa.tv/is-04/releases/v1.2.0/docs/2.5._APIs_-_Query_Parameters.html#pagination tai created; tai updated; + // when the most recently applied request was received + tai received; // see https://specs.amwa.tv/is-04/releases/v1.2.0/docs/4.1._Behaviour_-_Registration.html#heartbeating mutable details::copyable_atomic health; diff --git a/Development/nmos/settings.h b/Development/nmos/settings.h index 62be9c25a..cf55267fd 100644 --- a/Development/nmos/settings.h +++ b/Development/nmos/settings.h @@ -240,17 +240,26 @@ namespace nmos const web::json::field_as_integer_or mdns_port{ U("mdns_port"), 3208 }; const web::json::field_as_integer_or schemas_port{ U("schemas_port"), 3208 }; - // addresses [registry, node]: addresses on which to listen for each API, or empty string for the wildcard address + // addresses [registry, node]: IP addresses on which to listen for each API, or empty string for the wildcard address + + // server_address [registry, node]: if specified, this becomes the default address on which to listen for each API instead of the wildcard address + const web::json::field_as_string_or server_address{ U("server_address"), U("") }; + + // addresses [registry, node]: IP addresses on which to listen for specific APIs const web::json::field_as_string_or settings_address{ U("settings_address"), U("") }; const web::json::field_as_string_or logging_address{ U("logging_address"), U("") }; - // addresses [registry]: addresses on which to listen for each API, or empty string for the wildcard address + // addresses [registry]: IP addresses on which to listen for specific APIs const web::json::field_as_string_or admin_address{ U("admin_address"), U("") }; const web::json::field_as_string_or mdns_address{ U("mdns_address"), U("") }; const web::json::field_as_string_or schemas_address{ U("schemas_address"), U("") }; + // client_address [registry, node]: IP address of the network interface to bind client connections + // for now, only supporting HTTP/HTTPS client connections on Linux + const web::json::field_as_string_or client_address{ U("client_address"), U("") }; + // query_ws_paging_default/query_ws_paging_limit [registry]: default/maximum number of events per message when using the Query WebSocket API (a client may request a lower limit) const web::json::field_as_integer_or query_ws_paging_default{ U("query_ws_paging_default"), 10 }; const web::json::field_as_integer_or query_ws_paging_limit{ U("query_ws_paging_limit"), 100 }; diff --git a/Development/nmos/test/channels_test.cpp b/Development/nmos/test/channels_test.cpp index 1868ee853..ce316e592 100644 --- a/Development/nmos/test/channels_test.cpp +++ b/Development/nmos/test/channels_test.cpp @@ -28,3 +28,40 @@ BST_TEST_CASE(testMakeFmtpChannelOrder) const std::vector example_3{ M1, M1, M1, M1, L, R, C, LFE }; BST_REQUIRE_EQUAL(U("SMPTE2110.(M,M,M,M,ST,U02)"), nmos::make_fmtp_channel_order(example_3)); } + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testParseFmtpChannelOrder) +{ + using namespace nmos::channel_symbols; + + // two simple examples + + const std::vector stereo{ L, R }; + BST_REQUIRE_EQUAL(stereo, nmos::parse_fmtp_channel_order(U("SMPTE2110.(ST)"))); + + const std::vector dual_mono{ M1, M2 }; + BST_REQUIRE_EQUAL(dual_mono, nmos::parse_fmtp_channel_order(U("SMPTE2110.(DM)"))); + + // two examples from ST 2110-30:2017 Section 6.2.2 Channel Order Convention + + const std::vector example_1{ L, R, C, LFE, Ls, Rs, L, R }; + BST_REQUIRE_EQUAL(example_1, nmos::parse_fmtp_channel_order(U("SMPTE2110.(51,ST)"))); + + //const std::vector example_2{ M1, M1, M1, M1, L, R, Undefined(1), Undefined(2) }; + //BST_REQUIRE_EQUAL(example_2, nmos::parse_fmtp_channel_order(U("SMPTE2110.(M,M,M,M,ST,U02)"))); + + // bad examples + + const std::vector empty; + BST_REQUIRE_EQUAL(empty, nmos::parse_fmtp_channel_order(U("SMPTE2110.(51,ST)BAD"))); + BST_REQUIRE_EQUAL(empty, nmos::parse_fmtp_channel_order(U("SMPTE2110.(51,ST,)"))); + BST_REQUIRE_EQUAL(empty, nmos::parse_fmtp_channel_order(U("SMPTE2110.(51,,ST)"))); + BST_REQUIRE_EQUAL(empty, nmos::parse_fmtp_channel_order(U("SMPTE2110.(51,BAD)"))); + BST_REQUIRE_EQUAL(empty, nmos::parse_fmtp_channel_order(U("SMPTE2110.(51,ST"))); + BST_REQUIRE_EQUAL(empty, nmos::parse_fmtp_channel_order(U("SMPTE2110.(51,"))); + BST_REQUIRE_EQUAL(empty, nmos::parse_fmtp_channel_order(U("SMPTE2110.(51"))); + BST_REQUIRE_EQUAL(empty, nmos::parse_fmtp_channel_order(U("SMPTE2110.("))); + BST_REQUIRE_EQUAL(empty, nmos::parse_fmtp_channel_order(U("SMPTE2110."))); + BST_REQUIRE_EQUAL(empty, nmos::parse_fmtp_channel_order(U("BAD"))); + BST_REQUIRE_EQUAL(empty, nmos::parse_fmtp_channel_order(U(""))); +} diff --git a/Documents/Dependencies.md b/Documents/Dependencies.md index 58940e7ce..4c31f4243 100644 --- a/Documents/Dependencies.md +++ b/Documents/Dependencies.md @@ -54,11 +54,11 @@ By default nmos-cpp uses [Conan](https://conan.io) to download most of its depen 1. Install Python 3 if necessary Note: The Python scripts directory needs to be added to the `PATH`, so the Conan executable can be found -2. Install Conan using `pip install conan` +2. Install or upgrade Conan using `pip install --upgrade conan~=1.47` Notes: - - On some platforms with Python 2 and Python 3 both installed this may need to be `pip3 install conan` - - Currently, Conan 1.47 or higher is required; version 1.53.0 (latest release at the time) has been tested - - Conan evolves fairly quickly, so it's worth running `pip install --upgrade conan` regularly + - On some platforms with Python 2 and Python 3 both installed this may need to be `pip3 install --upgrade conan~=1.47` + - Currently, Conan 1.47 or higher (and lower than version 2.0) is required by the nmos-cpp recipe; dependencies may require a higher version; version 1.59.0 has been tested + - Conan evolves fairly quickly, so it's worth running `pip install --upgrade conan~=1.47` regularly - By default [Conan assumes semver compatibility](https://docs.conan.io/en/1.42/creating_packages/define_abi_compatibility.html#versioning-schema). Boost and other C++ libraries do not meet this expectation and break ABI compatibility between e.g. minor versions. Unfortunately, the recipes in Conan Center Index do not generally customize their `package_id` method to take this into account. diff --git a/README.md b/README.md index 41ded4df7..2d222743e 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ This repository contains an implementation of the [AMWA Networked Media Open Spe - [AMWA IS-08 NMOS Audio Channel Mapping Specification](https://specs.amwa.tv/is-08/) - [AMWA IS-09 NMOS System Parameters Specification](https://specs.amwa.tv/is-09/) (originally defined in JT-NM TR-1001-1:2018 Annex A) - [AMWA BCP-002-01 NMOS Grouping Recommendations - Natural Grouping](https://specs.amwa.tv/bcp-002-01/) +- [AMWA BCP-002-02 NMOS Asset Distinguishing Information](https://specs.amwa.tv/bcp-002-02/) - [AMWA BCP-003-01 Secure Communication in NMOS Systems](https://specs.amwa.tv/bcp-003-01/) - [AMWA BCP-004-01 NMOS Receiver Capabilities](https://specs.amwa.tv/bcp-004-01/) - [AMWA BCP-006-01 NMOS With JPEG XS](https://specs.amwa.tv/bcp-006-01/) diff --git a/Sandbox/run_nmos_testing.sh b/Sandbox/run_nmos_testing.sh index a2fd75147..9e45dcc6a 100755 --- a/Sandbox/run_nmos_testing.sh +++ b/Sandbox/run_nmos_testing.sh @@ -113,18 +113,18 @@ if [[ "${config_auth}" == "True" ]]; then else echo "Running non-Auth tests" auth=false - # 6 test cases per API under test - (( expected_disabled_IS_04_01+=6 )) - (( expected_disabled_IS_04_03+=6 )) - (( expected_disabled_IS_05_01+=6 )) - (( expected_disabled_IS_05_02+=12 )) - (( expected_disabled_IS_07_01+=6 )) - (( expected_disabled_IS_07_02+=18 )) - (( expected_disabled_IS_08_01+=6 )) - (( expected_disabled_IS_08_02+=12 )) + # 7 test cases per API under test + (( expected_disabled_IS_04_01+=7 )) + (( expected_disabled_IS_04_03+=7 )) + (( expected_disabled_IS_05_01+=7 )) + (( expected_disabled_IS_05_02+=14 )) + (( expected_disabled_IS_07_01+=7 )) + (( expected_disabled_IS_07_02+=21 )) + (( expected_disabled_IS_08_01+=7 )) + (( expected_disabled_IS_08_02+=14 )) # test_33, test_33_1 - (( expected_disabled_IS_04_02+=14 )) - (( expected_disabled_IS_09_01+=6 )) + (( expected_disabled_IS_04_02+=16 )) + (( expected_disabled_IS_09_01+=7 )) fi "${node_command}" "{\"how_many\":6,\"http_port\":1080 ${common_params}}" > ${results_dir}/nodeoutput 2>&1 &