Skip to content

Commit

Permalink
Add initial SSDP resolver
Browse files Browse the repository at this point in the history
  • Loading branch information
hasselmm committed Sep 11, 2024
1 parent 154ba78 commit c897b9f
Show file tree
Hide file tree
Showing 9 changed files with 500 additions and 2 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ add_subdirectory(.github)
add_subdirectory(http)
add_subdirectory(mdns)
add_subdirectory(qnc)
add_subdirectory(ssdp)

if (NOT IOS) # FIXME Figure out code signing on Github
add_subdirectory(tests)
Expand Down
11 changes: 9 additions & 2 deletions qnc/qncresolver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,18 @@ void MulticastResolver::submitQueries(const SocketTable &sockets)
const auto socket = static_cast<QUdpSocket *>(it->get());
const auto group = multicastGroup(address);

for (const auto &data: m_queries)
socket->writeDatagram(data, group, port());
for (const auto &data: m_queries) {
const auto query = finalizeQuery(address, data);
socket->writeDatagram(query, group, port());
}
}
}

QByteArray MulticastResolver::finalizeQuery(const QHostAddress &/*address*/, const QByteArray &query) const
{
return query;
}

void MulticastResolver::onDatagramReceived(QUdpSocket *socket)
{
while (socket->hasPendingDatagrams()) {
Expand Down
2 changes: 2 additions & 0 deletions qnc/qncresolver.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ class MulticastResolver : public GenericResolver

[[nodiscard]] virtual quint16 port() const = 0;
[[nodiscard]] virtual QHostAddress multicastGroup(const QHostAddress &address) const = 0;
[[nodiscard]] virtual QByteArray finalizeQuery(const QHostAddress &address, const QByteArray &query) const;

virtual void processDatagram(const QNetworkDatagram &message) = 0;

[[nodiscard]] static bool isSupportedInterfaceType(const QNetworkInterface &iface);
Expand Down
12 changes: 12 additions & 0 deletions ssdp/CMakeLists.txt
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()
224 changes: 224 additions & 0 deletions ssdp/ssdpresolver.cpp
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()
<< ")";
}
107 changes: 107 additions & 0 deletions ssdp/ssdpresolver.h
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
Loading

0 comments on commit c897b9f

Please sign in to comment.