diff --git a/autotests/fakeauthwidget.cpp b/autotests/fakeauthwidget.cpp index 598726a..aaa5ef2 100644 --- a/autotests/fakeauthwidget.cpp +++ b/autotests/fakeauthwidget.cpp @@ -1,85 +1,95 @@ /* * Copyright (C) 2018 Daniel Vrátil * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include "fakeauthwidget.h" #include "ui/authwidget_p.h" #include #include #include FakeAuthWidgetFactory::FakeAuthWidgetFactory() { sFactory = this; } FakeAuthWidgetFactory::~FakeAuthWidgetFactory() { sFactory = nullptr; } KGAPI2::AuthWidget * FakeAuthWidgetFactory::create(QWidget* parent) { return new FakeAuthWidget(parent); } class FakeAuthWidgetPrivate : public KGAPI2::AuthWidgetPrivate { Q_OBJECT public: FakeAuthWidgetPrivate(FakeAuthWidget *parent) : KGAPI2::AuthWidgetPrivate(parent) { serverPort = 42413; // random port, but must be stable } ~FakeAuthWidgetPrivate() override {} void setUrl(const QUrl &url) override { Q_UNUSED(url); // don't do anything, don't even try to load Google auth page. Instead // pretend the user have already authenticated and we've reached the // part where Google sends us the auth code QTimer::singleShot(0, this, [=]() { QTcpSocket socket; socket.connectToHost(QHostAddress::LocalHost, serverPort); if (!socket.waitForConnected()) { qWarning() << "Failed to connect to internal TCP server!"; return; } socket.write("GET http://127.0.0.1:42431?code=TheCakeIsALie HTTP/1.1"); socket.waitForBytesWritten(); socket.close(); }); } + + void setupUi() override + { + // do nothing + } + + void setVisible(bool) override + { + // do nothing + } }; FakeAuthWidget::FakeAuthWidget(QWidget *parent) : KGAPI2::AuthWidget(new FakeAuthWidgetPrivate(this), parent) { } #include "fakeauthwidget.moc" diff --git a/src/core/accountmanager.cpp b/src/core/accountmanager.cpp index 1cfd21b..207043b 100644 --- a/src/core/accountmanager.cpp +++ b/src/core/accountmanager.cpp @@ -1,280 +1,279 @@ /* Copyright (C) 2018 Daniel Vrátil This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #include "accountmanager.h" #include "authjob.h" #include "accountstorage_p.h" #include "../debug.h" #include #include #include #include #include #include #include #include #include namespace { static const QString FolderName = QStringLiteral("LibKGAPI"); static const QString AccountNameKey = QStringLiteral("name"); static const QString ScopesKey = QStringLiteral("scopes"); static const QString AccessTokenKey = QStringLiteral("accessToken"); static const QString RefreshTokenKey = QStringLiteral("refreshToken"); static const QString ExpiresKey = QStringLiteral("expires"); } namespace KGAPI2 { AccountManager *AccountManager::sInstance = nullptr; class AccountPromise::Private { public: Private(AccountPromise *q) : q(q) {} void setError(const QString &error) { this->error = error; emitFinished(); } void setAccount(const AccountPtr &account) { this->account = account; emitFinished(); } QString error; AccountPtr account; private: void emitFinished() { QTimer::singleShot(0, q, [this]() { Q_EMIT q->finished(); q->deleteLater(); }); } AccountPromise * const q; }; class AccountManager::Private { public: void updateAccount(AccountPromise *promise, const QString &apiKey, const QString &apiSecret, const AccountPtr &account, const QList &requestedScopes) { if (!requestedScopes.isEmpty()) { auto currentScopes = account->scopes(); for (const auto &requestedScope : requestedScopes) { if (!currentScopes.contains(requestedScope)) { currentScopes.push_back(requestedScope); } } account->setScopes(currentScopes); } AuthJob *job = new AuthJob(account, apiKey, apiSecret); job->setUsername(account->accountName()); connect(job, &AuthJob::finished, [=]() { if (job->error() != KGAPI2::NoError) { promise->d->setError(tr("Failed to authenticate additional scopes")); return; } mStore->storeAccount(apiKey, job->account()); promise->d->setAccount(job->account()); }); } void createAccount(AccountPromise *promise, const QString &apiKey, const QString &apiSecret, const QString &accountName, const QList &scopes) { const auto account = AccountPtr::create(accountName, QString{}, QString{}, scopes); updateAccount(promise, apiKey, apiSecret, account, {}); } bool compareScopes(const QList ¤tScopes, const QList &requestedScopes) const { for (const auto &scope : qAsConst(requestedScopes)) { if (!currentScopes.contains(scope)) { return false; } } return true; } void ensureStore(const std::function &callback) { if (!mStore) { mStore = AccountStorageFactory::instance()->create(); } if (!mStore->opened()) { mStore->open(callback); } else { callback(true); } } public: AccountStorage *mStore = nullptr; }; } using namespace KGAPI2; AccountPromise::AccountPromise(QObject *parent) : QObject(parent) , d(new Private(this)) { } AccountPromise::~AccountPromise() { } AccountPtr AccountPromise::account() const { return d->account; } AccountManager::AccountManager(QObject *parent) : QObject(parent) , d(new Private) { } AccountManager::~AccountManager() { } AccountManager *AccountManager::instance() { if (!sInstance) { sInstance = new AccountManager; } return sInstance; } AccountPromise *AccountManager::getAccount(const QString &apiKey, const QString &apiSecret, const QString &accountName, const QList &scopes) { auto promise = new AccountPromise(this); // Start the process asynchronously so that caller has a chance to connect // to AccountPromise signals. QTimer::singleShot(0, this, [=]() { - qDebug() << "==== " << promise << d->mStore << (d->mStore ? d->mStore->opened() : false); d->ensureStore([=](bool storeOpened) { if (!storeOpened) { promise->d->setError(tr("Failed to open account store")); return; } const auto account = d->mStore->getAccount(apiKey, accountName); if (!account) { d->createAccount(promise, apiKey, apiSecret, accountName, scopes); } else { if (d->compareScopes(account->scopes(), scopes)) { promise->d->setAccount(account); } else { // Since installed apps can't keep the API secret truly a secret // incremental authorization is not allowed by Google so we need // to request a completely new token from scratch. account->setAccessToken({}); account->setRefreshToken({}); account->setExpireDateTime({}); d->updateAccount(promise, apiKey, apiSecret, account, scopes); } } }); }); return promise; } AccountPromise *AccountManager::findAccount(const QString &apiKey, const QString &accountName, const QList &scopes) { auto promise = new AccountPromise(this); QTimer::singleShot(0, this, [=]() { d->ensureStore([=](bool storeOpened) { if (!storeOpened) { promise->d->setError(tr("Failed to open account store")); return; } const auto account = d->mStore->getAccount(apiKey, accountName); if (!account) { promise->d->setAccount({}); } else { const auto currentScopes = account->scopes(); if (scopes.isEmpty() || d->compareScopes(currentScopes, scopes)) { promise->d->setAccount(account); } else { promise->d->setAccount({}); } } }); }); return promise; } void AccountManager::removeScopes(const QString &apiKey, const QString &accountName, const QList &removedScopes) { d->ensureStore([=](bool storeOpened) { if (!storeOpened) { return; } const auto account = d->mStore->getAccount(apiKey, accountName); if (!account) { return; } for (const auto &scope : removedScopes) { account->removeScope(scope); } if (account->scopes().isEmpty()) { d->mStore->removeAccount(apiKey, account->accountName()); } else { // Since installed apps can't keep the API secret truly a secret // incremental authorization is not allowed by Google so we need // to request a completely new token from scratch. account->setAccessToken({}); account->setRefreshToken({}); account->setExpireDateTime({}); d->mStore->storeAccount(apiKey, account); } }); } diff --git a/src/core/ui/authwidget_p.cpp b/src/core/ui/authwidget_p.cpp index 7e77bf5..621c656 100644 --- a/src/core/ui/authwidget_p.cpp +++ b/src/core/ui/authwidget_p.cpp @@ -1,369 +1,369 @@ /* Copyright 2012, 2013 Daniel Vrátil This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #include "authwidget_p.h" #include "account.h" #include "accountinfo/accountinfo.h" #include "accountinfo/accountinfofetchjob.h" #include "private/newtokensfetchjob_p.h" #include "../../debug.h" #include #include #include #include #include #include #include #include #include #include #include using namespace KGAPI2; namespace { class WebView : public QWebEngineView { Q_OBJECT public: explicit WebView(QWidget *parent = nullptr) : QWebEngineView(parent) { // Don't store cookies, so that subsequent invocations of AuthJob won't remember // the previous accounts. QWebEngineProfile::defaultProfile()->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies); } void contextMenuEvent(QContextMenuEvent *e) override { // No menu e->accept(); } }; class WebPage : public QWebEnginePage { Q_OBJECT public: explicit WebPage(QObject *parent = nullptr) : QWebEnginePage(parent) , mLastError(nullptr) { } QWebEngineCertificateError *lastCertificateError() const { return mLastError; } bool certificateError(const QWebEngineCertificateError &err) override { if (mLastError) { delete mLastError; } mLastError = new QWebEngineCertificateError(err.error(), err.url(), err.isOverridable(), err.errorDescription()); Q_EMIT sslError(); return false; // don't let it through } Q_SIGNALS: void sslError(); private: QWebEngineCertificateError *mLastError; }; } AuthWidgetPrivate::AuthWidgetPrivate(AuthWidget *parent): QObject(), showProgressBar(true), progress(AuthWidget::None), q(parent) { } AuthWidgetPrivate::~AuthWidgetPrivate() { } void AuthWidgetPrivate::setSslIcon(const QString &iconName) { // FIXME: workaround for silly Breeze icons: the small 22x22 icons are // monochromatic, which is absolutely useless since we are trying to security // information here, so instead we force use the bigger 48x48 icons which // have colors and downscale them sslIndicator->setIcon(QIcon::fromTheme(iconName).pixmap(48)); } void AuthWidgetPrivate::setupUi() { vbox = new QVBoxLayout(q); q->setLayout(vbox); label = new QLabel(q); label->setText(QLatin1String("") % tr("Authorizing token. This should take just a moment...") % QLatin1String("")); label->setWordWrap(true); label->setAlignment(Qt::AlignCenter); label->setVisible(false); vbox->addWidget(label); auto hbox = new QHBoxLayout; hbox->setSpacing(0); sslIndicator = new QToolButton(q); connect(sslIndicator, &QToolButton::clicked, this, [this]() { auto page = qobject_cast(webview->page()); if (auto err = page->lastCertificateError()) { QMessageBox msg; msg.setIconPixmap(QIcon::fromTheme(QStringLiteral("security-low")).pixmap(64)); msg.setText(err->errorDescription()); msg.addButton(QMessageBox::Ok); msg.exec(); } }); hbox->addWidget(sslIndicator); urlEdit = new QLineEdit(q); urlEdit->setReadOnly(true); hbox->addWidget(urlEdit); vbox->addLayout(hbox); progressbar = new QProgressBar(q); progressbar->setMinimum(0); progressbar->setMaximum(100); progressbar->setValue(0); vbox->addWidget(progressbar); webview = new WebView(q); auto webpage = new WebPage(webview); connect(webpage, &WebPage::sslError, this, [this]() { setSslIcon(QStringLiteral("security-low")); }); webview->setPage(webpage); vbox->addWidget(webview); connect(webview, &QWebEngineView::loadProgress, progressbar, &QProgressBar::setValue); connect(webview, &QWebEngineView::urlChanged, this, &AuthWidgetPrivate::webviewUrlChanged); connect(webview, &QWebEngineView::loadFinished, this, &AuthWidgetPrivate::webviewFinished); } void AuthWidgetPrivate::setUrl(const QUrl &url) { webview->setUrl(url); webview->setFocus(); } void AuthWidgetPrivate::setVisible(bool visible) { sslIndicator->setVisible(visible); urlEdit->setVisible(visible); webview->setVisible(visible); if (showProgressBar && visible) { progressbar->setVisible(visible); } else { progressbar->setVisible(visible); } } void AuthWidgetPrivate::setProgress(AuthWidget::Progress progress) { qCDebug(KGAPIDebug) << progress; this->progress = progress; Q_EMIT q->progress(progress); } void AuthWidgetPrivate::emitError(const enum Error errCode, const QString& msg) { label->setVisible(true); sslIndicator->setVisible(false); urlEdit->setVisible(false); webview->setVisible(false); progressbar->setVisible(false); label->setText(QLatin1String("") % msg % QLatin1String("")); Q_EMIT q->error(errCode, msg); setProgress(AuthWidget::Error); } void AuthWidgetPrivate::webviewUrlChanged(const QUrl &url) { qCDebug(KGAPIDebug) << "URLChange:" << url; // Whoa! That should not happen! if (url.scheme() != QLatin1String("https")) { QTimer::singleShot(0, this, [this, url]() { QUrl sslUrl = url; sslUrl.setScheme(QStringLiteral("https")); webview->setUrl(sslUrl); }); return; } if (!isGoogleHost(url)) { // We handled SSL above, so we are secure. We are however outside of // accounts.google.com, which is a little suspicious in context of this class setSslIcon(QStringLiteral("security-medium")); return; } if (qobject_cast(webview->page())->lastCertificateError()) { setSslIcon(QStringLiteral("security-low")); } else { // We have no way of obtaining current SSL certifiace from QWebEngine, but we // handled SSL and accounts.google.com cases above and QWebEngine did not report // any SSL error to us, so we can assume we are safe. setSslIcon(QStringLiteral("security-high")); } // Username and password inputs are loaded dynamically, so we only get // urlChanged, but not urlFinished. if (isUsernameFrame(url)) { if (!username.isEmpty()) { webview->page()->runJavaScript(QStringLiteral("document.getElementById(\"identifierId\").value = \"%1\";").arg(username)); } } else if (isPasswordFrame(url)) { if (!password.isEmpty()) { webview->page()->runJavaScript(QStringLiteral("var elems = document.getElementsByTagName(\"input\");" "for (var i = 0; i < elems.length; i++) {" " if (elems[i].type == \"password\" && elems[i].name == \"password\") {" " elems[i].value = \"%1\";" " break;" " }" "}").arg(password)); } } } void AuthWidgetPrivate::webviewFinished(bool ok) { if (!ok) { qCWarning(KGAPIDebug) << "Failed to load" << webview->url(); } const QUrl url = webview->url(); urlEdit->setText(url.toDisplayString(QUrl::PrettyDecoded)); urlEdit->setCursorPosition(0); qCDebug(KGAPIDebug) << "URLFinished:" << url; } void AuthWidgetPrivate::socketError(QAbstractSocket::SocketError socketError) { if (connection) connection->deleteLater(); qCDebug(KGAPIDebug) << QStringLiteral("Socket error when receiving response: %1").arg(socketError); emitError(InvalidResponse, tr("Error receiving response: %1").arg(socketError)); } void AuthWidgetPrivate::socketReady() { Q_ASSERT(connection); const QByteArray data = connection->readLine(); connection->deleteLater(); qCDebug(KGAPIDebug) << QStringLiteral("Got connection on socket"); - webview->stop(); - - sslIndicator->setVisible(false); - urlEdit->setVisible(false); - webview->setVisible(false); - progressbar->setVisible(false); - label->setVisible(true); + if (webview) { // when running in tests we don't have webview or any other widgets + webview->stop(); + } + setVisible(false); + if (label) { + label->setVisible(true); + } const auto line = data.split(' '); if (line.size() != 3 || line.at(0) != QByteArray("GET") || !line.at(2).startsWith(QByteArray("HTTP/1.1"))) { qCDebug(KGAPIDebug) << QStringLiteral("Token response invalid"); emitError(InvalidResponse, tr("Token response invalid")); return; } //qCDebug(KGAPIRaw) << "Receiving data on socket: " << data; const QUrl url(QString::fromLatin1(line.at(1))); const QUrlQuery query(url); const QString code = query.queryItemValue(QStringLiteral("code")); if (code.isEmpty()) { const QString error = query.queryItemValue(QStringLiteral("error")); if (!error.isEmpty()) { emitError(UnknownError, error); qCDebug(KGAPIDebug) << error; } else { qCDebug(KGAPIDebug) << QStringLiteral("Could not extract token from HTTP answer"); emitError(InvalidResponse, tr("Could not extract token from HTTP answer")); } return; } Q_ASSERT(serverPort != -1); auto fetch = new KGAPI2::NewTokensFetchJob(code, apiKey, secretKey, serverPort); connect(fetch, &Job::finished, this, &AuthWidgetPrivate::tokensReceived); } void AuthWidgetPrivate::tokensReceived(KGAPI2::Job* job) { KGAPI2::NewTokensFetchJob *tokensFetchJob = qobject_cast(job); account->setAccessToken(tokensFetchJob->accessToken()); account->setRefreshToken(tokensFetchJob->refreshToken()); account->setExpireDateTime(QDateTime::currentDateTime().addSecs(tokensFetchJob->expiresIn())); tokensFetchJob->deleteLater(); KGAPI2::AccountInfoFetchJob *fetchJob = new KGAPI2::AccountInfoFetchJob(account, this); connect(fetchJob, &Job::finished, this, &AuthWidgetPrivate::accountInfoReceived); qCDebug(KGAPIDebug) << "Requesting AccountInfo"; } void AuthWidgetPrivate::accountInfoReceived(KGAPI2::Job* job) { if (job->error()) { qCDebug(KGAPIDebug) << "Error when retrieving AccountInfo:" << job->errorString(); emitError((enum Error) job->error(), job->errorString()); return; } KGAPI2::ObjectsList objects = qobject_cast(job)->items(); Q_ASSERT(!objects.isEmpty()); KGAPI2::AccountInfoPtr accountInfo = objects.first().staticCast(); account->setAccountName(accountInfo->email()); job->deleteLater(); Q_EMIT q->authenticated(account); setProgress(AuthWidget::Finished); } #include "authwidget_p.moc" diff --git a/src/core/ui/authwidget_p.h b/src/core/ui/authwidget_p.h index c816c45..61a5487 100644 --- a/src/core/ui/authwidget_p.h +++ b/src/core/ui/authwidget_p.h @@ -1,106 +1,106 @@ /* Copyright (C) 2012 - 2018 Daniel Vrátil This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #ifndef LIBKGAPI_AUTHWIDGET_P_H #define LIBKGAPI_AUTHWIDGET_P_H #include #include "ui/authwidget.h" #include "types.h" #include "kgapicore_export.h" #include #include #include #include class QVBoxLayout; class QLabel; class QWebEngineView; class QTcpServer; class QTcpSocket; namespace KGAPI2 { class Job; // Exported for tests, otherwise internal class KGAPICORE_EXPORT AuthWidgetPrivate: public QObject { Q_OBJECT public: explicit AuthWidgetPrivate(AuthWidget *parent); - void setupUi(); + virtual void setupUi(); virtual void setUrl(const QUrl &url); - void setVisible(bool visible); + virtual void setVisible(bool visible); ~AuthWidgetPrivate() override; bool showProgressBar; QString username; QString password; AccountPtr account; AuthWidget::Progress progress; QString apiKey; QString secretKey; - QToolButton *sslIndicator; - QLineEdit *urlEdit; - QProgressBar *progressbar; - QVBoxLayout *vbox; - QWebEngineView *webview; - QLabel *label; + QToolButton *sslIndicator = nullptr; + QLineEdit *urlEdit = nullptr; + QProgressBar *progressbar = nullptr; + QVBoxLayout *vbox = nullptr; + QWebEngineView *webview = nullptr; + QLabel *label = nullptr; QTcpServer *server = nullptr; int serverPort = 0; QTcpSocket *connection = nullptr; private Q_SLOTS: void emitError(const KGAPI2::Error errCode, const QString &msg); void webviewUrlChanged(const QUrl &url); void webviewFinished(bool ok); void socketReady(); void socketError(QAbstractSocket::SocketError error); void tokensReceived(KGAPI2::Job *job); void accountInfoReceived(KGAPI2::Job *job); private: void setProgress(AuthWidget::Progress progress); bool isGoogleHost(const QUrl &url) const { return url.host() == QLatin1String("accounts.google.com"); } bool isSigninPage(const QUrl &url) const { return url.path() == QLatin1String("/signin/oauth"); } bool isUsernameFrame(const QUrl &url) { return url.path() == QLatin1String("/signin/oauth/identifier"); } bool isPasswordFrame(const QUrl &url) { return url.path() == QLatin1String("/signin/v2/challenge/pwd"); } void setSslIcon(const QString &icon); AuthWidget *q; friend class AuthWidget; }; } // namespace KGAPI2 #endif // LIBKGAPI_AUTHWIDGET_P_H