1// Copyright (C) 2017 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3// Qt-Security score:critical reason:authorization-protocol
4
5#include <qabstractoauth.h>
6#include <qoauthhttpserverreplyhandler.h>
7#include "qabstractoauthreplyhandler_p.h"
8
9#include <private/qoauthhttpserverreplyhandler_p.h>
10
11#include <QtCore/qurl.h>
12#include <QtCore/qurlquery.h>
13#include <QtCore/qcoreapplication.h>
14#include <QtCore/qloggingcategory.h>
15#include <QtCore/private/qlocale_p.h>
16
17#ifndef QT_NO_SSL
18#include <QtNetwork/qsslconfiguration.h>
19#include <QtNetwork/qsslserver.h>
20#include <QtNetwork/qsslsocket.h>
21#endif
22#include <QtNetwork/qtcpsocket.h>
23#include <QtNetwork/qnetworkreply.h>
24
25QT_BEGIN_NAMESPACE
26
27using namespace Qt::StringLiterals;
28
29/*!
30 \class QOAuthHttpServerReplyHandler
31 \inmodule QtNetworkAuth
32 \ingroup oauth
33 \since 5.8
34
35 \brief Handles loopback redirects by setting up a local HTTP server.
36
37 This class serves as a reply handler for
38 \l {https://datatracker.ietf.org/doc/html/rfc6749}{OAuth 2.0} authorization
39 processes that use
40 \l {https://datatracker.ietf.org/doc/html/rfc8252#section-7.3}{loopback redirection}.
41
42 The \l {https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2}
43 {redirect URI} is where the authorization server redirects the
44 user-agent (typically, and preferably, the system browser) once
45 the authorization part of the flow is complete. Loopback redirect
46 URIs use \c http as the scheme and either \e localhost or an IP
47 address literal as the host (see \l {IPv4 and IPv6}).
48
49 QOAuthHttpServerReplyHandler sets up a localhost server. Once the
50 authorization server redirects the browser to this localhost address,
51 the reply handler parses the redirection URI query parameters,
52 and then signals authorization completion with
53 \l {QAbstractOAuthReplyHandler::callbackReceived}{a signal}.
54
55 To handle other redirect URI schemes, see QOAuthUriSchemeReplyHandler.
56
57 The following code illustrates the usage. First, the needed variables:
58
59 \snippet src_oauth_replyhandlers_p.h httpserver-variables
60
61 Followed up by the OAuth setup (error handling omitted for brevity):
62
63 \snippet src_oauth_replyhandlers.cpp httpserver-service-configuration
64 \codeline
65 \snippet src_oauth_replyhandlers.cpp httpserver-oauth-setup
66
67 Finally, we then set up the URI scheme reply-handler:
68
69 \snippet src_oauth_replyhandlers.cpp httpserver-handler-setup
70
71 \section1 IPv4 and IPv6
72
73 If the handler is an \e any address handler
74 (\l {QHostAddress::SpecialAddress}{AnyIPv4, AnyIPv6, or Any}),
75 the used callback is in the form of \c {http://localhost:{port}/{path}}.
76 Handler will first attempt to listen on IPv4 loopback address,
77 and then on IPv6. \c {localhost} is used because it resolves correctly
78 on both IPv4 and IPv6 interfaces.
79
80 For loopback addresses
81 (\l {QHostAddress::SpecialAddress}{LocalHost or LocalHostIPv6})
82 the IP literals (\c {127.0.0.1} and \c {::1}) are used.
83
84 For specific IP addresses the provided IP literal is used directly,
85 for instance:
86 \e {http://192.168.0.123:{port}/{path}} in the case of an IPv4 address.
87
88 It's also possible to specify the host part of the callback
89 URL manually with \l {setCallbackHost()}. For instance you can
90 specify the callback to be \c {localhost.localnet}.
91 Naturally you need to ensure that such address is reachable upon
92 redirection.
93
94 \code
95 auto replyHandler = new QOAuthHttpServerReplyHandler(QHostAddress::LocalHost, 1337, this);
96 replyHandler->setCallbackHost("localhost.localnet"_L1);
97 \endcode
98
99 \section1 HTTP and HTTPS Callbacks
100
101 Since Qt 6.9 it's possible to configure the handler to use
102 \c {https} URI scheme instead of \c {http}. This is done by
103 providing an appropriate \l QSslConfiguration when calling
104 \l {listen(const QSslConfiguration &, const QHostAddress &, quint16)}{listen()}.
105 Internally the handler will then use \l QSslServer, and the callback
106 (redirect URL) will be of the form \e {https://{host}:{port}/{path}}.
107
108 Following example illustrates this:
109 \snippet src_oauth_replyhandlers.cpp localhost-https-scheme-setup
110
111 When possible, it is recommended to use other redirect URI
112 options, see \l {Choosing A Reply Handler} and
113 \l {Qt OAuth2 Browser Support}.
114
115 The primary use cases for a localhost \c {https} handler
116 should be limited to development-time, or tightly controlled
117 and provisioned environments. For example, some Authorization
118 Servers won't allow plain \c {http} redirect URIs at all, in which
119 case this can add to development convenience.
120
121 From security perspective,
122 while using SSL/TLS does encrypt the localhost traffic, OAuth2
123 has also other security mechanisms in place such as
124 \l {QOAuth2AuthorizationCodeFlow::PkceMethod}{PKCE}.
125 Under no circumstances you should distribute private certificate
126 keys along with the application.
127
128 \note Browsers will issue severe warnings
129 if the certificate is not trusted. This is typical with
130 self-signed certificates, whose use should be limited to
131 development-time.
132*/
133QOAuthHttpServerReplyHandlerPrivate::QOAuthHttpServerReplyHandlerPrivate(
134 QOAuthHttpServerReplyHandler *p) :
135 text(QObject::tr(s: "Callback received. Feel free to close this page.")), path(u'/'), q_ptr(p)
136{
137}
138
139QOAuthHttpServerReplyHandlerPrivate::~QOAuthHttpServerReplyHandlerPrivate()
140{
141 if (httpServer->isListening())
142 httpServer->close();
143}
144
145QString QOAuthHttpServerReplyHandlerPrivate::callback() const
146{
147 QUrl url;
148#ifndef QT_NO_SSL
149 if (qobject_cast<QSslServer*>(object: httpServer))
150 url.setScheme(u"https"_s);
151 else
152 url.setScheme(u"http"_s);
153#else
154 url.setScheme(u"http"_s);
155#endif
156 url.setPort(callbackPort);
157 url.setPath(path);
158 url.setHost(host: callbackHost());
159 return url.toString(options: QUrl::EncodeSpaces | QUrl::EncodeUnicode | QUrl::EncodeDelimiters
160 | QUrl::EncodeReserved);
161}
162
163QString QOAuthHttpServerReplyHandlerPrivate::callbackHost() const
164{
165 QString host;
166 if (!callbackHostname.isEmpty()) {
167 // Use application-provided hostname
168 host = callbackHostname;
169 } else if (callbackAddress == QHostAddress::AnyIPv4 || callbackAddress == QHostAddress::Any
170 || callbackAddress == QHostAddress::AnyIPv6) {
171 // Convert Any addresses to "localhost"
172 host = u"localhost"_s;
173 } else {
174 // For other than Any addresses, use QHostAddress::toString() which returns an
175 // IP literal. This includes user-provided addresses, as well as special addresses
176 // such as LocalHost (127.0.0.1) and LocalHostIPv6 (::1)
177 host = callbackAddress.toString();
178 }
179 return host;
180}
181
182void QOAuthHttpServerReplyHandlerPrivate::_q_clientConnected()
183{
184 QTcpSocket *socket = httpServer->nextPendingConnection();
185
186 QObject::connect(sender: socket, signal: &QTcpSocket::disconnected, context: socket, slot: &QTcpSocket::deleteLater);
187 QObject::connect(sender: socket, signal: &QTcpSocket::readyRead, context: q_ptr,
188 slot: [this, socket]() { _q_readData(socket); });
189}
190
191void QOAuthHttpServerReplyHandlerPrivate::_q_readData(QTcpSocket *socket)
192{
193 QHttpRequest *request = nullptr;
194 if (auto it = clients.find(key: socket); it == clients.end()) {
195 request = &clients[socket]; // insert it
196 request->port = httpServer->serverPort();
197 } else {
198 request = &*it;
199 }
200
201 bool error = false;
202
203 if (Q_LIKELY(request->state == QHttpRequest::State::ReadingMethod))
204 if (Q_UNLIKELY(error = !request->readMethod(socket)))
205 qCWarning(lcReplyHandler, "Invalid Method");
206
207 if (Q_LIKELY(!error && request->state == QHttpRequest::State::ReadingUrl))
208 if (Q_UNLIKELY(error = !request->readUrl(socket)))
209 qCWarning(lcReplyHandler, "Invalid URL");
210
211 if (Q_LIKELY(!error && request->state == QHttpRequest::State::ReadingStatus))
212 if (Q_UNLIKELY(error = !request->readStatus(socket)))
213 qCWarning(lcReplyHandler, "Invalid Status");
214
215 if (Q_LIKELY(!error && request->state == QHttpRequest::State::ReadingHeader))
216 if (Q_UNLIKELY(error = !request->readHeader(socket)))
217 qCWarning(lcReplyHandler, "Invalid Header");
218
219 if (error) {
220 socket->disconnectFromHost();
221 clients.remove(key: socket);
222 } else if (!request->url.isEmpty()) {
223 Q_ASSERT(request->state != QHttpRequest::State::ReadingUrl);
224 _q_answerClient(socket, url: request->url);
225 clients.remove(key: socket);
226 }
227}
228
229void QOAuthHttpServerReplyHandlerPrivate::_q_answerClient(QTcpSocket *socket, const QUrl &url)
230{
231 Q_Q(QOAuthHttpServerReplyHandler);
232 if (url.path() != path) {
233 qCWarning(lcReplyHandler, "Invalid request: %s", qPrintable(url.toString()));
234 } else {
235 Q_EMIT q->callbackDataReceived(data: QUrl(callback()).resolved(relative: url).toEncoded());
236
237 QVariantMap receivedData;
238 const QUrlQuery query(url.query());
239 const auto items = query.queryItems();
240 for (auto it = items.begin(), end = items.end(); it != end; ++it)
241 receivedData.insert(key: it->first, value: it->second);
242 Q_EMIT q->callbackReceived(values: receivedData);
243
244 const QByteArray html = QByteArrayLiteral("<html><head><title>") +
245 qApp->applicationName().toUtf8() +
246 QByteArrayLiteral("</title></head><body>") +
247 text.toUtf8() +
248 QByteArrayLiteral("</body></html>");
249
250 const QByteArray htmlSize = QByteArray::number(html.size());
251 const QByteArray replyMessage = QByteArrayLiteral("HTTP/1.0 200 OK \r\n"
252 "Content-Type: text/html; "
253 "charset=\"utf-8\"\r\n"
254 "Content-Length: ") + htmlSize +
255 QByteArrayLiteral("\r\n\r\n") +
256 html;
257
258 socket->write(data: replyMessage);
259 }
260 socket->disconnectFromHost();
261}
262
263void QOAuthHttpServerReplyHandlerPrivate::initializeLocalServer()
264{
265 Q_Q(QOAuthHttpServerReplyHandler);
266 QObject::connect(sender: httpServer, signal: &QTcpServer::pendingConnectionAvailable, context: q, slot: [this]() {
267 _q_clientConnected();
268 });
269}
270
271bool QOAuthHttpServerReplyHandlerPrivate::listen(const QHostAddress &address, quint16 port)
272{
273 Q_ASSERT(httpServer);
274 bool success = false;
275
276 if (address.isNull()) {
277 // try IPv4 first, for greatest compatibility
278 success = httpServer->listen(address: QHostAddress::LocalHost, port);
279 if (!success)
280 success = httpServer->listen(address: QHostAddress::LocalHostIPv6, port);
281 }
282 if (!success)
283 success = httpServer->listen(address, port);
284
285 if (success) {
286 // Callback ('redirect_uri') value may be needed after this handler is closed
287 callbackAddress = httpServer->serverAddress();
288 callbackPort = httpServer->serverPort();
289 }
290 return success;
291}
292
293bool QOAuthHttpServerReplyHandlerPrivate::QHttpRequest::readMethod(QTcpSocket *socket)
294{
295 bool finished = false;
296 while (socket->bytesAvailable() && !finished) {
297 char c;
298 socket->getChar(c: &c);
299 if (std::isupper(c) && fragment.size() < 6)
300 fragment += c;
301 else
302 finished = true;
303 }
304 if (finished) {
305 if (fragment == "HEAD")
306 method = Method::Head;
307 else if (fragment == "GET")
308 method = Method::Get;
309 else if (fragment == "PUT")
310 method = Method::Put;
311 else if (fragment == "POST")
312 method = Method::Post;
313 else if (fragment == "DELETE")
314 method = Method::Delete;
315 else
316 qCWarning(lcReplyHandler, "Invalid operation %s", fragment.data());
317
318 state = State::ReadingUrl;
319 fragment.clear();
320
321 return method != Method::Unknown;
322 }
323 return true;
324}
325
326bool QOAuthHttpServerReplyHandlerPrivate::QHttpRequest::readUrl(QTcpSocket *socket)
327{
328 bool finished = false;
329 while (socket->bytesAvailable() && !finished) {
330 char c;
331 socket->getChar(c: &c);
332 if (ascii_isspace(c))
333 finished = true;
334 else
335 fragment += c;
336 }
337 if (finished) {
338 url = QUrl::fromEncoded(input: fragment);
339 state = State::ReadingStatus;
340
341 if (!fragment.startsWith(c: u'/') || !url.isValid() || !url.scheme().isNull()
342 || !url.host().isNull()) {
343 qCWarning(lcReplyHandler, "Invalid request: %s", fragment.constData());
344 return false;
345 }
346 fragment.clear();
347 return true;
348 }
349 return true;
350}
351
352bool QOAuthHttpServerReplyHandlerPrivate::QHttpRequest::readStatus(QTcpSocket *socket)
353{
354 bool finished = false;
355 while (socket->bytesAvailable() && !finished) {
356 char c;
357 socket->getChar(c: &c);
358 fragment += c;
359 if (fragment.endsWith(bv: "\r\n")) {
360 finished = true;
361 fragment.resize(size: fragment.size() - 2);
362 }
363 }
364 if (finished) {
365 if (!std::isdigit(fragment.at(i: fragment.size() - 3)) ||
366 !std::isdigit(fragment.at(i: fragment.size() - 1))) {
367 qCWarning(lcReplyHandler, "Invalid version");
368 return false;
369 }
370 version = std::make_pair(x: fragment.at(i: fragment.size() - 3) - '0',
371 y: fragment.at(i: fragment.size() - 1) - '0');
372 state = State::ReadingHeader;
373 fragment.clear();
374 }
375 return true;
376}
377
378bool QOAuthHttpServerReplyHandlerPrivate::QHttpRequest::readHeader(QTcpSocket *socket)
379{
380 while (socket->bytesAvailable()) {
381 char c;
382 socket->getChar(c: &c);
383 fragment += c;
384 if (fragment.endsWith(bv: "\r\n")) {
385 if (fragment == "\r\n") {
386 state = State::ReadingBody;
387 fragment.clear();
388 return true;
389 } else {
390 fragment.chop(n: 2);
391 const int index = fragment.indexOf(ch: ':');
392 if (index == -1)
393 return false;
394
395 const QByteArray key = fragment.mid(index: 0, len: index).trimmed();
396 const QByteArray value = fragment.mid(index: index + 1).trimmed();
397 headers.insert(key, value);
398 fragment.clear();
399 }
400 }
401 }
402 return false;
403}
404
405/*!
406 Constructs a QOAuthHttpServerReplyHandler object using \a parent as a
407 parent object. Calls \l {listen()} with port \c 0 and address
408 \l {QHostAddress::SpecialAddress}{LocalHost}.
409
410 \sa listen()
411*/
412QOAuthHttpServerReplyHandler::QOAuthHttpServerReplyHandler(QObject *parent) :
413 QOAuthHttpServerReplyHandler(QHostAddress::LocalHost, 0, parent)
414{}
415
416/*!
417 Constructs a QOAuthHttpServerReplyHandler object using \a parent as a
418 parent object. Calls \l {listen()} with \a port and address
419 \l {QHostAddress::SpecialAddress}{LocalHost}.
420
421 \sa listen()
422*/
423QOAuthHttpServerReplyHandler::QOAuthHttpServerReplyHandler(quint16 port, QObject *parent) :
424 QOAuthHttpServerReplyHandler(QHostAddress::LocalHost, port, parent)
425{}
426
427/*!
428 Constructs a QOAuthHttpServerReplyHandler object using \a parent as a
429 parent object. Calls \l {listen()} with \a address and \a port.
430
431 \sa listen()
432*/
433QOAuthHttpServerReplyHandler::QOAuthHttpServerReplyHandler(const QHostAddress &address,
434 quint16 port, QObject *parent) :
435 QOAuthOobReplyHandler(parent),
436 d_ptr(new QOAuthHttpServerReplyHandlerPrivate(this))
437{
438 Q_D(QOAuthHttpServerReplyHandler);
439 d->httpServer = new QTcpServer(this);
440 d->initializeLocalServer();
441 d->listen(address, port);
442}
443
444/*!
445 Destroys the QOAuthHttpServerReplyHandler object.
446 Stops listening for connections / redirections.
447
448 \sa close()
449*/
450QOAuthHttpServerReplyHandler::~QOAuthHttpServerReplyHandler()
451{}
452
453QString QOAuthHttpServerReplyHandler::callback() const
454{
455 Q_D(const QOAuthHttpServerReplyHandler);
456 return d->callback();
457}
458
459/*!
460 Returns the path that is used as the path component of the
461 \l callback() / \l{https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2}
462 {OAuth2 redirect_uri parameter}.
463
464 \sa setCallbackPath()
465*/
466QString QOAuthHttpServerReplyHandler::callbackPath() const
467{
468 Q_D(const QOAuthHttpServerReplyHandler);
469 return d->path;
470}
471
472/*!
473 Sets \a path to be used as the path component of the
474 \l callback().
475
476 \sa callbackPath()
477*/
478void QOAuthHttpServerReplyHandler::setCallbackPath(const QString &path)
479{
480 Q_D(QOAuthHttpServerReplyHandler);
481 // pass through QUrl to ensure normalization
482 QUrl url;
483 url.setPath(path);
484 d->path = url.path(options: QUrl::FullyEncoded);
485 if (d->path.isEmpty())
486 d->path = u'/';
487}
488
489/*!
490 \since 6.9
491
492 Returns the name that is used as the host component of the
493 \l callback() / \l{https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2}
494 {OAuth2 redirect_uri parameter}.
495
496 \sa setCallbackHost()
497*/
498QString QOAuthHttpServerReplyHandler::callbackHost() const
499{
500 Q_D(const QOAuthHttpServerReplyHandler);
501 return d->callbackHost();
502}
503
504/*!
505 \since 6.9
506
507 Sets \a host to be used as the hostname component of the
508 \l callback(). Providing a non-empty \a host overrides the
509 default behavior, see \l {IPv4 and IPv6}.
510
511 \sa callbackHost()
512*/
513void QOAuthHttpServerReplyHandler::setCallbackHost(const QString &host)
514{
515 Q_D(QOAuthHttpServerReplyHandler);
516 d->callbackHostname = host;
517}
518
519/*!
520 Returns the text that is used in response to the
521 redirection at the end of the authorization stage.
522
523 The text is wrapped in a simple HTML page, and displayed to
524 the user by the browser / user-agent which did the redirection.
525
526 The default text is
527 \badcode
528 Callback received. Feel free to close this page.
529 \endcode
530
531 \sa setCallbackText()
532*/
533QString QOAuthHttpServerReplyHandler::callbackText() const
534{
535 Q_D(const QOAuthHttpServerReplyHandler);
536 return d->text;
537}
538
539/*!
540 Sets \a text to be used in response to the
541 redirection at the end of the authorization stage.
542
543 \sa callbackText()
544*/
545void QOAuthHttpServerReplyHandler::setCallbackText(const QString &text)
546{
547 Q_D(QOAuthHttpServerReplyHandler);
548 d->text = text;
549}
550
551/*!
552 Returns the port on which this handler is listening,
553 otherwise returns 0.
554
555 \sa listen(), isListening()
556*/
557quint16 QOAuthHttpServerReplyHandler::port() const
558{
559 Q_D(const QOAuthHttpServerReplyHandler);
560 return d->httpServer->serverPort();
561}
562
563/*!
564 Tells this handler to listen for incoming connections / redirections
565 on \a address and \a port. Returns \c true if listening is successful,
566 and \c false otherwise.
567
568 Active listening is only required when performing the initial
569 authorization phase, typically initiated by a
570 QOAuth2AuthorizationCodeFlow::grant() call.
571
572 It is recommended to close the listener after successful authorization.
573 Listening is not needed for
574 \l {QOAuth2AuthorizationCodeFlow::requestAccessToken()}{requesting access tokens}
575 or refreshing them.
576
577 If this function is called with \l {QHostAddress::SpecialAddress}{Null}
578 as the \a address, the handler will attempt to listen to
579 \l {QHostAddress::SpecialAddress}{LocalHost}, and if that fails,
580 \l {QHostAddress::SpecialAddress}{LocalHostIPv6}.
581
582 See also \l {IPv4 and IPv6}.
583
584 \sa close(), isListening(), QTcpServer::listen()
585*/
586bool QOAuthHttpServerReplyHandler::listen(const QHostAddress &address, quint16 port)
587{
588 Q_D(QOAuthHttpServerReplyHandler);
589#ifndef QT_NO_SSL
590 if (qobject_cast<QSslServer*>(object: d->httpServer)) {
591 d->httpServer->close();
592 delete d->httpServer;
593 d->httpServer = new QTcpServer(this);
594 d->initializeLocalServer();
595 }
596#endif
597 return d->listen(address, port);
598}
599
600#ifndef QT_NO_SSL
601/*!
602 Tells this handler to listen for incoming \c https
603 connections / redirections on \a address and \a port. Returns
604 \c true if listening is successful, and \c false otherwise.
605
606 See \l {HTTP and HTTPS Callbacks} for further information.
607
608 \sa listen(const QHostAddress &, quint16), close(),
609 isListening(), QSslServer, QTcpServer::listen()
610*/
611bool QOAuthHttpServerReplyHandler::listen(const QSslConfiguration &configuration,
612 const QHostAddress &address, quint16 port)
613{
614 Q_D(QOAuthHttpServerReplyHandler);
615
616 if (!QSslSocket::supportsSsl()) {
617 qCWarning(lcReplyHandler, "SSL not supported, cannot listen");
618 d->httpServer->close();
619 return false;
620 }
621
622 if (configuration.isNull()) {
623 qCWarning(lcReplyHandler, "QSslConfiguration is null, cannot listen");
624 d->httpServer->close();
625 return false;
626 }
627
628 if (!qobject_cast<QSslServer*>(object: d->httpServer)) {
629 d->httpServer->close();
630 delete d->httpServer;
631 d->httpServer = new QSslServer(this);
632 d->initializeLocalServer();
633 }
634
635 auto sslServer = qobject_cast<QSslServer*>(object: d->httpServer);
636 sslServer->setSslConfiguration(configuration);
637 return d->listen(address, port);
638}
639#endif
640
641/*!
642 Tells this handler to stop listening for connections / redirections.
643
644 \sa listen()
645*/
646void QOAuthHttpServerReplyHandler::close()
647{
648 Q_D(QOAuthHttpServerReplyHandler);
649 d->httpServer->close();
650}
651
652/*!
653 Returns \c true if this handler is currently listening,
654 and \c false otherwise.
655
656 \sa listen(), close()
657*/
658bool QOAuthHttpServerReplyHandler::isListening() const
659{
660 Q_D(const QOAuthHttpServerReplyHandler);
661 return d->httpServer->isListening();
662}
663
664QT_END_NAMESPACE
665
666#include "moc_qoauthhttpserverreplyhandler.cpp"
667

source code of qtnetworkauth/src/oauth/qoauthhttpserverreplyhandler.cpp