From da30f70fea239f723f1d36b076bb3f5860f50ed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Nordheim?= Date: Mon, 12 Dec 2022 15:25:20 +0100 Subject: Support 401 response for websocket connections Adds the authenticationRequired signal. [ChangeLog][QWebSocket] QWebSocket now supports 401 Unauthorized. Connect to the authenticationRequired signal to handle authentication challenges, or pass your credentials along with the URL. Fixes: QTBUG-92858 Change-Id: Ic43d1c12529dea278b2951e6f991cc1004fc3713 Reviewed-by: Timur Pocheptsov Reviewed-by: Volker Hilsheimer --- src/websockets/qwebsocket.cpp | 21 ++++++++ src/websockets/qwebsocket.h | 1 + src/websockets/qwebsocket_p.cpp | 107 +++++++++++++++++++++++++++++++++++++++- src/websockets/qwebsocket_p.h | 10 ++++ 4 files changed, 137 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/websockets/qwebsocket.cpp b/src/websockets/qwebsocket.cpp index 93d07f4..b495ffd 100644 --- a/src/websockets/qwebsocket.cpp +++ b/src/websockets/qwebsocket.cpp @@ -98,6 +98,27 @@ not been filled in with new information when the signal returns. \sa QAuthenticator, QNetworkProxy */ + +/*! + \fn void QWebSocket::authenticationRequired(QAuthenticator *authenticator) + \since 6.6 + + This signal is emitted when the server requires authentication. + The \a authenticator object must then be filled in with the required details + to allow authentication and continue the connection. + + If you know that the server may require authentication, you can set the + username and password on the initial QUrl, using QUrl::setUserName and + QUrl::setPassword. QWebSocket will still try to connect \e{once} without + using the provided credentials. + + \note It is not possible to use a QueuedConnection to connect to + this signal, as the connection will fail if the authenticator has + not been filled in with new information when the signal returns. + + \sa QAuthenticator +*/ + /*! \fn void QWebSocket::stateChanged(QAbstractSocket::SocketState state); diff --git a/src/websockets/qwebsocket.h b/src/websockets/qwebsocket.h index 5cc6131..bf9e393 100644 --- a/src/websockets/qwebsocket.h +++ b/src/websockets/qwebsocket.h @@ -118,6 +118,7 @@ Q_SIGNALS: #ifndef QT_NO_NETWORKPROXY void proxyAuthenticationRequired(const QNetworkProxy &proxy, QAuthenticator *pAuthenticator); #endif + void authenticationRequired(QAuthenticator *authenticator); void readChannelFinished(); void textFrameReceived(const QString &frame, bool isLastFrame); void binaryFrameReceived(const QByteArray &frame, bool isLastFrame); diff --git a/src/websockets/qwebsocket_p.cpp b/src/websockets/qwebsocket_p.cpp index 08c170e..bca6d4f 100644 --- a/src/websockets/qwebsocket_p.cpp +++ b/src/websockets/qwebsocket_p.cpp @@ -28,13 +28,17 @@ #endif #include +#include #include #include +#include QT_BEGIN_NAMESPACE +using namespace Qt::StringLiterals; + namespace { constexpr int MAX_HEADERLINE_LENGTH = 8 * 1024; // maximum length of a http request header line @@ -1080,6 +1084,51 @@ void QWebSocketPrivate::processHandshake(QTcpSocket *pSocket) } break; } + case 401: { + // HTTP/1.1 401 UNAUTHORIZED + if (m_authenticator.isNull()) + m_authenticator.detach(); + auto *priv = QAuthenticatorPrivate::getPrivate(m_authenticator); + const QList challenges = parser.headerFieldValues("WWW-Authenticate"); + const bool isSupported = std::any_of(challenges.begin(), challenges.end(), + QAuthenticatorPrivate::isMethodSupported); + if (isSupported) + priv->parseHttpResponse(parser.headers(), /*isProxy=*/false, m_request.url().host()); + if (!isSupported || priv->method == QAuthenticatorPrivate::None) { + // Keep the error on a single line so it can easily be searched for: + errorDescription = + QWebSocket::tr("QWebSocketPrivate::processHandshake: " + "Unsupported WWW-Authenticate challenge(s) encountered!"); + break; + } + + const QUrl url = m_request.url(); + const bool hasCredentials = !url.userName().isEmpty() || !url.password().isEmpty(); + if (hasCredentials) { + m_authenticator.setUser(url.userName()); + m_authenticator.setPassword(url.password()); + // Unset username and password so we don't try it again + QUrl copy = url; + copy.setUserName({}); + copy.setPassword({}); + m_request.setUrl(copy); + } + if (priv->phase == QAuthenticatorPrivate::Done) { // No user/pass from URL: + emit q->authenticationRequired(&m_authenticator); + if (priv->phase == QAuthenticatorPrivate::Done) { + // user/pass was not updated: + errorDescription = QWebSocket::tr( + "QWebSocket::processHandshake: Host requires authentication"); + break; + } + } + m_needsResendWithCredentials = true; + if (parser.firstHeaderField("Connection").compare("close", Qt::CaseInsensitive) == 0) + m_needsReconnect = true; + else + m_bytesToSkipBeforeNewResponse = parser.firstHeaderField("Content-Length").toInt(); + break; + } default: { errorDescription = QWebSocket::tr("QWebSocketPrivate::processHandshake: Unhandled http status code: %1 (%2).") @@ -1092,6 +1141,16 @@ void QWebSocketPrivate::processHandshake(QTcpSocket *pSocket) setProtocol(protocol); setSocketState(QAbstractSocket::ConnectedState); Q_EMIT q->connected(); + } else if (m_needsResendWithCredentials) { + if (m_needsReconnect && m_pSocket->state() != QAbstractSocket::UnconnectedState) { + // Disconnect here, then in processStateChanged() we reconnect when + // we are unconnected. + m_pSocket->disconnectFromHost(); + } else { + // I'm cheating, this is how a handshake starts: + processStateChanged(QAbstractSocket::ConnectedState); + } + return; } else { // handshake failed setErrorString(errorDescription); @@ -1130,6 +1189,27 @@ void QWebSocketPrivate::processStateChanged(QAbstractSocket::SocketState socketS } const QStringList subProtocols = requestedSubProtocols(); + // Perform authorization if needed: + if (m_needsResendWithCredentials) { + m_needsResendWithCredentials = false; + // Based on QHttpNetworkRequest::uri: + auto uri = [](QUrl url) -> QByteArray { + QUrl::FormattingOptions format(QUrl::RemoveFragment | QUrl::RemoveUserInfo + | QUrl::FullyEncoded); + if (url.path().isEmpty()) + url.setPath(QStringLiteral("/")); + else + format |= QUrl::NormalizePathSegments; + return url.toEncoded(format); + }; + auto *priv = QAuthenticatorPrivate::getPrivate(m_authenticator); + Q_ASSERT(priv); + QByteArray response = priv->calculateResponse("GET", uri(m_request.url()), + m_request.url().host()); + if (!response.isEmpty()) + headers << qMakePair(u"Authorization"_s, QString::fromLatin1(response)); + } + const auto format = QUrl::RemoveScheme | QUrl::RemoveUserInfo | QUrl::RemovePath | QUrl::RemoveQuery | QUrl::RemoveFragment; @@ -1156,7 +1236,28 @@ void QWebSocketPrivate::processStateChanged(QAbstractSocket::SocketState socketS break; case QAbstractSocket::UnconnectedState: - if (webSocketState != QAbstractSocket::UnconnectedState) { + if (m_needsReconnect) { + // Need to reinvoke the lambda queued because the underlying socket + // isn't done cleaning up yet... + auto reconnect = [this]() { + m_needsReconnect = false; + const QUrl url = m_request.url(); +#if QT_CONFIG(ssl) + const bool isEncrypted = url.scheme().compare(u"wss", Qt::CaseInsensitive) == 0; + if (isEncrypted) { + // This has to work because we did it earlier; this is just us + // reconnecting! + auto *sslSocket = qobject_cast(m_pSocket); + Q_ASSERT(sslSocket); + sslSocket->connectToHostEncrypted(url.host(), quint16(url.port(443))); + } else +#endif + { + m_pSocket->connectToHost(url.host(), quint16(url.port(80))); + } + }; + QMetaObject::invokeMethod(q, reconnect, Qt::QueuedConnection); + } else if (webSocketState != QAbstractSocket::UnconnectedState) { setSocketState(QAbstractSocket::UnconnectedState); Q_EMIT q->disconnected(); } @@ -1187,7 +1288,9 @@ void QWebSocketPrivate::processData() if (!m_pSocket) // disconnected with data still in-bound return; if (state() == QAbstractSocket::ConnectingState) { - if (!m_pSocket->canReadLine()) + if (m_bytesToSkipBeforeNewResponse > 0) + m_bytesToSkipBeforeNewResponse -= m_pSocket->skip(m_bytesToSkipBeforeNewResponse); + if (m_bytesToSkipBeforeNewResponse > 0 || !m_pSocket->canReadLine()) return; processHandshake(m_pSocket); // That may have changed state(), recheck in the next 'if' below. diff --git a/src/websockets/qwebsocket_p.h b/src/websockets/qwebsocket_p.h index 08be774..f29b40a 100644 --- a/src/websockets/qwebsocket_p.h +++ b/src/websockets/qwebsocket_p.h @@ -20,6 +20,7 @@ #ifndef QT_NO_NETWORKPROXY #include #endif +#include #ifndef QT_NO_SSL #include #include @@ -205,12 +206,21 @@ private: QAbstractSocket::PauseModes m_pauseMode; qint64 m_readBufferSize; + // For WWW-Authenticate handling + QAuthenticator m_authenticator; + qint64 m_bytesToSkipBeforeNewResponse = 0; + QByteArray m_key; //identification key used in handshake requests bool m_mustMask; //a server must not mask the frames it sends bool m_isClosingHandshakeSent; bool m_isClosingHandshakeReceived; + + // For WWW-Authenticate handling + bool m_needsResendWithCredentials = false; + bool m_needsReconnect = false; + QWebSocketProtocol::CloseCode m_closeCode; QString m_closeReason; -- cgit v1.2.3