Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2022 MuseScore Limited and others
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 :
23 : #include "oauthhttpserverreplyhandler.h"
24 :
25 : #include <cctype>
26 : #include <cstring>
27 : #include <functional>
28 :
29 : #include <QCoreApplication>
30 : #include <QNetworkReply>
31 : #include <QTcpServer>
32 : #include <QTcpSocket>
33 : #include <QUrl>
34 : #include <QUrlQuery>
35 :
36 : #include "modularity/ioc.h"
37 : #include "icloudconfiguration.h"
38 :
39 : #include "translation.h"
40 :
41 : #include "log.h"
42 :
43 : using namespace muse::cloud;
44 :
45 : class OAuthHttpServerReplyHandler::Impl : public Contextable
46 : {
47 : muse::GlobalInject<ICloudConfiguration> configuration;
48 :
49 : public:
50 : explicit Impl(OAuthHttpServerReplyHandler* p, const modularity::ContextPtr& iocCtx);
51 : ~Impl();
52 :
53 : QTcpServer m_httpServer;
54 : QHostAddress m_listenAddress = QHostAddress::LocalHost;
55 : QString m_path;
56 :
57 : QUrl m_redirectUrl;
58 :
59 : private:
60 : void onClientConnected();
61 : void readData(QTcpSocket* socket);
62 : void answerClient(QTcpSocket* socket, const QUrl& url);
63 :
64 : struct HttpRequest {
65 : quint16 port = 0;
66 :
67 : bool readMethod(QTcpSocket* socket);
68 : bool readUrl(QTcpSocket* socket);
69 : bool readStatus(QTcpSocket* socket);
70 : bool readHeader(QTcpSocket* socket);
71 :
72 : enum class State {
73 : ReadingMethod,
74 : ReadingUrl,
75 : ReadingStatus,
76 : ReadingHeader,
77 : ReadingBody,
78 : AllDone
79 : } state = State::ReadingMethod;
80 :
81 : QByteArray m_fragment;
82 :
83 : enum class Method {
84 : Unknown,
85 : Head,
86 : Get,
87 : Put,
88 : Post,
89 : Delete,
90 : } method = Method::Unknown;
91 :
92 : QUrl url;
93 : QPair<quint8, quint8> version;
94 : QMap<QByteArray, QByteArray> headers;
95 : };
96 :
97 : QMap<QTcpSocket*, HttpRequest> m_clients;
98 :
99 : OAuthHttpServerReplyHandler* m_public = nullptr;
100 : };
101 :
102 0 : OAuthHttpServerReplyHandler::Impl::Impl(OAuthHttpServerReplyHandler* p, const modularity::ContextPtr& iocCtx)
103 0 : : Contextable(iocCtx), m_public(p)
104 : {
105 0 : QObject::connect(&m_httpServer, &QTcpServer::newConnection, [this]() { onClientConnected(); });
106 0 : }
107 :
108 0 : OAuthHttpServerReplyHandler::Impl::~Impl()
109 : {
110 0 : if (m_httpServer.isListening()) {
111 0 : m_httpServer.close();
112 : }
113 0 : }
114 :
115 0 : void OAuthHttpServerReplyHandler::Impl::onClientConnected()
116 : {
117 0 : QTcpSocket* socket = m_httpServer.nextPendingConnection();
118 :
119 0 : QObject::connect(socket, &QTcpSocket::disconnected, socket, &QTcpSocket::deleteLater);
120 0 : QObject::connect(socket, &QTcpSocket::readyRead, [this, socket]() { readData(socket); });
121 0 : }
122 :
123 0 : void OAuthHttpServerReplyHandler::Impl::readData(QTcpSocket* socket)
124 : {
125 0 : if (!m_clients.contains(socket)) {
126 0 : m_clients[socket].port = m_httpServer.serverPort();
127 : }
128 :
129 0 : HttpRequest* request = &m_clients[socket];
130 0 : bool error = false;
131 :
132 0 : if (Q_LIKELY(request->state == HttpRequest::State::ReadingMethod)) {
133 0 : if (Q_UNLIKELY(error = !request->readMethod(socket))) {
134 0 : LOGW() << "Invalid Method";
135 : }
136 : }
137 :
138 0 : if (Q_LIKELY(!error && request->state == HttpRequest::State::ReadingUrl)) {
139 0 : if (Q_UNLIKELY(error = !request->readUrl(socket))) {
140 0 : LOGW() << "Invalid URL";
141 : }
142 : }
143 :
144 0 : if (Q_LIKELY(!error && request->state == HttpRequest::State::ReadingStatus)) {
145 0 : if (Q_UNLIKELY(error = !request->readStatus(socket))) {
146 0 : LOGW() << "Invalid Status";
147 : }
148 : }
149 :
150 0 : if (Q_LIKELY(!error && request->state == HttpRequest::State::ReadingHeader)) {
151 0 : if (Q_UNLIKELY(error = !request->readHeader(socket))) {
152 0 : LOGW() << "Invalid Header";
153 : }
154 : }
155 :
156 0 : if (error) {
157 0 : socket->disconnectFromHost();
158 0 : m_clients.remove(socket);
159 0 : } else if (!request->url.isEmpty()) {
160 0 : Q_ASSERT(request->state != HttpRequest::State::ReadingUrl);
161 0 : answerClient(socket, request->url);
162 0 : m_clients.remove(socket);
163 : }
164 0 : }
165 :
166 0 : void OAuthHttpServerReplyHandler::Impl::answerClient(QTcpSocket* socket, const QUrl& url)
167 : {
168 0 : if (!url.path().startsWith(QLatin1String("/") + m_path)) {
169 0 : LOGW() << "Invalid request: " << url;
170 0 : socket->disconnectFromHost();
171 0 : return;
172 : }
173 :
174 0 : QVariantMap receivedData;
175 0 : const QUrlQuery query(url.query());
176 0 : const auto items = query.queryItems();
177 0 : for (auto it = items.begin(), end = items.end(); it != end; ++it) {
178 0 : receivedData.insert(it->first, it->second);
179 : }
180 :
181 0 : Q_EMIT m_public->callbackReceived(receivedData);
182 :
183 : // Fallback text, shown while redirecting
184 0 : const QString text = muse::qtrc("cloud", "Sign in successful! You’re good to go back to MuseScore Studio.");
185 :
186 0 : const QByteArray html = QByteArrayLiteral("<html><head><title>")
187 0 : + qApp->applicationName().toUtf8()
188 0 : + QByteArrayLiteral("</title></head><body>")
189 0 : + text.toUtf8()
190 0 : + ("</body></html>");
191 :
192 0 : const QByteArray htmlSize = QByteArray::number(html.size());
193 0 : const QByteArray replyMessage = QByteArrayLiteral("HTTP/1.0 301 Moved Permanently \r\n"
194 0 : "Location: ") + m_redirectUrl.toString().toUtf8()
195 0 : + QByteArrayLiteral("\r\nContent-Type: text/html; "
196 : "charset=\"utf-8\"\r\n"
197 0 : "Content-Length: ") + htmlSize
198 0 : + QByteArrayLiteral("\r\n\r\n")
199 0 : + html;
200 :
201 0 : socket->write(replyMessage);
202 0 : }
203 :
204 0 : bool OAuthHttpServerReplyHandler::Impl::HttpRequest::readMethod(QTcpSocket* socket)
205 : {
206 0 : bool finished = false;
207 0 : while (socket->bytesAvailable() && !finished) {
208 : char c;
209 0 : socket->getChar(&c);
210 0 : if (std::isupper(c) && m_fragment.size() < 6) {
211 0 : m_fragment += c;
212 : } else {
213 0 : finished = true;
214 : }
215 : }
216 0 : if (finished) {
217 0 : if (m_fragment == "HEAD") {
218 0 : method = Method::Head;
219 0 : } else if (m_fragment == "GET") {
220 0 : method = Method::Get;
221 0 : } else if (m_fragment == "PUT") {
222 0 : method = Method::Put;
223 0 : } else if (m_fragment == "POST") {
224 0 : method = Method::Post;
225 0 : } else if (m_fragment == "DELETE") {
226 0 : method = Method::Delete;
227 : } else {
228 0 : LOGW() << "Invalid operation: " << m_fragment;
229 : }
230 :
231 0 : state = State::ReadingUrl;
232 0 : m_fragment.clear();
233 :
234 0 : return method != Method::Unknown;
235 : }
236 0 : return true;
237 : }
238 :
239 0 : bool OAuthHttpServerReplyHandler::Impl::HttpRequest::readUrl(QTcpSocket* socket)
240 : {
241 0 : bool finished = false;
242 0 : while (socket->bytesAvailable() && !finished) {
243 : char c;
244 0 : socket->getChar(&c);
245 0 : if (std::isspace(c)) {
246 0 : finished = true;
247 : } else {
248 0 : m_fragment += c;
249 : }
250 : }
251 0 : if (finished) {
252 0 : if (!m_fragment.startsWith("/")) {
253 0 : LOGW() << "Invalid URL path: " << m_fragment;
254 0 : return false;
255 : }
256 0 : url.setUrl(QStringLiteral("http://127.0.0.1:") + QString::number(port)
257 0 : + QString::fromUtf8(m_fragment));
258 0 : state = State::ReadingStatus;
259 0 : if (!url.isValid()) {
260 0 : LOGW() << "Invalid URL " << m_fragment;
261 0 : return false;
262 : }
263 0 : m_fragment.clear();
264 0 : return true;
265 : }
266 0 : return true;
267 : }
268 :
269 0 : bool OAuthHttpServerReplyHandler::Impl::HttpRequest::readStatus(QTcpSocket* socket)
270 : {
271 0 : bool finished = false;
272 0 : while (socket->bytesAvailable() && !finished) {
273 : char c;
274 0 : socket->getChar(&c);
275 0 : m_fragment += c;
276 0 : if (m_fragment.endsWith("\r\n")) {
277 0 : finished = true;
278 0 : m_fragment.resize(m_fragment.size() - 2);
279 : }
280 : }
281 0 : if (finished) {
282 0 : if (!std::isdigit(m_fragment.at(m_fragment.size() - 3))
283 0 : || !std::isdigit(m_fragment.at(m_fragment.size() - 1))) {
284 0 : LOGW() << "Invalid version";
285 0 : return false;
286 : }
287 0 : version = qMakePair(m_fragment.at(m_fragment.size() - 3) - '0',
288 0 : m_fragment.at(m_fragment.size() - 1) - '0');
289 0 : state = State::ReadingHeader;
290 0 : m_fragment.clear();
291 : }
292 0 : return true;
293 : }
294 :
295 0 : bool OAuthHttpServerReplyHandler::Impl::HttpRequest::readHeader(QTcpSocket* socket)
296 : {
297 0 : while (socket->bytesAvailable()) {
298 : char c;
299 0 : socket->getChar(&c);
300 0 : m_fragment += c;
301 0 : if (m_fragment.endsWith("\r\n")) {
302 0 : if (m_fragment == "\r\n") {
303 0 : state = State::ReadingBody;
304 0 : m_fragment.clear();
305 0 : return true;
306 : } else {
307 0 : m_fragment.chop(2);
308 0 : const int index = m_fragment.indexOf(':');
309 0 : if (index == -1) {
310 0 : return false;
311 : }
312 :
313 0 : const QByteArray key = m_fragment.mid(0, index).trimmed();
314 0 : const QByteArray value = m_fragment.mid(index + 1).trimmed();
315 0 : headers.insert(key, value);
316 0 : m_fragment.clear();
317 0 : }
318 : }
319 : }
320 0 : return false;
321 : }
322 :
323 0 : OAuthHttpServerReplyHandler::OAuthHttpServerReplyHandler(const modularity::ContextPtr& iocCtx, QObject* parent)
324 0 : : OAuthHttpServerReplyHandler(QHostAddress::Any, 0, iocCtx, parent)
325 0 : {}
326 :
327 0 : OAuthHttpServerReplyHandler::OAuthHttpServerReplyHandler(quint16 port, const modularity::ContextPtr& iocCtx, QObject* parent)
328 0 : : OAuthHttpServerReplyHandler(QHostAddress::Any, port, iocCtx, parent)
329 0 : {}
330 :
331 0 : OAuthHttpServerReplyHandler::OAuthHttpServerReplyHandler(const QHostAddress& address, quint16 port, const modularity::ContextPtr& iocCtx,
332 0 : QObject* parent)
333 0 : : QOAuthOobReplyHandler(parent), m_impl(std::make_unique<Impl>(this, iocCtx))
334 : {
335 0 : listen(address, port);
336 0 : }
337 :
338 0 : OAuthHttpServerReplyHandler::~OAuthHttpServerReplyHandler()
339 0 : {}
340 :
341 0 : QString OAuthHttpServerReplyHandler::callback() const
342 : {
343 0 : Q_ASSERT(m_impl->m_httpServer.isListening());
344 0 : const QUrl url(QString::fromLatin1("http://127.0.0.1:%1/%2")
345 0 : .arg(m_impl->m_httpServer.serverPort()).arg(m_impl->m_path));
346 0 : return url.toString(QUrl::EncodeDelimiters);
347 0 : }
348 :
349 0 : QString OAuthHttpServerReplyHandler::callbackPath() const
350 : {
351 0 : return m_impl->m_path;
352 : }
353 :
354 0 : void OAuthHttpServerReplyHandler::setCallbackPath(const QString& path)
355 : {
356 0 : QString copy = path;
357 0 : while (copy.startsWith(QLatin1Char('/'))) {
358 0 : copy = copy.mid(1);
359 : }
360 :
361 0 : m_impl->m_path = copy;
362 0 : }
363 :
364 0 : quint16 OAuthHttpServerReplyHandler::port() const
365 : {
366 0 : return m_impl->m_httpServer.serverPort();
367 : }
368 :
369 0 : bool OAuthHttpServerReplyHandler::listen(const QHostAddress& address, quint16 port)
370 : {
371 0 : return m_impl->m_httpServer.listen(address, port);
372 : }
373 :
374 0 : void OAuthHttpServerReplyHandler::close()
375 : {
376 0 : return m_impl->m_httpServer.close();
377 : }
378 :
379 0 : bool OAuthHttpServerReplyHandler::isListening() const
380 : {
381 0 : return m_impl->m_httpServer.isListening();
382 : }
383 :
384 0 : void OAuthHttpServerReplyHandler::setRedirectUrl(const QUrl& url)
385 : {
386 0 : m_impl->m_redirectUrl = url;
387 0 : }
|