diff --git a/src/base/QXmppStream.cpp b/src/base/QXmppStream.cpp index 748c931ac..119170ef3 100644 --- a/src/base/QXmppStream.cpp +++ b/src/base/QXmppStream.cpp @@ -143,6 +143,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) { @@ -158,11 +160,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({}); @@ -234,6 +235,88 @@ std::variant StreamErrorElement::fromDom(const Q } /// \endcond +DomReader::State DomReader::process(QXmlStreamReader &r) +{ + while (true) { + switch (r.tokenType()) { + case QXmlStreamReader::Invalid: + // error received + if (r.error() == QXmlStreamReader::PrematureEndOfDocumentError) { + return Unfinished; + } + return ErrorOccurred; + case QXmlStreamReader::StartElement: { + qDebug() << "start element token"; + 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(QStringLiteral("xmlns"), ns.namespaceUri().toString()); + } else { + // namespace declarations are not supported in XMPP + qDebug() << "err namespace decl"; + return ErrorOccurred; + } + } + + // 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: + qDebug() << "end element token"; + if (depth == 0) { + qDebug() << "depth == 0"; + return ErrorOccurred; + } + + currentElement = currentElement.parentNode().toElement(); + depth--; + qDebug() << "depth" << depth; + if (depth == 0) { + return Finished; + } + break; + case QXmlStreamReader::Characters: + if (depth == 0) { + qDebug() << "depth == 0"; + return ErrorOccurred; + } + + currentElement.appendChild(doc.createTextNode(r.text().toString())); + break; + case QXmlStreamReader::NoToken: + // skip + break; + case QXmlStreamReader::StartDocument: + case QXmlStreamReader::EndDocument: + case QXmlStreamReader::Comment: + case QXmlStreamReader::DTD: + case QXmlStreamReader::EntityReference: + case QXmlStreamReader::ProcessingInstruction: + qDebug() << "not allowed"; + // not allowed or unexpected + return ErrorOccurred; + } + r.readNext(); + } +} + XmppSocket::XmppSocket(QObject *parent) : QXmppLoggable(parent) { @@ -250,14 +333,14 @@ void XmppSocket::setSocket(QSslSocket *socket) info(QStringLiteral("Socket connected to %1 %2") .arg(m_socket->peerAddress().toString(), QString::number(m_socket->peerPort()))); - m_dataBuffer.clear(); - m_streamOpenElement.clear(); + m_reader.clear(); + m_streamReceived = false; Q_EMIT started(); }); QObject::connect(socket, &QSslSocket::encrypted, this, [this]() { debug(QStringLiteral("Socket encrypted")); - m_dataBuffer.clear(); - m_streamOpenElement.clear(); + m_reader.clear(); + m_streamReceived = false; Q_EMIT started(); }); QObject::connect(socket, &QSslSocket::errorOccurred, this, [this](QAbstractSocket::SocketError) { @@ -297,102 +380,99 @@ bool XmppSocket::sendData(const QByteArray &data) void XmppSocket::processData(const QString &data) { - // 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(); - + if (data.isEmpty()) { + qDebug() << "empty ping received"; logReceived({}); Q_EMIT stanzaReceived(QDomElement()); return; } - // - // Check whether we received a stream open or closing tag - // - static const QRegularExpression streamStartRegex(QStringLiteral(R"(^(<\?xml.*\?>)?\s*]*>)")); - static const QRegularExpression streamEndRegex(QStringLiteral("$")); - - 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(QStringLiteral("")); - } - - // - // Try to parse the wrapped XML - // - QDomDocument doc; - if (!doc.setContent(wrappedStanzas, true)) { - 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()); - } - - // process stanzas - auto stanza = doc.documentElement().firstChildElement(); - for (; !stanza.isNull(); stanza = stanza.nextSiblingElement()) { - Q_EMIT stanzaReceived(stanza); + // log data received and process + logReceived(data); + m_reader.addData(data); + + // we're still reading a previously started top-level element + if (m_domReader) { + m_reader.readNext(); + switch (m_domReader->process(m_reader)) { + case DomReader::Finished: + Q_EMIT stanzaReceived(m_domReader->element()); + m_domReader.reset(); + break; + case DomReader::Unfinished: + return; + case DomReader::ErrorOccurred: + // emit error + break; + } } - // process stream end - if (hasStreamClose) { - Q_EMIT streamClosed(); - } + do { + switch (m_reader.readNext()) { + case QXmlStreamReader::Invalid: + // error received + if (m_reader.error() != QXmlStreamReader::PrematureEndOfDocumentError) { + // emit error + } + 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) { + m_streamReceived = true; + Q_EMIT streamReceived(StreamOpen::fromXml(m_reader)); + } else if (!m_streamReceived) { + // error: expected stream open element + qDebug() << "err no stream recevied"; + } else { + qDebug() << "start el"; + // parse top-level stream element + m_domReader = DomReader(); + + switch (m_domReader->process(m_reader)) { + case DomReader::Finished: + Q_EMIT stanzaReceived(m_domReader->element()); + m_domReader.reset(); + break; + case DomReader::Unfinished: + qDebug() << "unfi"; + return; + case DomReader::ErrorOccurred: + qDebug() << "el err"; + // emit error + break; + } + } + 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 + } + break; + case QXmlStreamReader::NoToken: + // skip + break; + case QXmlStreamReader::Comment: + case QXmlStreamReader::DTD: + case QXmlStreamReader::EntityReference: + case QXmlStreamReader::ProcessingInstruction: + // not allowed in XMPP: emit error + break; + } + } while (!m_reader.hasError()); } } // namespace QXmpp::Private diff --git a/src/base/QXmppStream.h b/src/base/QXmppStream.h index 482f0edb6..f1611b000 100644 --- a/src/base/QXmppStream.h +++ b/src/base/QXmppStream.h @@ -18,6 +18,7 @@ class QXmppStreamPrivate; namespace QXmpp::Private { class XmppSocket; +struct StreamOpen; } /// @@ -60,7 +61,7 @@ class QXMPP_EXPORT QXmppStream : public QXmppLoggable /// Handles an incoming XMPP stream start. /// /// \param element - virtual void handleStream(const QDomElement &element) = 0; + virtual void handleStream(const QXmpp::Private::StreamOpen &) = 0; public Q_SLOTS: virtual void disconnectFromHost(); diff --git a/src/base/Stream.h b/src/base/Stream.h index 1d99cb4ef..5501c9cfb 100644 --- a/src/base/Stream.h +++ b/src/base/Stream.h @@ -18,9 +18,13 @@ struct StreamOpen { QString to; QString from; + QString id; + QString version; QString xmlns; }; } // 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 edbaa6edb..91d66603c 100644 --- a/src/base/XmppSocket.h +++ b/src/base/XmppSocket.h @@ -7,18 +7,41 @@ #include "QXmppLogger.h" +#include +#include + class QDomElement; class QSslSocket; class TestStream; namespace QXmpp::Private { +struct StreamOpen; + class SendDataInterface { public: virtual bool sendData(const QByteArray &) = 0; }; +class DomReader +{ +public: + enum State { + Finished, + Unfinished, + ErrorOccurred, + }; + + State process(QXmlStreamReader &); + QDomElement element() const { return doc.documentElement(); } + +private: + QDomDocument doc; + QDomElement currentElement; + uint depth = 0; +}; + class QXMPP_EXPORT XmppSocket : public QXmppLoggable, public SendDataInterface { Q_OBJECT @@ -35,7 +58,7 @@ 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(); private: @@ -43,7 +66,10 @@ class QXMPP_EXPORT XmppSocket : public QXmppLoggable, public SendDataInterface friend class ::TestStream; - QString m_dataBuffer; + QXmlStreamReader m_reader; + std::optional m_domReader; + bool m_streamReceived = false; + QSslSocket *m_socket = nullptr; // incoming stream state diff --git a/src/client/QXmppOutgoingClient.cpp b/src/client/QXmppOutgoingClient.cpp index 810a5a61a..cbb4df97f 100644 --- a/src/client/QXmppOutgoingClient.cpp +++ b/src/client/QXmppOutgoingClient.cpp @@ -630,20 +630,22 @@ void QXmppOutgoingClient::handleStart() d->socket.sendData(serializeXml(StreamOpen { d->config.domain(), d->config.user().isEmpty() ? QString() : d->config.jidBare(), + {}, + QStringLiteral("1.0"), ns_client.toString(), })); } -void QXmppOutgoingClient::handleStream(const QDomElement &streamElement) +void QXmppOutgoingClient::handleStream(const StreamOpen &stream) { if (d->streamId.isEmpty()) { - d->streamId = streamElement.attribute(QStringLiteral("id")); + d->streamId = stream.id; } if (d->streamFrom.isEmpty()) { - d->streamFrom = streamElement.attribute(QStringLiteral("from")); + d->streamFrom = stream.from; } if (d->streamVersion.isEmpty()) { - d->streamVersion = streamElement.attribute(QStringLiteral("version")); + 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 eb36a63ac..27de469ca 100644 --- a/src/client/QXmppOutgoingClient.h +++ b/src/client/QXmppOutgoingClient.h @@ -33,6 +33,7 @@ class OutgoingIqManager; class PingManager; class StreamAckManager; class XmppSocket; +struct StreamOpen; enum HandleElementResult { Accepted, @@ -104,7 +105,7 @@ class QXMPP_EXPORT QXmppOutgoingClient : public QXmppLoggable void handleStart(); void handlePacketReceived(const QDomElement &element); QXmpp::Private::HandleElementResult handleElement(const QDomElement &nodeRecv); - void handleStream(const QDomElement &element); + void handleStream(const QXmpp::Private::StreamOpen &element); void _q_socketDisconnected(); void socketError(QAbstractSocket::SocketError); diff --git a/src/server/QXmppIncomingClient.cpp b/src/server/QXmppIncomingClient.cpp index 34d0b0f5e..b00dd1389 100644 --- a/src/server/QXmppIncomingClient.cpp +++ b/src/server/QXmppIncomingClient.cpp @@ -13,6 +13,8 @@ #include "QXmppUtils.h" #include "QXmppUtils_p.h" +#include "Stream.h" + #include #include #include @@ -155,7 +157,7 @@ void QXmppIncomingClient::setPasswordChecker(QXmppPasswordChecker *checker) } /// \cond -void QXmppIncomingClient::handleStream(const QDomElement &streamElement) +void QXmppIncomingClient::handleStream(const StreamOpen &stream) { if (d->idleTimer->interval()) { d->idleTimer->start(); @@ -175,14 +177,14 @@ void QXmppIncomingClient::handleStream(const QDomElement &streamElement) sendData(response.toUtf8()); // check requested domain - if (streamElement.attribute(QStringLiteral("to")) != d->domain) { + if (stream.to != d->domain) { QString response = QStringLiteral("" "" "" "This server does not serve %1" "" "") - .arg(streamElement.attribute(QStringLiteral("to"))); + .arg(stream.to); sendData(response.toUtf8()); disconnectFromHost(); return; diff --git a/src/server/QXmppIncomingClient.h b/src/server/QXmppIncomingClient.h index c2c97b256..146f15a0c 100644 --- a/src/server/QXmppIncomingClient.h +++ b/src/server/QXmppIncomingClient.h @@ -10,13 +10,18 @@ class QXmppIncomingClientPrivate; class QXmppPasswordChecker; +namespace QXmpp::Private { +struct StreamOpen; +} + +/// /// \brief Interface for password checkers. /// +/// /// \brief The QXmppIncomingClient class represents an incoming XMPP stream /// from an XMPP client. /// - class QXMPP_EXPORT QXmppIncomingClient : public QXmppStream { Q_OBJECT @@ -37,7 +42,7 @@ class QXMPP_EXPORT QXmppIncomingClient : public QXmppStream protected: /// \cond - void handleStream(const QDomElement &element) override; + void handleStream(const QXmpp::Private::StreamOpen &) override; void handleStanza(const QDomElement &element) override; /// \endcond diff --git a/src/server/QXmppIncomingServer.cpp b/src/server/QXmppIncomingServer.cpp index 4f19d88d1..1828273b6 100644 --- a/src/server/QXmppIncomingServer.cpp +++ b/src/server/QXmppIncomingServer.cpp @@ -11,11 +11,15 @@ #include "QXmppStreamFeatures.h" #include "QXmppUtils.h" +#include "Stream.h" + #include #include #include #include +using namespace QXmpp::Private; + class QXmppIncomingServerPrivate { public: @@ -79,11 +83,10 @@ QString QXmppIncomingServer::localStreamId() const } /// \cond -void QXmppIncomingServer::handleStream(const QDomElement &streamElement) +void QXmppIncomingServer::handleStream(const StreamOpen &stream) { - const QString from = streamElement.attribute(QStringLiteral("from")); - if (!from.isEmpty()) { - info(QStringLiteral("Incoming server stream from %1 on %2").arg(from, d->origin())); + if (!stream.from.isEmpty()) { + info(QStringLiteral("Incoming server stream from %1 on %2").arg(stream.from, d->origin())); } // start stream diff --git a/src/server/QXmppIncomingServer.h b/src/server/QXmppIncomingServer.h index bec03277a..b1038cc9f 100644 --- a/src/server/QXmppIncomingServer.h +++ b/src/server/QXmppIncomingServer.h @@ -11,10 +11,14 @@ class QXmppDialback; class QXmppIncomingServerPrivate; class QXmppOutgoingServer; +namespace QXmpp::Private { +struct StreamOpen; +} + +/// /// \brief The QXmppIncomingServer class represents an incoming XMPP stream /// from an XMPP server. /// - class QXMPP_EXPORT QXmppIncomingServer : public QXmppStream { Q_OBJECT @@ -36,7 +40,7 @@ class QXMPP_EXPORT QXmppIncomingServer : public QXmppStream protected: /// \cond void handleStanza(const QDomElement &stanzaElement) override; - void handleStream(const QDomElement &streamElement) override; + void handleStream(const QXmpp::Private::StreamOpen &) override; /// \endcond private Q_SLOTS: diff --git a/src/server/QXmppOutgoingServer.cpp b/src/server/QXmppOutgoingServer.cpp index 420f9806a..d6d914081 100644 --- a/src/server/QXmppOutgoingServer.cpp +++ b/src/server/QXmppOutgoingServer.cpp @@ -18,6 +18,8 @@ #include #include +using namespace QXmpp::Private; + class QXmppOutgoingServerPrivate { public: @@ -131,10 +133,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 5515bae68..256a8bd82 100644 --- a/src/server/QXmppOutgoingServer.h +++ b/src/server/QXmppOutgoingServer.h @@ -14,10 +14,14 @@ class QXmppDialback; class QXmppOutgoingServer; class QXmppOutgoingServerPrivate; +namespace QXmpp::Private { +struct StreamOpen; +} + +/// /// \brief The QXmppOutgoingServer class represents an outgoing XMPP stream /// to another XMPP server. /// - class QXMPP_EXPORT QXmppOutgoingServer : public QXmppStream { Q_OBJECT @@ -41,7 +45,7 @@ class QXMPP_EXPORT QXmppOutgoingServer : public QXmppStream protected: /// \cond void handleStart() override; - void handleStream(const QDomElement &streamElement) override; + void handleStream(const QXmpp::Private::StreamOpen &) override; void handleStanza(const QDomElement &stanzaElement) override; /// \endcond diff --git a/tests/qxmppstream/tst_qxmppstream.cpp b/tests/qxmppstream/tst_qxmppstream.cpp index 8edee098f..a20bcf13d 100644 --- a/tests/qxmppstream/tst_qxmppstream.cpp +++ b/tests/qxmppstream/tst_qxmppstream.cpp @@ -32,9 +32,9 @@ class TestStream : public QXmppStream Q_EMIT started(); } - void handleStream(const QDomElement &element) override + void handleStream(const StreamOpen &stream) override { - Q_EMIT streamReceived(element); + Q_EMIT streamReceived(stream); } void handleStanza(const QDomElement &element) override @@ -48,7 +48,7 @@ class TestStream : public QXmppStream } Q_SIGNAL void started(); - Q_SIGNAL void streamReceived(const QDomElement &element); + Q_SIGNAL void streamReceived(const QXmpp::Private::StreamOpen &); Q_SIGNAL void stanzaReceived(const QDomElement &element); }; @@ -72,6 +72,7 @@ class tst_QXmppStream : public QObject void tst_QXmppStream::initTestCase() { qRegisterMetaType(); + qRegisterMetaType(); } void tst_QXmppStream::testProcessData() @@ -97,16 +98,12 @@ void tst_QXmppStream::testProcessData() QCOMPARE(onStarted.size(), 0); // check stream information - const auto streamElement = onStreamReceived[0][0].value(); - QCOMPARE(streamElement.tagName(), QStringLiteral("stream")); - QCOMPARE(streamElement.namespaceURI(), QStringLiteral("http://etherx.jabber.org/streams")); - QCOMPARE(streamElement.attribute("from"), QStringLiteral("juliet@im.example.com")); - QCOMPARE(streamElement.attribute("to"), QStringLiteral("im.example.com")); - QCOMPARE(streamElement.attribute("version"), QStringLiteral("1.0")); - QCOMPARE(streamElement.attribute("lang"), QStringLiteral("en")); + 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")); - stream.processData(R"( - + stream.processData(R"( @@ -151,10 +148,9 @@ void tst_QXmppStream::streamOpen() QCOMPARE(r.readNext(), QXmlStreamReader::StartDocument); QCOMPARE(r.readNext(), QXmlStreamReader::StartElement); auto streamOpen = StreamOpen::fromXml(r); - QVERIFY(streamOpen.has_value()); - QCOMPARE(streamOpen->from, "juliet@im.example.com"); - QCOMPARE(streamOpen->to, "im.example.com"); - QCOMPARE(streamOpen->xmlns, ns_client); + QCOMPARE(streamOpen.from, "juliet@im.example.com"); + QCOMPARE(streamOpen.to, "im.example.com"); + QCOMPARE(streamOpen.xmlns, ns_client); } void tst_QXmppStream::testStreamError()