1// Copyright (C) 2017 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include <qabstractoauth.h>
5#include <qoauthhttpserverreplyhandler.h>
6#include "qabstractoauthreplyhandler_p.h"
7
8#include <private/qoauthhttpserverreplyhandler_p.h>
9
10#include <QtCore/qurl.h>
11#include <QtCore/qurlquery.h>
12#include <QtCore/qcoreapplication.h>
13#include <QtCore/qloggingcategory.h>
14#include <QtCore/private/qlocale_p.h>
15
16#include <QtNetwork/qtcpsocket.h>
17#include <QtNetwork/qnetworkreply.h>
18
19QT_BEGIN_NAMESPACE
20
21using namespace Qt::StringLiterals;
22
23/*!
24 \class QOAuthHttpServerReplyHandler
25 \inmodule QtNetworkAuth
26 \ingroup oauth
27 \since 5.8
28
29 \brief Handles loopback redirects by setting up a local HTTP server.
30
31 This class serves as a reply handler for
32 \l {https://datatracker.ietf.org/doc/html/rfc6749}{OAuth 2.0} authorization
33 processes that use
34 \l {https://datatracker.ietf.org/doc/html/rfc8252#section-7.3}{loopback redirection}.
35
36 The \l {https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2}
37 {redirect URI} is where the authorization server redirects the
38 user-agent (typically, and preferably, the system browser) once
39 the authorization part of the flow is complete. Loopback redirect
40 URIs use \c http as the scheme and either \e localhost or an IP
41 address literal as the host (see \l {IPv4 and IPv6}).
42
43 QOAuthHttpServerReplyHandler sets up a localhost server. Once the
44 authorization server redirects the browser to this localhost address,
45 the reply handler parses the redirection URI query parameters,
46 and then signals authorization completion with
47 \l {QAbstractOAuthReplyHandler::callbackReceived}{a signal}.
48
49 To handle other redirect URI schemes, see QOAuthUriSchemeReplyHandler.
50
51 The following code illustrates the usage. First, the needed variables:
52
53 \snippet src_oauth_replyhandlers.cpp httpserver-variables
54
55 Followed up by the OAuth setup (error handling omitted for brevity):
56
57 \snippet src_oauth_replyhandlers.cpp httpserver-oauth-setup
58
59 Finally, we then set up the URI scheme reply-handler:
60
61 \snippet src_oauth_replyhandlers.cpp httpserver-handler-setup
62
63 \section1 IPv4 and IPv6
64
65 If the handler is an \e any address handler
66 (\l {QHostAddress::SpecialAddress}{AnyIPv4, AnyIPv6, or Any}),
67 the used callback is in the form of \c {http://localhost:{port}/{path}}.
68 Handler will first attempt to listen on IPv4 loopback address,
69 and then on IPv6. \c {localhost} is used because it resolves correctly
70 on both IPv4 and IPv6 interfaces.
71
72 For loopback addresses
73 (\l {QHostAddress::SpecialAddress}{LocalHost or LocalHostIPv6})
74 the IP literals (\c {127.0.0.1} and \c {::1}) are used.
75
76 For specific IP addresses the provided IP literal is used directly,
77 for instance:
78 \e {http://192.168.0.123:{port}/{path}} in the case of an IPv4 address.
79*/
80QOAuthHttpServerReplyHandlerPrivate::QOAuthHttpServerReplyHandlerPrivate(
81 QOAuthHttpServerReplyHandler *p) :
82 text(QObject::tr(s: "Callback received. Feel free to close this page.")), path(u'/'), q_ptr(p)
83{
84 QObject::connect(sender: &httpServer, signal: &QTcpServer::newConnection, context: q_ptr,
85 slot: [this]() { _q_clientConnected(); });
86}
87
88QOAuthHttpServerReplyHandlerPrivate::~QOAuthHttpServerReplyHandlerPrivate()
89{
90 if (httpServer.isListening())
91 httpServer.close();
92}
93
94QString QOAuthHttpServerReplyHandlerPrivate::callback() const
95{
96 QUrl url;
97 url.setScheme(u"http"_s);
98 url.setPort(callbackPort);
99 url.setPath(path);
100 url.setHost(host: callbackHost());
101 return url.toString(options: QUrl::EncodeSpaces | QUrl::EncodeUnicode | QUrl::EncodeDelimiters
102 | QUrl::EncodeReserved);
103}
104
105QString QOAuthHttpServerReplyHandlerPrivate::callbackHost() const
106{
107 QString host;
108 if (callbackAddress == QHostAddress::AnyIPv4 || callbackAddress == QHostAddress::Any
109 || callbackAddress == QHostAddress::AnyIPv6) {
110 // Convert Any addresses to "localhost"
111 host = u"localhost"_s;
112 } else {
113 // For other than Any addresses, use QHostAddress::toString() which returns an
114 // IP literal. This includes user-provided addresses, as well as special addresses
115 // such as LocalHost (127.0.0.1) and LocalHostIPv6 (::1)
116 host = callbackAddress.toString();
117 }
118 return host;
119}
120
121void QOAuthHttpServerReplyHandlerPrivate::_q_clientConnected()
122{
123 QTcpSocket *socket = httpServer.nextPendingConnection();
124
125 QObject::connect(sender: socket, signal: &QTcpSocket::disconnected, context: socket, slot: &QTcpSocket::deleteLater);
126 QObject::connect(sender: socket, signal: &QTcpSocket::readyRead, context: q_ptr,
127 slot: [this, socket]() { _q_readData(socket); });
128}
129
130void QOAuthHttpServerReplyHandlerPrivate::_q_readData(QTcpSocket *socket)
131{
132 QHttpRequest *request = nullptr;
133 if (auto it = clients.find(key: socket); it == clients.end()) {
134 request = &clients[socket]; // insert it
135 request->port = httpServer.serverPort();
136 } else {
137 request = &*it;
138 }
139
140 bool error = false;
141
142 if (Q_LIKELY(request->state == QHttpRequest::State::ReadingMethod))
143 if (Q_UNLIKELY(error = !request->readMethod(socket)))
144 qCWarning(lcReplyHandler, "Invalid Method");
145
146 if (Q_LIKELY(!error && request->state == QHttpRequest::State::ReadingUrl))
147 if (Q_UNLIKELY(error = !request->readUrl(socket)))
148 qCWarning(lcReplyHandler, "Invalid URL");
149
150 if (Q_LIKELY(!error && request->state == QHttpRequest::State::ReadingStatus))
151 if (Q_UNLIKELY(error = !request->readStatus(socket)))
152 qCWarning(lcReplyHandler, "Invalid Status");
153
154 if (Q_LIKELY(!error && request->state == QHttpRequest::State::ReadingHeader))
155 if (Q_UNLIKELY(error = !request->readHeader(socket)))
156 qCWarning(lcReplyHandler, "Invalid Header");
157
158 if (error) {
159 socket->disconnectFromHost();
160 clients.remove(key: socket);
161 } else if (!request->url.isEmpty()) {
162 Q_ASSERT(request->state != QHttpRequest::State::ReadingUrl);
163 _q_answerClient(socket, url: request->url);
164 clients.remove(key: socket);
165 }
166}
167
168void QOAuthHttpServerReplyHandlerPrivate::_q_answerClient(QTcpSocket *socket, const QUrl &url)
169{
170 Q_Q(QOAuthHttpServerReplyHandler);
171 if (url.path() != path) {
172 qCWarning(lcReplyHandler, "Invalid request: %s", qPrintable(url.toString()));
173 } else {
174 QVariantMap receivedData;
175 const QUrlQuery query(url.query());
176 const auto items = query.queryItems();
177 for (auto it = items.begin(), end = items.end(); it != end; ++it)
178 receivedData.insert(key: it->first, value: it->second);
179 Q_EMIT q->callbackReceived(values: receivedData);
180
181 const QByteArray html = QByteArrayLiteral("<html><head><title>") +
182 qApp->applicationName().toUtf8() +
183 QByteArrayLiteral("</title></head><body>") +
184 text.toUtf8() +
185 QByteArrayLiteral("</body></html>");
186
187 const QByteArray htmlSize = QByteArray::number(html.size());
188 const QByteArray replyMessage = QByteArrayLiteral("HTTP/1.0 200 OK \r\n"
189 "Content-Type: text/html; "
190 "charset=\"utf-8\"\r\n"
191 "Content-Length: ") + htmlSize +
192 QByteArrayLiteral("\r\n\r\n") +
193 html;
194
195 socket->write(data: replyMessage);
196 }
197 socket->disconnectFromHost();
198}
199
200bool QOAuthHttpServerReplyHandlerPrivate::QHttpRequest::readMethod(QTcpSocket *socket)
201{
202 bool finished = false;
203 while (socket->bytesAvailable() && !finished) {
204 char c;
205 socket->getChar(c: &c);
206 if (std::isupper(c) && fragment.size() < 6)
207 fragment += c;
208 else
209 finished = true;
210 }
211 if (finished) {
212 if (fragment == "HEAD")
213 method = Method::Head;
214 else if (fragment == "GET")
215 method = Method::Get;
216 else if (fragment == "PUT")
217 method = Method::Put;
218 else if (fragment == "POST")
219 method = Method::Post;
220 else if (fragment == "DELETE")
221 method = Method::Delete;
222 else
223 qCWarning(lcReplyHandler, "Invalid operation %s", fragment.data());
224
225 state = State::ReadingUrl;
226 fragment.clear();
227
228 return method != Method::Unknown;
229 }
230 return true;
231}
232
233bool QOAuthHttpServerReplyHandlerPrivate::QHttpRequest::readUrl(QTcpSocket *socket)
234{
235 bool finished = false;
236 while (socket->bytesAvailable() && !finished) {
237 char c;
238 socket->getChar(c: &c);
239 if (ascii_isspace(c))
240 finished = true;
241 else
242 fragment += c;
243 }
244 if (finished) {
245 url = QUrl::fromEncoded(input: fragment);
246 state = State::ReadingStatus;
247
248 if (!fragment.startsWith(c: u'/') || !url.isValid() || !url.scheme().isNull()
249 || !url.host().isNull()) {
250 qCWarning(lcReplyHandler, "Invalid request: %s", fragment.constData());
251 return false;
252 }
253 fragment.clear();
254 return true;
255 }
256 return true;
257}
258
259bool QOAuthHttpServerReplyHandlerPrivate::QHttpRequest::readStatus(QTcpSocket *socket)
260{
261 bool finished = false;
262 while (socket->bytesAvailable() && !finished) {
263 char c;
264 socket->getChar(c: &c);
265 fragment += c;
266 if (fragment.endsWith(bv: "\r\n")) {
267 finished = true;
268 fragment.resize(size: fragment.size() - 2);
269 }
270 }
271 if (finished) {
272 if (!std::isdigit(fragment.at(i: fragment.size() - 3)) ||
273 !std::isdigit(fragment.at(i: fragment.size() - 1))) {
274 qCWarning(lcReplyHandler, "Invalid version");
275 return false;
276 }
277 version = std::make_pair(x: fragment.at(i: fragment.size() - 3) - '0',
278 y: fragment.at(i: fragment.size() - 1) - '0');
279 state = State::ReadingHeader;
280 fragment.clear();
281 }
282 return true;
283}
284
285bool QOAuthHttpServerReplyHandlerPrivate::QHttpRequest::readHeader(QTcpSocket *socket)
286{
287 while (socket->bytesAvailable()) {
288 char c;
289 socket->getChar(c: &c);
290 fragment += c;
291 if (fragment.endsWith(bv: "\r\n")) {
292 if (fragment == "\r\n") {
293 state = State::ReadingBody;
294 fragment.clear();
295 return true;
296 } else {
297 fragment.chop(n: 2);
298 const int index = fragment.indexOf(ch: ':');
299 if (index == -1)
300 return false;
301
302 const QByteArray key = fragment.mid(index: 0, len: index).trimmed();
303 const QByteArray value = fragment.mid(index: index + 1).trimmed();
304 headers.insert(key, value);
305 fragment.clear();
306 }
307 }
308 }
309 return false;
310}
311
312/*!
313 Constructs a QOAuthHttpServerReplyHandler object using \a parent as a
314 parent object. Calls \l {listen()} with port \c 0 and address
315 \l {QHostAddress::SpecialAddress}{LocalHost}.
316
317 \sa listen()
318*/
319QOAuthHttpServerReplyHandler::QOAuthHttpServerReplyHandler(QObject *parent) :
320 QOAuthHttpServerReplyHandler(QHostAddress::LocalHost, 0, parent)
321{}
322
323/*!
324 Constructs a QOAuthHttpServerReplyHandler object using \a parent as a
325 parent object. Calls \l {listen()} with \a port and address
326 \l {QHostAddress::SpecialAddress}{LocalHost}.
327
328 \sa listen()
329*/
330QOAuthHttpServerReplyHandler::QOAuthHttpServerReplyHandler(quint16 port, QObject *parent) :
331 QOAuthHttpServerReplyHandler(QHostAddress::LocalHost, port, parent)
332{}
333
334/*!
335 Constructs a QOAuthHttpServerReplyHandler object using \a parent as a
336 parent object. Calls \l {listen()} with \a address and \a port.
337
338 \sa listen()
339*/
340QOAuthHttpServerReplyHandler::QOAuthHttpServerReplyHandler(const QHostAddress &address,
341 quint16 port, QObject *parent) :
342 QOAuthOobReplyHandler(parent),
343 d_ptr(new QOAuthHttpServerReplyHandlerPrivate(this))
344{
345 listen(address, port);
346}
347
348/*!
349 Destroys the QOAuthHttpServerReplyHandler object.
350 Stops listening for connections / redirections.
351
352 \sa close()
353*/
354QOAuthHttpServerReplyHandler::~QOAuthHttpServerReplyHandler()
355{}
356
357QString QOAuthHttpServerReplyHandler::callback() const
358{
359 Q_D(const QOAuthHttpServerReplyHandler);
360 return d->callback();
361}
362
363/*!
364 Returns the path that is used as the path component of the
365 \l callback() / \l{https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2}
366 {OAuth2 redirect_uri parameter}.
367
368 \sa setCallbackPath()
369*/
370QString QOAuthHttpServerReplyHandler::callbackPath() const
371{
372 Q_D(const QOAuthHttpServerReplyHandler);
373 return d->path;
374}
375
376/*!
377 Sets \a path to be used as the path component of the
378 \l callback().
379
380 \sa callbackPath()
381*/
382void QOAuthHttpServerReplyHandler::setCallbackPath(const QString &path)
383{
384 Q_D(QOAuthHttpServerReplyHandler);
385 // pass through QUrl to ensure normalization
386 QUrl url;
387 url.setPath(path);
388 d->path = url.path(options: QUrl::FullyEncoded);
389 if (d->path.isEmpty())
390 d->path = u'/';
391}
392
393/*!
394 Returns the text that is used in response to the
395 redirection at the end of the authorization stage.
396
397 The text is wrapped in a simple HTML page, and displayed to
398 the user by the browser / user-agent which did the redirection.
399
400 The default text is
401 \badcode
402 Callback received. Feel free to close this page.
403 \endcode
404
405 \sa setCallbackText()
406*/
407QString QOAuthHttpServerReplyHandler::callbackText() const
408{
409 Q_D(const QOAuthHttpServerReplyHandler);
410 return d->text;
411}
412
413/*!
414 Sets \a text to be used in response to the
415 redirection at the end of the authorization stage.
416
417 \sa callbackText()
418*/
419void QOAuthHttpServerReplyHandler::setCallbackText(const QString &text)
420{
421 Q_D(QOAuthHttpServerReplyHandler);
422 d->text = text;
423}
424
425/*!
426 Returns the port on which this handler is listening,
427 otherwise returns 0.
428
429 \sa listen(), isListening()
430*/
431quint16 QOAuthHttpServerReplyHandler::port() const
432{
433 Q_D(const QOAuthHttpServerReplyHandler);
434 return d->httpServer.serverPort();
435}
436
437/*!
438 Tells this handler to listen for incoming connections / redirections
439 on \a address and \a port. Returns \c true if listening is successful,
440 and \c false otherwise.
441
442 Active listening is only required when performing the initial
443 authorization phase, typically initiated by a
444 QOAuth2AuthorizationCodeFlow::grant() call.
445
446 It is recommended to close the listener after successful authorization.
447 Listening is not needed for
448 \l {QOAuth2AuthorizationCodeFlow::requestAccessToken()}{requesting access tokens}
449 or refreshing them.
450
451 If this function is called with \l {QHostAddress::SpecialAddress}{Null}
452 as the \a address, the handler will attempt to listen to
453 \l {QHostAddress::SpecialAddress}{LocalHost}, and if that fails,
454 \l {QHostAddress::SpecialAddress}{LocalHostIPv6}.
455
456 See also \l {IPv4 and IPv6}.
457
458 \sa close(), isListening(), QTcpServer::listen()
459*/
460bool QOAuthHttpServerReplyHandler::listen(const QHostAddress &address, quint16 port)
461{
462 Q_D(QOAuthHttpServerReplyHandler);
463 bool success = false;
464
465 if (address.isNull()) {
466 // try IPv4 first, for greatest compatibility
467 success = d->httpServer.listen(address: QHostAddress::LocalHost, port);
468 if (!success)
469 success = d->httpServer.listen(address: QHostAddress::LocalHostIPv6, port);
470 }
471 if (!success)
472 success = d->httpServer.listen(address, port);
473
474 if (success) {
475 // Callback ('redirect_uri') value may be needed after this handler is closed
476 d->callbackAddress = d->httpServer.serverAddress();
477 d->callbackPort = d->httpServer.serverPort();
478 }
479
480 return success;
481}
482
483/*!
484 Tells this handler to stop listening for connections / redirections.
485
486 \sa listen()
487*/
488void QOAuthHttpServerReplyHandler::close()
489{
490 Q_D(QOAuthHttpServerReplyHandler);
491 return d->httpServer.close();
492}
493
494/*!
495 Returns \c true if this handler is currently listening,
496 and \c false otherwise.
497
498 \sa listen(), close()
499*/
500bool QOAuthHttpServerReplyHandler::isListening() const
501{
502 Q_D(const QOAuthHttpServerReplyHandler);
503 return d->httpServer.isListening();
504}
505
506QT_END_NAMESPACE
507
508#include "moc_qoauthhttpserverreplyhandler.cpp"
509

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