testoauth.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  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. bool redirectToPolicy = false;
  30. bool redirectToToken = false;
  31. FakePostReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request,
  32. std::unique_ptr<QIODevice> payload_, QObject *parent)
  33. : QNetworkReply{parent}, payload{std::move(payload_)}
  34. {
  35. setRequest(request);
  36. setUrl(request.url());
  37. setOperation(op);
  38. open(QIODevice::ReadOnly);
  39. payload->open(QIODevice::ReadOnly);
  40. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  41. }
  42. Q_INVOKABLE virtual void respond() {
  43. if (aborted) {
  44. setError(OperationCanceledError, "Operation Canceled");
  45. emit metaDataChanged();
  46. emit finished();
  47. return;
  48. } else if (redirectToPolicy) {
  49. setHeader(QNetworkRequest::LocationHeader, "/my.policy");
  50. setAttribute(QNetworkRequest::RedirectionTargetAttribute, "/my.policy");
  51. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 302); // 302 might or might not lose POST data in rfc
  52. setHeader(QNetworkRequest::ContentLengthHeader, 0);
  53. emit metaDataChanged();
  54. emit finished();
  55. return;
  56. } else if (redirectToToken) {
  57. // Redirect to self
  58. QVariant destination = QVariant(sOAuthTestServer.toString()+QLatin1String("/index.php/apps/oauth2/api/v1/token"));
  59. setHeader(QNetworkRequest::LocationHeader, destination);
  60. setAttribute(QNetworkRequest::RedirectionTargetAttribute, destination);
  61. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 307); // 307 explicitly in rfc says to not lose POST data
  62. setHeader(QNetworkRequest::ContentLengthHeader, 0);
  63. emit metaDataChanged();
  64. emit finished();
  65. return;
  66. }
  67. setHeader(QNetworkRequest::ContentLengthHeader, payload->size());
  68. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
  69. emit metaDataChanged();
  70. if (bytesAvailable())
  71. emit readyRead();
  72. emit finished();
  73. }
  74. void abort() override {
  75. aborted = true;
  76. }
  77. [[nodiscard]] qint64 bytesAvailable() const override {
  78. if (aborted)
  79. return 0;
  80. return payload->bytesAvailable();
  81. }
  82. qint64 readData(char *data, qint64 maxlen) override {
  83. return payload->read(data, maxlen);
  84. }
  85. };
  86. // Reply with a small delay
  87. class SlowFakePostReply : public FakePostReply {
  88. Q_OBJECT
  89. public:
  90. using FakePostReply::FakePostReply;
  91. void respond() override {
  92. // override of FakePostReply::respond, will call the real one with a delay.
  93. QTimer::singleShot(100, this, [this] { this->FakePostReply::respond(); });
  94. }
  95. };
  96. class OAuthTestCase : public QObject
  97. {
  98. Q_OBJECT
  99. DesktopServiceHook desktopServiceHook;
  100. public:
  101. enum State { StartState, BrowserOpened, TokenAsked, CustomState } state = StartState;
  102. Q_ENUM(State);
  103. bool replyToBrowserOk = false;
  104. bool gotAuthOk = false;
  105. [[nodiscard]] virtual bool done() const { return replyToBrowserOk && gotAuthOk; }
  106. FakeQNAM *fakeQnam = nullptr;
  107. QNetworkAccessManager realQNAM;
  108. QPointer<QNetworkReply> browserReply = nullptr;
  109. QString code = generateEtag();
  110. OCC::AccountPtr account;
  111. QScopedPointer<OAuth> oauth;
  112. virtual void test() {
  113. fakeQnam = new FakeQNAM({});
  114. account = OCC::Account::create();
  115. account->setUrl(sOAuthTestServer);
  116. account->setCredentials(new FakeCredentials{fakeQnam});
  117. fakeQnam->setParent(this);
  118. fakeQnam->setOverride([this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) {
  119. ASSERT(device);
  120. ASSERT(device->bytesAvailable()>0); // OAuth2 always sends around POST data.
  121. return this->tokenReply(op, req);
  122. });
  123. QObject::connect(&desktopServiceHook, &DesktopServiceHook::hooked,
  124. this, &OAuthTestCase::openBrowserHook);
  125. oauth.reset(new OAuth(account.data(), nullptr));
  126. QObject::connect(oauth.data(), &OAuth::result, this, &OAuthTestCase::oauthResult);
  127. oauth->start();
  128. QTRY_VERIFY(done());
  129. }
  130. virtual void openBrowserHook(const QUrl &url) {
  131. QCOMPARE(state, StartState);
  132. state = BrowserOpened;
  133. QCOMPARE(url.path(), QString(sOAuthTestServer.path() + "/index.php/apps/oauth2/authorize"));
  134. QVERIFY(url.toString().startsWith(sOAuthTestServer.toString()));
  135. QUrlQuery query(url);
  136. QCOMPARE(query.queryItemValue(QLatin1String("response_type")), QLatin1String("code"));
  137. QCOMPARE(query.queryItemValue(QLatin1String("client_id")), Theme::instance()->oauthClientId());
  138. QUrl redirectUri(query.queryItemValue(QLatin1String("redirect_uri")));
  139. QCOMPARE(redirectUri.host(), QLatin1String("localhost"));
  140. redirectUri.setQuery("code=" + code);
  141. createBrowserReply(QNetworkRequest(redirectUri));
  142. }
  143. virtual QNetworkReply *createBrowserReply(const QNetworkRequest &request) {
  144. browserReply = realQNAM.get(request);
  145. QObject::connect(browserReply, &QNetworkReply::finished, this, &OAuthTestCase::browserReplyFinished);
  146. return browserReply;
  147. }
  148. virtual void browserReplyFinished() {
  149. QCOMPARE(sender(), browserReply.data());
  150. QCOMPARE(state, TokenAsked);
  151. browserReply->deleteLater();
  152. QCOMPARE(browserReply->rawHeader("Location"), QByteArray("owncloud://success"));
  153. replyToBrowserOk = true;
  154. };
  155. virtual QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req)
  156. {
  157. ASSERT(state == BrowserOpened);
  158. state = TokenAsked;
  159. ASSERT(op == QNetworkAccessManager::PostOperation);
  160. ASSERT(req.url().toString().startsWith(sOAuthTestServer.toString()));
  161. ASSERT(req.url().path() == sOAuthTestServer.path() + "/index.php/apps/oauth2/api/v1/token");
  162. std::unique_ptr<QBuffer> payload(new QBuffer());
  163. payload->setData(tokenReplyPayload());
  164. return new FakePostReply(op, req, std::move(payload), fakeQnam);
  165. }
  166. [[nodiscard]] virtual QByteArray tokenReplyPayload() const {
  167. QJsonDocument jsondata(QJsonObject{
  168. { "access_token", "123" },
  169. { "refresh_token" , "456" },
  170. { "message_url", "owncloud://success"},
  171. { "user_id", "789" },
  172. { "token_type", "Bearer" }
  173. });
  174. return jsondata.toJson();
  175. }
  176. virtual void oauthResult(OAuth::Result result, const QString &user, const QString &token , const QString &refreshToken) {
  177. QCOMPARE(state, TokenAsked);
  178. QCOMPARE(result, OAuth::LoggedIn);
  179. QCOMPARE(user, QString("789"));
  180. QCOMPARE(token, QString("123"));
  181. QCOMPARE(refreshToken, QString("456"));
  182. gotAuthOk = true;
  183. }
  184. };
  185. class TestOAuth: public QObject
  186. {
  187. Q_OBJECT
  188. private slots:
  189. void testBasic()
  190. {
  191. OAuthTestCase test;
  192. test.test();
  193. }
  194. // Test for https://github.com/owncloud/client/pull/6057
  195. void testCloseBrowserDontCrash()
  196. {
  197. struct Test : OAuthTestCase {
  198. QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest & req) override
  199. {
  200. ASSERT(browserReply);
  201. // simulate the fact that the browser is closing the connection
  202. browserReply->abort();
  203. QCoreApplication::processEvents();
  204. ASSERT(state == BrowserOpened);
  205. state = TokenAsked;
  206. std::unique_ptr<QBuffer> payload(new QBuffer);
  207. payload->setData(tokenReplyPayload());
  208. return new SlowFakePostReply(op, req, std::move(payload), fakeQnam);
  209. }
  210. void browserReplyFinished() override
  211. {
  212. QCOMPARE(sender(), browserReply.data());
  213. QCOMPARE(browserReply->error(), QNetworkReply::OperationCanceledError);
  214. replyToBrowserOk = true;
  215. }
  216. } test;
  217. test.test();
  218. }
  219. void testRandomConnections()
  220. {
  221. // Test that we can send random garbage to the litening socket and it does not prevent the connection
  222. struct Test : OAuthTestCase {
  223. QNetworkReply *createBrowserReply(const QNetworkRequest &request) override {
  224. QTimer::singleShot(0, this, [this, request] {
  225. auto port = request.url().port();
  226. state = CustomState;
  227. QVector<QByteArray> payloads = {
  228. "GET FOFOFO HTTP 1/1\n\n",
  229. "GET /?code=invalie HTTP 1/1\n\n",
  230. "GET /?code=xxxxx&bar=fff",
  231. QByteArray("\0\0\0", 3),
  232. QByteArray("GET \0\0\0 \n\n\n\n\n\0", 14),
  233. QByteArray("GET /?code=éléphant\xa5 HTTP\n"),
  234. QByteArray("\n\n\n\n"),
  235. };
  236. foreach (const auto &x, payloads) {
  237. auto socket = new QTcpSocket(this);
  238. socket->connectToHost("localhost", port);
  239. QVERIFY(socket->waitForConnected());
  240. socket->write(x);
  241. }
  242. // Do the actual request a bit later
  243. QTimer::singleShot(100, this, [this, request] {
  244. QCOMPARE(state, CustomState);
  245. state = BrowserOpened;
  246. this->OAuthTestCase::createBrowserReply(request);
  247. });
  248. });
  249. return nullptr;
  250. }
  251. QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) override
  252. {
  253. if (state == CustomState)
  254. return new FakeErrorReply{op, req, this, 500};
  255. return OAuthTestCase::tokenReply(op, req);
  256. }
  257. void oauthResult(OAuth::Result result, const QString &user, const QString &token ,
  258. const QString &refreshToken) override {
  259. if (state != CustomState)
  260. return OAuthTestCase::oauthResult(result, user, token, refreshToken);
  261. QCOMPARE(result, OAuth::Error);
  262. }
  263. } test;
  264. test.test();
  265. }
  266. void testTokenUrlHasRedirect()
  267. {
  268. struct Test : OAuthTestCase {
  269. int redirectsDone = 0;
  270. QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest & request) override
  271. {
  272. ASSERT(browserReply);
  273. // Kind of reproduces what we had in https://github.com/owncloud/enterprise/issues/2951 (not 1:1)
  274. if (redirectsDone == 0) {
  275. std::unique_ptr<QBuffer> payload(new QBuffer());
  276. payload->setData("");
  277. auto *reply = new SlowFakePostReply(op, request, std::move(payload), this);
  278. reply->redirectToPolicy = true;
  279. redirectsDone++;
  280. return reply;
  281. } else if (redirectsDone == 1) {
  282. std::unique_ptr<QBuffer> payload(new QBuffer());
  283. payload->setData("");
  284. auto *reply = new SlowFakePostReply(op, request, std::move(payload), this);
  285. reply->redirectToToken = true;
  286. redirectsDone++;
  287. return reply;
  288. } else {
  289. // ^^ This is with a custom reply and not actually HTTP, so we're testing the HTTP redirect code
  290. // we have in AbstractNetworkJob::slotFinished()
  291. redirectsDone++;
  292. return OAuthTestCase::tokenReply(op, request);
  293. }
  294. }
  295. } test;
  296. test.test();
  297. }
  298. };
  299. QTEST_GUILESS_MAIN(TestOAuth)
  300. #include "testoauth.moc"