| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337 |
- /*
- * This software is in the public domain, furnished "as is", without technical
- * support, and with no warranty, express or implied, as to its usefulness for
- * any purpose.
- *
- */
- #include <QtTest/QtTest>
- #include <QDesktopServices>
- #include "gui/creds/oauth.h"
- #include "syncenginetestutils.h"
- #include "theme.h"
- #include "common/asserts.h"
- using namespace OCC;
- class DesktopServiceHook : public QObject
- {
- Q_OBJECT
- signals:
- void hooked(const QUrl &);
- public:
- DesktopServiceHook() { QDesktopServices::setUrlHandler("oauthtest", this, "hooked"); }
- };
- static const QUrl sOAuthTestServer("oauthtest://someserver/owncloud");
- class FakePostReply : public QNetworkReply
- {
- Q_OBJECT
- public:
- std::unique_ptr<QIODevice> payload;
- bool aborted = false;
- bool redirectToPolicy = false;
- bool redirectToToken = false;
- FakePostReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request,
- std::unique_ptr<QIODevice> payload_, QObject *parent)
- : QNetworkReply{parent}, payload{std::move(payload_)}
- {
- setRequest(request);
- setUrl(request.url());
- setOperation(op);
- open(QIODevice::ReadOnly);
- payload->open(QIODevice::ReadOnly);
- QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
- }
- Q_INVOKABLE virtual void respond() {
- if (aborted) {
- setError(OperationCanceledError, "Operation Canceled");
- emit metaDataChanged();
- emit finished();
- return;
- } else if (redirectToPolicy) {
- setHeader(QNetworkRequest::LocationHeader, "/my.policy");
- setAttribute(QNetworkRequest::RedirectionTargetAttribute, "/my.policy");
- setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 302); // 302 might or might not lose POST data in rfc
- setHeader(QNetworkRequest::ContentLengthHeader, 0);
- emit metaDataChanged();
- emit finished();
- return;
- } else if (redirectToToken) {
- // Redirect to self
- QVariant destination = QVariant(sOAuthTestServer.toString()+QLatin1String("/index.php/apps/oauth2/api/v1/token"));
- setHeader(QNetworkRequest::LocationHeader, destination);
- setAttribute(QNetworkRequest::RedirectionTargetAttribute, destination);
- setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 307); // 307 explicitly in rfc says to not lose POST data
- setHeader(QNetworkRequest::ContentLengthHeader, 0);
- emit metaDataChanged();
- emit finished();
- return;
- }
- setHeader(QNetworkRequest::ContentLengthHeader, payload->size());
- setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
- emit metaDataChanged();
- if (bytesAvailable())
- emit readyRead();
- emit finished();
- }
- void abort() override {
- aborted = true;
- }
- qint64 bytesAvailable() const override {
- if (aborted)
- return 0;
- return payload->bytesAvailable();
- }
- qint64 readData(char *data, qint64 maxlen) override {
- return payload->read(data, maxlen);
- }
- };
- // Reply with a small delay
- class SlowFakePostReply : public FakePostReply {
- Q_OBJECT
- public:
- using FakePostReply::FakePostReply;
- void respond() override {
- // override of FakePostReply::respond, will call the real one with a delay.
- QTimer::singleShot(100, this, [this] { this->FakePostReply::respond(); });
- }
- };
- class OAuthTestCase : public QObject
- {
- Q_OBJECT
- DesktopServiceHook desktopServiceHook;
- public:
- enum State { StartState, BrowserOpened, TokenAsked, CustomState } state = StartState;
- Q_ENUM(State);
- bool replyToBrowserOk = false;
- bool gotAuthOk = false;
- virtual bool done() const { return replyToBrowserOk && gotAuthOk; }
- FakeQNAM *fakeQnam = nullptr;
- QNetworkAccessManager realQNAM;
- QPointer<QNetworkReply> browserReply = nullptr;
- QString code = generateEtag();
- OCC::AccountPtr account;
- QScopedPointer<OAuth> oauth;
- virtual void test() {
- fakeQnam = new FakeQNAM({});
- account = OCC::Account::create();
- account->setUrl(sOAuthTestServer);
- account->setCredentials(new FakeCredentials{fakeQnam});
- fakeQnam->setParent(this);
- fakeQnam->setOverride([this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) {
- ASSERT(device);
- ASSERT(device->bytesAvailable()>0); // OAuth2 always sends around POST data.
- return this->tokenReply(op, req);
- });
- QObject::connect(&desktopServiceHook, &DesktopServiceHook::hooked,
- this, &OAuthTestCase::openBrowserHook);
- oauth.reset(new OAuth(account.data(), nullptr));
- QObject::connect(oauth.data(), &OAuth::result, this, &OAuthTestCase::oauthResult);
- oauth->start();
- QTRY_VERIFY(done());
- }
- virtual void openBrowserHook(const QUrl &url) {
- QCOMPARE(state, StartState);
- state = BrowserOpened;
- QCOMPARE(url.path(), QString(sOAuthTestServer.path() + "/index.php/apps/oauth2/authorize"));
- QVERIFY(url.toString().startsWith(sOAuthTestServer.toString()));
- QUrlQuery query(url);
- QCOMPARE(query.queryItemValue(QLatin1String("response_type")), QLatin1String("code"));
- QCOMPARE(query.queryItemValue(QLatin1String("client_id")), Theme::instance()->oauthClientId());
- QUrl redirectUri(query.queryItemValue(QLatin1String("redirect_uri")));
- QCOMPARE(redirectUri.host(), QLatin1String("localhost"));
- redirectUri.setQuery("code=" + code);
- createBrowserReply(QNetworkRequest(redirectUri));
- }
- virtual QNetworkReply *createBrowserReply(const QNetworkRequest &request) {
- browserReply = realQNAM.get(request);
- QObject::connect(browserReply, &QNetworkReply::finished, this, &OAuthTestCase::browserReplyFinished);
- return browserReply;
- }
- virtual void browserReplyFinished() {
- QCOMPARE(sender(), browserReply.data());
- QCOMPARE(state, TokenAsked);
- browserReply->deleteLater();
- QCOMPARE(browserReply->rawHeader("Location"), QByteArray("owncloud://success"));
- replyToBrowserOk = true;
- };
- virtual QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req)
- {
- ASSERT(state == BrowserOpened);
- state = TokenAsked;
- ASSERT(op == QNetworkAccessManager::PostOperation);
- ASSERT(req.url().toString().startsWith(sOAuthTestServer.toString()));
- ASSERT(req.url().path() == sOAuthTestServer.path() + "/index.php/apps/oauth2/api/v1/token");
- std::unique_ptr<QBuffer> payload(new QBuffer());
- payload->setData(tokenReplyPayload());
- return new FakePostReply(op, req, std::move(payload), fakeQnam);
- }
- virtual QByteArray tokenReplyPayload() const {
- QJsonDocument jsondata(QJsonObject{
- { "access_token", "123" },
- { "refresh_token" , "456" },
- { "message_url", "owncloud://success"},
- { "user_id", "789" },
- { "token_type", "Bearer" }
- });
- return jsondata.toJson();
- }
- virtual void oauthResult(OAuth::Result result, const QString &user, const QString &token , const QString &refreshToken) {
- QCOMPARE(state, TokenAsked);
- QCOMPARE(result, OAuth::LoggedIn);
- QCOMPARE(user, QString("789"));
- QCOMPARE(token, QString("123"));
- QCOMPARE(refreshToken, QString("456"));
- gotAuthOk = true;
- }
- };
- class TestOAuth: public QObject
- {
- Q_OBJECT
- private slots:
- void testBasic()
- {
- OAuthTestCase test;
- test.test();
- }
- // Test for https://github.com/owncloud/client/pull/6057
- void testCloseBrowserDontCrash()
- {
- struct Test : OAuthTestCase {
- QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest & req) override
- {
- ASSERT(browserReply);
- // simulate the fact that the browser is closing the connection
- browserReply->abort();
- QCoreApplication::processEvents();
- ASSERT(state == BrowserOpened);
- state = TokenAsked;
- std::unique_ptr<QBuffer> payload(new QBuffer);
- payload->setData(tokenReplyPayload());
- return new SlowFakePostReply(op, req, std::move(payload), fakeQnam);
- }
- void browserReplyFinished() override
- {
- QCOMPARE(sender(), browserReply.data());
- QCOMPARE(browserReply->error(), QNetworkReply::OperationCanceledError);
- replyToBrowserOk = true;
- }
- } test;
- test.test();
- }
- void testRandomConnections()
- {
- // Test that we can send random garbage to the litening socket and it does not prevent the connection
- struct Test : OAuthTestCase {
- QNetworkReply *createBrowserReply(const QNetworkRequest &request) override {
- QTimer::singleShot(0, this, [this, request] {
- auto port = request.url().port();
- state = CustomState;
- QVector<QByteArray> payloads = {
- "GET FOFOFO HTTP 1/1\n\n",
- "GET /?code=invalie HTTP 1/1\n\n",
- "GET /?code=xxxxx&bar=fff",
- QByteArray("\0\0\0", 3),
- QByteArray("GET \0\0\0 \n\n\n\n\n\0", 14),
- QByteArray("GET /?code=éléphant\xa5 HTTP\n"),
- QByteArray("\n\n\n\n"),
- };
- foreach (const auto &x, payloads) {
- auto socket = new QTcpSocket(this);
- socket->connectToHost("localhost", port);
- QVERIFY(socket->waitForConnected());
- socket->write(x);
- }
- // Do the actual request a bit later
- QTimer::singleShot(100, this, [this, request] {
- QCOMPARE(state, CustomState);
- state = BrowserOpened;
- this->OAuthTestCase::createBrowserReply(request);
- });
- });
- return nullptr;
- }
- QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) override
- {
- if (state == CustomState)
- return new FakeErrorReply{op, req, this, 500};
- return OAuthTestCase::tokenReply(op, req);
- }
- void oauthResult(OAuth::Result result, const QString &user, const QString &token ,
- const QString &refreshToken) override {
- if (state != CustomState)
- return OAuthTestCase::oauthResult(result, user, token, refreshToken);
- QCOMPARE(result, OAuth::Error);
- }
- } test;
- test.test();
- }
- void testTokenUrlHasRedirect()
- {
- struct Test : OAuthTestCase {
- int redirectsDone = 0;
- QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest & request) override
- {
- ASSERT(browserReply);
- // Kind of reproduces what we had in https://github.com/owncloud/enterprise/issues/2951 (not 1:1)
- if (redirectsDone == 0) {
- std::unique_ptr<QBuffer> payload(new QBuffer());
- payload->setData("");
- auto *reply = new SlowFakePostReply(op, request, std::move(payload), this);
- reply->redirectToPolicy = true;
- redirectsDone++;
- return reply;
- } else if (redirectsDone == 1) {
- std::unique_ptr<QBuffer> payload(new QBuffer());
- payload->setData("");
- auto *reply = new SlowFakePostReply(op, request, std::move(payload), this);
- reply->redirectToToken = true;
- redirectsDone++;
- return reply;
- } else {
- // ^^ This is with a custom reply and not actually HTTP, so we're testing the HTTP redirect code
- // we have in AbstractNetworkJob::slotFinished()
- redirectsDone++;
- return OAuthTestCase::tokenReply(op, request);
- }
- }
- } test;
- test.test();
- }
- };
- QTEST_GUILESS_MAIN(TestOAuth)
- #include "testoauth.moc"
|