-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
500 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
qnc_add_library( | ||
QncSsdp STATIC | ||
ssdpresolver.cpp | ||
ssdpresolver.h | ||
) | ||
|
||
target_link_libraries(QncSsdp PUBLIC QncHttp) | ||
|
||
if (NOT IOS) # FIXME Figure out code signing on Github | ||
qnc_add_executable(SSDPResolverDemo TYPE tool ssdpresolverdemo.cpp) | ||
target_link_libraries(SSDPResolverDemo PRIVATE QncSsdp) | ||
endif() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
/* QtNetworkCrumbs - Some networking toys for Qt | ||
* Copyright (C) 2019-2024 Mathias Hasselmann | ||
*/ | ||
#include "ssdpresolver.h" | ||
|
||
// QtNetworkCrumbs headers | ||
#include "httpparser.h" | ||
#include "qncliterals.h" | ||
|
||
// Qt headers | ||
#include <QHostAddress> | ||
#include <QLoggingCategory> | ||
#include <QNetworkDatagram> | ||
#include <QUrl> | ||
|
||
namespace qnc::ssdp { | ||
|
||
namespace { | ||
|
||
Q_LOGGING_CATEGORY(lcResolver, "qnc.ssdp.resolver") | ||
|
||
constexpr auto s_ssdpUnicastIPv4 = "239.255.255.250"_L1; | ||
constexpr auto s_ssdpUnicastIPv6 = "ff02::c"_L1; | ||
constexpr auto s_ssdpPort = quint16{1900}; | ||
|
||
constexpr auto s_ssdpKeyMulticastGroup = "{multicast-group}"_baview; | ||
constexpr auto s_ssdpKeyUdpPort = "{udp-port}"_baview; | ||
constexpr auto s_ssdpKeyMinimumDelay = "{minimum-delay}"_baview; | ||
constexpr auto s_ssdpKeyMaximumDelay = "{maximum-delay}"_baview; | ||
constexpr auto s_ssdpKeyServiceType = "{service-type}"_baview; | ||
|
||
constexpr auto s_ssdpQueryTemplate = "M-SEARCH * HTTP/1.1\r\n" | ||
"HOST: {multicast-group}:{udp-port}\r\n" | ||
"MAN: \"ssdp:discover\"\r\n" | ||
"MM: {minimum-delay}\r\n" | ||
"MX: {maximum-delay}\r\n" | ||
"ST: {service-type}\r\n" | ||
"\r\n"_baview; | ||
|
||
QList<QUrl> parseAlternativeLocations(QByteArrayView text) | ||
{ | ||
auto locations = QList<QUrl>{}; | ||
|
||
for (auto offset = qsizetype{0};;) { | ||
const auto start = text.indexOf('<', offset); | ||
|
||
if (start < 0) | ||
break; | ||
|
||
const auto end = text.indexOf('>', start + 1); | ||
|
||
if (end < 0) | ||
break; | ||
|
||
locations += QUrl::fromEncoded({text.data() + start + 1, end - start - 1}); | ||
offset = end + 1; | ||
} | ||
|
||
return locations; | ||
} | ||
|
||
} // namespace | ||
|
||
bool Resolver::lookupService(const ServiceLookupRequest &request) | ||
{ | ||
auto query = | ||
s_ssdpQueryTemplate.toByteArray(). | ||
replace(s_ssdpKeyUdpPort, QByteArray::number(s_ssdpPort)). | ||
replace(s_ssdpKeyMinimumDelay, QByteArray::number(request.minimumDelay.count())). | ||
replace(s_ssdpKeyMaximumDelay, QByteArray::number(request.maximumDelay.count())). | ||
replace(s_ssdpKeyServiceType, request.serviceType.toUtf8()); | ||
|
||
return addQuery(std::move(query)); | ||
} | ||
|
||
bool Resolver::lookupService(const QString &serviceType) | ||
{ | ||
return lookupService({.serviceType = serviceType}); | ||
} | ||
|
||
QHostAddress Resolver::multicastGroup(const QHostAddress &address) const | ||
{ | ||
switch (address.protocol()) { | ||
case QHostAddress::IPv4Protocol: | ||
return QHostAddress{s_ssdpUnicastIPv4}; | ||
|
||
case QHostAddress::IPv6Protocol: | ||
return QHostAddress{s_ssdpUnicastIPv6}; | ||
|
||
case QHostAddress::AnyIPProtocol: | ||
case QHostAddress::UnknownNetworkLayerProtocol: | ||
break; | ||
} | ||
|
||
Q_UNREACHABLE(); | ||
return {}; | ||
} | ||
|
||
quint16 Resolver::port() const | ||
{ | ||
return s_ssdpPort; | ||
} | ||
|
||
QByteArray Resolver::finalizeQuery(const QHostAddress &address, const QByteArray &query) const | ||
{ | ||
const auto &group = multicastGroup(address); | ||
return QByteArray{query}.replace(s_ssdpKeyMulticastGroup, group.toString().toLatin1()); | ||
} | ||
|
||
NotifyMessage NotifyMessage::parse(const QByteArray &data, const QDateTime &now) | ||
{ | ||
constexpr auto s_ssdpVerbNotify = "NOTIFY"_baview; | ||
constexpr auto s_ssdpResourceAny = "*"_baview; | ||
constexpr auto s_ssdpProtocolHttp11 = "HTTP/1.1"_baview; | ||
constexpr auto s_ssdpHeaderCacheControl = "Cache-Control"_baview; | ||
constexpr auto s_ssdpHeaderExpires = "Expires"_baview; | ||
constexpr auto s_ssdpHeaderLocation = "Location"_baview; | ||
constexpr auto s_ssdpHeaderAlternativeLocation = "AL"_baview; | ||
constexpr auto s_ssdpHeaderNotifySubType = "NTS"_baview; | ||
constexpr auto s_ssdpHeaderNotifyType = "NT"_baview; | ||
constexpr auto s_ssdpHeaderUniqueServiceName = "USN"_baview; | ||
constexpr auto s_ssdpNotifySubTypeAlive = "ssdp:alive"_baview; | ||
constexpr auto s_ssdpNotifySubTypeByeBye = "ssdp:byebye"_baview; | ||
|
||
const auto &message = http::Message::parse(data); | ||
|
||
if (message.verb.isEmpty()) { | ||
qCWarning(lcResolver, "Ignoring message with malformed HTTP header"); | ||
return {}; | ||
} | ||
|
||
if (message.protocol != s_ssdpProtocolHttp11) { | ||
qCWarning(lcResolver, "Ignoring unknown protocol: %s", message.protocol.constData()); | ||
return {}; | ||
} | ||
|
||
if (message.verb != s_ssdpVerbNotify) { | ||
qCDebug(lcResolver, "Ignoring %s message", message.verb.constData()); | ||
return {}; | ||
} | ||
|
||
if (message.resource != s_ssdpResourceAny) { | ||
qCDebug(lcResolver, "Ignoring unknown resource: %s", message.resource.constData()); | ||
return {}; | ||
} | ||
|
||
auto response = NotifyMessage{}; | ||
auto notifyType = QByteArray{}; | ||
auto cacheControl = QByteArray{}; | ||
auto expires = QByteArray{}; | ||
|
||
for (const auto &[name, value] : message.headers) { | ||
if (name == s_ssdpHeaderUniqueServiceName) | ||
response.serviceName = QUrl::fromPercentEncoding(value); | ||
else if (name == s_ssdpHeaderNotifyType) | ||
response.serviceType = QUrl::fromPercentEncoding(value); | ||
else if (name == s_ssdpHeaderNotifySubType) | ||
notifyType = value; | ||
else if (name == s_ssdpHeaderCacheControl) | ||
cacheControl = value; | ||
else if (name == s_ssdpHeaderExpires) | ||
expires = value; | ||
else if (name == s_ssdpHeaderLocation) | ||
response.locations += QUrl::fromEncoded(value); | ||
else if (name == s_ssdpHeaderAlternativeLocation) | ||
response.altLocations += parseAlternativeLocations(value); | ||
} | ||
|
||
if (notifyType == s_ssdpNotifySubTypeAlive) | ||
response.type = NotifyMessage::Type::Alive; | ||
else if (notifyType == s_ssdpNotifySubTypeByeBye) | ||
response.type = NotifyMessage::Type::ByeBye; | ||
else | ||
return {}; | ||
|
||
response.expiry = http::expiryDateTime(cacheControl, expires, now); | ||
|
||
return response; | ||
} | ||
|
||
NotifyMessage NotifyMessage::parse(const QByteArray &data) | ||
{ | ||
return parse(data, QDateTime::currentDateTimeUtc()); | ||
} | ||
|
||
void Resolver::processDatagram(const QNetworkDatagram &datagram) | ||
{ | ||
const auto response = NotifyMessage::parse(datagram.data()); | ||
|
||
switch (response.type) { | ||
case NotifyMessage::Type::Alive: | ||
emit serviceFound({response.serviceName, response.serviceType, | ||
response.locations, response.altLocations, | ||
response.expiry}); | ||
break; | ||
|
||
case NotifyMessage::Type::ByeBye: | ||
emit serviceLost(response.serviceName); | ||
break; | ||
|
||
case NotifyMessage::Type::Invalid: | ||
break; | ||
} | ||
} | ||
|
||
// namespace | ||
|
||
} // namespace qnc::ssdp | ||
|
||
QDebug operator<<(QDebug debug, const qnc::ssdp::ServiceDescription &service) | ||
{ | ||
const auto _ = QDebugStateSaver{debug}; | ||
|
||
if (debug.verbosity() >= QDebug::DefaultVerbosity) | ||
debug.nospace() << service.staticMetaObject.className(); | ||
|
||
return debug.nospace() | ||
<< "(" << service.name() | ||
<< ", type=" << service.type() | ||
<< ", location=" << service.locations() | ||
<< ", alt-location=" << service.alternativeLocations() | ||
<< ", expires=" << service.expires() | ||
<< ")"; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
/* QtNetworkCrumbs - Some networking toys for Qt | ||
* Copyright (C) 2019-2024 Mathias Hasselmann | ||
*/ | ||
#ifndef QNCSSDP_RESOLVER_H | ||
#define QNCSSDP_RESOLVER_H | ||
|
||
#include "qncresolver.h" | ||
|
||
#include <QDateTime> | ||
#include <QPointer> | ||
#include <QUrl> | ||
|
||
namespace qnc::ssdp { | ||
|
||
class ServiceDescription | ||
{ | ||
Q_GADGET | ||
Q_PROPERTY(QString name READ name CONSTANT FINAL) | ||
Q_PROPERTY(QString type READ type CONSTANT FINAL) | ||
Q_PROPERTY(QList<QUrl> locations READ locations CONSTANT FINAL) | ||
Q_PROPERTY(QList<QUrl> alternativeLocations READ alternativeLocations CONSTANT FINAL) | ||
Q_PROPERTY(QDateTime expires READ expires CONSTANT FINAL) | ||
|
||
public: | ||
ServiceDescription(const QString &name, | ||
const QString &type, | ||
const QList<QUrl> &locations, | ||
const QList<QUrl> &alternativeLocations, | ||
const QDateTime &expires) | ||
: m_name {name} | ||
, m_type {type} | ||
, m_locations {locations} | ||
, m_alternativeLocations{alternativeLocations} | ||
, m_expires {expires} | ||
{} | ||
|
||
|
||
QString name() const { return m_name; } | ||
QString type() const { return m_type; } | ||
QList<QUrl> locations() const { return m_locations; } | ||
QList<QUrl> alternativeLocations() const { return m_alternativeLocations; } | ||
QDateTime expires() const { return m_expires; } | ||
|
||
private: | ||
QString m_name; | ||
QString m_type; | ||
QList<QUrl> m_locations; | ||
QList<QUrl> m_alternativeLocations; | ||
QDateTime m_expires; | ||
}; | ||
|
||
struct NotifyMessage | ||
{ | ||
enum class Type { | ||
Invalid, | ||
Alive, | ||
ByeBye, | ||
}; | ||
|
||
Type type = Type::Invalid; | ||
QString serviceName = {}; | ||
QString serviceType = {}; | ||
QList<QUrl> locations = {}; | ||
QList<QUrl> altLocations = {}; | ||
QDateTime expiry = {}; | ||
|
||
static NotifyMessage parse(const QByteArray &data, const QDateTime &now); | ||
static NotifyMessage parse(const QByteArray &data); | ||
}; | ||
|
||
struct ServiceLookupRequest | ||
{ | ||
using seconds = std::chrono::seconds; | ||
|
||
QString serviceType; | ||
seconds minimumDelay = seconds{0}; | ||
seconds maximumDelay = seconds{5}; | ||
}; | ||
|
||
class Resolver : public core::MulticastResolver | ||
{ | ||
Q_OBJECT | ||
|
||
public: | ||
using core::MulticastResolver::MulticastResolver; | ||
|
||
public slots: | ||
bool lookupService(const ServiceLookupRequest &request); | ||
bool lookupService(const QString &serviceType); | ||
|
||
signals: | ||
void serviceFound(const qnc::ssdp::ServiceDescription &service); | ||
void serviceLost(const QString &uniqueServiceName); | ||
|
||
protected: | ||
[[nodiscard]] quint16 port() const override; | ||
[[nodiscard]] QHostAddress multicastGroup(const QHostAddress &address) const override; | ||
[[nodiscard]] QByteArray finalizeQuery(const QHostAddress &address, const QByteArray &query) const override; | ||
|
||
void processDatagram(const QNetworkDatagram &message) override; | ||
}; | ||
|
||
} // namespace qnc::ssdp | ||
|
||
QDebug operator<<(QDebug debug, const qnc::ssdp::ServiceDescription &service); | ||
|
||
#endif // QNCSSDP_RESOLVER_H |
Oops, something went wrong.