summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMÃ¥rten Nordheim <marten.nordheim@qt.io>2022-12-12 15:25:20 +0100
committerTimur Pocheptsov <timur.pocheptsov@qt.io>2022-12-21 15:30:54 +0100
commitda30f70fea239f723f1d36b076bb3f5860f50ed9 (patch)
treed3e7abdd6f860fcb5963a457655f00bef77ef65e /src
parent482034a8f25f02662df0a2558b7f771f46cec142 (diff)
downloadqtwebsockets-da30f70fea239f723f1d36b076bb3f5860f50ed9.tar.gz
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 <timur.pocheptsov@qt.io> Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
Diffstat (limited to 'src')
-rw-r--r--src/websockets/qwebsocket.cpp21
-rw-r--r--src/websockets/qwebsocket.h1
-rw-r--r--src/websockets/qwebsocket_p.cpp107
-rw-r--r--src/websockets/qwebsocket_p.h10
4 files changed, 137 insertions, 2 deletions
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 <QtNetwork/private/qhttpheaderparser_p.h>
+#include <QtNetwork/private/qauthenticator_p.h>
#include <QtCore/QDebug>
#include <limits>
+#include <memory>
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<QByteArray> 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<QSslSocket *>(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 <QtNetwork/QNetworkProxy>
#endif
+#include <QtNetwork/QAuthenticator>
#ifndef QT_NO_SSL
#include <QtNetwork/QSslConfiguration>
#include <QtNetwork/QSslError>
@@ -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;