diff options
| author | Juha Vuolle <juha.vuolle@qt.io> | 2024-11-29 12:50:13 +0200 |
|---|---|---|
| committer | Juha Vuolle <juha.vuolle@qt.io> | 2024-12-04 14:27:07 +0200 |
| commit | b1e67440a6c2fbccdecd599def6f9822a31d5b07 (patch) | |
| tree | 11ee156cd1150e0cf46f519256efb16131a4f3f9 | |
| parent | 201a224441c2dc83e54cbc606b04ea9006ada7f8 (diff) | |
Improve callback/redirect_uri hostname setting
Setting hostname part of redirect uri (callback) is important
part to get correct, because authorization servers often expect
a verbatim match between what has been registered, and what is
sent as part of authorization request. This commit clarifies and
improves specifying the hostname part.
Historically the callback host was fixed to "127.0.0.1" which has
its drawbacks (and can be outright wrong). The situation was
later improved by using "localhost" hostname, which correctly maps
to both IPv4 and IPv6 interfaces. However, there are authorization
servers which require the use of IP literals, and reject "localhost".
To address these issues, this commit consists of:
- Map Any, AnyIPv4, and AnyIPv6 to "localhost". This
mapping is logical because "localhost" will work then
independent of what the actual used address is
- Map IPv4 and IPv6 loopback addresses (LocalHost, LocalHostIPv6)
to their IP literals 127.0.0.1 and ::1
These are well-known IP literal addresses,
and mapping them to "localhost" does not bring an advantage
- If user has provided a string literal, use that directly
- Change default address to "LocalHost", which maps to hostname
"127.0.0.1". This, in part, undoes the changed behavior where
the default changed to "localhost". With this change,
users that don't define callback address shouldn't notice a
difference to the very original behavior
All in all these changes should improve the compatibility
with various authorization servers and provide more flexibility
to users.
Amends: fd49b7f6543e7b49be7847624c64ee86c4272ccd
Amends: 4e03167088181bf513adcfb8aac93fb8efb3f420
[ChangeLog][QOAuthHttpServerReplyHandler] Changed and clarified
callback hostname handling (especially localhost vs. 127.0.0.1)
Pick-to: 6.8
Fixes: QTBUG-130159
Change-Id: I25dfb996d10f95fe60bdb4f46ea848edcb2528be
Reviewed-by: Ivan Solovev <ivan.solovev@qt.io>
4 files changed, 86 insertions, 21 deletions
diff --git a/examples/oauth/redditclient/redditmodel.cpp b/examples/oauth/redditclient/redditmodel.cpp index 309ab8c..514a92c 100644 --- a/examples/oauth/redditclient/redditmodel.cpp +++ b/examples/oauth/redditclient/redditmodel.cpp @@ -29,7 +29,7 @@ RedditModel::RedditModel(const QString &clientId, QObject *parent) : redditApi.setBaseUrl(QUrl(hotUrl)); - auto replyHandler = new QOAuthHttpServerReplyHandler(1337, this); + auto replyHandler = new QOAuthHttpServerReplyHandler(QHostAddress::Any, 1337, this); oauth2.setReplyHandler(replyHandler); oauth2.setAuthorizationUrl(QUrl(authorizationUrl)); oauth2.setAccessTokenUrl(QUrl(accessTokenUrl)); diff --git a/src/oauth/qoauthhttpserverreplyhandler.cpp b/src/oauth/qoauthhttpserverreplyhandler.cpp index 4bd1c2d..7381899 100644 --- a/src/oauth/qoauthhttpserverreplyhandler.cpp +++ b/src/oauth/qoauthhttpserverreplyhandler.cpp @@ -72,11 +72,20 @@ using namespace Qt::StringLiterals; \section1 IPv4 and IPv6 - Currently if the handler is a loopback address, IPv4 any address, - or IPv6 any address, the used callback is in the form of - \e {http://localhost:{port}/{path}}. Otherwise, for specific - IP addresses, the actual IP literal is used. For instance - \e {http://192.168.0.2:{port}/{path}} in the case of IPv4. + If the handler is an \e any address handler + (\l {QHostAddress::SpecialAddress}{AnyIPv4, AnyIPv6, or Any}), + the used callback is in the form of \c {http://localhost:{port}/{path}}. + Handler will first attempt to listen on IPv4 loopback address, + and then on IPv6. \c {localhost} is used because it resolves correctly + on both IPv4 and IPv6 interfaces. + + For loopback addresses + (\l {QHostAddress::SpecialAddress}{LocalHost or LocalHostIPv6}) + the IP literals (\c {127.0.0.1} and \c {::1}) are used. + + For specific IP addresses the provided IP literal is used directly, + for instance: + \e {http://192.168.0.123:{port}/{path}} in the case of an IPv4 address. \section1 HTTP and HTTPS Callbacks @@ -85,7 +94,7 @@ using namespace Qt::StringLiterals; providing an appropriate \l QSslConfiguration when calling \l {listen(const QSslConfiguration &, const QHostAddress &, quint16)}{listen()}. Internally the handler will then use \l QSslServer, and the callback - (redirect URL) will be of the form \e {https://localhost:{port}/{path}}. + (redirect URL) will be of the form \e {https://{host}:{port}/{path}}. Following example illustrates this: \snippet src_oauth_replyhandlers.cpp localhost-https-scheme-setup @@ -137,17 +146,25 @@ QString QOAuthHttpServerReplyHandlerPrivate::callback() const #endif url.setPort(callbackPort); url.setPath(path); + url.setHost(callbackHost()); + return url.toString(QUrl::EncodeSpaces | QUrl::EncodeUnicode | QUrl::EncodeDelimiters + | QUrl::EncodeReserved); +} - // convert Any and Localhost addresses to "localhost" - if (callbackAddress.isLoopback() || callbackAddress == QHostAddress::AnyIPv4 - || callbackAddress == QHostAddress::Any || callbackAddress == QHostAddress::AnyIPv6) { - url.setHost(u"localhost"_s); +QString QOAuthHttpServerReplyHandlerPrivate::callbackHost() const +{ + QString host; + if (callbackAddress == QHostAddress::AnyIPv4 || callbackAddress == QHostAddress::Any + || callbackAddress == QHostAddress::AnyIPv6) { + // Convert Any addresses to "localhost" + host = u"localhost"_s; } else { - url.setHost(callbackAddress.toString()); + // For other than Any addresses, use QHostAddress::toString() which returns an + // IP literal. This includes user-provided addresses, as well as special addresses + // such as LocalHost (127.0.0.1) and LocalHostIPv6 (::1) + host = callbackAddress.toString(); } - - return url.toString(QUrl::EncodeSpaces | QUrl::EncodeUnicode | QUrl::EncodeDelimiters - | QUrl::EncodeReserved); + return host; } void QOAuthHttpServerReplyHandlerPrivate::_q_clientConnected() @@ -376,23 +393,23 @@ bool QOAuthHttpServerReplyHandlerPrivate::QHttpRequest::readHeader(QTcpSocket *s /*! Constructs a QOAuthHttpServerReplyHandler object using \a parent as a parent object. Calls \l {listen()} with port \c 0 and address - \l {QHostAddress::SpecialAddress}{Null}. + \l {QHostAddress::SpecialAddress}{LocalHost}. \sa listen() */ QOAuthHttpServerReplyHandler::QOAuthHttpServerReplyHandler(QObject *parent) : - QOAuthHttpServerReplyHandler(QHostAddress::Null, 0, parent) + QOAuthHttpServerReplyHandler(QHostAddress::LocalHost, 0, parent) {} /*! Constructs a QOAuthHttpServerReplyHandler object using \a parent as a parent object. Calls \l {listen()} with \a port and address - \l {QHostAddress::SpecialAddress}{Null}. + \l {QHostAddress::SpecialAddress}{LocalHost}. \sa listen() */ QOAuthHttpServerReplyHandler::QOAuthHttpServerReplyHandler(quint16 port, QObject *parent) : - QOAuthHttpServerReplyHandler(QHostAddress::Null, port, parent) + QOAuthHttpServerReplyHandler(QHostAddress::LocalHost, port, parent) {} /*! diff --git a/src/oauth/qoauthhttpserverreplyhandler_p.h b/src/oauth/qoauthhttpserverreplyhandler_p.h index 57b8ff6..104ed68 100644 --- a/src/oauth/qoauthhttpserverreplyhandler_p.h +++ b/src/oauth/qoauthhttpserverreplyhandler_p.h @@ -36,6 +36,7 @@ public: ~QOAuthHttpServerReplyHandlerPrivate(); QString callback() const; + QString callbackHost() const; QTcpServer *httpServer = nullptr; QString text; diff --git a/tests/auto/oauthhttpserverreplyhandler/tst_oauthhttpserverreplyhandler.cpp b/tests/auto/oauthhttpserverreplyhandler/tst_oauthhttpserverreplyhandler.cpp index e811283..932c05d 100644 --- a/tests/auto/oauthhttpserverreplyhandler/tst_oauthhttpserverreplyhandler.cpp +++ b/tests/auto/oauthhttpserverreplyhandler/tst_oauthhttpserverreplyhandler.cpp @@ -36,6 +36,8 @@ private Q_SLOTS: #ifndef QT_NO_SSL void localhostHttps(); #endif + void callbackHost_data(); + void callbackHost(); private: QString testDataDir; @@ -219,7 +221,7 @@ void tst_QOAuthHttpServerReplyHandler::callbackCaching() { QOAuthHttpServerReplyHandler replyHandler; constexpr auto callbackPath = "/foo"_L1; - constexpr auto callbackHost = "localhost"_L1; + constexpr auto callbackHost = "127.0.0.1"_L1; QVERIFY(replyHandler.isListening()); replyHandler.setCallbackPath(callbackPath); @@ -234,7 +236,7 @@ void tst_QOAuthHttpServerReplyHandler::callbackCaching() QCOMPARE(callback.path(), callbackPath); QCOMPARE(callback.host(), callbackHost); - replyHandler.listen(); + replyHandler.listen({QHostAddress::SpecialAddress::LocalHost}); QVERIFY(replyHandler.isListening()); callback = replyHandler.callback(); QCOMPARE(callback.path(), callbackPath); @@ -457,6 +459,51 @@ void tst_QOAuthHttpServerReplyHandler::localhostHttps() } #endif // QT_NO_SSL +void tst_QOAuthHttpServerReplyHandler::callbackHost_data() +{ + QTest::addColumn<QHostAddress>("hostAddress"); + QTest::addColumn<QUrl>("expectedCallback"); + + const QString localhost = u"localhost"_s; + const QUrl localhostCallback = u"http://localhost/"_s; + + const QString ipv4literal = u"127.0.0.1"_s; + const QUrl ipv4literalCallback = u"http://127.0.0.1/"_s; + + const QString ipv6literal = u"::1"_s; + const QUrl ipv6literalCallback = u"http://[::1]/"_s; + + QTest::newRow("default") + << QHostAddress() << ipv4literalCallback; + QTest::newRow("LocalHost") + << QHostAddress(QHostAddress::SpecialAddress::LocalHost) << ipv4literalCallback; + QTest::newRow("127.0.0.1") + << QHostAddress(ipv4literal) << ipv4literalCallback; + QTest::newRow("LocalHostIPv6") + << QHostAddress(QHostAddress::SpecialAddress::LocalHostIPv6) << ipv6literalCallback; + QTest::newRow("::1") + << QHostAddress(ipv6literal) << ipv6literalCallback; + QTest::newRow("AnyIPv4") + << QHostAddress(QHostAddress::SpecialAddress::AnyIPv4) << localhostCallback; + QTest::newRow("0.0.0.0") + << QHostAddress("0.0.0.0") << localhostCallback; + QTest::newRow("AnyIPv6") + << QHostAddress(QHostAddress::SpecialAddress::AnyIPv6) << localhostCallback; + QTest::newRow("::") + << QHostAddress("::") << localhostCallback; +} + +void tst_QOAuthHttpServerReplyHandler::callbackHost() +{ + QFETCH(const QHostAddress, hostAddress); + QFETCH(QUrl, expectedCallback); + + QOAuthHttpServerReplyHandler replyHandler(hostAddress, 0); // 0 for any port + + QVERIFY(replyHandler.isListening()); + expectedCallback.setPort(replyHandler.port()); + QCOMPARE(QUrl(replyHandler.callback()), expectedCallback); +} QTEST_MAIN(tst_QOAuthHttpServerReplyHandler) #include "tst_oauthhttpserverreplyhandler.moc" |
