Skip to content
Merged
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
3 changes: 3 additions & 0 deletions Development/nmos-cpp-node/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@
// proxy_port [registry, node]: forward proxy port
//"proxy_port": 8080,

// discovery_mode [node]: whether the discovered host name (1) or resolved addresses (2) are used to construct request URLs for Registration APIs or System APIs
//"discovery_mode": 1,

// href_mode [registry, node]: whether the host name (1), addresses (2) or both (3) are used to construct response headers, and host and URL fields in the data model
//"href_mode": 1,

Expand Down
161 changes: 134 additions & 27 deletions Development/nmos/mdns.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,24 @@ namespace nmos
const std::string ver_dvc{ "ver_dvc" };
const std::string ver_snd{ "ver_snd" };
const std::string ver_rcv{ "ver_rcv" };

const std::string api_selector{ "api_selector" };
}

// returns true if the specified service protocol is secure
bool is_service_protocol_secure(const service_protocol& api_proto)
{
return service_protocols::https == api_proto || service_protocols::secure_mqtt == api_proto;
}

// returns "http" or "https" depending on settings
service_protocol get_service_protocol(const nmos::settings& settings)
// returns e.g. "http" or "https" depending on settings
service_protocol get_service_protocol(const nmos::service_type& service, const nmos::settings& settings)
{
if (nmos::service_types::mqtt == service) {
return nmos::experimental::fields::client_secure(settings)
? service_protocols::secure_mqtt
: service_protocols::mqtt;
}
return nmos::experimental::fields::client_secure(settings)
? service_protocols::https
: service_protocols::http;
Expand All @@ -62,6 +75,7 @@ namespace nmos
// find and parse the 'api_proto' TXT record (or return the default)
service_protocol parse_api_proto_record(const mdns::structured_txt_records& records)
{
// hmm, default not appropriate for nmos::service_types::mqtt
return mdns::parse_txt_record(records, txt_record_keys::api_proto, details::parse_api_proto_value, service_protocols::http);
}

Expand All @@ -87,11 +101,16 @@ namespace nmos
// find and parse the 'api_ver' TXT record (or return the default)
std::set<api_version> parse_api_ver_record(const mdns::structured_txt_records& records)
{
// hm, logically speaking default only appropriate for IS-04
return mdns::parse_txt_record(records, txt_record_keys::api_ver, details::parse_api_ver_value, is04_versions::unspecified);
}

bool get_service_authorization(const nmos::settings& settings)
bool get_service_authorization(const nmos::service_type& service, const nmos::settings& settings)
{
// IS-09 System API does not use authorization
// See https://github.com/AMWA-TV/is-09/issues/21
if (nmos::service_types::system == service) return false;

const auto client_authorization = false;
return client_authorization;
}
Expand Down Expand Up @@ -161,15 +180,19 @@ namespace nmos
{
if (service == nmos::service_types::node)
{
// see https://specs.amwa.tv/is-04/releases/v1.3.1/docs/3.2._Discovery_-_Peer_to_Peer_Operation.html#dns-sd-txt-records
// IS-04 Node API is not distributed so does not use 'pri' TXT record
return
{
{ txt_record_keys::api_proto, details::make_api_proto_value(api_proto) },
{ txt_record_keys::api_ver, details::make_api_ver_value(api_ver) },
{ txt_record_keys::api_auth, details::make_api_auth_value(api_auth) }
};
}
else
else if (service == nmos::service_types::query || service == nmos::service_types::registration)
{
// see https://specs.amwa.tv/is-04/releases/v1.3.1/docs/3.1._Discovery_-_Registered_Operation.html#dns-sd-txt-records-1
// and https://specs.amwa.tv/is-04/releases/v1.3.1/docs/3.1._Discovery_-_Registered_Operation.html#dns-sd-txt-records
return
{
{ txt_record_keys::api_proto, details::make_api_proto_value(api_proto) },
Expand All @@ -178,6 +201,39 @@ namespace nmos
{ txt_record_keys::pri, details::make_pri_value(pri) }
};
}
else if (service == nmos::service_types::system)
{
// see https://specs.amwa.tv/is-09/releases/v1.0.0/docs/3.1._Discovery_-_Operation.html#dns-sd-txt-records
// IS-09 System API does not use authorization
return
{
{ txt_record_keys::api_proto, details::make_api_proto_value(api_proto) },
{ txt_record_keys::api_ver, details::make_api_ver_value(api_ver) },
{ txt_record_keys::pri, details::make_pri_value(pri) }
};
}
else if (service == nmos::service_types::authorization)
{
// see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/3.0._Discovery.html#dns-sd-txt-records
// hm, IS-10 Authorization may also need an 'api_selector' TXT record
return
{
{ txt_record_keys::api_proto, details::make_api_proto_value(api_proto) },
{ txt_record_keys::api_ver, details::make_api_ver_value(api_ver) },
{ txt_record_keys::pri, details::make_pri_value(pri) }
};
}
else if (service == nmos::service_types::mqtt)
{
// see https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.1._Transport_-_MQTT.html#7-broker-discovery
// the 'pri' TXT record could make sense for the MQTT Broker but that wasn't adopted for IS-07 v1.0
return
{
{ txt_record_keys::api_proto, details::make_api_proto_value(api_proto) },
{ txt_record_keys::api_auth, details::make_api_auth_value(api_auth) }
};
}
return {};
}

// find and parse the Node 'ver_' TXT records
Expand Down Expand Up @@ -291,7 +347,7 @@ namespace nmos
if (0 > instance_port_or_disabled) return;
const auto instance_port = (uint16_t)instance_port_or_disabled;
const auto api_ver = details::service_versions(service, settings);
const auto records = nmos::make_txt_records(service, nmos::fields::pri(settings), api_ver, nmos::get_service_protocol(settings), nmos::get_service_authorization(settings));
const auto records = nmos::make_txt_records(service, nmos::fields::pri(settings), api_ver, nmos::get_service_protocol(service, settings), nmos::get_service_authorization(service, settings));
const auto txt_records = mdns::make_txt_records(records);

// advertise "_nmos-register._tcp" for v1.3 (and as an experimental extension, for lower versions)
Expand Down Expand Up @@ -369,7 +425,7 @@ namespace nmos
{
const auto instance_name = service_name(service, settings);
const auto api_ver = details::service_versions(service, settings);
auto records = nmos::make_txt_records(service, nmos::fields::pri(settings), api_ver, nmos::get_service_protocol(settings), nmos::get_service_authorization(settings));
auto records = nmos::make_txt_records(service, nmos::fields::pri(settings), api_ver, nmos::get_service_protocol(service, settings), nmos::get_service_authorization(service, settings));
records.insert(records.end(), std::make_move_iterator(add_records.begin()), std::make_move_iterator(add_records.end()));
const auto txt_records = mdns::make_txt_records(records);

Expand Down Expand Up @@ -406,13 +462,46 @@ namespace nmos
update_service(advertiser, service, domain, settings, std::move(add_records));
}

enum discovery_mode
{
discovery_mode_default = 0,
discovery_mode_name = 1,
discovery_mode_addresses = 2
};

namespace details
{
typedef std::pair<api_version, service_priority> api_ver_pri;
typedef std::pair<api_ver_pri, web::uri> resolved_service;
typedef std::vector<resolved_service> resolved_services;

pplx::task<bool> resolve_service(std::shared_ptr<resolved_services> results, mdns::service_discovery& discovery, const nmos::service_type& service, const std::string& browse_domain, const std::set<nmos::api_version>& api_ver, const std::pair<nmos::service_priority, nmos::service_priority>& priorities, const std::set<nmos::service_protocol>& api_proto, const std::set<bool>& api_auth, const std::chrono::steady_clock::time_point& timeout, const pplx::cancellation_token& token)
std::vector<utility::string_t> get_resolved_hosts(const mdns::resolve_result& resolved, const nmos::service_protocol& resolved_proto, discovery_mode mode)
{
std::vector<utility::string_t> results;

// by default, use the host name if secure communications are in use
if (mode == discovery_mode_name || (mode == discovery_mode_default && is_service_protocol_secure(resolved_proto)))
{
auto host_name = utility::s2us(resolved.host_name);
// remove a trailing '.' to turn an FQDN into a DNS name, for SSL certificate matching
// hmm, this might be more appropriately done by tweaking the Host header in the client request?
if (!host_name.empty() && U('.') == host_name.back()) host_name.pop_back();

results.push_back(host_name);
}

if (mode == discovery_mode_addresses || (mode == discovery_mode_default && !is_service_protocol_secure(resolved_proto)))
{
for (const auto& ip_address : resolved.ip_addresses)
{
results.push_back(utility::s2us(ip_address));
}
}

return results;
}

pplx::task<bool> resolve_service(std::shared_ptr<resolved_services> results, mdns::service_discovery& discovery, discovery_mode discovery_mode, const nmos::service_type& service, const std::string& browse_domain, const std::set<nmos::api_version>& api_ver, const std::pair<nmos::service_priority, nmos::service_priority>& priorities, const std::set<nmos::service_protocol>& api_proto, const std::set<bool>& api_auth, const std::chrono::steady_clock::time_point& timeout, const pplx::cancellation_token& token)
{
return discovery.browse([=, &discovery](const mdns::browse_result& resolving)
{
Expand Down Expand Up @@ -454,22 +543,12 @@ namespace nmos
.set_port(resolved.port)
.set_path(U("/x-nmos/") + utility::s2us(details::service_api(service)));

if (nmos::service_protocols::https == resolved_proto)
{
auto host_name = utility::s2us(resolved.host_name);
// remove a trailing '.' to turn an FQDN into a DNS name, for SSL certificate matching
// hmm, this might be more appropriately done by tweaking the Host header in the client request?
if (!host_name.empty() && U('.') == host_name.back()) host_name.pop_back();
auto resolved_hosts = get_resolved_hosts(resolved, resolved_proto, discovery_mode);

results->push_back({ { *resolved_ver, resolved_pri }, resolved_uri
.set_host(host_name)
.to_uri()
});
}
else for (const auto& ip_address : resolved.ip_addresses)
for (const auto& host : resolved_hosts)
{
results->push_back({ { *resolved_ver, resolved_pri }, resolved_uri
.set_host(utility::s2us(ip_address))
.set_host(host)
.to_uri()
});
}
Expand All @@ -486,11 +565,14 @@ namespace nmos
// the higher version is preferred; for the same version, the 'higher' priority is preferred
return lhs.first > rhs.first || (lhs.first == rhs.first && lhs.second < rhs.second);
}

std::pair<nmos::service_priority, nmos::service_priority> service_priorities(const nmos::service_type& service, const nmos::settings& settings)
{
return { nmos::fields::highest_pri(settings), nmos::fields::lowest_pri(settings) };
}
}

// helper function for resolving instances of the specified service (API)
// with the highest version, highest priority instances at the front, and (by default) services with the same priority ordered randomly
pplx::task<std::list<web::uri>> resolve_service(mdns::service_discovery& discovery, const nmos::service_type& service, const std::string& browse_domain, const std::set<nmos::api_version>& api_ver, const std::pair<nmos::service_priority, nmos::service_priority>& priorities, const std::set<nmos::service_protocol>& api_proto, const std::set<bool>& api_auth, bool randomize, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token)
pplx::task<std::list<web::uri>> resolve_service(mdns::service_discovery& discovery, discovery_mode mode, const nmos::service_type& service, const std::string& browse_domain, const std::set<nmos::api_version>& api_ver, const std::pair<nmos::service_priority, nmos::service_priority>& priorities, const std::set<nmos::service_protocol>& api_proto, const std::set<bool>& api_auth, bool randomize, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token)
{
const auto absolute_timeout = std::chrono::steady_clock::now() + timeout;

Expand Down Expand Up @@ -518,8 +600,8 @@ namespace nmos
};

const std::vector<pplx::task<bool>> both_tasks{
details::resolve_service(both_results[0], discovery, nmos::service_types::register_, browse_domain, api_ver, priorities, api_proto, api_auth, absolute_timeout, linked_token),
details::resolve_service(both_results[1], discovery, service, browse_domain, api_ver, priorities, api_proto, api_auth, absolute_timeout, linked_token)
details::resolve_service(both_results[0], discovery, mode, nmos::service_types::register_, browse_domain, api_ver, priorities, api_proto, api_auth, absolute_timeout, linked_token),
details::resolve_service(both_results[1], discovery, mode, service, browse_domain, api_ver, priorities, api_proto, api_auth, absolute_timeout, linked_token)
};

// when either task is completed, cancel and wait for the other to be completed
Expand Down Expand Up @@ -547,12 +629,12 @@ namespace nmos
}
else
{
resolve_task = details::resolve_service(results, discovery, nmos::service_types::register_, browse_domain, api_ver, priorities, api_proto, api_auth, absolute_timeout, token);
resolve_task = details::resolve_service(results, discovery, mode, nmos::service_types::register_, browse_domain, api_ver, priorities, api_proto, api_auth, absolute_timeout, token);
}
}
else
{
resolve_task = details::resolve_service(results, discovery, service, browse_domain, api_ver, priorities, api_proto, api_auth, absolute_timeout, token);
resolve_task = details::resolve_service(results, discovery, mode, service, browse_domain, api_ver, priorities, api_proto, api_auth, absolute_timeout, token);
}

return resolve_task.then([results, randomize](bool)
Expand Down Expand Up @@ -591,5 +673,30 @@ namespace nmos
}));
});
}

// helper function for resolving instances of the specified service (API)
// with the highest version, highest priority instances at the front, and (by default) services with the same priority ordered randomly
pplx::task<std::list<web::uri>> resolve_service(mdns::service_discovery& discovery, const nmos::service_type& service, const std::string& browse_domain, const std::set<nmos::api_version>& api_ver, const std::pair<nmos::service_priority, nmos::service_priority>& priorities, const std::set<nmos::service_protocol>& api_proto, const std::set<bool>& api_auth, bool randomize, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token)
{
return resolve_service(discovery, discovery_mode_default, service, browse_domain, api_ver, priorities, api_proto, api_auth, randomize, timeout, token);
}

// helper function for resolving instances of the specified service (API) based on the specified settings
// with the highest version, highest priority instances at the front, and services with the same priority ordered randomly
pplx::task<std::list<web::uri>> resolve_service(mdns::service_discovery& discovery, const nmos::service_type& service, const nmos::settings& settings, const pplx::cancellation_token& token)
{
const auto mode = discovery_mode(nmos::experimental::fields::discovery_mode(settings));
const auto browse_domain = utility::us2s(nmos::get_domain(settings));
const auto versions = details::service_versions(service, settings);
const auto priorities = details::service_priorities(service, settings);
const auto protocols = std::set<nmos::service_protocol>{ nmos::get_service_protocol(service, settings) };
const auto authorization = std::set<bool>{ nmos::get_service_authorization(service, settings) };

// use a short timeout that's long enough to ensure the daemon's cache is exhausted
// when no cancellation token is specified
const auto timeout = token.is_cancelable() ? nmos::fields::discovery_backoff_max(settings) : 1;

return resolve_service(discovery, mode, service, browse_domain, versions, priorities, protocols, authorization, true, std::chrono::seconds(timeout), token);
}
}
}
Loading