Skip to content
This repository was archived by the owner on Mar 20, 2025. It is now read-only.

Commit 2a7fcad

Browse files
committed
XmppSocket: Parse whole stream through QXmlStreamReader
1 parent 6bb28e3 commit 2a7fcad

12 files changed

+258
-133
lines changed

src/base/Stream.cpp

Lines changed: 177 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ StreamOpen StreamOpen::fromXml(QXmlStreamReader &reader)
4343

4444
out.from = attribute({}, u"from");
4545
out.to = attribute({}, u"to");
46+
out.id = attribute({}, u"id");
47+
out.version = attribute({}, u"version");
4648

4749
const auto namespaceDeclarations = reader.namespaceDeclarations();
4850
for (const auto &ns : namespaceDeclarations) {
@@ -58,11 +60,10 @@ void StreamOpen::toXml(QXmlStreamWriter *writer) const
5860
{
5961
writer->writeStartDocument();
6062
writer->writeStartElement(QSL65("stream:stream"));
61-
if (!from.isEmpty()) {
62-
writer->writeAttribute(QSL65("from"), from);
63-
}
64-
writer->writeAttribute(QSL65("to"), to);
65-
writer->writeAttribute(QSL65("version"), QSL65("1.0"));
63+
writeOptionalXmlAttribute(writer, u"from", from);
64+
writeOptionalXmlAttribute(writer, u"to", to);
65+
writeOptionalXmlAttribute(writer, u"id", id);
66+
writeOptionalXmlAttribute(writer, u"version", version);
6667
writer->writeDefaultNamespace(xmlns);
6768
writer->writeNamespace(toString65(ns_stream), QSL65("stream"));
6869
writer->writeCharacters({});
@@ -170,6 +171,88 @@ std::variant<StreamErrorElement, QXmppError> StreamErrorElement::fromDom(const Q
170171
}
171172
/// \endcond
172173

174+
DomReader::State DomReader::process(QXmlStreamReader &r)
175+
{
176+
while (true) {
177+
switch (r.tokenType()) {
178+
case QXmlStreamReader::Invalid:
179+
// error received
180+
if (r.error() == QXmlStreamReader::PrematureEndOfDocumentError) {
181+
return Unfinished;
182+
}
183+
return ErrorOccurred;
184+
case QXmlStreamReader::StartElement: {
185+
qDebug() << "start element token";
186+
auto child = r.prefix().isNull()
187+
? doc.createElement(r.name().toString())
188+
: doc.createElementNS(r.namespaceUri().toString(), r.qualifiedName().toString());
189+
190+
// xmlns attribute
191+
const auto nsDeclarations = r.namespaceDeclarations();
192+
for (const auto &ns : nsDeclarations) {
193+
if (ns.prefix().isEmpty()) {
194+
child.setAttribute(QStringLiteral("xmlns"), ns.namespaceUri().toString());
195+
} else {
196+
// namespace declarations are not supported in XMPP
197+
qDebug() << "err namespace decl";
198+
return ErrorOccurred;
199+
}
200+
}
201+
202+
// other attributes
203+
const auto attributes = r.attributes();
204+
for (const auto &a : attributes) {
205+
child.setAttribute(a.name().toString(), a.value().toString());
206+
}
207+
208+
if (currentElement.isNull()) {
209+
doc.appendChild(child);
210+
} else {
211+
currentElement.appendChild(child);
212+
}
213+
depth++;
214+
currentElement = child;
215+
break;
216+
}
217+
case QXmlStreamReader::EndElement:
218+
qDebug() << "end element token";
219+
if (depth == 0) {
220+
qDebug() << "depth == 0";
221+
return ErrorOccurred;
222+
}
223+
224+
currentElement = currentElement.parentNode().toElement();
225+
depth--;
226+
qDebug() << "depth" << depth;
227+
if (depth == 0) {
228+
return Finished;
229+
}
230+
break;
231+
case QXmlStreamReader::Characters:
232+
if (depth == 0) {
233+
qDebug() << "depth == 0";
234+
return ErrorOccurred;
235+
}
236+
237+
currentElement.appendChild(doc.createTextNode(r.text().toString()));
238+
break;
239+
case QXmlStreamReader::NoToken:
240+
// skip
241+
break;
242+
case QXmlStreamReader::StartDocument:
243+
case QXmlStreamReader::EndDocument:
244+
case QXmlStreamReader::Comment:
245+
case QXmlStreamReader::DTD:
246+
case QXmlStreamReader::EntityReference:
247+
case QXmlStreamReader::ProcessingInstruction:
248+
qDebug() << "not allowed";
249+
// not allowed or unexpected
250+
return ErrorOccurred;
251+
}
252+
r.readNext();
253+
}
254+
}
255+
173256
XmppSocket::XmppSocket(QObject *parent)
174257
: QXmppLoggable(parent)
175258
{
@@ -189,16 +272,16 @@ void XmppSocket::setSocket(QSslSocket *socket)
189272

190273
// do not emit started() with direct TLS (this happens in encrypted())
191274
if (!m_directTls) {
192-
m_dataBuffer.clear();
193-
m_streamOpenElement.clear();
275+
m_reader.clear();
276+
m_streamReceived = false;
194277
Q_EMIT started();
195278
}
196279
});
197280
QObject::connect(socket, &QSslSocket::encrypted, this, [this]() {
198281
debug(u"Socket encrypted"_s);
199282
// this happens with direct TLS or STARTTLS
200-
m_dataBuffer.clear();
201-
m_streamOpenElement.clear();
283+
m_reader.clear();
284+
m_streamReceived = false;
202285
Q_EMIT started();
203286
});
204287
QObject::connect(socket, &QSslSocket::errorOccurred, this, [this](QAbstractSocket::SocketError) {
@@ -256,102 +339,99 @@ bool XmppSocket::sendData(const QByteArray &data)
256339

257340
void XmppSocket::processData(const QString &data)
258341
{
259-
// As we may only have partial XML content, we need to cache the received
260-
// data until it has been successfully parsed. In case it can't be parsed,
261-
//
262-
// There are only two small problems with the current strategy:
263-
// * When we receive a full stanza + a partial one, we can't parse the
264-
// first stanza until another stanza arrives that is complete.
265-
// * We don't know when we received invalid XML (would cause a growing
266-
// cache and a timeout after some time).
267-
// However, both issues could only be solved using an XML stream reader
268-
// which would cause many other problems since we don't actually use it for
269-
// parsing the content.
270-
m_dataBuffer.append(data);
271-
272-
//
273342
// Check for whitespace pings
274-
//
275-
if (m_dataBuffer.isEmpty() || m_dataBuffer.trimmed().isEmpty()) {
276-
m_dataBuffer.clear();
277-
343+
if (data.isEmpty()) {
344+
qDebug() << "empty ping received";
278345
logReceived({});
279346
Q_EMIT stanzaReceived(QDomElement());
280347
return;
281348
}
282349

283-
//
284-
// Check whether we received a stream open or closing tag
285-
//
286-
static const QRegularExpression streamStartRegex(uR"(^(<\?xml.*\?>)?\s*<stream:stream[^>]*>)"_s);
287-
static const QRegularExpression streamEndRegex(u"</stream:stream>$"_s);
288-
289-
auto streamOpenMatch = streamStartRegex.match(m_dataBuffer);
290-
bool hasStreamOpen = streamOpenMatch.hasMatch();
291-
292-
bool hasStreamClose = streamEndRegex.match(m_dataBuffer).hasMatch();
293-
294-
//
295-
// The stream start/end and stanza packets can't be parsed without any
296-
// modifications with QDomDocument. This is because of multiple reasons:
297-
// * The <stream:stream> open element is not considered valid without the
298-
// closing tag.
299-
// * Only the closing tag is of course not valid too.
300-
// * Stanzas/Nonzas need to have the correct stream namespaces set:
301-
// * For being able to parse <stream:features/>
302-
// * For having the correct namespace (e.g. 'jabber:client') set to
303-
// stanzas and their child elements (e.g. <body/> of a message).
304-
//
305-
// The wrapping strategy looks like this:
306-
// * The stream open tag is cached once it arrives, for later access
307-
// * Incoming XML that has no <stream> open tag will be prepended by the
308-
// cached <stream> tag.
309-
// * Incoming XML that has no <stream> close tag will be appended by a
310-
// generic string "</stream:stream>"
311-
//
312-
// The result is parsed by QDomDocument and the child elements of the stream
313-
// are processed. In case the received data contained a stream open tag,
314-
// the stream is processed (before the stanzas are processed). In case we
315-
// received a </stream> closing tag, the connection is closed.
316-
//
317-
auto wrappedStanzas = m_dataBuffer;
318-
if (!hasStreamOpen) {
319-
wrappedStanzas.prepend(m_streamOpenElement);
320-
}
321-
if (!hasStreamClose) {
322-
wrappedStanzas.append(u"</stream:stream>"_s);
323-
}
324-
325-
//
326-
// Try to parse the wrapped XML
327-
//
328-
QDomDocument doc;
329-
if (!doc.setContent(wrappedStanzas, true)) {
330-
return;
331-
}
332-
333-
//
334-
// Success: We can clear the buffer and send a 'received' log message
335-
//
336-
logReceived(m_dataBuffer);
337-
m_dataBuffer.clear();
338-
339-
// process stream start
340-
if (hasStreamOpen) {
341-
m_streamOpenElement = streamOpenMatch.captured();
342-
Q_EMIT streamReceived(doc.documentElement());
343-
}
344-
345-
// process stanzas
346-
auto stanza = doc.documentElement().firstChildElement();
347-
for (; !stanza.isNull(); stanza = stanza.nextSiblingElement()) {
348-
Q_EMIT stanzaReceived(stanza);
350+
// log data received and process
351+
logReceived(data);
352+
m_reader.addData(data);
353+
354+
// we're still reading a previously started top-level element
355+
if (m_domReader) {
356+
m_reader.readNext();
357+
switch (m_domReader->process(m_reader)) {
358+
case DomReader::Finished:
359+
Q_EMIT stanzaReceived(m_domReader->element());
360+
m_domReader.reset();
361+
break;
362+
case DomReader::Unfinished:
363+
return;
364+
case DomReader::ErrorOccurred:
365+
// emit error
366+
break;
367+
}
349368
}
350369

351-
// process stream end
352-
if (hasStreamClose) {
353-
Q_EMIT streamClosed();
354-
}
370+
do {
371+
switch (m_reader.readNext()) {
372+
case QXmlStreamReader::Invalid:
373+
// error received
374+
if (m_reader.error() != QXmlStreamReader::PrematureEndOfDocumentError) {
375+
// emit error
376+
}
377+
break;
378+
case QXmlStreamReader::StartDocument:
379+
// pre-stream open
380+
break;
381+
case QXmlStreamReader::EndDocument:
382+
// post-stream close
383+
break;
384+
case QXmlStreamReader::StartElement:
385+
// stream open or stream-level element
386+
if (m_reader.name() == u"stream" && m_reader.namespaceUri() == ns_stream) {
387+
m_streamReceived = true;
388+
Q_EMIT streamReceived(StreamOpen::fromXml(m_reader));
389+
} else if (!m_streamReceived) {
390+
// error: expected stream open element
391+
qDebug() << "err no stream recevied";
392+
} else {
393+
qDebug() << "start el";
394+
// parse top-level stream element
395+
m_domReader = DomReader();
396+
397+
switch (m_domReader->process(m_reader)) {
398+
case DomReader::Finished:
399+
Q_EMIT stanzaReceived(m_domReader->element());
400+
m_domReader.reset();
401+
break;
402+
case DomReader::Unfinished:
403+
qDebug() << "unfi";
404+
return;
405+
case DomReader::ErrorOccurred:
406+
qDebug() << "el err";
407+
// emit error
408+
break;
409+
}
410+
}
411+
break;
412+
case QXmlStreamReader::EndElement:
413+
// end of stream
414+
Q_EMIT streamClosed();
415+
break;
416+
case QXmlStreamReader::Characters:
417+
if (m_reader.isWhitespace()) {
418+
logReceived({});
419+
Q_EMIT stanzaReceived(QDomElement());
420+
} else {
421+
// invalid: emit error
422+
}
423+
break;
424+
case QXmlStreamReader::NoToken:
425+
// skip
426+
break;
427+
case QXmlStreamReader::Comment:
428+
case QXmlStreamReader::DTD:
429+
case QXmlStreamReader::EntityReference:
430+
case QXmlStreamReader::ProcessingInstruction:
431+
// not allowed in XMPP: emit error
432+
break;
433+
}
434+
} while (!m_reader.hasError());
355435
}
356436

357437
} // namespace QXmpp::Private

src/base/Stream.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#include <optional>
99

10+
#include <QMetaType>
1011
#include <QString>
1112

1213
class QDomElement;
@@ -21,6 +22,8 @@ struct StreamOpen {
2122

2223
QString to;
2324
QString from;
25+
QString id;
26+
QString version;
2427
QString xmlns;
2528
};
2629

@@ -44,4 +47,6 @@ struct CsiInactive {
4447

4548
} // namespace QXmpp::Private
4649

50+
Q_DECLARE_METATYPE(QXmpp::Private::StreamOpen)
51+
4752
#endif // STREAM_H

0 commit comments

Comments
 (0)