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 | |
19 | QT_BEGIN_NAMESPACE |
20 | |
21 | using 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 | */ |
80 | QOAuthHttpServerReplyHandlerPrivate::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 | |
88 | QOAuthHttpServerReplyHandlerPrivate::~QOAuthHttpServerReplyHandlerPrivate() |
89 | { |
90 | if (httpServer.isListening()) |
91 | httpServer.close(); |
92 | } |
93 | |
94 | QString 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 | |
105 | QString 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 | |
121 | void 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 | |
130 | void 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 | |
168 | void 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 | |
200 | bool 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 | |
233 | bool 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 | |
259 | bool 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 | |
285 | bool 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 | */ |
319 | QOAuthHttpServerReplyHandler::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 | */ |
330 | QOAuthHttpServerReplyHandler::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 | */ |
340 | QOAuthHttpServerReplyHandler::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 | */ |
354 | QOAuthHttpServerReplyHandler::~QOAuthHttpServerReplyHandler() |
355 | {} |
356 | |
357 | QString 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 | */ |
370 | QString 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 | */ |
382 | void 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 | */ |
407 | QString 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 | */ |
419 | void 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 | */ |
431 | quint16 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 | */ |
460 | bool 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 | */ |
488 | void 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 | */ |
500 | bool QOAuthHttpServerReplyHandler::isListening() const |
501 | { |
502 | Q_D(const QOAuthHttpServerReplyHandler); |
503 | return d->httpServer.isListening(); |
504 | } |
505 | |
506 | QT_END_NAMESPACE |
507 | |
508 | #include "moc_qoauthhttpserverreplyhandler.cpp" |
509 | |