1 | /* |
2 | * Copyright (C) by Olivier Goffart <ogoffart@woboq.com> |
3 | * |
4 | * This program is free software; you can redistribute it and/or modify |
5 | * it under the terms of the GNU General Public License as published by |
6 | * the Free Software Foundation; either version 2 of the License, or |
7 | * (at your option) any later version. |
8 | * |
9 | * This program is distributed in the hope that it will be useful, but |
10 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY |
11 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
12 | * for more details. |
13 | */ |
14 | |
15 | #include <QDesktopServices> |
16 | #include <QNetworkReply> |
17 | #include <QTimer> |
18 | #include <QBuffer> |
19 | #include "account.h" |
20 | #include "creds/oauth.h" |
21 | #include <QJsonObject> |
22 | #include <QJsonDocument> |
23 | #include "theme.h" |
24 | #include "networkjobs.h" |
25 | #include "creds/httpcredentials.h" |
26 | |
27 | namespace OCC { |
28 | |
29 | Q_LOGGING_CATEGORY(lcOauth, "sync.credentials.oauth" , QtInfoMsg) |
30 | |
31 | OAuth::~OAuth() |
32 | { |
33 | } |
34 | |
35 | static void httpReplyAndClose(QTcpSocket *socket, const char *code, const char *html, |
36 | const char * = nullptr) |
37 | { |
38 | if (!socket) |
39 | return; // socket can have been deleted if the browser was closed |
40 | socket->write("HTTP/1.1 " ); |
41 | socket->write(code); |
42 | socket->write("\r\nContent-Type: text/html\r\nConnection: close\r\nContent-Length: " ); |
43 | socket->write(QByteArray::number(qstrlen(html))); |
44 | if (moreHeaders) { |
45 | socket->write("\r\n" ); |
46 | socket->write(moreHeaders); |
47 | } |
48 | socket->write("\r\n\r\n" ); |
49 | socket->write(html); |
50 | socket->disconnectFromHost(); |
51 | // We don't want that deleting the server too early prevent queued data to be sent on this socket. |
52 | // The socket will be deleted after disconnection because disconnected is connected to deleteLater |
53 | socket->setParent(nullptr); |
54 | } |
55 | |
56 | void OAuth::start() |
57 | { |
58 | // Listen on the socket to get a port which will be used in the redirect_uri |
59 | if (!_server.listen(QHostAddress::LocalHost)) { |
60 | emit result(NotSupported, QString()); |
61 | return; |
62 | } |
63 | |
64 | if (!openBrowser()) |
65 | return; |
66 | |
67 | QObject::connect(&_server, &QTcpServer::newConnection, this, [this] { |
68 | while (QPointer<QTcpSocket> socket = _server.nextPendingConnection()) { |
69 | QObject::connect(socket.data(), &QTcpSocket::disconnected, socket.data(), &QTcpSocket::deleteLater); |
70 | QObject::connect(socket.data(), &QIODevice::readyRead, this, [this, socket] { |
71 | QByteArray peek = socket->peek(qMin(socket->bytesAvailable(), 4000LL)); //The code should always be within the first 4K |
72 | if (peek.indexOf('\n') < 0) |
73 | return; // wait until we find a \n |
74 | QRegExp rx("^GET /\\?code=([a-zA-Z0-9]+)[& ]" ); // Match a /?code=... URL |
75 | if (rx.indexIn(peek) != 0) { |
76 | httpReplyAndClose(socket, "404 Not Found" , "<html><head><title>404 Not Found</title></head><body><center><h1>404 Not Found</h1></center></body></html>" ); |
77 | return; |
78 | } |
79 | |
80 | QString code = rx.cap(1); // The 'code' is the first capture of the regexp |
81 | |
82 | QUrl requestToken = Utility::concatUrlPath(_account->url().toString(), QLatin1String("/index.php/apps/oauth2/api/v1/token" )); |
83 | QNetworkRequest req; |
84 | req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded" ); |
85 | |
86 | QString basicAuth = QString("%1:%2" ).arg( |
87 | Theme::instance()->oauthClientId(), Theme::instance()->oauthClientSecret()); |
88 | req.setRawHeader("Authorization" , "Basic " + basicAuth.toUtf8().toBase64()); |
89 | // We just added the Authorization header, don't let HttpCredentialsAccessManager tamper with it |
90 | req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true); |
91 | |
92 | auto requestBody = new QBuffer; |
93 | QUrlQuery arguments(QString( |
94 | "grant_type=authorization_code&code=%1&redirect_uri=http://localhost:%2" ) |
95 | .arg(code, QString::number(_server.serverPort()))); |
96 | requestBody->setData(arguments.query(QUrl::FullyEncoded).toLatin1()); |
97 | |
98 | auto job = _account->sendRequest("POST" , requestToken, req, requestBody); |
99 | job->setTimeout(qMin(30 * 1000ll, job->timeoutMsec())); |
100 | QObject::connect(job, &SimpleNetworkJob::finishedSignal, this, [this, socket](QNetworkReply *reply) { |
101 | auto jsonData = reply->readAll(); |
102 | QJsonParseError jsonParseError; |
103 | QJsonObject json = QJsonDocument::fromJson(jsonData, &jsonParseError).object(); |
104 | QString accessToken = json["access_token" ].toString(); |
105 | QString refreshToken = json["refresh_token" ].toString(); |
106 | QString user = json["user_id" ].toString(); |
107 | QUrl messageUrl = json["message_url" ].toString(); |
108 | |
109 | if (reply->error() != QNetworkReply::NoError || jsonParseError.error != QJsonParseError::NoError |
110 | || json.isEmpty() || refreshToken.isEmpty() || accessToken.isEmpty() |
111 | || json["token_type" ].toString() != QLatin1String("Bearer" )) { |
112 | QString errorReason; |
113 | QString errorFromJson = json["error" ].toString(); |
114 | if (!errorFromJson.isEmpty()) { |
115 | errorReason = tr("Error returned from the server: <em>%1</em>" ) |
116 | .arg(errorFromJson.toHtmlEscaped()); |
117 | } else if (reply->error() != QNetworkReply::NoError) { |
118 | errorReason = tr("There was an error accessing the 'token' endpoint: <br><em>%1</em>" ) |
119 | .arg(reply->errorString().toHtmlEscaped()); |
120 | } else if (jsonParseError.error != QJsonParseError::NoError) { |
121 | errorReason = tr("Could not parse the JSON returned from the server: <br><em>%1</em>" ) |
122 | .arg(jsonParseError.errorString()); |
123 | } else { |
124 | errorReason = tr("The reply from the server did not contain all expected fields" ); |
125 | } |
126 | qCWarning(lcOauth) << "Error when getting the accessToken" << json << errorReason; |
127 | httpReplyAndClose(socket, "500 Internal Server Error" , |
128 | tr("<h1>Login Error</h1><p>%1</p>" ).arg(errorReason).toUtf8().constData()); |
129 | emit result(Error); |
130 | return; |
131 | } |
132 | if (!_expectedUser.isNull() && user != _expectedUser) { |
133 | // Connected with the wrong user |
134 | QString message = tr("<h1>Wrong user</h1>" |
135 | "<p>You logged-in with user <em>%1</em>, but must login with user <em>%2</em>.<br>" |
136 | "Please log out of %3 in another tab, then <a href='%4'>click here</a> " |
137 | "and log in as user %2</p>" ) |
138 | .arg(user, _expectedUser, Theme::instance()->appNameGUI(), |
139 | authorisationLink().toString(QUrl::FullyEncoded)); |
140 | httpReplyAndClose(socket, "200 OK" , message.toUtf8().constData()); |
141 | // We are still listening on the socket so we will get the new connection |
142 | return; |
143 | } |
144 | const char *loginSuccessfullHtml = "<h1>Login Successful</h1><p>You can close this window.</p>" ; |
145 | if (messageUrl.isValid()) { |
146 | httpReplyAndClose(socket, "303 See Other" , loginSuccessfullHtml, |
147 | QByteArray("Location: " + messageUrl.toEncoded()).constData()); |
148 | } else { |
149 | httpReplyAndClose(socket, "200 OK" , loginSuccessfullHtml); |
150 | } |
151 | emit result(LoggedIn, user, accessToken, refreshToken); |
152 | }); |
153 | }); |
154 | } |
155 | }); |
156 | } |
157 | |
158 | QUrl OAuth::authorisationLink() const |
159 | { |
160 | Q_ASSERT(_server.isListening()); |
161 | QUrlQuery query; |
162 | query.setQueryItems({ { QLatin1String("response_type" ), QLatin1String("code" ) }, |
163 | { QLatin1String("client_id" ), Theme::instance()->oauthClientId() }, |
164 | { QLatin1String("redirect_uri" ), QLatin1String("http://localhost:" ) + QString::number(_server.serverPort()) } }); |
165 | if (!_expectedUser.isNull()) |
166 | query.addQueryItem("user" , _expectedUser); |
167 | QUrl url = Utility::concatUrlPath(_account->url(), QLatin1String("/index.php/apps/oauth2/authorize" ), query); |
168 | return url; |
169 | } |
170 | |
171 | bool OAuth::openBrowser() |
172 | { |
173 | if (!QDesktopServices::openUrl(authorisationLink())) { |
174 | // We cannot open the browser, then we claim we don't support OAuth. |
175 | emit result(NotSupported, QString()); |
176 | return false; |
177 | } |
178 | return true; |
179 | } |
180 | |
181 | } // namespace OCC |
182 | |