/**************************************************************************** ** ** Copyright (C) 2018 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtWebEngine module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include #include "httpserver.h" #include #include #include #include #include #include #include #include #if defined(WEBSOCKETS) #include #include #include #endif #include #define QSL QStringLiteral #define QBAL QByteArrayLiteral void registerSchemes() { { QWebEngineUrlScheme scheme(QBAL("PathSyntax")); QWebEngineUrlScheme::registerScheme(scheme); } { QWebEngineUrlScheme scheme(QBAL("PathSyntax-Secure")); scheme.setFlags(QWebEngineUrlScheme::SecureScheme); QWebEngineUrlScheme::registerScheme(scheme); } { QWebEngineUrlScheme scheme(QBAL("PathSyntax-Secure-ServiceWorkersAllowed")); scheme.setFlags(QWebEngineUrlScheme::SecureScheme | QWebEngineUrlScheme::ServiceWorkersAllowed); QWebEngineUrlScheme::registerScheme(scheme); } { QWebEngineUrlScheme scheme(QBAL("PathSyntax-Local")); scheme.setFlags(QWebEngineUrlScheme::LocalScheme); QWebEngineUrlScheme::registerScheme(scheme); } { QWebEngineUrlScheme scheme(QBAL("PathSyntax-LocalAccessAllowed")); scheme.setFlags(QWebEngineUrlScheme::LocalAccessAllowed); QWebEngineUrlScheme::registerScheme(scheme); } { QWebEngineUrlScheme scheme(QBAL("PathSyntax-NoAccessAllowed")); scheme.setFlags(QWebEngineUrlScheme::NoAccessAllowed); QWebEngineUrlScheme::registerScheme(scheme); } { QWebEngineUrlScheme scheme(QBAL("PathSyntax-ServiceWorkersAllowed")); scheme.setFlags(QWebEngineUrlScheme::ServiceWorkersAllowed); QWebEngineUrlScheme::registerScheme(scheme); } { QWebEngineUrlScheme scheme(QBAL("PathSyntax-ViewSourceAllowed")); scheme.setFlags(QWebEngineUrlScheme::ViewSourceAllowed); QWebEngineUrlScheme::registerScheme(scheme); } { QWebEngineUrlScheme scheme(QBAL("HostSyntax")); scheme.setSyntax(QWebEngineUrlScheme::Syntax::Host); QWebEngineUrlScheme::registerScheme(scheme); } { QWebEngineUrlScheme scheme(QBAL("HostSyntax-ContentSecurityPolicyIgnored")); scheme.setSyntax(QWebEngineUrlScheme::Syntax::Host); scheme.setFlags(QWebEngineUrlScheme::ContentSecurityPolicyIgnored); QWebEngineUrlScheme::registerScheme(scheme); } { QWebEngineUrlScheme scheme(QBAL("HostAndPortSyntax")); scheme.setSyntax(QWebEngineUrlScheme::Syntax::HostAndPort); scheme.setDefaultPort(42); QWebEngineUrlScheme::registerScheme(scheme); } { QWebEngineUrlScheme scheme(QBAL("HostPortAndUserInformationSyntax")); scheme.setSyntax(QWebEngineUrlScheme::Syntax::HostPortAndUserInformation); scheme.setDefaultPort(42); QWebEngineUrlScheme::registerScheme(scheme); } { QWebEngineUrlScheme scheme(QBAL("redirect1")); scheme.setFlags(QWebEngineUrlScheme::CorsEnabled); QWebEngineUrlScheme::registerScheme(scheme); } { QWebEngineUrlScheme scheme(QBAL("redirect2")); scheme.setFlags(QWebEngineUrlScheme::CorsEnabled); QWebEngineUrlScheme::registerScheme(scheme); } { QWebEngineUrlScheme scheme(QBAL("cors")); scheme.setFlags(QWebEngineUrlScheme::CorsEnabled); QWebEngineUrlScheme::registerScheme(scheme); } } Q_CONSTRUCTOR_FUNCTION(registerSchemes) class TstUrlSchemeHandler final : public QWebEngineUrlSchemeHandler { Q_OBJECT public: TstUrlSchemeHandler(QWebEngineProfile *profile) { profile->installUrlSchemeHandler(QBAL("tst"), this); profile->installUrlSchemeHandler(QBAL("PathSyntax"), this); profile->installUrlSchemeHandler(QBAL("PathSyntax-Secure"), this); profile->installUrlSchemeHandler(QBAL("PathSyntax-Secure-ServiceWorkersAllowed"), this); profile->installUrlSchemeHandler(QBAL("PathSyntax-Local"), this); profile->installUrlSchemeHandler(QBAL("PathSyntax-LocalAccessAllowed"), this); profile->installUrlSchemeHandler(QBAL("PathSyntax-NoAccessAllowed"), this); profile->installUrlSchemeHandler(QBAL("PathSyntax-ServiceWorkersAllowed"), this); profile->installUrlSchemeHandler(QBAL("PathSyntax-ViewSourceAllowed"), this); profile->installUrlSchemeHandler(QBAL("HostSyntax"), this); profile->installUrlSchemeHandler(QBAL("HostSyntax-ContentSecurityPolicyIgnored"), this); profile->installUrlSchemeHandler(QBAL("HostAndPortSyntax"), this); profile->installUrlSchemeHandler(QBAL("HostPortAndUserInformationSyntax"), this); profile->installUrlSchemeHandler(QBAL("redirect1"), this); profile->installUrlSchemeHandler(QBAL("redirect2"), this); profile->installUrlSchemeHandler(QBAL("cors"), this); } QList &requests() { return m_requests; } private: void requestStarted(QWebEngineUrlRequestJob *job) override { QUrl url = job->requestUrl(); m_requests << url; if (url.scheme() == QBAL("redirect1")) { url.setScheme(QBAL("redirect2")); job->redirect(url); return; } QString pathPrefix = QDir(QT_TESTCASE_SOURCEDIR).canonicalPath(); if (url.path().startsWith("/qtwebchannel/")) pathPrefix = QSL(":"); QString pathSuffix = url.path(); QFile *file = new QFile(pathPrefix + pathSuffix, job); if (!file->open(QIODevice::ReadOnly)) { job->fail(QWebEngineUrlRequestJob::RequestFailed); return; } QByteArray mimeType = QBAL("text/html"); if (pathSuffix.endsWith(QSL(".js"))) mimeType = QBAL("application/javascript"); else if (pathSuffix.endsWith(QSL(".css"))) mimeType = QBAL("text/css"); job->reply(mimeType, file); } QList m_requests; }; class tst_Origins final : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void cleanupTestCase(); void init(); void cleanup(); void jsUrlCanon(); void jsUrlRelative(); void jsUrlOrigin(); void subdirWithAccess(); void subdirWithoutAccess(); void fileAccessRemoteUrl_data(); void fileAccessRemoteUrl(); void mixedSchemes(); void mixedSchemesWithCsp(); void mixedXHR_data(); void mixedXHR(); #if defined(WEBSOCKETS) void webSocket(); #endif void dedicatedWorker(); void sharedWorker(); void serviceWorker(); void viewSource(); void createObjectURL(); void redirect(); private: bool verifyLoad(const QUrl &url) { QSignalSpy spy(m_page, &QWebEnginePage::loadFinished); m_page->load(url); [&spy]() { QTRY_VERIFY_WITH_TIMEOUT(!spy.isEmpty(), 90000); }(); return !spy.isEmpty() && spy.front().value(0).toBool(); } QVariant eval(const QString &code) { return evaluateJavaScriptSync(m_page, code); } QWebEngineProfile m_profile; QWebEnginePage *m_page = nullptr; TstUrlSchemeHandler *m_handler = nullptr; }; void tst_Origins::initTestCase() { QTest::ignoreMessage( QtWarningMsg, QRegularExpression("Please register the custom scheme 'tst'.*")); m_handler = new TstUrlSchemeHandler(&m_profile); } void tst_Origins::cleanupTestCase() { QVERIFY(!m_page); delete m_handler; } void tst_Origins::init() { m_page = new QWebEnginePage(&m_profile, nullptr); } void tst_Origins::cleanup() { delete m_page; m_page = nullptr; m_handler->requests().clear(); } // Test URL parsing and canonicalization in Blink. The implementation of this // part is mostly shared between Blink and Chromium proper. void tst_Origins::jsUrlCanon() { QVERIFY(verifyLoad(QSL("about:blank"))); // Standard schemes are biased towards the authority part. QCOMPARE(eval(QSL("new URL(\"http:foo/bar\").href")), QVariant(QSL("http://foo/bar"))); QCOMPARE(eval(QSL("new URL(\"http:/foo/bar\").href")), QVariant(QSL("http://foo/bar"))); QCOMPARE(eval(QSL("new URL(\"http://foo/bar\").href")), QVariant(QSL("http://foo/bar"))); QCOMPARE(eval(QSL("new URL(\"http:///foo/bar\").href")), QVariant(QSL("http://foo/bar"))); // The file scheme is however a (particularly) special case. #ifdef Q_OS_WIN QCOMPARE(eval(QSL("new URL(\"file:foo/bar\").href")), QVariant(QSL("file://foo/bar"))); QCOMPARE(eval(QSL("new URL(\"file:/foo/bar\").href")), QVariant(QSL("file://foo/bar"))); QCOMPARE(eval(QSL("new URL(\"file://foo/bar\").href")), QVariant(QSL("file://foo/bar"))); QCOMPARE(eval(QSL("new URL(\"file:///foo/bar\").href")), QVariant(QSL("file:///foo/bar"))); #else QCOMPARE(eval(QSL("new URL(\"file:foo/bar\").href")), QVariant(QSL("file:///foo/bar"))); QCOMPARE(eval(QSL("new URL(\"file:/foo/bar\").href")), QVariant(QSL("file:///foo/bar"))); QCOMPARE(eval(QSL("new URL(\"file://foo/bar\").href")), QVariant(QSL("file://foo/bar"))); QCOMPARE(eval(QSL("new URL(\"file:///foo/bar\").href")), QVariant(QSL("file:///foo/bar"))); #endif // The qrc scheme is a PathSyntax scheme, having only a path and nothing else. QCOMPARE(eval(QSL("new URL(\"qrc:foo/bar\").href")), QVariant(QSL("qrc:foo/bar"))); QCOMPARE(eval(QSL("new URL(\"qrc:/foo/bar\").href")), QVariant(QSL("qrc:/foo/bar"))); QCOMPARE(eval(QSL("new URL(\"qrc://foo/bar\").href")), QVariant(QSL("qrc://foo/bar"))); QCOMPARE(eval(QSL("new URL(\"qrc:///foo/bar\").href")), QVariant(QSL("qrc:///foo/bar"))); // Same for unregistered schemes. QCOMPARE(eval(QSL("new URL(\"tst:foo/bar\").href")), QVariant(QSL("tst:foo/bar"))); QCOMPARE(eval(QSL("new URL(\"tst:/foo/bar\").href")), QVariant(QSL("tst:/foo/bar"))); QCOMPARE(eval(QSL("new URL(\"tst://foo/bar\").href")), QVariant(QSL("tst://foo/bar"))); QCOMPARE(eval(QSL("new URL(\"tst:///foo/bar\").href")), QVariant(QSL("tst:///foo/bar"))); // A HostSyntax scheme is like http without the port & user information. QCOMPARE(eval(QSL("new URL(\"HostSyntax:foo/bar\").href")), QVariant(QSL("hostsyntax://foo/bar"))); QCOMPARE(eval(QSL("new URL(\"HostSyntax:foo:42/bar\").href")), QVariant(QSL("hostsyntax://foo/bar"))); QCOMPARE(eval(QSL("new URL(\"HostSyntax:a:b@foo/bar\").href")), QVariant(QSL("hostsyntax://foo/bar"))); // A HostAndPortSyntax scheme is like http without the user information. QCOMPARE(eval(QSL("new URL(\"HostAndPortSyntax:foo/bar\").href")), QVariant(QSL("hostandportsyntax://foo/bar"))); QCOMPARE(eval(QSL("new URL(\"HostAndPortSyntax:foo:41/bar\").href")), QVariant(QSL("hostandportsyntax://foo:41/bar"))); QCOMPARE(eval(QSL("new URL(\"HostAndPortSyntax:foo:42/bar\").href")), QVariant(QSL("hostandportsyntax://foo/bar"))); QCOMPARE(eval(QSL("new URL(\"HostAndPortSyntax:a:b@foo/bar\").href")), QVariant(QSL("hostandportsyntax://foo/bar"))); // A HostPortAndUserInformationSyntax scheme is exactly like http. QCOMPARE(eval(QSL("new URL(\"HostPortAndUserInformationSyntax:foo/bar\").href")), QVariant(QSL("hostportanduserinformationsyntax://foo/bar"))); QCOMPARE(eval(QSL("new URL(\"HostPortAndUserInformationSyntax:foo:41/bar\").href")), QVariant(QSL("hostportanduserinformationsyntax://foo:41/bar"))); QCOMPARE(eval(QSL("new URL(\"HostPortAndUserInformationSyntax:foo:42/bar\").href")), QVariant(QSL("hostportanduserinformationsyntax://foo/bar"))); QCOMPARE(eval(QSL("new URL(\"HostPortAndUserInformationSyntax:a:b@foo/bar\").href")), QVariant(QSL("hostportanduserinformationsyntax://a:b@foo/bar"))); } // Test relative URL resolution. void tst_Origins::jsUrlRelative() { QVERIFY(verifyLoad(QSL("about:blank"))); // Schemes with hosts, like http, work as expected. QCOMPARE(eval(QSL("new URL('bar', 'http://foo').href")), QVariant(QSL("http://foo/bar"))); QCOMPARE(eval(QSL("new URL('baz', 'http://foo/bar').href")), QVariant(QSL("http://foo/baz"))); QCOMPARE(eval(QSL("new URL('baz', 'http://foo/bar/').href")), QVariant(QSL("http://foo/bar/baz"))); QCOMPARE(eval(QSL("new URL('/baz', 'http://foo/bar/').href")), QVariant(QSL("http://foo/baz"))); QCOMPARE(eval(QSL("new URL('./baz', 'http://foo/bar/').href")), QVariant(QSL("http://foo/bar/baz"))); QCOMPARE(eval(QSL("new URL('../baz', 'http://foo/bar/').href")), QVariant(QSL("http://foo/baz"))); QCOMPARE(eval(QSL("new URL('../../baz', 'http://foo/bar/').href")), QVariant(QSL("http://foo/baz"))); QCOMPARE(eval(QSL("new URL('//baz', 'http://foo/bar/').href")), QVariant(QSL("http://baz/"))); // In the case of schemes without hosts, relative URLs only work if the URL // starts with a single slash -- and canonicalization does not guarantee // this. The following cases all fail with TypeErrors. QCOMPARE(eval(QSL("new URL('bar', 'tst:foo').href")), QVariant()); QCOMPARE(eval(QSL("new URL('baz', 'tst:foo/bar').href")), QVariant()); QCOMPARE(eval(QSL("new URL('bar', 'tst://foo').href")), QVariant()); QCOMPARE(eval(QSL("new URL('bar', 'tst:///foo').href")), QVariant()); // However, registered custom schemes have been patched to allow relative // URLs even without an initial slash. QCOMPARE(eval(QSL("new URL('bar', 'qrc:foo').href")), QVariant(QSL("qrc:bar"))); QCOMPARE(eval(QSL("new URL('baz', 'qrc:foo/bar').href")), QVariant(QSL("qrc:foo/baz"))); QCOMPARE(eval(QSL("new URL('bar', 'qrc://foo').href")), QVariant()); QCOMPARE(eval(QSL("new URL('bar', 'qrc:///foo').href")), QVariant()); // With a slash it works the same as http except 'foo' is part of the path and not the host. QCOMPARE(eval(QSL("new URL('bar', 'qrc:/foo').href")), QVariant(QSL("qrc:/bar"))); QCOMPARE(eval(QSL("new URL('bar', 'qrc:/foo/').href")), QVariant(QSL("qrc:/foo/bar"))); QCOMPARE(eval(QSL("new URL('baz', 'qrc:/foo/bar').href")), QVariant(QSL("qrc:/foo/baz"))); QCOMPARE(eval(QSL("new URL('baz', 'qrc:/foo/bar/').href")), QVariant(QSL("qrc:/foo/bar/baz"))); QCOMPARE(eval(QSL("new URL('/baz', 'qrc:/foo/bar/').href")), QVariant(QSL("qrc:/baz"))); QCOMPARE(eval(QSL("new URL('./baz', 'qrc:/foo/bar/').href")), QVariant(QSL("qrc:/foo/bar/baz"))); QCOMPARE(eval(QSL("new URL('../baz', 'qrc:/foo/bar/').href")), QVariant(QSL("qrc:/foo/baz"))); QCOMPARE(eval(QSL("new URL('../../baz', 'qrc:/foo/bar/').href")), QVariant(QSL("qrc:/baz"))); QCOMPARE(eval(QSL("new URL('../../../baz', 'qrc:/foo/bar/').href")), QVariant(QSL("qrc:/baz"))); // If the relative URL begins with >= 2 slashes, then the scheme is treated // not as a Syntax::Path scheme but as a Syntax::HostPortAndUserInformation // scheme. QCOMPARE(eval(QSL("new URL('//baz', 'qrc:/foo/bar/').href")), QVariant(QSL("qrc://baz/"))); QCOMPARE(eval(QSL("new URL('///baz', 'qrc:/foo/bar/').href")), QVariant(QSL("qrc://baz/"))); } // Test origin serialization in Blink, implemented by blink::KURL and // blink::SecurityOrigin as opposed to GURL and url::Origin. void tst_Origins::jsUrlOrigin() { QVERIFY(verifyLoad(QSL("about:blank"))); // For network protocols the origin string must include the domain and port. QCOMPARE(eval(QSL("new URL(\"http://foo.com/page.html\").origin")), QVariant(QSL("http://foo.com"))); QCOMPARE(eval(QSL("new URL(\"https://foo.com/page.html\").origin")), QVariant(QSL("https://foo.com"))); // Even though file URL can also have domains, these are not included in the // origin string by Chromium. The standard does not specify a value here, // but suggests 'null' (https://url.spec.whatwg.org/#origin). QCOMPARE(eval(QSL("new URL(\"file:/etc/passwd\").origin")), QVariant(QSL("file://"))); QCOMPARE(eval(QSL("new URL(\"file://foo.com/etc/passwd\").origin")), QVariant(QSL("file://"))); // Unregistered schemes behave like file. QCOMPARE(eval(QSL("new URL(\"tst:/banana\").origin")), QVariant(QSL("tst://"))); QCOMPARE(eval(QSL("new URL(\"tst://foo.com/banana\").origin")), QVariant(QSL("tst://"))); // The non-PathSyntax schemes should have hosts and potentially ports. QCOMPARE(eval(QSL("new URL(\"HostSyntax:foo:41/bar\").origin")), QVariant(QSL("hostsyntax://foo"))); QCOMPARE(eval(QSL("new URL(\"HostAndPortSyntax:foo:41/bar\").origin")), QVariant(QSL("hostandportsyntax://foo:41"))); QCOMPARE(eval(QSL("new URL(\"HostAndPortSyntax:foo:42/bar\").origin")), QVariant(QSL("hostandportsyntax://foo"))); QCOMPARE(eval(QSL("new URL(\"HostPortAndUserInformationSyntax:foo:41/bar\").origin")), QVariant(QSL("hostportanduserinformationsyntax://foo:41"))); QCOMPARE(eval(QSL("new URL(\"HostPortAndUserInformationSyntax:foo:42/bar\").origin")), QVariant(QSL("hostportanduserinformationsyntax://foo"))); // A PathSyntax scheme should have a 'universal' origin. QCOMPARE(eval(QSL("new URL(\"PathSyntax:foo\").origin")), QVariant(QSL("pathsyntax:"))); QCOMPARE(eval(QSL("new URL(\"qrc:/crysis.css\").origin")), QVariant(QSL("qrc:"))); QCOMPARE(eval(QSL("new URL(\"qrc://foo.com/crysis.css\").origin")), QVariant(QSL("qrc:"))); // The NoAccessAllowed flag forces opaque origins. QCOMPARE(eval(QSL("new URL(\"PathSyntax-NoAccessAllowed:foo\").origin")), QVariant(QSL("null"))); } class ScopedAttribute { public: ScopedAttribute(QWebEngineSettings *settings, QWebEngineSettings::WebAttribute attribute, bool newValue) : m_settings(settings) , m_attribute(attribute) , m_oldValue(m_settings->testAttribute(m_attribute)) { m_settings->setAttribute(m_attribute, newValue); } ~ScopedAttribute() { m_settings->setAttribute(m_attribute, m_oldValue); } private: QWebEngineSettings *m_settings; QWebEngineSettings::WebAttribute m_attribute; bool m_oldValue; }; // Test same-origin policy of file, qrc and custom schemes. // // Note the test case involves the main page trying to load an iframe from a // file that resides in a parent directory. This is just a small detail to // demonstrate the difference with Firefox where such access is not allowed. void tst_Origins::subdirWithAccess() { ScopedAttribute sa(m_page->settings(), QWebEngineSettings::LocalContentCanAccessFileUrls, true); QVERIFY(verifyLoad("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources/subdir/index.html")); QCOMPARE(eval(QSL("msg[0]")), QVariant(QSL("hello"))); QCOMPARE(eval(QSL("msg[1]")), QVariant(QSL("world"))); QVERIFY(verifyLoad(QSL("qrc:/resources/subdir/index.html"))); QCOMPARE(eval(QSL("msg[0]")), QVariant(QSL("hello"))); QCOMPARE(eval(QSL("msg[1]")), QVariant(QSL("world"))); QVERIFY(verifyLoad(QSL("tst:/resources/subdir/index.html"))); QCOMPARE(eval(QSL("msg[0]")), QVariant(QSL("hello"))); QCOMPARE(eval(QSL("msg[1]")), QVariant(QSL("world"))); } // In this variation the LocalContentCanAccessFileUrls attribute is disabled. As // a result all file URLs will be considered to have unique/opaque origins, that // is, they are not the 'same origin as' any other origin. // // Note that this applies only to file URLs and not qrc or custom schemes. // // See also (in Blink): // - the allow_file_access_from_file_urls option and // - the blink::SecurityOrigin::BlockLocalAccessFromLocalOrigin() method. void tst_Origins::subdirWithoutAccess() { ScopedAttribute sa(m_page->settings(), QWebEngineSettings::LocalContentCanAccessFileUrls, false); QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); QVERIFY(verifyLoad("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources/subdir/index.html")); QCOMPARE(eval(QSL("msg[0]")), QVariant()); QCOMPARE(eval(QSL("msg[1]")), QVariant()); QVERIFY(verifyLoad(QSL("qrc:/resources/subdir/index.html"))); QCOMPARE(eval(QSL("msg[0]")), QVariant(QSL("hello"))); QCOMPARE(eval(QSL("msg[1]")), QVariant(QSL("world"))); QVERIFY(verifyLoad(QSL("tst:/resources/subdir/index.html"))); QCOMPARE(eval(QSL("msg[0]")), QVariant(QSL("hello"))); QCOMPARE(eval(QSL("msg[1]")), QVariant(QSL("world"))); } void tst_Origins::fileAccessRemoteUrl_data() { QTest::addColumn("EnableAccess"); QTest::addRow("enabled") << true; QTest::addRow("disabled") << false; } void tst_Origins::fileAccessRemoteUrl() { QFETCH(bool, EnableAccess); HttpServer server; server.setResourceDirs({ QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources" }); QVERIFY(server.start()); ScopedAttribute sa(m_page->settings(), QWebEngineSettings::LocalContentCanAccessRemoteUrls, EnableAccess); if (!EnableAccess) QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("blocked by CORS policy"))); QVERIFY(verifyLoad("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources/mixedXHR.html")); eval("sendXHR('" + server.url("/mixedXHR.txt").toString() + "')"); QTRY_COMPARE(eval("result"), (EnableAccess ? QString("ok") : QString("error"))); } // Load the main page over one scheme with an iframe over another scheme. // // For file and qrc schemes, the iframe should load but it should not be // possible for scripts in different frames to interact. // // Additionally for unregistered custom schemes and custom schemes without // LocalAccessAllowed it should not be possible to load an iframe over the // file: scheme. void tst_Origins::mixedSchemes() { QVERIFY(verifyLoad("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources/mixedSchemes.html")); eval("setIFrameUrl('file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources/mixedSchemes_frame.html')"); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadAndAccess"))); QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('qrc:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('tst:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); QVERIFY(verifyLoad(QSL("qrc:/resources/mixedSchemes.html"))); QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Not allowed to load local resource"))); eval("setIFrameUrl('file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources/mixedSchemes_frame.html')"); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("cannotLoad"))); eval(QSL("setIFrameUrl('qrc:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadAndAccess"))); QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('tst:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); QVERIFY(verifyLoad(QSL("tst:/resources/mixedSchemes.html"))); QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Not allowed to load local resource"))); eval("setIFrameUrl('file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources/mixedSchemes_frame.html')"); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("cannotLoad"))); QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('qrc:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); eval(QSL("setIFrameUrl('tst:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadAndAccess"))); QVERIFY(verifyLoad(QSL("PathSyntax:/resources/mixedSchemes.html"))); eval(QSL("setIFrameUrl('PathSyntax:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadAndAccess"))); QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Not allowed to load local resource"))); eval(QSL("setIFrameUrl('PathSyntax-Local:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("cannotLoad"))); eval(QSL("setIFrameUrl('PathSyntax-LocalAccessAllowed:/resources/mixedSchemes_frame.html')")); QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('PathSyntax-NoAccessAllowed:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); QVERIFY(verifyLoad(QSL("PathSyntax-LocalAccessAllowed:/resources/mixedSchemes.html"))); QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('PathSyntax:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('PathSyntax-Local:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); eval(QSL("setIFrameUrl('PathSyntax-LocalAccessAllowed:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadAndAccess"))); QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('PathSyntax-NoAccessAllowed:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); QVERIFY(verifyLoad(QSL("PathSyntax-NoAccessAllowed:/resources/mixedSchemes.html"))); QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('PathSyntax:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Not allowed to load local resource"))); eval(QSL("setIFrameUrl('PathSyntax-Local:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("cannotLoad"))); QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('PathSyntax-LocalAccessAllowed:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('PathSyntax-NoAccessAllowed:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); QVERIFY(verifyLoad(QSL("HostSyntax://a/resources/mixedSchemes.html"))); eval(QSL("setIFrameUrl('HostSyntax://a/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadAndAccess"))); QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('HostSyntax://b/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); } // Like mixedSchemes but adds a Content-Security-Policy: frame-src 'none' header. void tst_Origins::mixedSchemesWithCsp() { QVERIFY(verifyLoad(QSL("HostSyntax://a/resources/mixedSchemesWithCsp.html"))); QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("violates the following Content Security Policy"))); eval(QSL("setIFrameUrl('HostSyntax://a/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("violates the following Content Security Policy"))); eval(QSL("setIFrameUrl('HostSyntax://b/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); QVERIFY(verifyLoad(QSL("HostSyntax-ContentSecurityPolicyIgnored://a/resources/mixedSchemesWithCsp.html"))); eval(QSL("setIFrameUrl('HostSyntax-ContentSecurityPolicyIgnored://a/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadAndAccess"))); QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('HostSyntax-ContentSecurityPolicyIgnored://b/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); } // Load the main page over one scheme, then make an XMLHttpRequest to a // different scheme. // // Cross-origin XMLHttpRequests can only be made to CORS-enabled schemes. These // include the builtin schemes http, https, data, and chrome, as well as custom // schemes with the CorsEnabled flag. void tst_Origins::mixedXHR_data() { QTest::addColumn("url"); QTest::addColumn("command"); QTest::addColumn("result"); QTest::newRow("file->file") << QString("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources/mixedXHR.html") << QString("sendXHR('file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources/mixedXHR.txt')") << QVariant(QString("ok")); QTest::newRow("file->qrc") << QString("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources/mixedXHR.html") << QString("sendXHR('qrc:/resources/mixedXHR.txt')") << QVariant(QString("error")); QTest::newRow("file->tst") << QString("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources/mixedXHR.html") << QString("sendXHR('tst:/resources/mixedXHR.txt')") << QVariant(QString("error")); QTest::newRow("file->data") << QString("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources/mixedXHR.html") << QString("sendXHR('data:,ok')") << QVariant(QString("ok")); QTest::newRow("file->cors") << QString("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources/mixedXHR.html") << QString("sendXHR('cors:/resources/mixedXHR.txt')") << QVariant(QString("ok")); QTest::newRow("qrc->file") << QString("qrc:/resources/mixedXHR.html") << QString("sendXHR('file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources/mixedXHR.txt')") << QVariant(QString("error")); QTest::newRow("qrc->qrc") << QString("qrc:/resources/mixedXHR.html") << QString("sendXHR('qrc:/resources/mixedXHR.txt')") << QVariant(QString("ok")); QTest::newRow("qrc->tst") << QString("qrc:/resources/mixedXHR.html") << QString("sendXHR('tst:/resources/mixedXHR.txt')") << QVariant(QString("error")); QTest::newRow("qrc->data") << QString("qrc:/resources/mixedXHR.html") << QString("sendXHR('data:,ok')") << QVariant(QString("ok")); QTest::newRow("qrc->cors") << QString("qrc:/resources/mixedXHR.html") << QString("sendXHR('cors:/resources/mixedXHR.txt')") << QVariant(QString("ok")); QTest::newRow("tst->file") << QString("tst:/resources/mixedXHR.html") << QString("sendXHR('file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources/mixedXHR.txt')") << QVariant(QString("error")); QTest::newRow("tst->qrc") << QString("tst:/resources/mixedXHR.html") << QString("sendXHR('qrc:/resources/mixedXHR.txt')") << QVariant(QString("error")); QTest::newRow("tst->tst") << QString("tst:/resources/mixedXHR.html") << QString("sendXHR('tst:/resources/mixedXHR.txt')") << QVariant(QString("ok")); QTest::newRow("tst->data") << QString("tst:/resources/mixedXHR.html") << QString("sendXHR('data:,ok')") << QVariant(QString("ok")); QTest::newRow("tst->cors") << QString("tst:/resources/mixedXHR.html") << QString("sendXHR('cors:/resources/mixedXHR.txt')") << QVariant(QString("ok")); } void tst_Origins::mixedXHR() { QFETCH(QString, url); QFETCH(QString, command); QFETCH(QVariant, result); QVERIFY(verifyLoad(url)); eval(command); QTRY_COMPARE(eval(QString("result")), result); } #if defined(WEBSOCKETS) class EchoServer : public QObject { Q_OBJECT Q_PROPERTY(QUrl url READ url NOTIFY urlChanged) public: EchoServer() : webSocketServer(QSL("EchoServer"), QWebSocketServer::NonSecureMode) { connect(&webSocketServer, &QWebSocketServer::newConnection, this, &EchoServer::onNewConnection); } bool listen() { if (webSocketServer.listen(QHostAddress::Any)) { Q_EMIT urlChanged(); return true; } return false; } QUrl url() const { return webSocketServer.serverUrl(); } Q_SIGNALS: void urlChanged(); private: void onNewConnection() { QWebSocket *socket = webSocketServer.nextPendingConnection(); connect(socket, &QWebSocket::textMessageReceived, this, &EchoServer::onTextMessageReceived); connect(socket, &QWebSocket::disconnected, socket, &QObject::deleteLater); } void onTextMessageReceived(const QString &message) { QWebSocket *socket = qobject_cast(sender()); socket->sendTextMessage(message); } QWebSocketServer webSocketServer; }; // Try opening a WebSocket from pages loaded over various URL schemes. void tst_Origins::webSocket() { EchoServer echoServer; QWebChannel channel; channel.registerObject(QSL("echoServer"), &echoServer); m_page->setWebChannel(&channel); QVERIFY(echoServer.listen()); QVERIFY(verifyLoad("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources/websocket.html")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("ok"))); QVERIFY(verifyLoad(QSL("qrc:/resources/websocket.html"))); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("ok"))); // Unregistered schemes can also open WebSockets (since Chromium 71) QVERIFY(verifyLoad(QSL("tst:/resources/websocket2.html"))); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("ok"))); // Even an insecure registered scheme can open WebSockets. QVERIFY(verifyLoad(QSL("PathSyntax:/resources/websocket2.html"))); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("ok"))); } #endif // Create a (Dedicated)Worker. Since dedicated workers can only be accessed from // one page, there is not much need for security restrictions. void tst_Origins::dedicatedWorker() { QVERIFY(verifyLoad("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources/dedicatedWorker.html")); QTRY_VERIFY(eval(QSL("done")).toBool()); QCOMPARE(eval(QSL("result")), QVariant(42)); QVERIFY(verifyLoad(QSL("qrc:/resources/dedicatedWorker.html"))); QTRY_VERIFY(eval(QSL("done")).toBool()); QCOMPARE(eval(QSL("result")), QVariant(42)); // Unregistered schemes can also create Workers (since Chromium 71) QVERIFY(verifyLoad(QSL("tst:/resources/dedicatedWorker.html"))); QTRY_VERIFY(eval(QSL("done")).toBool()); QCOMPARE(eval(QSL("result")), QVariant(42)); // Even an insecure registered scheme can create Workers. QVERIFY(verifyLoad(QSL("PathSyntax:/resources/dedicatedWorker.html"))); QTRY_VERIFY(eval(QSL("done")).toBool()); QCOMPARE(eval(QSL("result")), QVariant(42)); // But not if the NoAccessAllowed flag is set. QVERIFY(verifyLoad(QSL("PathSyntax-NoAccessAllowed:/resources/dedicatedWorker.html"))); QTRY_VERIFY(eval(QSL("done")).toBool()); QVERIFY(eval(QSL("error")).toString() .contains(QSL("cannot be accessed from origin 'null'"))); } // Create a SharedWorker. Shared workers can be accessed from multiple pages, // and therefore the same-origin policy applies. void tst_Origins::sharedWorker() { { ScopedAttribute sa(m_page->settings(), QWebEngineSettings::LocalContentCanAccessFileUrls, false); QVERIFY(verifyLoad("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources/sharedWorker.html")); QTRY_VERIFY_WITH_TIMEOUT(eval(QSL("done")).toBool(), 10000); QVERIFY(eval(QSL("error")).toString() .contains(QSL("cannot be accessed from origin 'null'"))); } { ScopedAttribute sa(m_page->settings(), QWebEngineSettings::LocalContentCanAccessFileUrls, true); QVERIFY(verifyLoad("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources/sharedWorker.html")); QTRY_VERIFY_WITH_TIMEOUT(eval(QSL("done")).toBool(), 10000); QCOMPARE(eval(QSL("result")), QVariant(42)); } QVERIFY(verifyLoad(QSL("qrc:/resources/sharedWorker.html"))); QTRY_VERIFY_WITH_TIMEOUT(eval(QSL("done")).toBool(), 10000); QCOMPARE(eval(QSL("result")), QVariant(42)); // Unregistered schemes should not create SharedWorkers. QVERIFY(verifyLoad(QSL("PathSyntax:/resources/sharedWorker.html"))); QTRY_VERIFY_WITH_TIMEOUT(eval(QSL("done")).toBool(), 10000); QCOMPARE(eval(QSL("result")), QVariant(42)); QVERIFY(verifyLoad(QSL("PathSyntax-NoAccessAllowed:/resources/sharedWorker.html"))); QTRY_VERIFY_WITH_TIMEOUT(eval(QSL("done")).toBool(), 10000); QVERIFY(eval(QSL("error")).toString() .contains(QSL("denied to origin 'null'"))); } // Service workers have to be explicitly enabled for a scheme. void tst_Origins::serviceWorker() { QVERIFY(verifyLoad("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources/serviceWorker.html")); QTRY_VERIFY(eval(QSL("done")).toBool()); QVERIFY(eval(QSL("error")).toString() .contains(QSL("The URL protocol of the current origin ('file://') is not supported."))); QVERIFY(verifyLoad(QSL("qrc:/resources/serviceWorker.html"))); QTRY_VERIFY(eval(QSL("done")).toBool()); QVERIFY(eval(QSL("error")).toString() .contains(QSL("The URL protocol of the current origin ('qrc:') is not supported."))); QVERIFY(verifyLoad(QSL("tst:/resources/serviceWorker.html"))); QTRY_VERIFY(eval(QSL("done")).toBool()); QVERIFY(eval(QSL("error")).toString() .contains(QSL("Cannot read property 'register' of undefined"))); QVERIFY(verifyLoad(QSL("PathSyntax:/resources/serviceWorker.html"))); QTRY_VERIFY(eval(QSL("done")).toBool()); QVERIFY(eval(QSL("error")).toString() .contains(QSL("Cannot read property 'register' of undefined"))); QVERIFY(verifyLoad(QSL("PathSyntax-Secure:/resources/serviceWorker.html"))); QTRY_VERIFY(eval(QSL("done")).toBool()); QVERIFY(eval(QSL("error")).toString() .contains(QSL("The URL protocol of the current origin ('pathsyntax-secure:') is not supported."))); QVERIFY(verifyLoad(QSL("PathSyntax-ServiceWorkersAllowed:/resources/serviceWorker.html"))); QTRY_VERIFY(eval(QSL("done")).toBool()); QVERIFY(eval(QSL("error")).toString() .contains(QSL("Cannot read property 'register' of undefined"))); QVERIFY(verifyLoad(QSL("PathSyntax-Secure-ServiceWorkersAllowed:/resources/serviceWorker.html"))); QTRY_VERIFY(eval(QSL("done")).toBool()); QCOMPARE(eval(QSL("error")), QVariant()); QVERIFY(verifyLoad(QSL("PathSyntax-NoAccessAllowed:/resources/serviceWorker.html"))); QTRY_VERIFY(eval(QSL("done")).toBool()); QVERIFY(eval(QSL("error")).toString() .contains(QSL("Cannot read property 'register' of undefined"))); } // Support for view-source must be enabled explicitly. void tst_Origins::viewSource() { QVERIFY(verifyLoad("view-source:file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources/viewSource.html")); #ifdef Q_OS_WIN QCOMPARE(m_page->requestedUrl().toString(), "file:///" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources/viewSource.html"); #else QCOMPARE(m_page->requestedUrl().toString(), "file://" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources/viewSource.html"); #endif QVERIFY(verifyLoad(QSL("view-source:qrc:/resources/viewSource.html"))); QCOMPARE(m_page->requestedUrl().toString(), QSL("qrc:/resources/viewSource.html")); QVERIFY(verifyLoad(QSL("view-source:tst:/resources/viewSource.html"))); QCOMPARE(m_page->requestedUrl().toString(), QSL("about:blank")); QVERIFY(verifyLoad(QSL("view-source:PathSyntax:/resources/viewSource.html"))); QCOMPARE(m_page->requestedUrl().toString(), QSL("about:blank")); QVERIFY(verifyLoad(QSL("view-source:PathSyntax-ViewSourceAllowed:/resources/viewSource.html"))); QCOMPARE(m_page->requestedUrl().toString(), QSL("pathsyntax-viewsourceallowed:/resources/viewSource.html")); } void tst_Origins::createObjectURL() { // Legal for registered custom schemes. QVERIFY(verifyLoad(QSL("qrc:/resources/createObjectURL.html"))); QVERIFY(eval(QSL("result")).toString().startsWith(QSL("blob:qrc:"))); // Also legal for unregistered schemes (since Chromium 71) QVERIFY(verifyLoad(QSL("tst:/resources/createObjectURL.html"))); QVERIFY(eval(QSL("result")).toString().startsWith(QSL("blob:tst:"))); } void tst_Origins::redirect() { QVERIFY(verifyLoad(QSL("redirect1:/resources/redirect.html"))); QTRY_COMPARE(m_handler->requests().size(), 7); QCOMPARE(m_handler->requests()[0], QUrl(QStringLiteral("redirect1:/resources/redirect.html"))); QCOMPARE(m_handler->requests()[1], QUrl(QStringLiteral("redirect2:/resources/redirect.html"))); QCOMPARE(m_handler->requests()[2], QUrl(QStringLiteral("redirect1:/resources/redirect.css"))); QCOMPARE(m_handler->requests()[3], QUrl(QStringLiteral("redirect2:/resources/redirect.css"))); QCOMPARE(m_handler->requests()[4], QUrl(QStringLiteral("redirect1:/resources/Akronim-Regular.woff2"))); QCOMPARE(m_handler->requests()[5], QUrl(QStringLiteral("redirect1:/resources/Akronim-Regular.woff2"))); QCOMPARE(m_handler->requests()[6], QUrl(QStringLiteral("redirect2:/resources/Akronim-Regular.woff2"))); } QTEST_MAIN(tst_Origins) #include "tst_origins.moc"