diff options
author | Arno Rehn <a.rehn@menlosystems.com> | 2022-01-02 22:31:03 +0100 |
---|---|---|
committer | Arno Rehn <a.rehn@menlosystems.com> | 2022-03-30 17:45:18 +0200 |
commit | cc4c24b99a87629aeb60df5af96d9bb991b56635 (patch) | |
tree | 62459a42457b0680022bb1fcb759f9c4e64d2c4b /src/websockets/qwebsocket_p.cpp | |
parent | 8545bb57efbfabf0dc7bc4b76efd6a99b4022669 (diff) | |
download | qtwebsockets-cc4c24b99a87629aeb60df5af96d9bb991b56635.tar.gz |
Add support for WebSocket Sub-Protocols
Sub-Protocol support follows RFC 6455 Sections 4.1 and 4.2. See also
https://datatracker.ietf.org/doc/html/rfc6455.
This patch introduces a new class QWebSocketHandshakeOptions which
collects options for the WebSocket handshake. At the moment, this
contains only accessors for sub-protocols. In the future, it can be
extended with things like WebSocket extensions.
[ChangeLog] Add support for WebSocket Sub-Protocols
Fixes: QTBUG-38742
Change-Id: Ibdcef17f717f0a76caab54f65c550865df1ec78d
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: MÃ¥rten Nordheim <marten.nordheim@qt.io>
Diffstat (limited to 'src/websockets/qwebsocket_p.cpp')
-rw-r--r-- | src/websockets/qwebsocket_p.cpp | 100 |
1 files changed, 85 insertions, 15 deletions
diff --git a/src/websockets/qwebsocket_p.cpp b/src/websockets/qwebsocket_p.cpp index b0c3e4e..2cb5419 100644 --- a/src/websockets/qwebsocket_p.cpp +++ b/src/websockets/qwebsocket_p.cpp @@ -78,6 +78,40 @@ constexpr int MAX_HEADERLINES = 100; // maximum number of http reques constexpr quint64 MAX_OUTGOING_FRAME_SIZE_IN_BYTES = std::numeric_limits<int>::max() - 1; constexpr quint64 DEFAULT_OUTGOING_FRAME_SIZE_IN_BYTES = 512 * 512 * 2; // default size of a frame when sending a message +// Based on isSeperator() from qtbase/src/network/access/qhsts.cpp +// https://datatracker.ietf.org/doc/html/rfc2616#section-2.2: +// +// separators = "(" | ")" | "<" | ">" | "@" +// | "," | ";" | ":" | "\" | <"> +// | "/" | "[" | "]" | "?" | "=" +// | "{" | "}" | SP | HT +// TODO: Should probably make things like this re-usable as private API of QtNetwork +bool isSeparator(char c) +{ + // separators = "(" | ")" | "<" | ">" | "@" + // | "," | ";" | ":" | "\" | <"> + // | "/" | "[" | "]" | "?" | "=" + // | "{" | "}" | SP | HT + static const char separators[] = "()<>@,;:\\\"/[]?={} \t"; + static const char *end = separators + sizeof separators - 1; + return std::find(separators, end, c) != end; +} + +// https://datatracker.ietf.org/doc/html/rfc6455#section-4.1: +// 10. The request MAY include a header field with the name +// |Sec-WebSocket-Protocol|. If present, this value indicates one +// or more comma-separated subprotocol the client wishes to speak, +// ordered by preference. The elements that comprise this value +// MUST be non-empty strings with characters in the range U+0021 to +// U+007E not including separator characters as defined in +// [RFC2616] and MUST all be unique strings. +bool isValidSubProtocolName(const QString &protocol) +{ + return std::all_of(protocol.begin(), protocol.end(), [](const QChar &c) { + return c.unicode() >= 0x21 && c.unicode() <= 0x7E && !isSeparator(c.toLatin1()); + }); +} + } QWebSocketConfiguration::QWebSocketConfiguration() : @@ -339,9 +373,12 @@ QWebSocket *QWebSocketPrivate::upgradeFrom(QTcpSocket *pTcpSocket, if (QSslSocket *sslSock = qobject_cast<QSslSocket *>(pTcpSocket)) pWebSocket->setSslConfiguration(sslSock->sslConfiguration()); #endif + QWebSocketHandshakeOptions options; + options.setSubprotocols(request.protocols()); + pWebSocket->d_func()->setExtension(response.acceptedExtension()); pWebSocket->d_func()->setOrigin(request.origin()); - pWebSocket->d_func()->setRequest(netRequest); + pWebSocket->d_func()->setRequest(netRequest, options); pWebSocket->d_func()->setProtocol(response.acceptedProtocol()); pWebSocket->d_func()->setResourceName(request.requestUrl().toString(QUrl::RemoveUserInfo)); //a server should not send masked frames @@ -394,7 +431,8 @@ void QWebSocketPrivate::close(QWebSocketProtocol::CloseCode closeCode, QString r /*! \internal */ -void QWebSocketPrivate::open(const QNetworkRequest &request, bool mask) +void QWebSocketPrivate::open(const QNetworkRequest &request, + const QWebSocketHandshakeOptions &options, bool mask) { //just delete the old socket for the moment; //later, we can add more 'intelligent' handling by looking at the URL @@ -417,7 +455,7 @@ void QWebSocketPrivate::open(const QNetworkRequest &request, bool mask) m_isClosingHandshakeReceived = false; m_isClosingHandshakeSent = false; - setRequest(request); + setRequest(request, options); QString resourceName = url.path(QUrl::FullyEncoded); // Check for encoded \r\n if (resourceName.contains(QStringLiteral("%0D%0A"))) { @@ -553,10 +591,13 @@ void QWebSocketPrivate::setResourceName(const QString &resourceName) /*! \internal */ -void QWebSocketPrivate::setRequest(const QNetworkRequest &request) +void QWebSocketPrivate::setRequest(const QNetworkRequest &request, + const QWebSocketHandshakeOptions &options) { if (m_request != request) m_request = request; + if (m_options != options) + m_options = options; } /*! @@ -721,6 +762,14 @@ QString QWebSocketPrivate::origin() const /*! \internal */ +QWebSocketHandshakeOptions QWebSocketPrivate::handshakeOptions() const +{ + return m_options; +} + +/*! + \internal + */ QString QWebSocketPrivate::protocol() const { return m_protocol; @@ -988,9 +1037,18 @@ void QWebSocketPrivate::processHandshake(QTcpSocket *pSocket) #if 0 // unused for the moment const QString extensions = QString::fromLatin1(parser.combinedHeaderValue( QByteArrayLiteral("sec-websocket-extensions")); - const QString protocol = QString::fromLatin1(parser.combinedHeaderValue( - QByteArrayLiteral("sec-websocket-protocol")); #endif + const QString protocol = QString::fromLatin1(parser.combinedHeaderValue( + QByteArrayLiteral("sec-websocket-protocol"))); + + if (!protocol.isEmpty() && !handshakeOptions().subprotocols().contains(protocol)) { + setErrorString(QWebSocket::tr("WebSocket server has chosen protocol %1 which has not been " + "requested") + .arg(protocol)); + Q_EMIT q->error(QAbstractSocket::ConnectionRefusedError); + return; + } + const QString version = QString::fromLatin1(parser.combinedHeaderValue( QByteArrayLiteral("sec-websocket-version"))); bool ok = false; @@ -1043,6 +1101,7 @@ void QWebSocketPrivate::processHandshake(QTcpSocket *pSocket) if (ok) { // handshake succeeded + setProtocol(protocol); setSocketState(QAbstractSocket::ConnectedState); Q_EMIT q->connected(); } else { @@ -1084,7 +1143,7 @@ void QWebSocketPrivate::processStateChanged(QAbstractSocket::SocketState socketS host, origin(), QString(), - QString(), + m_options.subprotocols(), m_key, headers); if (handshake.isEmpty()) { @@ -1189,7 +1248,7 @@ QString QWebSocketPrivate::createHandShakeRequest(QString resourceName, QString host, QString origin, QString extensions, - QString protocols, + const QStringList &protocols, QByteArray key, const QList<QPair<QString, QString> > &headers) { @@ -1214,11 +1273,6 @@ QString QWebSocketPrivate::createHandShakeRequest(QString resourceName, "Possible attack detected.")); return QString(); } - if (protocols.contains(QStringLiteral("\r\n"))) { - setErrorString(QWebSocket::tr("The protocols attribute contains newlines. " \ - "Possible attack detected.")); - return QString(); - } handshakeRequest << QStringLiteral("GET ") % resourceName % QStringLiteral(" HTTP/1.1") << QStringLiteral("Host: ") % host << @@ -1231,8 +1285,24 @@ QString QWebSocketPrivate::createHandShakeRequest(QString resourceName, % QString::number(QWebSocketProtocol::currentVersion()); if (extensions.length() > 0) handshakeRequest << QStringLiteral("Sec-WebSocket-Extensions: ") % extensions; - if (protocols.length() > 0) - handshakeRequest << QStringLiteral("Sec-WebSocket-Protocol: ") % protocols; + + const QStringList validProtocols = [&] { + QStringList validProtocols; + validProtocols.reserve(protocols.size()); + for (const auto &p : protocols) { + if (isValidSubProtocolName(p)) + validProtocols.append(p); + else + qWarning() << "Ignoring invalid WebSocket subprotocol name" << p; + } + + return validProtocols; + }(); + + if (!protocols.isEmpty()) { + handshakeRequest << QStringLiteral("Sec-WebSocket-Protocol: ") + % validProtocols.join(QLatin1String(", ")); + } for (const auto &header : headers) handshakeRequest << header.first % QStringLiteral(": ") % header.second; |