From 50ae50e87154ffb169b9d691e65dada691ab9eef Mon Sep 17 00:00:00 2001 From: Zack Weinberg Date: Thu, 20 Nov 2014 21:00:45 -0500 Subject: [PATCH] Add a command-line option --local-urls={true,false} The default is 'true'. When set 'false', file: and qrc: URLs are treated as invalid (unknown scheme) rather than opening local files, as requested in issue #12752. In order to test this, I added a mechanism to test/run-tests.py allowing individual tests to be annotated with command-line options to pass to phantomjs or the script. --- src/config.cpp | 17 ++++++++ src/config.h | 5 +++ src/networkaccessmanager.cpp | 42 +++++++++++++++++-- src/networkaccessmanager.h | 13 ++++++ test/module/webpage/local-urls-disabled.js | 18 ++++++++ test/module/webpage/local-urls-enabled.js | 19 +++++++++ test/run-tests.py | 48 ++++++++++++++-------- 7 files changed, 141 insertions(+), 21 deletions(-) create mode 100644 test/module/webpage/local-urls-disabled.js create mode 100644 test/module/webpage/local-urls-enabled.js diff --git a/src/config.cpp b/src/config.cpp index c9da6a86..ff7be96d 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -56,6 +56,7 @@ static const struct QCommandLineConfigEntry flags[] = { QCommandLine::Option, '\0', "load-images", "Loads all inlined images: 'true' (default) or 'false'", QCommandLine::Optional }, { QCommandLine::Option, '\0', "local-storage-path", "Specifies the location for offline local storage", QCommandLine::Optional }, { QCommandLine::Option, '\0', "local-storage-quota", "Sets the maximum size of the offline local storage (in KB)", QCommandLine::Optional }, + { QCommandLine::Option, '\0', "local-url-access", "Allows use of 'file:///' URLs: 'true' (default) or 'false'", QCommandLine::Optional }, { QCommandLine::Option, '\0', "local-to-remote-url-access", "Allows local content to access remote URL: 'true' or 'false' (default)", QCommandLine::Optional }, { QCommandLine::Option, '\0', "max-disk-cache-size", "Limits the size of the disk cache (in KB)", QCommandLine::Optional }, { QCommandLine::Option, '\0', "output-encoding", "Sets the encoding for the terminal output, default is 'utf8'", QCommandLine::Optional }, @@ -249,6 +250,16 @@ void Config::setIgnoreSslErrors(const bool value) m_ignoreSslErrors = value; } +bool Config::localUrlAccessEnabled() const +{ + return m_localUrlAccessEnabled; +} + +void Config::setLocalUrlAccessEnabled(const bool value) +{ + m_localUrlAccessEnabled = value; +} + bool Config::localToRemoteUrlAccessEnabled() const { return m_localToRemoteUrlAccessEnabled; @@ -535,6 +546,7 @@ void Config::resetToDefaults() m_diskCacheEnabled = false; m_maxDiskCacheSize = -1; m_ignoreSslErrors = false; + m_localUrlAccessEnabled = true; m_localToRemoteUrlAccessEnabled = false; m_outputEncoding = "UTF-8"; m_proxyType = "http"; @@ -643,6 +655,7 @@ void Config::handleOption(const QString &option, const QVariant &value) booleanFlags << "disk-cache"; booleanFlags << "ignore-ssl-errors"; booleanFlags << "load-images"; + booleanFlags << "local-url-access"; booleanFlags << "local-to-remote-url-access"; booleanFlags << "remote-debugger-autorun"; booleanFlags << "web-security"; @@ -686,6 +699,10 @@ void Config::handleOption(const QString &option, const QVariant &value) setOfflineStorageDefaultQuota(value.toInt()); } + if (option == "local-url-access") { + setLocalUrlAccessEnabled(boolValue); + } + if (option == "local-to-remote-url-access") { setLocalToRemoteUrlAccessEnabled(boolValue); } diff --git a/src/config.h b/src/config.h index c9f95efd..d1240124 100644 --- a/src/config.h +++ b/src/config.h @@ -45,6 +45,7 @@ class Config: public QObject Q_PROPERTY(bool diskCacheEnabled READ diskCacheEnabled WRITE setDiskCacheEnabled) Q_PROPERTY(int maxDiskCacheSize READ maxDiskCacheSize WRITE setMaxDiskCacheSize) Q_PROPERTY(bool ignoreSslErrors READ ignoreSslErrors WRITE setIgnoreSslErrors) + Q_PROPERTY(bool localUrlAccessEnabled READ localUrlAccessEnabled WRITE setLocalUrlAccessEnabled) Q_PROPERTY(bool localToRemoteUrlAccessEnabled READ localToRemoteUrlAccessEnabled WRITE setLocalToRemoteUrlAccessEnabled) Q_PROPERTY(QString outputEncoding READ outputEncoding WRITE setOutputEncoding) Q_PROPERTY(QString proxyType READ proxyType WRITE setProxyType) @@ -95,6 +96,9 @@ public: bool ignoreSslErrors() const; void setIgnoreSslErrors(const bool value); + bool localUrlAccessEnabled() const; + void setLocalUrlAccessEnabled(const bool value); + bool localToRemoteUrlAccessEnabled() const; void setLocalToRemoteUrlAccessEnabled(const bool value); @@ -201,6 +205,7 @@ private: bool m_diskCacheEnabled; int m_maxDiskCacheSize; bool m_ignoreSslErrors; + bool m_localUrlAccessEnabled; bool m_localToRemoteUrlAccessEnabled; QString m_outputEncoding; QString m_proxyType; diff --git a/src/networkaccessmanager.cpp b/src/networkaccessmanager.cpp index f6514e99..4b331f40 100644 --- a/src/networkaccessmanager.cpp +++ b/src/networkaccessmanager.cpp @@ -73,6 +73,30 @@ static const char *toString(QNetworkAccessManager::Operation op) return str; } +// Stub QNetworkReply used when file:/// URLs are disabled. +// Somewhat cargo-culted from QDisabledNetworkReply. + +NoFileAccessReply::NoFileAccessReply(QObject *parent, const QNetworkRequest &req, const QNetworkAccessManager::Operation op) + : QNetworkReply(parent) +{ + setRequest(req); + setUrl(req.url()); + setOperation(op); + + qRegisterMetaType(); + QString msg = (QCoreApplication::translate("QNetworkReply", "Protocol \"%1\" is unknown") + .arg(req.url().scheme())); + setError(ProtocolUnknownError, msg); + + QMetaObject::invokeMethod(this, "error", Qt::QueuedConnection, + Q_ARG(QNetworkReply::NetworkError, ProtocolUnknownError)); + QMetaObject::invokeMethod(this, "finished", Qt::QueuedConnection); +} + +// The destructor must be out-of-line in order to trigger generation of the vtable. +NoFileAccessReply::~NoFileAccessReply() {} + + TimeoutTimer::TimeoutTimer(QObject* parent) : QTimer(parent) { @@ -129,6 +153,7 @@ const ssl_protocol_option ssl_protocol_options[] = { NetworkAccessManager::NetworkAccessManager(QObject *parent, const Config *config) : QNetworkAccessManager(parent) , m_ignoreSslErrors(config->ignoreSslErrors()) + , m_localUrlAccessEnabled(config->localUrlAccessEnabled()) , m_authAttempts(0) , m_maxAuthAttempts(3) , m_resourceTimeout(0) @@ -238,10 +263,12 @@ void NetworkAccessManager::setCookieJar(QNetworkCookieJar *cookieJar) QNetworkReply *NetworkAccessManager::createRequest(Operation op, const QNetworkRequest & request, QIODevice * outgoingData) { QNetworkRequest req(request); + QString scheme = req.url().scheme().toLower(); + bool isLocalFile = req.url().isLocalFile(); if (!QSslSocket::supportsSsl()) { - if (req.url().scheme().toLower() == QLatin1String("https")) - qWarning() << "Request using https scheme without SSL support"; + if (scheme == QLatin1String("https")) + qWarning() << "Request using https scheme without SSL support"; } else { req.setSslConfiguration(m_sslConfiguration); } @@ -288,8 +315,15 @@ QNetworkReply *NetworkAccessManager::createRequest(Operation op, const QNetworkR JsNetworkRequest jsNetworkRequest(&req, this); emit resourceRequested(data, &jsNetworkRequest); - // Pass duty to the superclass - Nothing special to do here (yet?) - QNetworkReply *reply = QNetworkAccessManager::createRequest(op, req, outgoingData); + // Pass duty to the superclass - special case: file:/// may be disabled. + // This conditional must match QNetworkAccessManager's own idea of what a + // local file URL is. + QNetworkReply *reply; + if (!m_localUrlAccessEnabled && (isLocalFile || scheme == QLatin1String("qrc"))) { + reply = new NoFileAccessReply(this, req, op); + } else { + reply = QNetworkAccessManager::createRequest(op, req, outgoingData); + } // reparent jsNetworkRequest to make sure that it will be destroyed with QNetworkReply jsNetworkRequest.setParent(reply); diff --git a/src/networkaccessmanager.h b/src/networkaccessmanager.h index 1b1a8af6..f4a41fbc 100644 --- a/src/networkaccessmanager.h +++ b/src/networkaccessmanager.h @@ -67,6 +67,18 @@ private: QNetworkRequest* m_networkRequest; }; +class NoFileAccessReply : public QNetworkReply +{ + Q_OBJECT + +public: + NoFileAccessReply(QObject *parent, const QNetworkRequest &req, const QNetworkAccessManager::Operation op); + ~NoFileAccessReply(); + void abort() {} +protected: + qint64 readData(char *, qint64) { return -1; } +}; + class NetworkAccessManager : public QNetworkAccessManager { Q_OBJECT @@ -83,6 +95,7 @@ public: protected: bool m_ignoreSslErrors; + bool m_localUrlAccessEnabled; int m_authAttempts; int m_maxAuthAttempts; int m_resourceTimeout; diff --git a/test/module/webpage/local-urls-disabled.js b/test/module/webpage/local-urls-disabled.js new file mode 100644 index 00000000..620c20b6 --- /dev/null +++ b/test/module/webpage/local-urls-disabled.js @@ -0,0 +1,18 @@ +// phantomjs: --local-url-access=no + +var assert = require("../../assert"); +var p = require("webpage").create(); + +var url = "file:///nonexistent"; + +var rsErrorCalled = false; +p.onResourceError = function (error) { + rsErrorCalled = true; + assert.strictEqual(error.url, url); + assert.strictEqual(error.errorCode, 301); + assert.strictEqual(error.errorString, 'Protocol "file" is unknown'); +}; + +p.open(url, function () { + assert(rsErrorCalled); +}); diff --git a/test/module/webpage/local-urls-enabled.js b/test/module/webpage/local-urls-enabled.js new file mode 100644 index 00000000..456cb37c --- /dev/null +++ b/test/module/webpage/local-urls-enabled.js @@ -0,0 +1,19 @@ +// phantomjs: --local-url-access=yes + +var assert = require("../../assert"); +var p = require("webpage").create(); + +var url = "file:///nonexistent"; + +var rsErrorCalled = false; +p.onResourceError = function (error) { + rsErrorCalled = true; + assert.strictEqual(error.url, url); + assert.strictEqual(error.errorCode, 203); + assert.strictEqual(/^Error opening\b.*?\bnonexistent:/.test(error.errorString), + true); +}; + +p.open(url, function () { + assert(rsErrorCalled); +}); diff --git a/test/run-tests.py b/test/run-tests.py index 76e73196..60080d62 100755 --- a/test/run-tests.py +++ b/test/run-tests.py @@ -6,6 +6,7 @@ import imp import optparse import os import posixpath +import shlex import SimpleHTTPServer import SocketServer import socket @@ -216,10 +217,12 @@ def init(): print 'Checking PhantomJS version %s' % version -def run_phantomjs(script, args=[]): +def run_phantomjs(script, script_args=[], pjs_args=[]): output = [] - command = [phantomjs_exe, script] - command.extend(args) + command = [phantomjs_exe] + command.extend(pjs_args) + command.append(script) + command.extend(script_args) process = subprocess.Popen(command, stderr=subprocess.STDOUT, stdout=subprocess.PIPE) def runner(): @@ -244,23 +247,34 @@ def run_phantomjs(script, args=[]): def run_test(script, name): - args = [] + script_args = [] + pjs_args = [] if options.verbose: - args.append('--verbose') + script_args.append('--verbose') - result = 0 - if not os.path.isfile(script): - print 'Could not locate %s' % name - result = 1 - else: - print '%s:' % name - returncode, output = run_phantomjs(script, args) - if returncode != 0: - if not options.verbose: - print '%s' % output - result = 1 + try: + with open(script, "rt") as s: + p_prefix = "// phantomjs: " + s_prefix = "// script: " + for line in s: + if line.startswith(p_prefix): + pjs_args.extend(shlex.split(line[len(p_prefix):])) + if line.startswith(s_prefix): + script_args.extend(shlex.split(line[len(s_prefix):])) + if not line.startswith("//"): + break + except OSError as e: + print '%s: %s: %s' % (name, e.filename, e.strerror) + return 1 - return result + print '%s:' % name + returncode, output = run_phantomjs(script, script_args, pjs_args) + if returncode != 0: + if not options.verbose: + print '%s' % output + return 1 + + return 0 def run_tests():