Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix seemingly eratic SSDP discovery #32

Merged
merged 7 commits into from
Sep 25, 2024
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
80 changes: 68 additions & 12 deletions http/httpparser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ constexpr auto s_ascTimeDateFormat = "ddd MMM d hh:mm:ss yyyy"_L1; // e
constexpr auto s_cacheControlNoCache = "no-cache"_baview;
constexpr auto s_cacheControlMaxAge = "max-age="_baview;

constexpr auto s_protocolPrefixHttp = "HTTP/"_baview;

template <class Container,
typename ValueType = typename Container::value_type,
class Iterator = typename Container::const_iterator>
Expand All @@ -45,6 +47,66 @@ StringView suffixView(const String &text, qsizetype offset)

} // namespace

Message Message::parseStatusLine(const QByteArray &line)
{
auto message = Message{};

if (const auto &status = line.trimmed().split(' ');
Q_LIKELY(status.size() == 3)) {
if (status.first().startsWith(s_protocolPrefixHttp)) {
message.m_type = Type::Response;
message.m_status = status;
} else if (status.last().startsWith(s_protocolPrefixHttp)) {
message.m_type = Type::Request;
message.m_status = status;
}
}

return message;
}

QByteArray Message::statusField(Type expectedType, int index) const
{
if (Q_UNLIKELY(type() != expectedType))
return {};

return m_status[index];
}

QByteArray Message::protocol() const
{
switch (type()) {
case Type::Request:
return m_status.last();
case Type::Response:
return m_status.first();
case Type::Invalid:
break;
}

return {};
}

QByteArray Message::verb() const
{
return statusField(Type::Request, 0);
}

QByteArray Message::resource() const
{
return statusField(Type::Request, 1);
}

std::optional<uint> Message::statusCode() const
{
return qnc::parse<uint>(statusField(Type::Response, 1));
}

QByteArray Message::statusPhrase() const
{
return statusField(Type::Response, 2);
}

Message Message::parse(const QByteArray &data)
{
auto buffer = QBuffer{};
Expand All @@ -61,17 +123,11 @@ Message Message::parse(QIODevice *device)
if (!device->canReadLine())
return {};

const auto &statusLine = device->readLine().trimmed().split(' ');
auto message = Message::parseStatusLine(device->readLine());

if (statusLine.size() != 3)
if (message.isInvalid())
return {};

auto message = Message {
statusLine[0],
statusLine[1],
statusLine[2],
};

while (device->canReadLine()) {
const auto &line = device->readLine();
const auto &trimmedLine = line.trimmed();
Expand All @@ -80,20 +136,20 @@ Message Message::parse(QIODevice *device)
break;

if (line[0] == ' ') {
if (message.headers.isEmpty()) {
if (message.m_headers.isEmpty()) {
qCWarning(lcHttpParser, "Ignoring invalid header line: %s", line.constData());
continue;
}

message.headers.last().second.append(trimmedLine);
message.m_headers.last().second.append(trimmedLine);
} else if (const auto colon = line.indexOf(':'); colon > 0) {
auto name = line.left(colon).trimmed();
auto value = line.mid(colon + 1).trimmed();

#if QT_VERSION_MAJOR >= 6
message.headers.emplaceBack(std::move(name), std::move(value));
message.m_headers.emplaceBack(std::move(name), std::move(value));
#else // QT_VERSION_MAJOR < 6
message.headers.append({std::move(name), std::move(value)});
message.m_headers.append({std::move(name), std::move(value)});
#endif // QT_VERSION_MAJOR < 6
} else {
qCWarning(lcHttpParser, "Ignoring invalid header line: %s", line.constData());
Expand Down
40 changes: 35 additions & 5 deletions http/httpparser.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
#include <QByteArray>
#include <QList>

// STL headers
#include <optional>

class QDateTime;
class QIODevice;

Expand Down Expand Up @@ -102,17 +105,44 @@ static_assert(std::is_same_v<QStringView, CaseInsensitive<QLatin1String>::view_t
static_assert(std::is_same_v<QStringView, CaseInsensitive<QString>::view_type>);
static_assert(std::is_same_v<QStringView, CaseInsensitive<QStringView>::view_type>);

struct Message
class Message
{
Q_GADGET

public:
using HeaderList = QList<std::pair<CaseInsensitive<QByteArray>, QByteArray>>;

QByteArray verb;
QByteArray resource;
QByteArray protocol;
HeaderList headers = {};
enum class Type {
Invalid,
Request,
Response,
};

Q_ENUM(Type)

constexpr Message() noexcept = default;

[[nodiscard]] constexpr Type type() const { return m_type; }
[[nodiscard]] constexpr bool isInvalid() const { return type() == Type::Invalid; }
[[nodiscard]] HeaderList headers() const { return m_headers; }


[[nodiscard]] QByteArray protocol() const;
[[nodiscard]] QByteArray verb() const;
[[nodiscard]] QByteArray resource() const;
[[nodiscard]] std::optional<uint> statusCode() const;
[[nodiscard]] QByteArray statusPhrase() const;

[[nodiscard]] static Message parse(const QByteArray &data);
[[nodiscard]] static Message parse(QIODevice *device);

private:
[[nodiscard]] static Message parseStatusLine(const QByteArray &line);
[[nodiscard]] QByteArray statusField(Type expectedType, int index) const;

Type m_type = Type::Invalid;
QByteArrayList m_status = {};
HeaderList m_headers = {};
};

QDateTime parseDateTime(const QByteArray &text);
Expand Down
3 changes: 2 additions & 1 deletion qnc/qncresolver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,9 @@ MulticastResolver::createSocket(const QNetworkInterface &iface, const QHostAddre

const auto &bindAddress = wildcardAddress(address);
const auto &bindMode = QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint;
constexpr auto randomPort = 0;

if (!socket->bind(bindAddress, port(), bindMode)) {
if (!socket->bind(bindAddress, randomPort, bindMode)) {
qCWarning(lcMulticast, "Could not bind multicast socket to %ls: %ls",
qUtf16Printable(address.toString()),
qUtf16Printable(socket->errorString()));
Expand Down
92 changes: 66 additions & 26 deletions ssdp/ssdpresolver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ 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"
"ST: {service-type}\r\n"
"MAN: \"ssdp:discover\"\r\n"
"MM: {minimum-delay}\r\n"
"HOST: {multicast-group}:{udp-port}\r\n"
"MX: {maximum-delay}\r\n"
"ST: {service-type}\r\n"
"MM: {minimum-delay}\r\n"
"Content-Length: 0\r\n"
"\r\n"_baview;

QList<QUrl> parseAlternativeLocations(compat::ByteArrayView text)
Expand Down Expand Up @@ -108,12 +109,16 @@ quint16 Resolver::port() const

QByteArray Resolver::finalizeQuery(const QHostAddress &address, const QByteArray &query) const
{
const auto &group = multicastGroup(address);
return QByteArray{query}.replace(s_ssdpKeyMulticastGroup, group.toString().toLatin1());
const auto &group = multicastGroup(address).toString();

auto finalizedQuery = QByteArray{query};
finalizedQuery.replace(s_ssdpKeyMulticastGroup, group.toLatin1());
return finalizedQuery;
}

NotifyMessage NotifyMessage::parse(const QByteArray &data, const QDateTime &now)
{
constexpr auto s_ssdpVerbSearch = "M-SEARCH"_baview;
constexpr auto s_ssdpVerbNotify = "NOTIFY"_baview;
constexpr auto s_ssdpResourceAny = "*"_baview;
constexpr auto s_ssdpProtocolHttp11 = "HTTP/1.1"_baview;
Expand All @@ -129,23 +134,36 @@ NotifyMessage NotifyMessage::parse(const QByteArray &data, const QDateTime &now)

const auto &message = http::Message::parse(data);

if (message.verb.isEmpty()) {
qCWarning(lcResolver, "Ignoring message with malformed HTTP header");
if (message.isInvalid()) {
qCWarning(lcResolver, "Ignoring malformed HTTP message");
return {};
}

if (message.protocol != s_ssdpProtocolHttp11) {
qCWarning(lcResolver, "Ignoring unknown protocol: %s", message.protocol.constData());
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());
if (message.type() == http::Message::Type::Request) {
if (message.verb() == s_ssdpVerbSearch)
return {};

if (message.verb() != s_ssdpVerbNotify) {
qCDebug(lcResolver, "Ignoring unsupported verb: %s", message.verb().constData());
return {};
}

if (message.resource() != s_ssdpResourceAny) {
qCDebug(lcResolver, "Ignoring unsupported resource: %s", message.resource().constData());
return {};
}
} else if (message.type() == http::Message::Type::Response) {
if (message.statusCode() != 200) {
qCDebug(lcResolver, "Ignoring unsupported status code: %d", message.statusCode().value());
return {};
}
} else {
qCWarning(lcResolver, "Ignoring unexpected HTTP message");
return {};
}

Expand All @@ -154,7 +172,7 @@ NotifyMessage NotifyMessage::parse(const QByteArray &data, const QDateTime &now)
auto cacheControl = QByteArray{};
auto expires = QByteArray{};

for (const auto &[name, value] : message.headers) {
for (const auto &[name, value] : message.headers()) {
if (name == s_ssdpHeaderUniqueServiceName)
response.serviceName = QUrl::fromPercentEncoding(value);
else if (name == s_ssdpHeaderNotifyType)
Expand All @@ -171,12 +189,16 @@ NotifyMessage NotifyMessage::parse(const QByteArray &data, const QDateTime &now)
response.altLocations += parseAlternativeLocations(value);
}

if (notifyType == s_ssdpNotifySubTypeAlive)
if (message.type() == http::Message::Type::Request) {
if (notifyType == s_ssdpNotifySubTypeAlive)
response.type = NotifyMessage::Type::Alive;
else if (notifyType == s_ssdpNotifySubTypeByeBye)
response.type = NotifyMessage::Type::ByeBye;
else
return {};
} else if (message.type() == http::Message::Type::Response) {
response.type = NotifyMessage::Type::Alive;
else if (notifyType == s_ssdpNotifySubTypeByeBye)
response.type = NotifyMessage::Type::ByeBye;
else
return {};
}

response.expiry = http::expiryDateTime(cacheControl, expires, now);

Expand Down Expand Up @@ -212,6 +234,24 @@ void Resolver::processDatagram(const QNetworkDatagram &datagram)

} // namespace qnc::ssdp

QDebug operator<<(QDebug debug, const qnc::ssdp::NotifyMessage &message)
{
const auto _ = QDebugStateSaver{debug};

if (debug.verbosity() >= QDebug::DefaultVerbosity)
debug.nospace() << message.staticMetaObject.className();


return debug.nospace()
<< "(" << message.type
<< ", serviceName=" << message.serviceName
<< ", serviceType=" << message.serviceType
<< ", locations=" << message.locations
<< ", altLocations=" << message.altLocations
<< ", expiry=" << message.expiry
<< ")";
}

QDebug operator<<(QDebug debug, const qnc::ssdp::ServiceDescription &service)
{
const auto _ = QDebugStateSaver{debug};
Expand All @@ -220,10 +260,10 @@ QDebug operator<<(QDebug debug, const qnc::ssdp::ServiceDescription &service)
debug.nospace() << service.staticMetaObject.className();

return debug.nospace()
<< "(" << service.name()
<< ", type=" << service.type()
<< ", location=" << service.locations()
<< "(" << service.name()
<< ", type=" << service.type()
<< ", location=" << service.locations()
<< ", alt-location=" << service.alternativeLocations()
<< ", expires=" << service.expires()
<< ", expires=" << service.expires()
<< ")";
}
5 changes: 5 additions & 0 deletions ssdp/ssdpresolver.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ struct NotifyMessage
ByeBye,
};

Q_ENUM(Type)

Type type = Type::Invalid;
QString serviceName = {};
QString serviceType = {};
Expand All @@ -66,6 +68,8 @@ struct NotifyMessage

static NotifyMessage parse(const QByteArray &data, const QDateTime &now);
static NotifyMessage parse(const QByteArray &data);

Q_GADGET
};

struct ServiceLookupRequest
Expand Down Expand Up @@ -102,6 +106,7 @@ public slots:

} // namespace qnc::ssdp

QDebug operator<<(QDebug debug, const qnc::ssdp::NotifyMessage &message);
QDebug operator<<(QDebug debug, const qnc::ssdp::ServiceDescription &service);

#endif // QNCSSDP_RESOLVER_H
Loading
Loading