| 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 | |