-
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.
Merge pull request #20 from hasselmm/feature/ssdp-resolver
Add a minimal SSDP resolver
- Loading branch information
Showing
19 changed files
with
1,447 additions
and
245 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,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 |
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,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 |
Oops, something went wrong.