From f45dcddc50ea03e97bfc34654f8f085cb5d15d21 Mon Sep 17 00:00:00 2001 From: Linus Jahn Date: Thu, 22 Aug 2024 19:44:55 +0200 Subject: [PATCH 1/4] StreamErrorElement: Implement serialization --- src/base/QXmppStreamError_p.h | 1 + src/base/Stream.cpp | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/base/QXmppStreamError_p.h b/src/base/QXmppStreamError_p.h index 67afebc6e..abf216ce2 100644 --- a/src/base/QXmppStreamError_p.h +++ b/src/base/QXmppStreamError_p.h @@ -28,6 +28,7 @@ struct StreamErrorElement { static QString streamErrorToString(StreamError); static std::variant fromDom(const QDomElement &); + void toXml(QXmlStreamWriter *) const; Condition condition; QString text; diff --git a/src/base/Stream.cpp b/src/base/Stream.cpp index cb066184b..3dab49359 100644 --- a/src/base/Stream.cpp +++ b/src/base/Stream.cpp @@ -102,7 +102,6 @@ constexpr auto STREAM_ERROR_CONDITIONS = to_array({ u"unsupported-version", }); -/// \cond QString StreamErrorElement::streamErrorToString(StreamError e) { return STREAM_ERROR_CONDITIONS.at(size_t(e)).toString(); @@ -139,7 +138,23 @@ std::variant StreamErrorElement::fromDom(const Q std::move(errorText), }; } -/// \endcond + +void StreamErrorElement::toXml(QXmlStreamWriter *writer) const +{ + writer->writeStartElement(u"stream:error"_s); + if (const auto *streamError = std::get_if(&condition)) { + writer->writeStartElement(toString65(STREAM_ERROR_CONDITIONS.at(size_t(*streamError)))); + writer->writeDefaultNamespace(toString65(ns_stream_error)); + writer->writeEndElement(); + } else if (const auto *seeOtherHost = std::get_if(&condition)) { + writer->writeStartElement(u"see-other-host"_s); + writer->writeDefaultNamespace(toString65(ns_stream_error)); + writer->writeCharacters(seeOtherHost->host + u':' + QString::number(seeOtherHost->port)); + writer->writeEndElement(); + } + writeOptionalXmlTextElement(writer, u"text", text); + writer->writeEndElement(); +} XmppSocket::XmppSocket(QObject *parent) : QXmppLoggable(parent) From 6066c05fa95dd9c7810bc2c32f376ab5cae62880 Mon Sep 17 00:00:00 2001 From: Linus Jahn Date: Sun, 28 Apr 2024 22:18:45 +0200 Subject: [PATCH 2/4] StreamOpen: Impl fromXml() using QXmlStreamReader --- src/base/Stream.cpp | 32 ++++++++++++++++++++++++++- src/base/Stream.h | 4 +++- src/client/QXmppOutgoingClient.cpp | 2 +- tests/qxmppstream/tst_qxmppstream.cpp | 11 ++++++++- 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/base/Stream.cpp b/src/base/Stream.cpp index 3dab49359..376a3bb2e 100644 --- a/src/base/Stream.cpp +++ b/src/base/Stream.cpp @@ -24,6 +24,36 @@ using namespace QXmpp::Private; namespace QXmpp::Private { +StreamOpen StreamOpen::fromXml(QXmlStreamReader &reader) +{ + Q_ASSERT(reader.isStartElement()); + Q_ASSERT(reader.name() == u"stream"); + Q_ASSERT(reader.namespaceUri() == ns_stream); + + StreamOpen out; + const auto attributes = reader.attributes(); + auto attribute = [&](QStringView ns, QStringView name) { + for (const auto &a : attributes) { + if (a.name() == name && a.namespaceUri() == ns) { + return a.value().toString(); + } + } + return QString(); + }; + + out.from = attribute({}, u"from"); + out.to = attribute({}, u"to"); + + const auto namespaceDeclarations = reader.namespaceDeclarations(); + for (const auto &ns : namespaceDeclarations) { + if (ns.prefix().isEmpty()) { + out.xmlns = ns.namespaceUri().toString(); + } + } + + return out; +} + void StreamOpen::toXml(QXmlStreamWriter *writer) const { writer->writeStartDocument(); @@ -33,7 +63,7 @@ void StreamOpen::toXml(QXmlStreamWriter *writer) const } writer->writeAttribute(QSL65("to"), to); writer->writeAttribute(QSL65("version"), QSL65("1.0")); - writer->writeDefaultNamespace(toString65(xmlns)); + writer->writeDefaultNamespace(xmlns); writer->writeNamespace(toString65(ns_stream), QSL65("stream")); writer->writeCharacters({}); } diff --git a/src/base/Stream.h b/src/base/Stream.h index 7678bbb57..a74a7f11d 100644 --- a/src/base/Stream.h +++ b/src/base/Stream.h @@ -10,16 +10,18 @@ #include class QDomElement; +class QXmlStreamReader; class QXmlStreamWriter; namespace QXmpp::Private { struct StreamOpen { + static StreamOpen fromXml(QXmlStreamReader &reader); void toXml(QXmlStreamWriter *) const; QString to; QString from; - QStringView xmlns; + QString xmlns; }; struct StarttlsRequest { diff --git a/src/client/QXmppOutgoingClient.cpp b/src/client/QXmppOutgoingClient.cpp index 65e256348..e43362a18 100644 --- a/src/client/QXmppOutgoingClient.cpp +++ b/src/client/QXmppOutgoingClient.cpp @@ -636,7 +636,7 @@ void QXmppOutgoingClient::handleStart() d->socket.sendData(serializeXml(StreamOpen { d->config.domain(), d->config.user().isEmpty() ? QString() : d->config.jidBare(), - ns_client, + ns_client.toString(), })); } diff --git a/tests/qxmppstream/tst_qxmppstream.cpp b/tests/qxmppstream/tst_qxmppstream.cpp index ad95be180..d8810939a 100644 --- a/tests/qxmppstream/tst_qxmppstream.cpp +++ b/tests/qxmppstream/tst_qxmppstream.cpp @@ -107,8 +107,17 @@ void tst_QXmppStream::testProcessData() void tst_QXmppStream::streamOpen() { auto xml = ""; - StreamOpen s { "im.example.com", "juliet@im.example.com", ns_client }; + + StreamOpen s { "im.example.com", "juliet@im.example.com", ns_client.toString() }; serializePacket(s, xml); + + QXmlStreamReader r(xml); + QCOMPARE(r.readNext(), QXmlStreamReader::StartDocument); + QCOMPARE(r.readNext(), QXmlStreamReader::StartElement); + auto streamOpen = StreamOpen::fromXml(r); + QCOMPARE(streamOpen.from, "juliet@im.example.com"); + QCOMPARE(streamOpen.to, "im.example.com"); + QCOMPARE(streamOpen.xmlns, ns_client); } void tst_QXmppStream::testStreamError() From 2b20abd89cc429117f4541ed484d72b0913b103b Mon Sep 17 00:00:00 2001 From: Linus Jahn Date: Thu, 22 Aug 2024 16:21:05 +0200 Subject: [PATCH 3/4] StreamOpen: Add id and version attributes --- src/base/Stream.cpp | 11 ++++++----- src/base/Stream.h | 2 ++ src/client/QXmppOutgoingClient.cpp | 2 ++ tests/qxmppstream/tst_qxmppstream.cpp | 14 +++++++++++--- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/base/Stream.cpp b/src/base/Stream.cpp index 376a3bb2e..01201b5cc 100644 --- a/src/base/Stream.cpp +++ b/src/base/Stream.cpp @@ -43,6 +43,8 @@ StreamOpen StreamOpen::fromXml(QXmlStreamReader &reader) out.from = attribute({}, u"from"); out.to = attribute({}, u"to"); + out.id = attribute({}, u"id"); + out.version = attribute({}, u"version"); const auto namespaceDeclarations = reader.namespaceDeclarations(); for (const auto &ns : namespaceDeclarations) { @@ -58,11 +60,10 @@ void StreamOpen::toXml(QXmlStreamWriter *writer) const { writer->writeStartDocument(); writer->writeStartElement(QSL65("stream:stream")); - if (!from.isEmpty()) { - writer->writeAttribute(QSL65("from"), from); - } - writer->writeAttribute(QSL65("to"), to); - writer->writeAttribute(QSL65("version"), QSL65("1.0")); + writeOptionalXmlAttribute(writer, u"from", from); + writeOptionalXmlAttribute(writer, u"to", to); + writeOptionalXmlAttribute(writer, u"id", id); + writeOptionalXmlAttribute(writer, u"version", version); writer->writeDefaultNamespace(xmlns); writer->writeNamespace(toString65(ns_stream), QSL65("stream")); writer->writeCharacters({}); diff --git a/src/base/Stream.h b/src/base/Stream.h index a74a7f11d..fa2f64024 100644 --- a/src/base/Stream.h +++ b/src/base/Stream.h @@ -21,6 +21,8 @@ struct StreamOpen { QString to; QString from; + QString id; + QString version; QString xmlns; }; diff --git a/src/client/QXmppOutgoingClient.cpp b/src/client/QXmppOutgoingClient.cpp index e43362a18..84f52de71 100644 --- a/src/client/QXmppOutgoingClient.cpp +++ b/src/client/QXmppOutgoingClient.cpp @@ -636,6 +636,8 @@ void QXmppOutgoingClient::handleStart() d->socket.sendData(serializeXml(StreamOpen { d->config.domain(), d->config.user().isEmpty() ? QString() : d->config.jidBare(), + {}, + u"1.0"_s, ns_client.toString(), })); } diff --git a/tests/qxmppstream/tst_qxmppstream.cpp b/tests/qxmppstream/tst_qxmppstream.cpp index d8810939a..edaf172f3 100644 --- a/tests/qxmppstream/tst_qxmppstream.cpp +++ b/tests/qxmppstream/tst_qxmppstream.cpp @@ -106,9 +106,15 @@ void tst_QXmppStream::testProcessData() #ifdef BUILD_INTERNAL_TESTS void tst_QXmppStream::streamOpen() { - auto xml = ""; - - StreamOpen s { "im.example.com", "juliet@im.example.com", ns_client.toString() }; + auto xml = ""; + + StreamOpen s { + .to = "im.example.com", + .from = "juliet@im.example.com", + .id = "abcdefg", + .version = "1.0", + .xmlns = ns_client.toString(), + }; serializePacket(s, xml); QXmlStreamReader r(xml); @@ -117,6 +123,8 @@ void tst_QXmppStream::streamOpen() auto streamOpen = StreamOpen::fromXml(r); QCOMPARE(streamOpen.from, "juliet@im.example.com"); QCOMPARE(streamOpen.to, "im.example.com"); + QCOMPARE(streamOpen.id, "abcdefg"); + QCOMPARE(streamOpen.version, "1.0"); QCOMPARE(streamOpen.xmlns, ns_client); } From 805873ab9d53d524c7b82467c38729c52e006ad5 Mon Sep 17 00:00:00 2001 From: Linus Jahn Date: Sun, 28 Apr 2024 23:15:12 +0200 Subject: [PATCH 4/4] XmppSocket: Parse whole stream through QXmlStreamReader --- src/base/Stream.cpp | 338 +++++++++++++++++++------- src/base/Stream.h | 3 + src/base/XmppSocket.h | 41 +++- src/client/QXmppOutgoingClient.cpp | 8 +- src/client/QXmppOutgoingClient.h | 3 +- src/server/QXmppIncomingClient.cpp | 6 +- src/server/QXmppIncomingClient.h | 6 +- src/server/QXmppIncomingServer.cpp | 7 +- src/server/QXmppIncomingServer.h | 6 +- src/server/QXmppOutgoingServer.cpp | 4 +- src/server/QXmppOutgoingServer.h | 6 +- tests/qxmppstream/tst_qxmppstream.cpp | 15 +- 12 files changed, 323 insertions(+), 120 deletions(-) diff --git a/src/base/Stream.cpp b/src/base/Stream.cpp index 01201b5cc..3f7c17cee 100644 --- a/src/base/Stream.cpp +++ b/src/base/Stream.cpp @@ -22,6 +22,15 @@ using namespace QXmpp; using namespace QXmpp::Private; +// helper for std::visit +template +struct overloaded : Ts... { + using Ts::operator()...; +}; +// explicit deduction guide (not needed as of C++20) +template +overloaded(Ts...) -> overloaded; + namespace QXmpp::Private { StreamOpen StreamOpen::fromXml(QXmlStreamReader &reader) @@ -187,6 +196,103 @@ void StreamErrorElement::toXml(QXmlStreamWriter *writer) const writer->writeEndElement(); } +static QString restrictedXmlErrorText(QXmlStreamReader::TokenType token) +{ + switch (token) { + case QXmlStreamReader::Comment: + return u"XML comments are not allowed in XMPP."_s; + case QXmlStreamReader::DTD: + return u"XML DTDs are not allowed in XMPP."_s; + case QXmlStreamReader::EntityReference: + return u"XML entity references are not allowed in XMPP."_s; + case QXmlStreamReader::ProcessingInstruction: + return u"XML processing instructions are not allowed in XMPP."_s; + default: + return {}; + } +} + +DomReader::Result DomReader::process(QXmlStreamReader &r) +{ + while (true) { + switch (r.tokenType()) { + case QXmlStreamReader::Invalid: + // error received + if (r.error() == QXmlStreamReader::PrematureEndOfDocumentError) { + return Unfinished {}; + } + return Error { NotWellFormed, r.errorString() }; + case QXmlStreamReader::StartElement: { + auto child = r.prefix().isNull() + ? doc.createElement(r.name().toString()) + : doc.createElementNS(r.namespaceUri().toString(), r.qualifiedName().toString()); + + // xmlns attribute + const auto nsDeclarations = r.namespaceDeclarations(); + for (const auto &ns : nsDeclarations) { + if (ns.prefix().isEmpty()) { + child.setAttribute(u"xmlns"_s, ns.namespaceUri().toString()); + } else { + // namespace declarations are not supported in XMPP + return Error { UnsupportedXmlFeature, u"XML namespace declarations are not allowed in XMPP."_s }; + } + } + + // other attributes + const auto attributes = r.attributes(); + for (const auto &a : attributes) { + child.setAttribute(a.name().toString(), a.value().toString()); + } + + if (currentElement.isNull()) { + doc.appendChild(child); + } else { + currentElement.appendChild(child); + } + depth++; + currentElement = child; + break; + } + case QXmlStreamReader::EndElement: + Q_ASSERT(depth > 0); + if (depth == 0) { + return Error { InvalidState, u"Invalid state: Received element end instead of element start."_s }; + } + + currentElement = currentElement.parentNode().toElement(); + depth--; + // if top-level element is complete: return + if (depth == 0) { + return doc.documentElement(); + } + break; + case QXmlStreamReader::Characters: + // DOM reader must only be used on element start: characters on level 0 are not allowed + Q_ASSERT(depth > 0); + if (depth == 0) { + return Error { InvalidState, u"Invalid state: Received top-level character data instead of element begin."_s }; + } + + currentElement.appendChild(doc.createTextNode(r.text().toString())); + break; + case QXmlStreamReader::NoToken: + // skip + break; + case QXmlStreamReader::StartDocument: + case QXmlStreamReader::EndDocument: + Q_ASSERT_X(false, "DomReader", "Received document begin or end."); + return Error { InvalidState, u"Invalid state: Received document begin or end."_s }; + break; + case QXmlStreamReader::Comment: + case QXmlStreamReader::DTD: + case QXmlStreamReader::EntityReference: + case QXmlStreamReader::ProcessingInstruction: + return Error { UnsupportedXmlFeature, restrictedXmlErrorText(r.tokenType()) }; + } + r.readNext(); + } +} + XmppSocket::XmppSocket(QObject *parent) : QXmppLoggable(parent) { @@ -206,16 +312,20 @@ void XmppSocket::setSocket(QSslSocket *socket) // do not emit started() with direct TLS (this happens in encrypted()) if (!m_directTls) { - m_dataBuffer.clear(); - m_streamOpenElement.clear(); + m_reader.clear(); + m_streamReceived = false; Q_EMIT started(); } }); + QObject::connect(socket, &QAbstractSocket::disconnected, this, [this]() { + // reset error state + m_errorOccurred = false; + }); QObject::connect(socket, &QSslSocket::encrypted, this, [this]() { debug(u"Socket encrypted"_s); // this happens with direct TLS or STARTTLS - m_dataBuffer.clear(); - m_streamOpenElement.clear(); + m_reader.clear(); + m_streamReceived = false; Q_EMIT started(); }); QObject::connect(socket, &QSslSocket::errorOccurred, this, [this](QAbstractSocket::SocketError) { @@ -271,104 +381,150 @@ bool XmppSocket::sendData(const QByteArray &data) return m_socket->write(data) == data.size(); } -void XmppSocket::processData(const QString &data) +void XmppSocket::throwStreamError(const StreamErrorElement &error) { - // As we may only have partial XML content, we need to cache the received - // data until it has been successfully parsed. In case it can't be parsed, - // - // There are only two small problems with the current strategy: - // * When we receive a full stanza + a partial one, we can't parse the - // first stanza until another stanza arrives that is complete. - // * We don't know when we received invalid XML (would cause a growing - // cache and a timeout after some time). - // However, both issues could only be solved using an XML stream reader - // which would cause many other problems since we don't actually use it for - // parsing the content. - m_dataBuffer.append(data); - - // - // Check for whitespace pings - // - if (m_dataBuffer.isEmpty() || m_dataBuffer.trimmed().isEmpty()) { - m_dataBuffer.clear(); + Q_ASSERT(!m_errorOccurred); + m_errorOccurred = true; - logReceived({}); - Q_EMIT stanzaReceived(QDomElement()); - return; - } + sendData(serializeXml(error)); + m_socket->disconnectFromHost(); + Q_EMIT streamErrorSent(error); +} - // - // Check whether we received a stream open or closing tag - // - static const QRegularExpression streamStartRegex(uR"(^(<\?xml.*\?>)?\s*]*>)"_s); - static const QRegularExpression streamEndRegex(u"$"_s); - - auto streamOpenMatch = streamStartRegex.match(m_dataBuffer); - bool hasStreamOpen = streamOpenMatch.hasMatch(); - - bool hasStreamClose = streamEndRegex.match(m_dataBuffer).hasMatch(); - - // - // The stream start/end and stanza packets can't be parsed without any - // modifications with QDomDocument. This is because of multiple reasons: - // * The open element is not considered valid without the - // closing tag. - // * Only the closing tag is of course not valid too. - // * Stanzas/Nonzas need to have the correct stream namespaces set: - // * For being able to parse - // * For having the correct namespace (e.g. 'jabber:client') set to - // stanzas and their child elements (e.g. of a message). - // - // The wrapping strategy looks like this: - // * The stream open tag is cached once it arrives, for later access - // * Incoming XML that has no open tag will be prepended by the - // cached tag. - // * Incoming XML that has no close tag will be appended by a - // generic string "" - // - // The result is parsed by QDomDocument and the child elements of the stream - // are processed. In case the received data contained a stream open tag, - // the stream is processed (before the stanzas are processed). In case we - // received a closing tag, the connection is closed. - // - auto wrappedStanzas = m_dataBuffer; - if (!hasStreamOpen) { - wrappedStanzas.prepend(m_streamOpenElement); - } - if (!hasStreamClose) { - wrappedStanzas.append(u""_s); +void XmppSocket::processData(const QString &data) +{ + // stop parsing after an error has occurred + if (m_errorOccurred) { + return; } - // - // Try to parse the wrapped XML - // - QDomDocument doc; - if (!doc.setContent(wrappedStanzas, true)) { + // Check for whitespace pings + if (data.isEmpty()) { + logReceived({}); + Q_EMIT stanzaReceived(QDomElement()); return; } - // - // Success: We can clear the buffer and send a 'received' log message - // - logReceived(m_dataBuffer); - m_dataBuffer.clear(); - - // process stream start - if (hasStreamOpen) { - m_streamOpenElement = streamOpenMatch.captured(); - Q_EMIT streamReceived(doc.documentElement()); - } + // log data received and process + logReceived(data); + m_reader.addData(data); + + // 'm_reader' parses the XML stream and 'm_domReader' creates DOM elements with the data from + // 'm_reader'. 'm_domReader' lives as long as one stanza element is parsed. + + auto readDomElement = [this]() { + return std::visit( + overloaded { + [this](const QDomElement &element) { + m_domReader.reset(); + Q_EMIT stanzaReceived(element); + return true; + }, + [](DomReader::Unfinished) { + return false; + }, + [this](const DomReader::Error &error) { + switch (error.type) { + case DomReader::InvalidState: + throwStreamError({ + StreamError::InternalServerError, + u"Experienced internal error while parsing XML."_s, + }); + break; + case DomReader::NotWellFormed: + throwStreamError({ + StreamError::NotWellFormed, + u"Not well-formed: "_s + error.text, + }); + break; + case DomReader::UnsupportedXmlFeature: + throwStreamError({ StreamError::RestrictedXml, error.text }); + break; + } + return false; + }, + }, + m_domReader->process(m_reader)); + }; - // process stanzas - auto stanza = doc.documentElement().firstChildElement(); - for (; !stanza.isNull(); stanza = stanza.nextSiblingElement()) { - Q_EMIT stanzaReceived(stanza); + // we're still reading a previously started top-level element + if (m_domReader) { + m_reader.readNext(); + if (!readDomElement()) { + return; + } } - // process stream end - if (hasStreamClose) { - Q_EMIT streamClosed(); - } + do { + switch (m_reader.readNext()) { + case QXmlStreamReader::Invalid: + // error received + if (m_reader.error() != QXmlStreamReader::PrematureEndOfDocumentError) { + return throwStreamError({ StreamError::NotWellFormed, m_reader.errorString() }); + } + break; + case QXmlStreamReader::StartDocument: + // pre-stream open + break; + case QXmlStreamReader::EndDocument: + // post-stream close + break; + case QXmlStreamReader::StartElement: + // stream open or stream-level element + if (m_reader.name() == u"stream" && m_reader.namespaceUri() == ns_stream) { + // check for 'stream:stream' (this is required by the spec) + if (m_reader.prefix() != u"stream") { + throwStreamError({ + StreamError::BadNamespacePrefix, + u"Top-level stream element must have a namespace prefix of 'stream'."_s, + }); + return; + } + + m_streamReceived = true; + Q_EMIT streamReceived(StreamOpen::fromXml(m_reader)); + } else if (!m_streamReceived) { + throwStreamError({ + StreamError::BadFormat, + u"Invalid element received. Expected 'stream' element qualified by 'http://etherx.jabber.org/streams' namespace."_s, + }); + return; + } else { + // parse top-level stream element + m_domReader = DomReader(); + if (!readDomElement()) { + return; + } + } + break; + case QXmlStreamReader::EndElement: + // end of stream + Q_EMIT streamClosed(); + break; + case QXmlStreamReader::Characters: + if (m_reader.isWhitespace()) { + logReceived({}); + Q_EMIT stanzaReceived(QDomElement()); + } else { + // invalid: emit error + throwStreamError({ + StreamError::BadFormat, + u"Top-level, non-whitespace character data is not allowed in XMPP."_s, + }); + return; + } + break; + case QXmlStreamReader::NoToken: + // skip + break; + case QXmlStreamReader::Comment: + case QXmlStreamReader::DTD: + case QXmlStreamReader::EntityReference: + case QXmlStreamReader::ProcessingInstruction: + throwStreamError({ StreamError::RestrictedXml, restrictedXmlErrorText(m_reader.tokenType()) }); + return; + } + } while (!m_reader.hasError()); } } // namespace QXmpp::Private diff --git a/src/base/Stream.h b/src/base/Stream.h index fa2f64024..f4cd630cf 100644 --- a/src/base/Stream.h +++ b/src/base/Stream.h @@ -7,6 +7,7 @@ #include +#include #include class QDomElement; @@ -46,4 +47,6 @@ struct CsiInactive { } // namespace QXmpp::Private +Q_DECLARE_METATYPE(QXmpp::Private::StreamOpen) + #endif // STREAM_H diff --git a/src/base/XmppSocket.h b/src/base/XmppSocket.h index 0e5b7bea6..660fb619d 100644 --- a/src/base/XmppSocket.h +++ b/src/base/XmppSocket.h @@ -6,6 +6,10 @@ #define XMPPSOCKET_H #include "QXmppLogger.h" +#include "QXmppStreamError_p.h" + +#include +#include class QDomElement; class QSslSocket; @@ -14,6 +18,8 @@ class tst_QXmppStream; namespace QXmpp::Private { +struct StreamOpen; + struct ServerAddress { enum ConnectionType { Tcp, @@ -31,6 +37,31 @@ class SendDataInterface virtual bool sendData(const QByteArray &) = 0; }; +class DomReader +{ +public: + struct Unfinished { }; + + enum ErrorType { + InvalidState, + NotWellFormed, + UnsupportedXmlFeature, + }; + struct Error { + ErrorType type; + QString text; + }; + + using Result = std::variant; + + Result process(QXmlStreamReader &); + +private: + QDomDocument doc; + QDomElement currentElement; + uint depth = 0; +}; + class QXMPP_EXPORT XmppSocket : public QXmppLoggable, public SendDataInterface { Q_OBJECT @@ -48,16 +79,22 @@ class QXMPP_EXPORT XmppSocket : public QXmppLoggable, public SendDataInterface Q_SIGNAL void started(); Q_SIGNAL void stanzaReceived(const QDomElement &); - Q_SIGNAL void streamReceived(const QDomElement &); + Q_SIGNAL void streamReceived(const QXmpp::Private::StreamOpen &); Q_SIGNAL void streamClosed(); + Q_SIGNAL void streamErrorSent(const StreamErrorElement &error); private: + void throwStreamError(const StreamErrorElement &); void processData(const QString &data); friend class ::tst_QXmppStream; - QString m_dataBuffer; + QXmlStreamReader m_reader; + std::optional m_domReader; + bool m_streamReceived = false; bool m_directTls = false; + bool m_errorOccurred = false; + QSslSocket *m_socket = nullptr; // incoming stream state diff --git a/src/client/QXmppOutgoingClient.cpp b/src/client/QXmppOutgoingClient.cpp index 84f52de71..20eb68234 100644 --- a/src/client/QXmppOutgoingClient.cpp +++ b/src/client/QXmppOutgoingClient.cpp @@ -642,16 +642,16 @@ void QXmppOutgoingClient::handleStart() })); } -void QXmppOutgoingClient::handleStream(const QDomElement &streamElement) +void QXmppOutgoingClient::handleStream(const StreamOpen &stream) { if (d->streamId.isEmpty()) { - d->streamId = streamElement.attribute(u"id"_s); + d->streamId = stream.id; } if (d->streamFrom.isEmpty()) { - d->streamFrom = streamElement.attribute(u"from"_s); + d->streamFrom = stream.from; } if (d->streamVersion.isEmpty()) { - d->streamVersion = streamElement.attribute(u"version"_s); + d->streamVersion = stream.version; // no version specified, signals XMPP Version < 1.0. // switch to old auth mechanism if enabled diff --git a/src/client/QXmppOutgoingClient.h b/src/client/QXmppOutgoingClient.h index 145c7891e..d1f2b8e4a 100644 --- a/src/client/QXmppOutgoingClient.h +++ b/src/client/QXmppOutgoingClient.h @@ -43,6 +43,7 @@ struct SmEnabled; struct SmFailed; struct SmResumed; struct StreamErrorElement; +struct StreamOpen; enum HandleElementResult { Accepted, @@ -133,7 +134,7 @@ class QXMPP_EXPORT QXmppOutgoingClient : public QXmppLoggable private: void handleStart(); - void handleStream(const QDomElement &element); + void handleStream(const QXmpp::Private::StreamOpen &stream); void handlePacketReceived(const QDomElement &element); QXmpp::Private::HandleElementResult handleElement(const QDomElement &nodeRecv); void handleStreamFeatures(const QXmppStreamFeatures &features); diff --git a/src/server/QXmppIncomingClient.cpp b/src/server/QXmppIncomingClient.cpp index e1f152a5c..d739ee51e 100644 --- a/src/server/QXmppIncomingClient.cpp +++ b/src/server/QXmppIncomingClient.cpp @@ -189,7 +189,7 @@ void QXmppIncomingClient::handleStart() { } -void QXmppIncomingClient::handleStream(const QDomElement &streamElement) +void QXmppIncomingClient::handleStream(const StreamOpen &stream) { if (d->idleTimer->interval()) { d->idleTimer->start(); @@ -209,14 +209,14 @@ void QXmppIncomingClient::handleStream(const QDomElement &streamElement) sendData(response.toUtf8()); // check requested domain - if (streamElement.attribute(u"to"_s) != d->domain) { + if (stream.to != d->domain) { QString response = u"" "" "" "This server does not serve %1" "" ""_s - .arg(streamElement.attribute(u"to"_s)); + .arg(stream.to); sendData(response.toUtf8()); disconnectFromHost(); return; diff --git a/src/server/QXmppIncomingClient.h b/src/server/QXmppIncomingClient.h index 2b7231fe9..8686cacc2 100644 --- a/src/server/QXmppIncomingClient.h +++ b/src/server/QXmppIncomingClient.h @@ -15,6 +15,10 @@ class QXmppNonza; class QXmppIncomingClientPrivate; class QXmppPasswordChecker; +namespace QXmpp::Private { +struct StreamOpen; +} + /// /// \brief The QXmppIncomingClient class represents an incoming XMPP stream /// from an XMPP client. @@ -48,7 +52,7 @@ class QXMPP_EXPORT QXmppIncomingClient : public QXmppLoggable protected: /// \cond void handleStart(); - void handleStream(const QDomElement &element); + void handleStream(const QXmpp::Private::StreamOpen &); void handleStanza(const QDomElement &element); /// \endcond diff --git a/src/server/QXmppIncomingServer.cpp b/src/server/QXmppIncomingServer.cpp index 707f3795a..a67a960c5 100644 --- a/src/server/QXmppIncomingServer.cpp +++ b/src/server/QXmppIncomingServer.cpp @@ -120,11 +120,10 @@ void QXmppIncomingServer::handleStart() { } -void QXmppIncomingServer::handleStream(const QDomElement &streamElement) +void QXmppIncomingServer::handleStream(const StreamOpen &stream) { - const QString from = streamElement.attribute(u"from"_s); - if (!from.isEmpty()) { - info(u"Incoming server stream from %1 on %2"_s.arg(from, d->origin())); + if (!stream.from.isEmpty()) { + info(u"Incoming server stream from %1 on %2"_s.arg(stream.from, d->origin())); } // start stream diff --git a/src/server/QXmppIncomingServer.h b/src/server/QXmppIncomingServer.h index 734d5f428..fee8e45d6 100644 --- a/src/server/QXmppIncomingServer.h +++ b/src/server/QXmppIncomingServer.h @@ -15,6 +15,10 @@ class QXmppDialback; class QXmppIncomingServerPrivate; class QXmppNonza; +namespace QXmpp::Private { +struct StreamOpen; +} + /// /// \brief The QXmppIncomingServer class represents an incoming XMPP stream /// from an XMPP server. @@ -47,7 +51,7 @@ class QXMPP_EXPORT QXmppIncomingServer : public QXmppLoggable private: void handleStart(); void handleStanza(const QDomElement &element); - void handleStream(const QDomElement &element); + void handleStream(const QXmpp::Private::StreamOpen &element); void slotDialbackResponseReceived(const QXmppDialback &dialback); void slotSocketDisconnected(); diff --git a/src/server/QXmppOutgoingServer.cpp b/src/server/QXmppOutgoingServer.cpp index cd9b5fc80..546e8ceab 100644 --- a/src/server/QXmppOutgoingServer.cpp +++ b/src/server/QXmppOutgoingServer.cpp @@ -148,10 +148,8 @@ void QXmppOutgoingServer::handleStart() sendData(data.toUtf8()); } -void QXmppOutgoingServer::handleStream(const QDomElement &streamElement) +void QXmppOutgoingServer::handleStream(const StreamOpen &) { - Q_UNUSED(streamElement); - // gmail.com servers are broken: they never send , // so we schedule sending the dialback in a couple of seconds d->dialbackTimer->start(); diff --git a/src/server/QXmppOutgoingServer.h b/src/server/QXmppOutgoingServer.h index ec919b3a0..d0c188433 100644 --- a/src/server/QXmppOutgoingServer.h +++ b/src/server/QXmppOutgoingServer.h @@ -16,6 +16,10 @@ class QXmppNonza; class QXmppOutgoingServer; class QXmppOutgoingServerPrivate; +namespace QXmpp::Private { +struct StreamOpen; +} + /// /// \brief The QXmppOutgoingServer class represents an outgoing XMPP stream /// to another XMPP server. @@ -52,7 +56,7 @@ class QXMPP_EXPORT QXmppOutgoingServer : public QXmppLoggable private: void handleStart(); - void handleStream(const QDomElement &streamElement); + void handleStream(const QXmpp::Private::StreamOpen &streamElement); void handleStanza(const QDomElement &stanzaElement); void onDnsLookupFinished(); diff --git a/tests/qxmppstream/tst_qxmppstream.cpp b/tests/qxmppstream/tst_qxmppstream.cpp index edaf172f3..59518c4a8 100644 --- a/tests/qxmppstream/tst_qxmppstream.cpp +++ b/tests/qxmppstream/tst_qxmppstream.cpp @@ -36,6 +36,7 @@ class tst_QXmppStream : public QObject void tst_QXmppStream::initTestCase() { qRegisterMetaType(); + qRegisterMetaType(); } void tst_QXmppStream::testProcessData() @@ -61,16 +62,12 @@ void tst_QXmppStream::testProcessData() QCOMPARE(onStarted.size(), 0); // check stream information - const auto streamElement = onStreamReceived[0][0].value(); - QCOMPARE(streamElement.tagName(), u"stream"_s); - QCOMPARE(streamElement.namespaceURI(), u"http://etherx.jabber.org/streams"_s); - QCOMPARE(streamElement.attribute("from"), u"juliet@im.example.com"_s); - QCOMPARE(streamElement.attribute("to"), u"im.example.com"_s); - QCOMPARE(streamElement.attribute("version"), u"1.0"_s); - QCOMPARE(streamElement.attribute("lang"), u"en"_s); + const auto streamElement = onStreamReceived[0][0].value(); + QCOMPARE(streamElement.from, QStringLiteral("juliet@im.example.com")); + QCOMPARE(streamElement.to, QStringLiteral("im.example.com")); + QCOMPARE(streamElement.version, QStringLiteral("1.0")); - socket.processData(R"( - + socket.processData(R"(