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 | |
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')
-rw-r--r-- | src/imports/qmlwebsockets/qqmlwebsocket.cpp | 43 | ||||
-rw-r--r-- | src/imports/qmlwebsockets/qqmlwebsocket.h | 12 | ||||
-rw-r--r-- | src/imports/qmlwebsockets/qqmlwebsocketserver.cpp | 25 | ||||
-rw-r--r-- | src/imports/qmlwebsockets/qqmlwebsocketserver.h | 7 | ||||
-rw-r--r-- | src/websockets/CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/websockets/qwebsocket.cpp | 63 | ||||
-rw-r--r-- | src/websockets/qwebsocket.h | 8 | ||||
-rw-r--r-- | src/websockets/qwebsocket_p.cpp | 100 | ||||
-rw-r--r-- | src/websockets/qwebsocket_p.h | 9 | ||||
-rw-r--r-- | src/websockets/qwebsocket_wasm_p.cpp | 16 | ||||
-rw-r--r-- | src/websockets/qwebsockethandshakeoptions.cpp | 147 | ||||
-rw-r--r-- | src/websockets/qwebsockethandshakeoptions.h | 83 | ||||
-rw-r--r-- | src/websockets/qwebsockethandshakeoptions_p.h | 71 | ||||
-rw-r--r-- | src/websockets/qwebsockethandshakeresponse.cpp | 22 | ||||
-rw-r--r-- | src/websockets/qwebsocketserver.cpp | 24 | ||||
-rw-r--r-- | src/websockets/qwebsocketserver.h | 3 | ||||
-rw-r--r-- | src/websockets/qwebsocketserver_p.cpp | 15 | ||||
-rw-r--r-- | src/websockets/qwebsocketserver_p.h | 4 |
18 files changed, 611 insertions, 42 deletions
diff --git a/src/imports/qmlwebsockets/qqmlwebsocket.cpp b/src/imports/qmlwebsockets/qqmlwebsocket.cpp index a7bb0e0..a96b2c0 100644 --- a/src/imports/qmlwebsockets/qqmlwebsocket.cpp +++ b/src/imports/qmlwebsockets/qqmlwebsocket.cpp @@ -58,6 +58,18 @@ */ /*! + \qmlproperty QStringList WebSocket::requestedSubprotocols + \since 6.4 + The list of WebSocket subprotocols to send in the WebSocket handshake. +*/ + +/*! + \qmlproperty QString WebSocket::negotiatedSubprotocol + \since 6.4 + The WebSocket subprotocol that has been negotiated with the server. +*/ + +/*! \qmlproperty Status WebSocket::status Status of the WebSocket. @@ -118,6 +130,7 @@ #include "qqmlwebsocket.h" #include <QtWebSockets/QWebSocket> +#include <QtWebSockets/QWebSocketHandshakeOptions> QT_BEGIN_NAMESPACE @@ -136,6 +149,7 @@ QQmlWebSocket::QQmlWebSocket(QWebSocket *socket, QObject *parent) : QObject(parent), m_status(Closed), m_url(socket->requestUrl()), + m_requestedProtocols(socket->handshakeOptions().subprotocols()), m_isActive(true), m_componentCompleted(true), m_errorString(socket->errorString()) @@ -168,6 +182,20 @@ qint64 QQmlWebSocket::sendBinaryMessage(const QByteArray &message) return m_webSocket->sendBinaryMessage(message); } +QStringList QQmlWebSocket::requestedSubprotocols() const +{ + return m_requestedProtocols; +} + +void QQmlWebSocket::setRequestedSubprotocols(const QStringList &requestedSubprotocols) +{ + if (m_requestedProtocols == requestedSubprotocols) + return; + + m_requestedProtocols = requestedSubprotocols; + emit requestedSubprotocolsChanged(); +} + QUrl QQmlWebSocket::url() const { return m_url; @@ -186,6 +214,11 @@ void QQmlWebSocket::setUrl(const QUrl &url) open(); } +QString QQmlWebSocket::negotiatedSubprotocol() const +{ + return m_negotiatedProtocol; +} + QQmlWebSocket::Status QQmlWebSocket::status() const { return m_status; @@ -281,6 +314,12 @@ void QQmlWebSocket::setStatus(QQmlWebSocket::Status status) setErrorString(); } Q_EMIT statusChanged(m_status); + + const auto protocol = m_status == Open && m_webSocket ? m_webSocket->subprotocol() : QString(); + if (m_negotiatedProtocol != protocol) { + m_negotiatedProtocol = protocol; + Q_EMIT negotiatedSubprotocolChanged(); + } } void QQmlWebSocket::setActive(bool active) @@ -308,7 +347,9 @@ bool QQmlWebSocket::isActive() const void QQmlWebSocket::open() { if (m_componentCompleted && m_isActive && m_url.isValid() && Q_LIKELY(m_webSocket)) { - m_webSocket->open(m_url); + QWebSocketHandshakeOptions opt; + opt.setSubprotocols(m_requestedProtocols); + m_webSocket->open(m_url, opt); } } diff --git a/src/imports/qmlwebsockets/qqmlwebsocket.h b/src/imports/qmlwebsockets/qqmlwebsocket.h index 7662607..1cd28c4 100644 --- a/src/imports/qmlwebsockets/qqmlwebsocket.h +++ b/src/imports/qmlwebsockets/qqmlwebsocket.h @@ -55,9 +55,13 @@ class QQmlWebSocket : public QObject, public QQmlParserStatus Q_INTERFACES(QQmlParserStatus) Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged) + Q_PROPERTY(QStringList requestedSubprotocols READ requestedSubprotocols + WRITE setRequestedSubprotocols NOTIFY requestedSubprotocolsChanged) Q_PROPERTY(Status status READ status NOTIFY statusChanged) Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged) Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged) + Q_PROPERTY(QString negotiatedSubprotocol READ negotiatedSubprotocol + NOTIFY negotiatedSubprotocolChanged) public: explicit QQmlWebSocket(QObject *parent = 0); @@ -76,6 +80,10 @@ public: QUrl url() const; void setUrl(const QUrl &url); + QStringList requestedSubprotocols() const; + void setRequestedSubprotocols(const QStringList &subprotocols); + + QString negotiatedSubprotocol() const; Status status() const; QString errorString() const; @@ -92,6 +100,8 @@ Q_SIGNALS: void activeChanged(bool isActive); void errorStringChanged(QString errorString); void urlChanged(); + void requestedSubprotocolsChanged(); + void negotiatedSubprotocolChanged(); public: void classBegin() override; @@ -103,8 +113,10 @@ private Q_SLOTS: private: QScopedPointer<QWebSocket> m_webSocket; + QString m_negotiatedProtocol; Status m_status; QUrl m_url; + QStringList m_requestedProtocols; bool m_isActive; bool m_componentCompleted; QString m_errorString; diff --git a/src/imports/qmlwebsockets/qqmlwebsocketserver.cpp b/src/imports/qmlwebsockets/qqmlwebsocketserver.cpp index 4ff304c..a59ca94 100644 --- a/src/imports/qmlwebsockets/qqmlwebsocketserver.cpp +++ b/src/imports/qmlwebsockets/qqmlwebsocketserver.cpp @@ -76,6 +76,12 @@ QT_USE_NAMESPACE */ /*! + \qmlproperty QStringList WebSocketServer::supportedSubprotocols + \since 6.4 + The list of protocols supported by the server. +*/ + +/*! \qmlproperty QString WebSocketServer::errorString The stringified error message in case an error occurred. */ @@ -200,6 +206,19 @@ void QQmlWebSocketServer::setName(const QString &name) } } +void QQmlWebSocketServer::setSupportedSubprotocols(const QStringList &supportedSubprotocols) +{ + if (m_supportedSubprotocols == supportedSubprotocols) + return; + + m_supportedSubprotocols = supportedSubprotocols; + + if (m_server) + m_server->setSupportedSubprotocols(m_supportedSubprotocols); + + emit supportedSubprotocolsChanged(m_supportedSubprotocols); +} + bool QQmlWebSocketServer::listen() const { return m_listen; @@ -252,6 +271,8 @@ void QQmlWebSocketServer::init() connect(m_server.data(), &QWebSocketServer::closed, this, &QQmlWebSocketServer::closed); + m_server->setSupportedSubprotocols(m_supportedSubprotocols); + updateListening(); } @@ -287,3 +308,7 @@ void QQmlWebSocketServer::closed() setListen(false); } +QStringList QQmlWebSocketServer::supportedSubprotocols() const +{ + return m_supportedSubprotocols; +} diff --git a/src/imports/qmlwebsockets/qqmlwebsocketserver.h b/src/imports/qmlwebsockets/qqmlwebsocketserver.h index 98dbfc4..d0f6a63 100644 --- a/src/imports/qmlwebsockets/qqmlwebsocketserver.h +++ b/src/imports/qmlwebsockets/qqmlwebsocketserver.h @@ -57,6 +57,8 @@ class QQmlWebSocketServer : public QObject, public QQmlParserStatus Q_PROPERTY(QString host READ host WRITE setHost NOTIFY hostChanged) Q_PROPERTY(int port READ port WRITE setPort NOTIFY portChanged) Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) + Q_PROPERTY(QStringList supportedSubprotocols READ supportedSubprotocols + WRITE setSupportedSubprotocols NOTIFY supportedSubprotocolsChanged) Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged) Q_PROPERTY(bool listen READ listen WRITE setListen NOTIFY listenChanged) Q_PROPERTY(bool accept READ accept WRITE setAccept NOTIFY acceptChanged) @@ -79,6 +81,9 @@ public: QString name() const; void setName(const QString &name); + QStringList supportedSubprotocols() const; + void setSupportedSubprotocols(const QStringList &supportedSubprotocols); + QString errorString() const; bool listen() const; @@ -94,6 +99,7 @@ Q_SIGNALS: void urlChanged(const QUrl &url); void portChanged(int port); void nameChanged(const QString &name); + void supportedSubprotocolsChanged(const QStringList &supportedProtocols); void hostChanged(const QString &host); void listenChanged(bool listen); void acceptChanged(bool accept); @@ -108,6 +114,7 @@ private: QScopedPointer<QWebSocketServer> m_server; QString m_host; QString m_name; + QStringList m_supportedSubprotocols; int m_port; bool m_listen; bool m_accept; diff --git a/src/websockets/CMakeLists.txt b/src/websockets/CMakeLists.txt index e03ba54..9dc96cd 100644 --- a/src/websockets/CMakeLists.txt +++ b/src/websockets/CMakeLists.txt @@ -12,6 +12,7 @@ qt_internal_add_module(WebSockets qwebsocketcorsauthenticator.cpp qwebsocketcorsauthenticator.h qwebsocketcorsauthenticator_p.h qwebsocketdataprocessor.cpp qwebsocketdataprocessor_p.h qwebsocketframe.cpp qwebsocketframe_p.h + qwebsockethandshakeoptions.cpp qwebsockethandshakeoptions.h qwebsockethandshakeoptions_p.h qwebsockethandshakerequest.cpp qwebsockethandshakerequest_p.h qwebsockethandshakeresponse.cpp qwebsockethandshakeresponse_p.h qwebsocketprotocol.cpp qwebsocketprotocol.h qwebsocketprotocol_p.h diff --git a/src/websockets/qwebsocket.cpp b/src/websockets/qwebsocket.cpp index 63122d1..956c9dc 100644 --- a/src/websockets/qwebsocket.cpp +++ b/src/websockets/qwebsocket.cpp @@ -52,9 +52,7 @@ This class was modeled after QAbstractSocket. - QWebSocket currently does not support - \l {WebSocket Extensions} and - \l {WebSocket Subprotocols}. + QWebSocket currently does not support \l {WebSocket Extensions}. QWebSocket only supports version 13 of the WebSocket protocol, as outlined in \l {RFC 6455}. @@ -332,6 +330,7 @@ not been filled in with new information when the signal returns. */ #include "qwebsocket.h" #include "qwebsocket_p.h" +#include "qwebsockethandshakeoptions.h" #include <QtCore/QUrl> #include <QtNetwork/QTcpSocket> @@ -484,7 +483,7 @@ void QWebSocket::open(const QUrl &url) { Q_D(QWebSocket); QNetworkRequest request(url); - d->open(request, true); + d->open(request, QWebSocketHandshakeOptions{}, true); } /*! @@ -498,7 +497,41 @@ void QWebSocket::open(const QUrl &url) void QWebSocket::open(const QNetworkRequest &request) { Q_D(QWebSocket); - d->open(request, true); + d->open(request, QWebSocketHandshakeOptions{}, true); +} + +/*! + \brief Opens a WebSocket connection using the given \a url and \a options. + \since 6.4 + + If the url contains newline characters (\\r\\n), then the error signal will be emitted + with QAbstractSocket::ConnectionRefusedError as error type. + + Additional options for the WebSocket handshake such as subprotocols can be specified in + \a options. + */ +void QWebSocket::open(const QUrl &url, const QWebSocketHandshakeOptions &options) +{ + Q_D(QWebSocket); + QNetworkRequest request(url); + d->open(request, options, true); +} + +/*! + \brief Opens a WebSocket connection using the given \a request and \a options. + \since 6.4 + + The \a request url will be used to open the WebSocket connection. + Headers present in the request will be sent to the server in the upgrade request, + together with the ones needed for the websocket handshake. + + Additional options for the WebSocket handshake such as subprotocols can be specified in + \a options. + */ +void QWebSocket::open(const QNetworkRequest &request, const QWebSocketHandshakeOptions &options) +{ + Q_D(QWebSocket); + d->open(request, options, true); } /*! @@ -654,6 +687,16 @@ QNetworkRequest QWebSocket::request() const } /*! + \brief Returns the handshake options that were used to open this socket. + \since 6.4 + */ +QWebSocketHandshakeOptions QWebSocket::handshakeOptions() const +{ + Q_D(const QWebSocket); + return d->handshakeOptions(); +} + +/*! \brief Returns the current origin. */ QString QWebSocket::origin() const @@ -663,6 +706,16 @@ QString QWebSocket::origin() const } /*! + \brief Returns the used WebSocket protocol. + \since 6.4 + */ +QString QWebSocket::subprotocol() const +{ + Q_D(const QWebSocket); + return d->protocol(); +} + +/*! \brief Returns the code indicating why the socket was closed. \sa QWebSocketProtocol::CloseCode, closeReason() */ diff --git a/src/websockets/qwebsocket.h b/src/websockets/qwebsocket.h index 18e8019..7337588 100644 --- a/src/websockets/qwebsocket.h +++ b/src/websockets/qwebsocket.h @@ -58,6 +58,7 @@ QT_BEGIN_NAMESPACE class QTcpSocket; class QWebSocketPrivate; class QMaskGenerator; +class QWebSocketHandshakeOptions; class Q_WEBSOCKETS_EXPORT QWebSocket : public QObject { @@ -100,7 +101,9 @@ public: QString resourceName() const; QUrl requestUrl() const; QNetworkRequest request() const; + QWebSocketHandshakeOptions handshakeOptions() const; QString origin() const; + QString subprotocol() const; QWebSocketProtocol::CloseCode closeCode() const; QString closeReason() const; @@ -131,8 +134,13 @@ public: public Q_SLOTS: void close(QWebSocketProtocol::CloseCode closeCode = QWebSocketProtocol::CloseCodeNormal, const QString &reason = QString()); + + // ### Qt7: Merge overloads void open(const QUrl &url); void open(const QNetworkRequest &request); + void open(const QUrl &url, const QWebSocketHandshakeOptions &options); + void open(const QNetworkRequest &request, const QWebSocketHandshakeOptions &options); + void ping(const QByteArray &payload = QByteArray()); #ifndef QT_NO_SSL void ignoreSslErrors(); 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; diff --git a/src/websockets/qwebsocket_p.h b/src/websockets/qwebsocket_p.h index 77cb3e8..5461e2f 100644 --- a/src/websockets/qwebsocket_p.h +++ b/src/websockets/qwebsocket_p.h @@ -65,6 +65,7 @@ #include <private/qobject_p.h> #include "qwebsocket.h" +#include "qwebsockethandshakeoptions.h" #include "qwebsocketprotocol.h" #include "qwebsocketdataprocessor_p.h" #include "qdefaultmaskgenerator_p.h" @@ -138,6 +139,7 @@ public: QString resourceName() const; QNetworkRequest request() const; QString origin() const; + QWebSocketHandshakeOptions handshakeOptions() const; QString protocol() const; QString extension() const; QWebSocketProtocol::CloseCode closeCode() const; @@ -157,7 +159,7 @@ public: void closeGoingAway(); void close(QWebSocketProtocol::CloseCode closeCode, QString reason); - void open(const QNetworkRequest &request, bool mask); + void open(const QNetworkRequest &request, const QWebSocketHandshakeOptions &options, bool mask); void ping(const QByteArray &payload); void setSocketState(QAbstractSocket::SocketState state); @@ -176,7 +178,7 @@ private: QWebSocketPrivate(QTcpSocket *pTcpSocket, QWebSocketProtocol::Version version); void setVersion(QWebSocketProtocol::Version version); void setResourceName(const QString &resourceName); - void setRequest(const QNetworkRequest &request); + void setRequest(const QNetworkRequest &request, const QWebSocketHandshakeOptions &options = {}); void setOrigin(const QString &origin); void setProtocol(const QString &protocol); void setExtension(const QString &extension); @@ -204,7 +206,7 @@ private: QString host, QString origin, QString extensions, - QString protocols, + const QStringList &options, QByteArray key, const QList<QPair<QString, QString> > &headers); @@ -225,6 +227,7 @@ private: QUrl m_resource; QString m_resourceName; QNetworkRequest m_request; + QWebSocketHandshakeOptions m_options; QString m_origin; QString m_protocol; QString m_extension; diff --git a/src/websockets/qwebsocket_wasm_p.cpp b/src/websockets/qwebsocket_wasm_p.cpp index 73c596c..dfbffaf 100644 --- a/src/websockets/qwebsocket_wasm_p.cpp +++ b/src/websockets/qwebsocket_wasm_p.cpp @@ -165,7 +165,8 @@ void QWebSocketPrivate::close(QWebSocketProtocol::CloseCode closeCode, QString r reason.toLatin1().toStdString()); } -void QWebSocketPrivate::open(const QNetworkRequest &request, bool mask) +void QWebSocketPrivate::open(const QNetworkRequest &request, + const QWebSocketHandshakeOptions &options, bool mask) { Q_UNUSED(mask); Q_Q(QWebSocket); @@ -183,11 +184,16 @@ void QWebSocketPrivate::open(const QNetworkRequest &request, bool mask) // do support the WebSocket protocol header. This header is // required for some use cases like MQTT. const std::string protocolHeaderValue = request.rawHeader("Sec-WebSocket-Protocol").toStdString(); - val webSocket = val::global("WebSocket"); - socketContext = !protocolHeaderValue.empty() - ? webSocket.new_(urlbytes, protocolHeaderValue) - : webSocket.new_(urlbytes); + val jsSubprotocols = val::array(); + if (!protocolHeaderValue.empty()) + jsSubprotocols.call<void>("push", protocolHeaderValue); + const auto requestedSubProtocols = options.subprotocols(); + for (const auto &protocol : requestedSubProtocols) + jsSubprotocols.call<void>("push", protocol.toStdString()); + + val webSocket = val::global("WebSocket"); + socketContext = webSocket.new_(urlbytes, jsSubprotocols); socketContext.set("onerror", val::module_property("QWebSocketPrivate_onErrorCallback")); socketContext.set("onclose", val::module_property("QWebSocketPrivate_onCloseCallback")); diff --git a/src/websockets/qwebsockethandshakeoptions.cpp b/src/websockets/qwebsockethandshakeoptions.cpp new file mode 100644 index 0000000..0d90a92 --- /dev/null +++ b/src/websockets/qwebsockethandshakeoptions.cpp @@ -0,0 +1,147 @@ +/**************************************************************************** +** +** Copyright (C) 2022 Menlo Systems GmbH, author Arno Rehn <a.rehn@menlosystems.com>. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtWebSockets module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qwebsockethandshakeoptions_p.h" + +QT_BEGIN_NAMESPACE + +/*! + \class QWebSocketHandshakeOptions + + \inmodule QtWebSockets + \since 6.4 + \brief Collects options for the WebSocket handshake. + + QWebSocketHandshakeOptions collects options that are passed along to the + WebSocket handshake, such as WebSocket subprotocols and WebSocket + Extensions. + + At the moment, only WebSocket subprotocols are supported. + + \sa QWebSocket::open() +*/ + +/*! + \brief Constructs an empty QWebSocketHandshakeOptions object. +*/ +QWebSocketHandshakeOptions::QWebSocketHandshakeOptions() + : d(new QWebSocketHandshakeOptionsPrivate) +{ +} + +/*! + \brief Constructs a QWebSocketHandshakeOptions that is a copy of \a other. +*/ +QWebSocketHandshakeOptions::QWebSocketHandshakeOptions(const QWebSocketHandshakeOptions &other) + : d(other.d) +{ +} + +/*! + \brief Constructs a QWebSocketHandshakeOptions that is moved from \a other. +*/ +QWebSocketHandshakeOptions::QWebSocketHandshakeOptions(QWebSocketHandshakeOptions &&other) + : d(std::move(other.d)) +{ +} + +/*! + \brief Destroys this object. +*/ +QWebSocketHandshakeOptions::~QWebSocketHandshakeOptions() +{ +} + +/*! + \fn QWebSocketHandshakeOptions &operator=(QWebSocketHandshakeOptions &&other) noexcept + \brief Moves \a other to this object. +*/ + +/*! + \brief Assigns \a other to this object. +*/ +QWebSocketHandshakeOptions &QWebSocketHandshakeOptions::operator=( + const QWebSocketHandshakeOptions &other) +{ + d = other.d; + return *this; +} + +/*! + \fn void swap(QWebSocketHandshakeOptions &other) noexcept + \brief Swaps this object with \a other. +*/ + +/*! + \brief Returns the list of WebSocket subprotocols to send along with the + websocket handshake. +*/ +QStringList QWebSocketHandshakeOptions::subprotocols() const +{ + return d->subprotocols; +} + +/*! + \brief Sets the list of WebSocket subprotocols to send along with the + websocket handshake. + + WebSocket subprotocol names may only consist of those US-ASCII characters + that are in the unreserved group. Invalid protocol names will not be + included in the handshake. +*/ +void QWebSocketHandshakeOptions::setSubprotocols(const QStringList &protocols) +{ + d->subprotocols = protocols; +} + +bool QWebSocketHandshakeOptions::equals(const QWebSocketHandshakeOptions &other) const +{ + return *d == *other.d; +} + +/*! + \fn operator==(const QWebSocketHandshakeOptions &lhs, const QWebSocketHandshakeOptions &rhs) + \brief Compares \a lhs for equality with \a rhs. +*/ +/*! + \fn operator!=(const QWebSocketHandshakeOptions &lhs, const QWebSocketHandshakeOptions &rhs) + \brief Compares \a lhs for inequality with \a rhs. +*/ + +QT_END_NAMESPACE diff --git a/src/websockets/qwebsockethandshakeoptions.h b/src/websockets/qwebsockethandshakeoptions.h new file mode 100644 index 0000000..82eaec7 --- /dev/null +++ b/src/websockets/qwebsockethandshakeoptions.h @@ -0,0 +1,83 @@ +/**************************************************************************** +** +** Copyright (C) 2022 Menlo Systems GmbH, author Arno Rehn <a.rehn@menlosystems.com>. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtWebSockets module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + + +#ifndef QWEBSOCKETHANDSHAKEOPTIONS_H +#define QWEBSOCKETHANDSHAKEOPTIONS_H + +#include <QtCore/QSharedDataPointer> +#include <QtCore/QStringList> + +#include "QtWebSockets/qwebsockets_global.h" + +QT_BEGIN_NAMESPACE + +class QWebSocketHandshakeOptionsPrivate; + +class Q_WEBSOCKETS_EXPORT QWebSocketHandshakeOptions +{ +public: + QWebSocketHandshakeOptions(); + QWebSocketHandshakeOptions(const QWebSocketHandshakeOptions &other); + QWebSocketHandshakeOptions(QWebSocketHandshakeOptions &&other); + ~QWebSocketHandshakeOptions(); + + QT_MOVE_ASSIGNMENT_OPERATOR_IMPL_VIA_PURE_SWAP(QWebSocketHandshakeOptions) + QWebSocketHandshakeOptions &operator=(const QWebSocketHandshakeOptions &other); + + void swap(QWebSocketHandshakeOptions &other) noexcept { qSwap(d, other.d); } + + QStringList subprotocols() const; + void setSubprotocols(const QStringList &protocols); + +private: + bool equals(const QWebSocketHandshakeOptions &other) const; + + friend bool operator==(const QWebSocketHandshakeOptions &lhs, + const QWebSocketHandshakeOptions &rhs) { return lhs.equals(rhs); } + friend bool operator!=(const QWebSocketHandshakeOptions &lhs, + const QWebSocketHandshakeOptions &rhs) { return !lhs.equals(rhs); } + + QSharedDataPointer<QWebSocketHandshakeOptionsPrivate> d; + friend class QWebSocketHandshakeOptionsPrivate; +}; + +QT_END_NAMESPACE + +#endif // QWEBSOCKETHANDSHAKEOPTIONS_H diff --git a/src/websockets/qwebsockethandshakeoptions_p.h b/src/websockets/qwebsockethandshakeoptions_p.h new file mode 100644 index 0000000..2a7e77e --- /dev/null +++ b/src/websockets/qwebsockethandshakeoptions_p.h @@ -0,0 +1,71 @@ +/**************************************************************************** +** +** Copyright (C) 2022 Menlo Systems GmbH, author Arno Rehn <a.rehn@menlosystems.com>. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtWebSockets module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QWEBSOCKETHANDSHAKEOPTIONS_P_H +#define QWEBSOCKETHANDSHAKEOPTIONS_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QSharedData> + +#include "qwebsockethandshakeoptions.h" + +QT_BEGIN_NAMESPACE + +class Q_AUTOTEST_EXPORT QWebSocketHandshakeOptionsPrivate : public QSharedData +{ +public: + inline bool operator==(const QWebSocketHandshakeOptionsPrivate &other) const + { return subprotocols == other.subprotocols; } + + QStringList subprotocols; +}; + +QT_END_NAMESPACE + +#endif // QWEBSOCKETHANDSHAKEOPTIONS_P_H diff --git a/src/websockets/qwebsockethandshakeresponse.cpp b/src/websockets/qwebsockethandshakeresponse.cpp index d3ef609..5e4a061 100644 --- a/src/websockets/qwebsockethandshakeresponse.cpp +++ b/src/websockets/qwebsockethandshakeresponse.cpp @@ -161,9 +161,21 @@ QString QWebSocketHandshakeResponse::getHandshakeResponse( } else { if (request.isValid()) { const QString acceptKey = calculateAcceptKey(request.key()); - const QList<QString> matchingProtocols = - listIntersection(supportedProtocols, request.protocols(), - std::less<QString>()); + + // Find first client protocol that is supported. Order is important! + const QString protocol = [&] { + const auto clientProtocols = request.protocols(); + + const auto isSupportedProtocol = [&](const QString &protocol) { + return supportedProtocols.contains(protocol); + }; + const auto it = std::find_if( + clientProtocols.constBegin(), clientProtocols.constEnd(), + isSupportedProtocol); + + return it == clientProtocols.constEnd() ? QString() : *it; + }(); + //TODO: extensions must be kept in the order in which they arrive //cannot use set.intersect() to get the supported extensions const QList<QString> matchingExtensions = @@ -182,8 +194,8 @@ QString QWebSocketHandshakeResponse::getHandshakeResponse( QStringLiteral("Upgrade: websocket") << QStringLiteral("Connection: Upgrade") << QStringLiteral("Sec-WebSocket-Accept: ") % acceptKey; - if (!matchingProtocols.isEmpty()) { - m_acceptedProtocol = matchingProtocols.first(); + if (!protocol.isEmpty()) { + m_acceptedProtocol = protocol; response << QStringLiteral("Sec-WebSocket-Protocol: ") % m_acceptedProtocol; } if (!matchingExtensions.isEmpty()) { diff --git a/src/websockets/qwebsocketserver.cpp b/src/websockets/qwebsocketserver.cpp index 5cbdbec..e132915 100644 --- a/src/websockets/qwebsocketserver.cpp +++ b/src/websockets/qwebsocketserver.cpp @@ -65,9 +65,7 @@ Calling close() makes QWebSocketServer stop listening for incoming connections. - QWebSocketServer currently does not support - \l {WebSocket Extensions} and - \l {WebSocket Subprotocols}. + QWebSocketServer currently does not support \l {WebSocket Extensions}. \note When working with self-signed certificates, \l{Firefox bug 594502} prevents \l{Firefox} to connect to a secure WebSocket server. To work around this problem, first browse to the @@ -537,6 +535,26 @@ QString QWebSocketServer::serverName() const } /*! + \brief Sets the list of protocols supported by the server to \a protocols. + \since 6.4 + */ +void QWebSocketServer::setSupportedSubprotocols(const QStringList &protocols) +{ + Q_D(QWebSocketServer); + d->setSupportedSubprotocols(protocols); +} + +/*! + \brief Returns the list of protocols supported by the server. + \since 6.4 + */ +QStringList QWebSocketServer::supportedSubprotocols() const +{ + Q_D(const QWebSocketServer); + return d->supportedSubprotocols(); +} + +/*! Returns the server's address if the server is listening for connections; otherwise returns QHostAddress::Null. diff --git a/src/websockets/qwebsocketserver.h b/src/websockets/qwebsocketserver.h index 686c281..e6085f9 100644 --- a/src/websockets/qwebsocketserver.h +++ b/src/websockets/qwebsocketserver.h @@ -130,6 +130,9 @@ public: void setServerName(const QString &serverName); QString serverName() const; + void setSupportedSubprotocols(const QStringList &protocols); + QStringList supportedSubprotocols() const; + #ifndef QT_NO_NETWORKPROXY void setProxy(const QNetworkProxy &networkProxy); QNetworkProxy proxy() const; diff --git a/src/websockets/qwebsocketserver_p.cpp b/src/websockets/qwebsocketserver_p.cpp index 23b38aa..c91c436 100644 --- a/src/websockets/qwebsocketserver_p.cpp +++ b/src/websockets/qwebsocketserver_p.cpp @@ -320,10 +320,17 @@ QList<QWebSocketProtocol::Version> QWebSocketServerPrivate::supportedVersions() /*! \internal */ -QStringList QWebSocketServerPrivate::supportedProtocols() const +void QWebSocketServerPrivate::setSupportedSubprotocols(const QStringList &protocols) { - QStringList supportedProtocols; - return supportedProtocols; //no protocols are currently supported + m_supportedSubprotocols = protocols; +} + +/*! + \internal + */ +QStringList QWebSocketServerPrivate::supportedSubprotocols() const +{ + return m_supportedSubprotocols; } /*! @@ -489,7 +496,7 @@ void QWebSocketServerPrivate::handshakeReceived() m_serverName, corsAuthenticator.allowed(), supportedVersions(), - supportedProtocols(), + supportedSubprotocols(), supportedExtensions()); if (Q_LIKELY(response.isValid())) { diff --git a/src/websockets/qwebsocketserver_p.h b/src/websockets/qwebsocketserver_p.h index 1f1cbd9..baf60d1 100644 --- a/src/websockets/qwebsocketserver_p.h +++ b/src/websockets/qwebsocketserver_p.h @@ -111,7 +111,8 @@ public: qintptr socketDescriptor() const; QList<QWebSocketProtocol::Version> supportedVersions() const; - QStringList supportedProtocols() const; + void setSupportedSubprotocols(const QStringList &protocols); + QStringList supportedSubprotocols() const; QStringList supportedExtensions() const; void setServerName(const QString &serverName); @@ -135,6 +136,7 @@ private: QTcpServer *m_pTcpServer; QString m_serverName; SslMode m_secureMode; + QStringList m_supportedSubprotocols; QQueue<QWebSocket *> m_pendingConnections; QWebSocketProtocol::CloseCode m_error; QString m_errorString; |