testoauth.cpp 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. /*
  2. * This software is in the public domain, furnished "as is", without technical
  3. * support, and with no warranty, express or implied, as to its usefulness for
  4. * any purpose.
  5. *
  6. */
  7. #include <QtTest/QtTest>
  8. #include <QDesktopServices>
  9. #include "gui/creds/oauth.h"
  10. #include "syncenginetestutils.h"
  11. #include "theme.h"
  12. #include "common/asserts.h"
  13. using namespace OCC;
  14. class DesktopServiceHook : public QObject
  15. {
  16. Q_OBJECT
  17. signals:
  18. void hooked(const QUrl &);
  19. public:
  20. DesktopServiceHook() { QDesktopServices::setUrlHandler("oauthtest", this, "hooked"); }
  21. };
  22. static const QUrl sOAuthTestServer("oauthtest://someserver/owncloud");
  23. class FakePostReply : public QNetworkReply
  24. {
  25. Q_OBJECT
  26. public:
  27. std::unique_ptr<QIODevice> payload;
  28. bool aborted = false;
  29. FakePostReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request,
  30. std::unique_ptr<QIODevice> payload_, QObject *parent)
  31. : QNetworkReply{parent}, payload{std::move(payload_)}
  32. {
  33. setRequest(request);
  34. setUrl(request.url());
  35. setOperation(op);
  36. open(QIODevice::ReadOnly);
  37. payload->open(QIODevice::ReadOnly);
  38. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  39. }
  40. Q_INVOKABLE virtual void respond() {
  41. if (aborted) {
  42. setError(OperationCanceledError, "Operation Canceled");
  43. emit metaDataChanged();
  44. emit finished();
  45. return;
  46. }
  47. setHeader(QNetworkRequest::ContentLengthHeader, payload->size());
  48. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
  49. emit metaDataChanged();
  50. if (bytesAvailable())
  51. emit readyRead();
  52. emit finished();
  53. }
  54. void abort() override {
  55. aborted = true;
  56. }
  57. qint64 bytesAvailable() const override {
  58. if (aborted)
  59. return 0;
  60. return payload->bytesAvailable();
  61. }
  62. qint64 readData(char *data, qint64 maxlen) override {
  63. return payload->read(data, maxlen);
  64. }
  65. };
  66. // Reply with a small delay
  67. class SlowFakePostReply : public FakePostReply {
  68. Q_OBJECT
  69. public:
  70. using FakePostReply::FakePostReply;
  71. void respond() override {
  72. // override of FakePostReply::respond, will call the real one with a delay.
  73. QTimer::singleShot(100, this, [this] { this->FakePostReply::respond(); });
  74. }
  75. };
  76. class OAuthTestCase : public QObject
  77. {
  78. Q_OBJECT
  79. DesktopServiceHook desktopServiceHook;
  80. public:
  81. enum State { StartState, BrowserOpened, TokenAsked, CustomState } state = StartState;
  82. Q_ENUM(State);
  83. bool replyToBrowserOk = false;
  84. bool gotAuthOk = false;
  85. virtual bool done() const { return replyToBrowserOk && gotAuthOk; }
  86. FakeQNAM *fakeQnam = nullptr;
  87. QNetworkAccessManager realQNAM;
  88. QPointer<QNetworkReply> browserReply = nullptr;
  89. QString code = generateEtag();
  90. OCC::AccountPtr account;
  91. QScopedPointer<OAuth> oauth;
  92. virtual void test() {
  93. fakeQnam = new FakeQNAM({});
  94. account = OCC::Account::create();
  95. account->setUrl(sOAuthTestServer);
  96. account->setCredentials(new FakeCredentials{fakeQnam});
  97. fakeQnam->setParent(this);
  98. fakeQnam->setOverride([this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *) {
  99. return this->tokenReply(op, req);
  100. });
  101. QObject::connect(&desktopServiceHook, &DesktopServiceHook::hooked,
  102. this, &OAuthTestCase::openBrowserHook);
  103. oauth.reset(new OAuth(account.data(), nullptr));
  104. QObject::connect(oauth.data(), &OAuth::result, this, &OAuthTestCase::oauthResult);
  105. oauth->start();
  106. QTRY_VERIFY(done());
  107. }
  108. virtual void openBrowserHook(const QUrl &url) {
  109. QCOMPARE(state, StartState);
  110. state = BrowserOpened;
  111. QCOMPARE(url.path(), QString(sOAuthTestServer.path() + "/index.php/apps/oauth2/authorize"));
  112. QVERIFY(url.toString().startsWith(sOAuthTestServer.toString()));
  113. QUrlQuery query(url);
  114. QCOMPARE(query.queryItemValue(QLatin1String("response_type")), QLatin1String("code"));
  115. QCOMPARE(query.queryItemValue(QLatin1String("client_id")), Theme::instance()->oauthClientId());
  116. QUrl redirectUri(query.queryItemValue(QLatin1String("redirect_uri")));
  117. QCOMPARE(redirectUri.host(), QLatin1String("localhost"));
  118. redirectUri.setQuery("code=" + code);
  119. createBrowserReply(QNetworkRequest(redirectUri));
  120. }
  121. virtual QNetworkReply *createBrowserReply(const QNetworkRequest &request) {
  122. browserReply = realQNAM.get(request);
  123. QObject::connect(browserReply, &QNetworkReply::finished, this, &OAuthTestCase::browserReplyFinished);
  124. return browserReply;
  125. }
  126. virtual void browserReplyFinished() {
  127. QCOMPARE(sender(), browserReply.data());
  128. QCOMPARE(state, TokenAsked);
  129. browserReply->deleteLater();
  130. QCOMPARE(browserReply->rawHeader("Location"), QByteArray("owncloud://success"));
  131. replyToBrowserOk = true;
  132. };
  133. virtual QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req)
  134. {
  135. ASSERT(state == BrowserOpened);
  136. state = TokenAsked;
  137. ASSERT(op == QNetworkAccessManager::PostOperation);
  138. ASSERT(req.url().toString().startsWith(sOAuthTestServer.toString()));
  139. ASSERT(req.url().path() == sOAuthTestServer.path() + "/index.php/apps/oauth2/api/v1/token");
  140. std::unique_ptr<QBuffer> payload(new QBuffer());
  141. payload->setData(tokenReplyPayload());
  142. return new FakePostReply(op, req, std::move(payload), fakeQnam);
  143. }
  144. virtual QByteArray tokenReplyPayload() const {
  145. QJsonDocument jsondata(QJsonObject{
  146. { "access_token", "123" },
  147. { "refresh_token" , "456" },
  148. { "message_url", "owncloud://success"},
  149. { "user_id", "789" },
  150. { "token_type", "Bearer" }
  151. });
  152. return jsondata.toJson();
  153. }
  154. virtual void oauthResult(OAuth::Result result, const QString &user, const QString &token , const QString &refreshToken) {
  155. QCOMPARE(state, TokenAsked);
  156. QCOMPARE(result, OAuth::LoggedIn);
  157. QCOMPARE(user, QString("789"));
  158. QCOMPARE(token, QString("123"));
  159. QCOMPARE(refreshToken, QString("456"));
  160. gotAuthOk = true;
  161. }
  162. };
  163. class TestOAuth: public QObject
  164. {
  165. Q_OBJECT
  166. private slots:
  167. void testBasic()
  168. {
  169. OAuthTestCase test;
  170. test.test();
  171. }
  172. // Test for https://github.com/owncloud/client/pull/6057
  173. void testCloseBrowserDontCrash()
  174. {
  175. struct Test : OAuthTestCase {
  176. QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest & req) override
  177. {
  178. ASSERT(browserReply);
  179. // simulate the fact that the browser is closing the connection
  180. browserReply->abort();
  181. QCoreApplication::processEvents();
  182. ASSERT(state == BrowserOpened);
  183. state = TokenAsked;
  184. std::unique_ptr<QBuffer> payload(new QBuffer);
  185. payload->setData(tokenReplyPayload());
  186. return new SlowFakePostReply(op, req, std::move(payload), fakeQnam);
  187. }
  188. void browserReplyFinished() override
  189. {
  190. QCOMPARE(sender(), browserReply.data());
  191. QCOMPARE(browserReply->error(), QNetworkReply::OperationCanceledError);
  192. replyToBrowserOk = true;
  193. }
  194. } test;
  195. test.test();
  196. }
  197. void testRandomConnections()
  198. {
  199. // Test that we can send random garbage to the litening socket and it does not prevent the connection
  200. struct Test : OAuthTestCase {
  201. QNetworkReply *createBrowserReply(const QNetworkRequest &request) override {
  202. QTimer::singleShot(0, this, [this, request] {
  203. auto port = request.url().port();
  204. state = CustomState;
  205. QVector<QByteArray> payloads = {
  206. "GET FOFOFO HTTP 1/1\n\n",
  207. "GET /?code=invalie HTTP 1/1\n\n",
  208. "GET /?code=xxxxx&bar=fff",
  209. QByteArray("\0\0\0", 3),
  210. QByteArray("GET \0\0\0 \n\n\n\n\n\0", 14),
  211. QByteArray("GET /?code=éléphant\xa5 HTTP\n"),
  212. QByteArray("\n\n\n\n"),
  213. };
  214. foreach (const auto &x, payloads) {
  215. auto socket = new QTcpSocket(this);
  216. socket->connectToHost("localhost", port);
  217. QVERIFY(socket->waitForConnected());
  218. socket->write(x);
  219. }
  220. // Do the actual request a bit later
  221. QTimer::singleShot(100, this, [this, request] {
  222. QCOMPARE(state, CustomState);
  223. state = BrowserOpened;
  224. this->OAuthTestCase::createBrowserReply(request);
  225. });
  226. });
  227. return nullptr;
  228. }
  229. QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) override
  230. {
  231. if (state == CustomState)
  232. return new FakeErrorReply{op, req, this, 500};
  233. return OAuthTestCase::tokenReply(op, req);
  234. }
  235. void oauthResult(OAuth::Result result, const QString &user, const QString &token ,
  236. const QString &refreshToken) override {
  237. if (state != CustomState)
  238. return OAuthTestCase::oauthResult(result, user, token, refreshToken);
  239. QCOMPARE(result, OAuth::Error);
  240. }
  241. } test;
  242. test.test();
  243. }
  244. };
  245. QTEST_GUILESS_MAIN(TestOAuth)
  246. #include "testoauth.moc"