Skip to content

Commit

Permalink
Merge pull request #20 from hasselmm/feature/ssdp-resolver
Browse files Browse the repository at this point in the history
Add a minimal SSDP resolver
  • Loading branch information
hasselmm authored Sep 11, 2024
2 parents ac28f00 + 0787c82 commit f82cfaf
Show file tree
Hide file tree
Showing 19 changed files with 1,447 additions and 245 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
61 changes: 54 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,58 @@
# QtNetworkCrumbs

## What's this?
## What is this?

This are some tiny networking toys written in C++17 for [Qt](https://qt.io).
This are some tiny networking toys written in C++17 for [Qt][qt-opensource].

- a [tiny mDNS resolver](mdns/mdnsresolver.h) ([usage demo](mdns/mdnsresolverdemo.cpp))
- a [compressing HTTP/1.1 server](http/compressingserver.cpp)
### A minimal mDNS-SD resolver

## What's needed?
[DNS Service Discovery][DNS-SD], also known as "DNS-SD" and "Bonjour", is an efficient method to discover
network services based. Using [Multicast DNS][mDNS] it's a "Zeroconf" technique for local networks. Via
traditional [Unicast DNS][DNS] it also works over the Internet.

With this library discovering DNS-SD services is as simple as this:

```C++
const auto resolver = new mdns::Resolver;
connect(resolver, &mdns::Resolver::serviceResolved, this, [](const auto &service) {
qInfo() << "mDNS service found:" << service;
});
resolver->lookupServices({"_http._tcp"_L1, "_googlecast._tcp"_L1});
```
The C++ definition of the mDNS resolver can be found in [mdnsresolver.h](mdns/mdnsresolver.h).
A slightly more complex example can be found in [mdnsresolverdemo.cpp](mdns/mdnsresolverdemo.cpp).
### A minimal SSDP resolver
The [Simple Service Discovery Protocol (SSDP)][SSDP] is another "Zeroconf" technique based on multicast messages.
It's also the basis [Univeral Plug and Play (UPnP)][UPnP].
With this library discovering SSDP services is as simple as this:
```C++
const auto resolver = new ssdp::Resolver;
connect(resolver, &ssdp::Resolver::serviceFound, this, [](const auto &service) {
qInfo() << "SSDP service found:" << service;
});
resolver->lookupService("ssdp:all"_L1);
```

The C++ definition of the SSDP resolver can be found in [ssdpresolver.h](ssdp/ssdpresolver.h).
A slightly more complex example can be found in [ssdpresolverdemo.cpp](ssdp/ssdpresolverdemo.cpp).

### A compressing HTTP server

This library also contains a very, very minimal [compressing HTTP/1.1 server](http/compressingserver.cpp).
More a demonstration of the concept, than a real implementation.

## What is needed?

- [CMake 3.10](https://cmake.org/)
- [Qt 6.5](https://qt.io), or Qt 5.15
- [Qt 6.5][qt-opensource], or Qt 5.15
- C++17

## What's supported?
## What is supported?

This is build and tested on all the major platforms supported by Qt:
Android, iOS, Linux, macOS, Windows.
Expand All @@ -28,3 +67,11 @@ Licensed under MIT License
<!-- some more complex links -->
[build-status.svg]: https://github.com/hasselmm/QtNetworkCrumbs/actions/workflows/integration.yaml/badge.svg
[build-status]: https://github.com/hasselmm/QtNetworkCrumbs/actions/workflows/integration.yaml

[qt-opensource]: https://www.qt.io/download-open-source

[DNS-SD]: https://en.wikipedia.org/wiki/Zero-configuration_networking#DNS-SD
[DNS]: https://en.wikipedia.org/wiki/Domain_Name_System#DNS_message_format
[mDNS]: https://en.wikipedia.org/wiki/Multicast_DNS
[SSDP]: https://en.wikipedia.org/wiki/Simple_Service_Discovery_Protocol
[UPnP]: https://en.wikipedia.org/wiki/Universal_Plug_and_Play
8 changes: 8 additions & 0 deletions http/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
qnc_add_library(
QncHttp STATIC
httpparser.cpp
httpparser.h
)

target_link_libraries(QncHttp PUBLIC QncCore)

if (NOT IOS) # FIXME Figure out code signing on Github
qnc_add_executable(HttpCompressingServer TYPE tool compressingserver.cpp)
target_link_libraries(HttpCompressingServer PUBLIC Qt::Network QtNetworkCrumbs::Zlib)
Expand Down
158 changes: 158 additions & 0 deletions http/httpparser.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/* QtNetworkCrumbs - Some networking toys for Qt
* Copyright (C) 2019-2024 Mathias Hasselmann
*/
#include "httpparser.h"

// QtNetworkCrumbs headers
#include "qncliterals.h"
#include "qncparse.h"

// Qt headers
#include <QBuffer>
#include <QDateTime>
#include <QLoggingCategory>

namespace qnc::http {

namespace {

Q_LOGGING_CATEGORY(lcHttpParser, "qnc.http.parser")

// examples from https://www.rfc-editor.org/rfc/rfc9110#section-5.6.7
constexpr auto s_rfc1123DateFormat = "ddd, dd MMM yyyy hh:mm:ss 'GMT'"_L1; // e.g. "Sun, 06 Nov 1994 08:49:37 GMT"
constexpr auto s_rfc850DateFormat = "dddd, dd-MMM-yy hh:mm:ss 'GMT'"_L1; // e.g. "Sunday, 06-Nov-94 08:49:37 GMT"
constexpr auto s_ascTimeDateFormat = "ddd MMM d hh:mm:ss yyyy"_L1; // e.g. "Sun Nov 6 08:49:37 1994"

constexpr auto s_cacheControlNoCache = "no-cache"_baview;
constexpr auto s_cacheControlMaxAge = "max-age="_baview;

template <class Container,
typename ValueType = typename Container::value_type,
class Iterator = typename Container::const_iterator>
Iterator findPrefix(const Container &container, const ValueType &prefix)
{
return std::find_if(container.cbegin(), container.cend(),
[prefix](const ValueType &token) {
return token.startsWith(prefix);
});
}

template <class String, class StringView = typename detail::view_trait<String>::type>
StringView suffixView(const String &text, qsizetype offset)
{
return {text.cbegin() + offset, static_cast<compat::lentype>(text.length() - offset)};
}

} // namespace

Message Message::parse(const QByteArray &data)
{
auto buffer = QBuffer{};
buffer.setData(data);

if (!buffer.open(QBuffer::ReadOnly))
return {};

return parse(&buffer);
}

Message Message::parse(QIODevice *device)
{
if (!device->canReadLine())
return {};

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

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

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

while (device->canReadLine()) {
const auto &line = device->readLine();
const auto &trimmedLine = line.trimmed();

if (trimmedLine.isEmpty())
break;

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

message.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));
#else // QT_VERSION_MAJOR < 6
message.headers.append({std::move(name), std::move(value)});
#endif // QT_VERSION_MAJOR < 6
} else {
qCWarning(lcHttpParser, "Ignoring invalid header line: %s", line.constData());
continue;
}
}

return message;
}

QDateTime parseDateTime(const QByteArray &text)
{
return parseDateTime(QString::fromUtf8(text));
}

QDateTime parseDateTime(const QString &text)
{
const auto locale = QLocale::c(); // for language neutral (that is English) weekdays

if (auto dt = locale.toDateTime(text, s_rfc1123DateFormat); dt.isValid()) {
dt.setTimeSpec(Qt::UTC);
return dt;
} else if (auto dt = locale.toDateTime(text, s_rfc850DateFormat); dt.isValid()) {
dt.setTimeSpec(Qt::UTC);
return dt;
} else if (auto dt = locale.toDateTime(QString{text}.replace(" "_L1, " "_L1),
s_ascTimeDateFormat); dt.isValid()) {
dt.setTimeSpec(Qt::UTC);
return dt;
}

return {};
}

QDateTime expiryDateTime(const QByteArray &cacheControl, const QByteArray &expires, const QDateTime &now)
{
auto cacheControlWithoutSpaces = QByteArray{cacheControl}.replace(' ', compat::ByteArrayView{});
const auto &cacheControlList = cacheControlWithoutSpaces.split(',');

if (cacheControlList.contains(CaseInsensitive{s_cacheControlNoCache}))
return now;

if (const auto maxAge = findPrefix(cacheControlList, CaseInsensitive{s_cacheControlMaxAge});
maxAge != cacheControlList.cend()) {
const auto &value = suffixView(*maxAge, s_cacheControlMaxAge.length());

if (const auto seconds = qnc::parse<uint>(value))
return now.addSecs(*seconds);
}

if (!expires.isEmpty())
return parseDateTime(expires);

return {};
}

QDateTime expiryDateTime(const QByteArray &cacheControl, const QByteArray &expires)
{
return expiryDateTime(cacheControl, expires, QDateTime::currentDateTimeUtc());
}

} // namespace qnc::http
126 changes: 126 additions & 0 deletions http/httpparser.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/* QtNetworkCrumbs - Some networking toys for Qt
* Copyright (C) 2019-2024 Mathias Hasselmann
*/
#ifndef QNC_HTTPPARSER_H
#define QNC_HTTPPARSER_H

// QtNetworkCrumbs headers
#include "qncliterals.h"

// Qt headers
#include <QByteArray>
#include <QList>

class QDateTime;
class QIODevice;

namespace qnc::http {

namespace detail {

template <class T> struct view_trait;
template <> struct view_trait<QByteArray> { using type = compat::ByteArrayView; };
template <> struct view_trait<compat::ByteArrayView> { using type = compat::ByteArrayView; };
template <> struct view_trait<QLatin1String> { using type = QStringView; };
template <> struct view_trait<QString> { using type = QStringView; };
template <> struct view_trait<QStringView> { using type = QStringView; };

template <class T, class U = T, typename = void>
struct hasStartsWith : std::false_type {};
template <class T, class U>
struct hasStartsWith<T, U, std::void_t<decltype(T{}.startsWith(U{}, Qt::CaseInsensitive))>> : std::true_type {};

template <class T, class U = T, typename = void>
struct hasEndsWith : std::false_type {};
template <class T, class U>
struct hasEndsWith<T, U, std::void_t<decltype(T{}.endsWith(U{}, Qt::CaseInsensitive))>> : std::true_type {};

static_assert(hasStartsWith<QByteArray>() == false);
static_assert(hasEndsWith<QByteArray>() == false);

static_assert(hasStartsWith<QString>() == true);
static_assert(hasEndsWith<QString>() == true);

} // namespace detail

template <class T>
class CaseInsensitive : public T
{
public:
using T::T;

using view_type = typename detail::view_trait<T>::type;

CaseInsensitive(const T &init) : T{init} {}
CaseInsensitive(T &&init) : T{std::move(init)} {}

template <class U>
friend bool operator==(const CaseInsensitive &l, const U &r)
{ return l.compare(r, Qt::CaseInsensitive) == 0; }

template <class U>
friend bool operator==(const U &l, const CaseInsensitive &r)
{ return l.compare(r, Qt::CaseInsensitive) == 0; }

friend bool operator==(const CaseInsensitive &l, const CaseInsensitive &r)
{ return l.compare(r, Qt::CaseInsensitive) == 0; }

template <class U>
bool startsWith(const U &r) const
{
if constexpr (detail::hasStartsWith<T, U>()) {
return T::startsWith(r, Qt::CaseInsensitive);
} else {
if (T::size() < r.size())
return false;

const auto view = view_type{T::cbegin(), static_cast<compat::lentype>(r.size())};
return view.compare(r, Qt::CaseInsensitive) == 0;
}
}

template <class U>
bool endsWith(const U &r) const
{
if constexpr (detail::hasEndsWith<T, U>()) {
return T::endsWith(r, Qt::CaseInsensitive);
} else {
if (T::size() < r.size())
return false;

return view_type{T::cend() - r.size(), r.size()}.compare(r, Qt::CaseInsensitive) == 0;
}
}
};

template <class T> CaseInsensitive(const T &) -> CaseInsensitive<T>;
template <class T> CaseInsensitive(T &&) -> CaseInsensitive<T>;

static_assert(std::is_same_v<compat::ByteArrayView, CaseInsensitive<QByteArray>::view_type>);
static_assert(std::is_same_v<compat::ByteArrayView, CaseInsensitive<compat::ByteArrayView>::view_type>);
static_assert(std::is_same_v<QStringView, CaseInsensitive<QLatin1String>::view_type>);
static_assert(std::is_same_v<QStringView, CaseInsensitive<QString>::view_type>);
static_assert(std::is_same_v<QStringView, CaseInsensitive<QStringView>::view_type>);

struct Message
{
using HeaderList = QList<std::pair<CaseInsensitive<QByteArray>, QByteArray>>;

QByteArray verb;
QByteArray resource;
QByteArray protocol;
HeaderList headers = {};

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

QDateTime parseDateTime(const QByteArray &text);
QDateTime parseDateTime(const QString &text);

QDateTime expiryDateTime(const QByteArray &cacheControl, const QByteArray &expires, const QDateTime &now);
QDateTime expiryDateTime(const QByteArray &cacheControl, const QByteArray &expires);

} // namespace qnc::http

#endif // QNC_HTTPPARSER_H
Loading

0 comments on commit f82cfaf

Please sign in to comment.