summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Bruhin <git@the-compiler.org>2016-12-22 09:13:04 +0100
committerFlorian Bruhin <git@the-compiler.org>2016-12-22 09:13:04 +0100
commitc1c184645d2ca78659a2efe69e74c4cba60cdac0 (patch)
tree18c2474600d1c107722b0894fe63eb4a21d29eac
parent40c397ebaffd66a53d6dcf69923900228de85388 (diff)
parenta3d0ea7e01b854d25ee4efd475a9c0085bd723ed (diff)
downloadqutebrowser-c1c184645d2ca78659a2efe69e74c4cba60cdac0.tar.gz
qutebrowser-c1c184645d2ca78659a2efe69e74c4cba60cdac0.zip
Merge branch 'abbradar-pac'
-rw-r--r--CHANGELOG.asciidoc3
-rw-r--r--README.asciidoc1
-rw-r--r--qutebrowser/browser/webkit/network/networkmanager.py8
-rw-r--r--qutebrowser/browser/webkit/network/pac.py305
-rw-r--r--qutebrowser/browser/webkit/network/proxy.py20
-rw-r--r--qutebrowser/config/configtypes.py41
-rw-r--r--qutebrowser/javascript/.eslintignore2
-rw-r--r--qutebrowser/javascript/pac_utils.js257
-rw-r--r--qutebrowser/utils/log.py3
-rw-r--r--scripts/dev/ci/travis_install.sh2
-rw-r--r--tests/unit/browser/webkit/network/test_pac.py173
-rw-r--r--tests/unit/config/test_configtypes.py2
-rw-r--r--tox.ini3
13 files changed, 804 insertions, 16 deletions
diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 054eb3bd1..3b68428e8 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -20,6 +20,8 @@ v0.9.0 (unreleased)
Added
~~~~~
+- *New dependency:* qutebrowser now depends on the Qt Quick module, which is
+ packaged separately in some distributions.
- New `:rl-backward-kill-word` command which does what `:rl-unix-word-rubout`
did before v0.8.0.
- New `:rl-unix-filename-rubout` command which is similar to readline's
@@ -54,6 +56,7 @@ Added
`user-stylesheet` setting.
- New `general -> default-open-dispatcher` setting to configure what to open
downloads with (instead of e.g. `xdg-open` on Linux).
+- Support for PAC (proxy autoconfig) with QtWebKit
Changed
~~~~~~~
diff --git a/README.asciidoc b/README.asciidoc
index de96063ab..ef255fb47 100644
--- a/README.asciidoc
+++ b/README.asciidoc
@@ -196,6 +196,7 @@ Contributors, sorted by the number of commits in descending order:
* Tomasz Kramkowski
* Samuel Walladge
* Peter Rice
+* Nikolay Amiantov
* Ismail S
* Halfwit
* Fritz Reichwald
diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py
index 477a9958a..5e26893f5 100644
--- a/qutebrowser/browser/webkit/network/networkmanager.py
+++ b/qutebrowser/browser/webkit/network/networkmanager.py
@@ -396,6 +396,14 @@ class NetworkManager(QNetworkAccessManager):
Return:
A QNetworkReply.
"""
+ proxy_factory = objreg.get('proxy-factory', None)
+ if proxy_factory is not None:
+ proxy_error = proxy_factory.get_error()
+ if proxy_error is not None:
+ return networkreply.ErrorNetworkReply(
+ req, proxy_error, QNetworkReply.UnknownProxyError,
+ self)
+
scheme = req.url().scheme()
if scheme in self._scheme_handlers:
result = self._scheme_handlers[scheme].createRequest(
diff --git a/qutebrowser/browser/webkit/network/pac.py b/qutebrowser/browser/webkit/network/pac.py
new file mode 100644
index 000000000..dfa4d42d6
--- /dev/null
+++ b/qutebrowser/browser/webkit/network/pac.py
@@ -0,0 +1,305 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# This file is part of qutebrowser.
+#
+# qutebrowser is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# qutebrowser is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
+
+"""Evaluation of PAC scripts."""
+
+import sys
+import functools
+
+from PyQt5.QtCore import (QObject, pyqtSignal, pyqtSlot)
+from PyQt5.QtNetwork import (QNetworkProxy, QNetworkRequest, QHostInfo,
+ QNetworkReply, QNetworkAccessManager,
+ QHostAddress)
+from PyQt5.QtQml import QJSEngine, QJSValue
+
+from qutebrowser.utils import log, utils, qtutils
+
+
+class ParseProxyError(Exception):
+
+ """Error while parsing PAC result string."""
+
+ pass
+
+
+class EvalProxyError(Exception):
+
+ """Error while evaluating PAC script."""
+
+ pass
+
+
+def _js_slot(*args):
+ """Wrap a methods as a JavaScript function.
+
+ Register a PACContext method as a JavaScript function, and catch
+ exceptions returning them as JavaScript Error objects.
+
+ Args:
+ args: Types of method arguments.
+
+ Return: Wrapped method.
+ """
+ def _decorator(method):
+ @functools.wraps(method)
+ def new_method(self, *args, **kwargs):
+ try:
+ return method(self, *args, **kwargs)
+ except:
+ e = str(sys.exc_info()[0])
+ log.network.exception("PAC evaluation error")
+ # pylint: disable=protected-access
+ return self._error_con.callAsConstructor([e])
+ # pylint: enable=protected-access
+ return pyqtSlot(*args, result=QJSValue)(new_method)
+ return _decorator
+
+
+class _PACContext(QObject):
+
+ """Implementation of PAC API functions that require native calls.
+
+ See https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Necko/Proxy_Auto-Configuration_(PAC)_file
+ """
+
+ JS_DEFINITIONS = """
+ function dnsResolve(host) {
+ return PAC.dnsResolve(host);
+ }
+
+ function myIpAddress() {
+ return PAC.myIpAddress();
+ }
+ """
+
+ def __init__(self, engine):
+ """Create a new PAC API implementation instance.
+
+ Args:
+ engine: QJSEngine which is used for running PAC.
+ """
+ super().__init__(parent=engine)
+ self._engine = engine
+ self._error_con = engine.globalObject().property("Error")
+
+ @_js_slot(str)
+ def dnsResolve(self, host):
+ """Resolve a DNS hostname.
+
+ Resolves the given DNS hostname into an IP address, and returns it
+ in the dot-separated format as a string.
+
+ Args:
+ host: hostname to resolve.
+ """
+ ips = QHostInfo.fromName(host)
+ if ips.error() != QHostInfo.NoError or not ips.addresses():
+ err_f = "Failed to resolve host during PAC evaluation: {}"
+ log.network.info(err_f.format(host))
+ return QJSValue(QJSValue.NullValue)
+ else:
+ return ips.addresses()[0].toString()
+
+ @_js_slot()
+ def myIpAddress(self):
+ """Get host IP address.
+
+ Return the server IP address of the current machine, as a string in
+ the dot-separated integer format.
+ """
+ return QHostAddress(QHostAddress.LocalHost).toString()
+
+
+class PACResolver(object):
+
+ """Evaluate PAC script files and resolve proxies."""
+
+ @staticmethod
+ def _parse_proxy_host(host_str):
+ host, _colon, port_str = host_str.partition(':')
+ try:
+ port = int(port_str)
+ except ValueError:
+ raise ParseProxyError("Invalid port number")
+ return (host, port)
+
+ @staticmethod
+ def _parse_proxy_entry(proxy_str):
+ """Parse one proxy string entry, as described in PAC specification."""
+ config = [c.strip() for c in proxy_str.split(' ') if c]
+ if not config:
+ raise ParseProxyError("Empty proxy entry")
+ elif config[0] == "DIRECT":
+ if len(config) != 1:
+ raise ParseProxyError("Invalid number of parameters for " +
+ "DIRECT")
+ return QNetworkProxy(QNetworkProxy.NoProxy)
+ elif config[0] == "PROXY":
+ if len(config) != 2:
+ raise ParseProxyError("Invalid number of parameters for PROXY")
+ host, port = PACResolver._parse_proxy_host(config[1])
+ return QNetworkProxy(QNetworkProxy.HttpProxy, host, port)
+ elif config[0] == "SOCKS":
+ if len(config) != 2:
+ raise ParseProxyError("Invalid number of parameters for SOCKS")
+ host, port = PACResolver._parse_proxy_host(config[1])
+ return QNetworkProxy(QNetworkProxy.Socks5Proxy, host, port)
+ else:
+ err = "Unknown proxy type: {}"
+ raise ParseProxyError(err.format(config[0]))
+
+ @staticmethod
+ def _parse_proxy_string(proxy_str):
+ proxies = proxy_str.split(';')
+ return [PACResolver._parse_proxy_entry(x) for x in proxies]
+
+ def _evaluate(self, js_code, js_file):
+ ret = self._engine.evaluate(js_code, js_file)
+ if ret.isError():
+ err = "JavaScript error while evaluating PAC file: {}"
+ raise EvalProxyError(err.format(ret.toString()))
+
+ def __init__(self, pac_str):
+ """Create a PAC resolver.
+
+ Args:
+ pac_str: JavaScript code containing PAC resolver.
+ """
+ self._engine = QJSEngine()
+
+ self._ctx = _PACContext(self._engine)
+ self._engine.globalObject().setProperty(
+ "PAC", self._engine.newQObject(self._ctx))
+ self._evaluate(_PACContext.JS_DEFINITIONS, "pac_js_definitions")
+ self._evaluate(utils.read_file("javascript/pac_utils.js"), "pac_utils")
+ proxy_config = self._engine.newObject()
+ proxy_config.setProperty("bindings", self._engine.newObject())
+ self._engine.globalObject().setProperty("ProxyConfig", proxy_config)
+
+ self._evaluate(pac_str, "pac")
+ global_js_object = self._engine.globalObject()
+ self._resolver = global_js_object.property("FindProxyForURL")
+ if not self._resolver.isCallable():
+ err = "Cannot resolve FindProxyForURL function, got '{}' instead"
+ raise EvalProxyError(err.format(self._resolver.toString()))
+
+ def resolve(self, query):
+ """Resolve a proxy via PAC.
+
+ Args:
+ query: QNetworkProxyQuery.
+
+ Return:
+ A list of QNetworkProxy objects in order of preference.
+ """
+ result = self._resolver.call([query.url().toString(),
+ query.peerHostName()])
+ result_str = result.toString()
+ if not result.isString():
+ err = "Got strange value from FindProxyForURL: '{}'"
+ raise EvalProxyError(err.format(result_str))
+ return self._parse_proxy_string(result_str)
+
+
+class PACFetcher(QObject):
+
+ """Asynchronous fetcher of PAC files."""
+
+ finished = pyqtSignal()
+
+ def __init__(self, url, parent=None):
+ """Resolve a PAC proxy from URL.
+
+ Args:
+ url: QUrl of a PAC proxy.
+ """
+ super().__init__(parent)
+
+ pac_prefix = "pac+"
+
+ assert url.scheme().startswith(pac_prefix)
+ url.setScheme(url.scheme()[len(pac_prefix):])
+
+ self._manager = QNetworkAccessManager()
+ self._manager.setProxy(QNetworkProxy(QNetworkProxy.NoProxy))
+ self._reply = self._manager.get(QNetworkRequest(url))
+ self._reply.finished.connect(self._finish)
+ self._pac = None
+ self._error_message = None
+
+ @pyqtSlot()
+ def _finish(self):
+ if self._reply.error() != QNetworkReply.NoError:
+ error = "Can't fetch PAC file from URL, error code {}: {}"
+ self._error_message = error.format(
+ self._reply.error(), self._reply.errorString())
+ log.network.error(self._error_message)
+ else:
+ try:
+ pacscript = bytes(self._reply.readAll()).decode("utf-8")
+ except UnicodeError as e:
+ error = "Invalid encoding of a PAC file: {}"
+ self._error_message = error.format(e)
+ log.network.exception(self._error_message)
+ try:
+ self._pac = PACResolver(pacscript)
+ log.network.debug("Successfully evaluated PAC file.")
+ except EvalProxyError as e:
+ error = "Error in PAC evaluation: {}"
+ self._error_message = error.format(e)
+ log.network.exception(self._error_message)
+ self._manager = None
+ self._reply = None
+ self.finished.emit()
+
+ def _wait(self):
+ """Wait until a reply from the remote server is received."""
+ if self._manager is not None:
+ loop = qtutils.EventLoop()
+ self.finished.connect(loop.quit)
+ loop.exec_()
+
+ def fetch_error(self):
+ """Check if PAC script is successfully fetched.
+
+ Return None iff PAC script is downloaded and evaluated successfully,
+ error string otherwise.
+ """
+ self._wait()
+ return self._error_message
+
+ def resolve(self, query):
+ """Resolve a query via PAC.
+
+ Args: QNetworkProxyQuery.
+
+ Return a list of QNetworkProxy objects in order of preference.
+ """
+ self._wait()
+ try:
+ return self._pac.resolve(query)
+ except (EvalProxyError, ParseProxyError) as e:
+ log.network.exception("Error in PAC resolution: {}.".format(e))
+ # .invalid is guaranteed to be inaccessible in RFC 6761.
+ # Port 9 is for DISCARD protocol -- DISCARD servers act like
+ # /dev/null.
+ # Later NetworkManager.createRequest will detect this and display
+ # an error message.
+ error_host = "pac-resolve-error.qutebrowser.invalid"
+ return QNetworkProxy(QNetworkProxy.HttpProxy, error_host, 9)
diff --git a/qutebrowser/browser/webkit/network/proxy.py b/qutebrowser/browser/webkit/network/proxy.py
index 2469bd0c7..db52482d2 100644
--- a/qutebrowser/browser/webkit/network/proxy.py
+++ b/qutebrowser/browser/webkit/network/proxy.py
@@ -23,17 +23,33 @@
from PyQt5.QtNetwork import QNetworkProxy, QNetworkProxyFactory
from qutebrowser.config import config, configtypes
+from qutebrowser.utils import objreg
+from qutebrowser.browser.webkit.network import pac
def init():
"""Set the application wide proxy factory."""
- QNetworkProxyFactory.setApplicationProxyFactory(ProxyFactory())
+ proxy_factory = ProxyFactory()
+ objreg.register('proxy-factory', proxy_factory)
+ QNetworkProxyFactory.setApplicationProxyFactory(proxy_factory)
class ProxyFactory(QNetworkProxyFactory):
"""Factory for proxies to be used by qutebrowser."""
+ def get_error(self):
+ """Check if proxy can't be resolved.
+
+ Return:
+ None if proxy is correct, otherwise an error message.
+ """
+ proxy = config.get('network', 'proxy')
+ if isinstance(proxy, pac.PACFetcher):
+ return proxy.fetch_error()
+ else:
+ return None
+
def queryProxy(self, query):
"""Get the QNetworkProxies for a query.
@@ -46,6 +62,8 @@ class ProxyFactory(QNetworkProxyFactory):
proxy = config.get('network', 'proxy')
if proxy is configtypes.SYSTEM_PROXY:
proxies = QNetworkProxyFactory.systemProxyForQuery(query)
+ elif isinstance(proxy, pac.PACFetcher):
+ proxies = proxy.resolve(query)
else:
proxies = [proxy]
for p in proxies:
diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py
index 457781e77..7c5f9721c 100644
--- a/qutebrowser/config/configtypes.py
+++ b/qutebrowser/config/configtypes.py
@@ -28,6 +28,7 @@ import itertools
import collections
import warnings
import datetime
+import functools
from PyQt5.QtCore import QUrl, Qt
from PyQt5.QtGui import QColor, QFont
@@ -37,6 +38,7 @@ from PyQt5.QtWidgets import QTabWidget, QTabBar
from qutebrowser.commands import cmdutils
from qutebrowser.config import configexc
from qutebrowser.utils import standarddir, utils
+from qutebrowser.browser.webkit.network import pac
SYSTEM_PROXY = object() # Return value for Proxy type
@@ -1014,14 +1016,36 @@ class ShellCommand(BaseType):
return shlex.split(value)
+def proxy_from_url(typ, url):
+ """Create a QNetworkProxy from QUrl and a proxy type.
+
+ Args:
+ typ: QNetworkProxy::ProxyType.
+ url: URL of a proxy (possibly with credentials).
+
+ Return:
+ New QNetworkProxy.
+ """
+ proxy = QNetworkProxy(typ, url.host())
+ if url.port() != -1:
+ proxy.setPort(url.port())
+ if url.userName():
+ proxy.setUser(url.userName())
+ if url.password():
+ proxy.setPassword(url.password())
+ return proxy
+
+
class Proxy(BaseType):
"""A proxy URL or special value."""
PROXY_TYPES = {
- 'http': QNetworkProxy.HttpProxy,
- 'socks': QNetworkProxy.Socks5Proxy,
- 'socks5': QNetworkProxy.Socks5Proxy,
+ 'http': functools.partial(proxy_from_url, QNetworkProxy.HttpProxy),
+ 'pac+http': pac.PACFetcher,
+ 'pac+https': pac.PACFetcher,
+ 'socks': functools.partial(proxy_from_url, QNetworkProxy.Socks5Proxy),
+ 'socks5': functools.partial(proxy_from_url, QNetworkProxy.Socks5Proxy),
}
def __init__(self, none_ok=False):
@@ -1053,6 +1077,7 @@ class Proxy(BaseType):
out.append(('socks://', 'SOCKS proxy URL'))
out.append(('socks://localhost:9050/', 'Tor via SOCKS'))
out.append(('http://localhost:8080/', 'Local HTTP proxy'))
+ out.append(('pac+https://example.com/proxy.pac', 'Proxy autoconfiguration file URL'))
return out
def transform(self, value):
@@ -1063,15 +1088,7 @@ class Proxy(BaseType):
elif value == 'none':
return QNetworkProxy(QNetworkProxy.NoProxy)
url = QUrl(value)
- typ = self.PROXY_TYPES[url.scheme()]
- proxy = QNetworkProxy(typ, url.host())
- if url.port() != -1:
- proxy.setPort(url.port())
- if url.userName():
- proxy.setUser(url.userName())
- if url.password():
- proxy.setPassword(url.password())
- return proxy
+ return self.PROXY_TYPES[url.scheme()](url)
class SearchEngineName(BaseType):
diff --git a/qutebrowser/javascript/.eslintignore b/qutebrowser/javascript/.eslintignore
new file mode 100644
index 000000000..ca4d3c667
--- /dev/null
+++ b/qutebrowser/javascript/.eslintignore
@@ -0,0 +1,2 @@
+# Upstream Mozilla's code
+pac_utils.js
diff --git a/qutebrowser/javascript/pac_utils.js b/qutebrowser/javascript/pac_utils.js
new file mode 100644
index 000000000..a6102df9f
--- /dev/null
+++ b/qutebrowser/javascript/pac_utils.js
@@ -0,0 +1,257 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1998
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Akhil Arora <akhil.arora@sun.com>
+ * Tomi Leppikangas <Tomi.Leppikangas@oulu.fi>
+ * Darin Fisher <darin@meer.net>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ Script for Proxy Auto Config in the new world order.
+ - Gagan Saksena 04/24/00
+*/
+
+function dnsDomainIs(host, domain) {
+ return (host.length >= domain.length &&
+ host.substring(host.length - domain.length) == domain);
+}
+
+function dnsDomainLevels(host) {
+ return host.split('.').length-1;
+}
+
+function convert_addr(ipchars) {
+ var bytes = ipchars.split('.');
+ var result = ((bytes[0] & 0xff) << 24) |
+ ((bytes[1] & 0xff) << 16) |
+ ((bytes[2] & 0xff) << 8) |
+ (bytes[3] & 0xff);
+ return result;
+}
+
+function isInNet(ipaddr, pattern, maskstr) {
+ var test = /^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/
+ .exec(ipaddr);
+ if (test == null) {
+ ipaddr = dnsResolve(ipaddr);
+ if (ipaddr == null)
+ return false;
+ } else if (test[1] > 255 || test[2] > 255 ||
+ test[3] > 255 || test[4] > 255) {
+ return false; // not an IP address
+ }
+ var host = convert_addr(ipaddr);
+ var pat = convert_addr(pattern);
+ var mask = convert_addr(maskstr);
+ return ((host & mask) == (pat & mask));
+}
+
+function isPlainHostName(host) {
+ return (host.search('\\\\.') == -1);
+}
+
+function isResolvable(host) {
+ var ip = dnsResolve(host);
+ return (ip != null);
+}
+
+function localHostOrDomainIs(host, hostdom) {
+ return (host == hostdom) ||
+ (hostdom.lastIndexOf(host + '.', 0) == 0);
+}
+
+function shExpMatch(url, pattern) {
+ pattern = pattern.replace(/\\./g, '\\\\.');
+ pattern = pattern.replace(/\\*/g, '.*');
+ pattern = pattern.replace(/\\?/g, '.');
+ var newRe = new RegExp('^'+pattern+'$');
+ return newRe.test(url);
+}
+
+var wdays = {SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6};
+
+var months = {JAN: 0, FEB: 1, MAR: 2, APR: 3, MAY: 4, JUN: 5, JUL: 6,
+ AUG: 7, SEP: 8, OCT: 9, NOV: 10, DEC: 11};
+
+function weekdayRange() {
+ function getDay(weekday) {
+ if (weekday in wdays) {
+ return wdays[weekday];
+ }
+ return -1;
+ }
+ var date = new Date();
+ var argc = arguments.length;
+ var wday;
+ if (argc < 1)
+ return false;
+ if (arguments[argc - 1] == 'GMT') {
+ argc--;
+ wday = date.getUTCDay();
+ } else {
+ wday = date.getDay();
+ }
+ var wd1 = getDay(arguments[0]);
+ var wd2 = (argc == 2) ? getDay(arguments[1]) : wd1;
+ return (wd1 == -1 || wd2 == -1) ? false
+ : (wd1 <= wday && wday <= wd2);
+}
+
+function dateRange() {
+ function getMonth(name) {
+ if (name in months) {
+ return months[name];
+ }
+ return -1;
+ }
+ var date = new Date();
+ var argc = arguments.length;
+ if (argc < 1) {
+ return false;
+ }
+ var isGMT = (arguments[argc - 1] == 'GMT');
+
+ if (isGMT) {
+ argc--;
+ }
+ // function will work even without explict handling of this case
+ if (argc == 1) {
+ var tmp = parseInt(arguments[0]);
+ if (isNaN(tmp)) {
+ return ((isGMT ? date.getUTCMonth() : date.getMonth()) ==
+ getMonth(arguments[0]));
+ } else if (tmp < 32) {
+ return ((isGMT ? date.getUTCDate() : date.getDate()) == tmp);
+ } else {
+ return ((isGMT ? date.getUTCFullYear() : date.getFullYear()) ==
+ tmp);
+ }
+ }
+ var year = date.getFullYear();
+ var date1, date2;
+ date1 = new Date(year, 0, 1, 0, 0, 0);
+ date2 = new Date(year, 11, 31, 23, 59, 59);
+ var adjustMonth = false;
+ for (var i = 0; i < (argc >> 1); i++) {
+ var tmp = parseInt(arguments[i]);
+ if (isNaN(tmp)) {
+ var mon = getMonth(arguments[i]);
+ date1.setMonth(mon);
+ } else if (tmp < 32) {
+ adjustMonth = (argc <= 2);
+ date1.setDate(tmp);
+ } else {
+ date1.setFullYear(tmp);
+ }
+ }
+ for (var i = (argc >> 1); i < argc; i++) {
+ var tmp = parseInt(arguments[i]);
+ if (isNaN(tmp)) {
+ var mon = getMonth(arguments[i]);
+ date2.setMonth(mon);
+ } else if (tmp < 32) {
+ date2.setDate(tmp);
+ } else {
+ date2.setFullYear(tmp);
+ }
+ }
+ if (adjustMonth) {
+ date1.setMonth(date.getMonth());
+ date2.setMonth(date.getMonth());
+ }
+ if (isGMT) {
+ var tmp = date;
+ tmp.setFullYear(date.getUTCFullYear());
+ tmp.setMonth(date.getUTCMonth());
+ tmp.setDate(date.getUTCDate());
+ tmp.setHours(date.getUTCHours());
+ tmp.setMinutes(date.getUTCMinutes());
+ tmp.setSeconds(date.getUTCSeconds());
+ date = tmp;
+ }
+ return ((date1 <= date) && (date <= date2));
+}
+
+function timeRange() {
+ var argc = arguments.length;
+ var date = new Date();
+ var isGMT= false;
+
+ if (argc < 1) {
+ return false;
+ }
+ if (arguments[argc - 1] == 'GMT') {
+ isGMT = true;
+ argc--;
+ }
+
+ var hour = isGMT ? date.getUTCHours() : date.getHours();
+ var date1, date2;
+ date1 = new Date();
+ date2 = new Date();
+
+ if (argc == 1) {
+ return (hour == arguments[0]);
+ } else if (argc == 2) {
+ return ((arguments[0] <= hour) && (hour <= arguments[1]));
+ } else {
+ switch (argc) {
+ case 6:
+ date1.setSeconds(arguments[2]);
+ date2.setSeconds(arguments[5]);
+ case 4:
+ var middle = argc >> 1;
+ date1.setHours(arguments[0]);
+ date1.setMinutes(arguments[1]);
+ date2.setHours(arguments[middle]);
+ date2.setMinutes(arguments[middle + 1]);
+ if (middle == 2) {
+ date2.setSeconds(59);
+ }
+ break;
+ default:
+ throw 'timeRange: bad number of arguments'
+ }
+ }
+
+ if (isGMT) {
+ date.setFullYear(date.getUTCFullYear());
+ date.setMonth(date.getUTCMonth());
+ date.setDate(date.getUTCDate());
+ date.setHours(date.getUTCHours());
+ date.setMinutes(date.getUTCMinutes());
+ date.setSeconds(date.getUTCSeconds());
+ }
+ return ((date1 <= date) && (date <= date2));
+}
diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py
index 05163577c..07ec78741 100644
--- a/qutebrowser/utils/log.py
+++ b/qutebrowser/utils/log.py
@@ -94,7 +94,7 @@ LOGGER_NAMES = [
'commands', 'signals', 'downloads',
'js', 'qt', 'rfc6266', 'ipc', 'shlexer',
'save', 'message', 'config', 'sessions',
- 'webelem', 'prompt'
+ 'webelem', 'prompt', 'network'
]
@@ -140,6 +140,7 @@ config = logging.getLogger('config')
sessions = logging.getLogger('sessions')
webelem = logging.getLogger('webelem')
prompt = logging.getLogger('prompt')
+network = logging.getLogger('network')
ram_handler = None
diff --git a/scripts/dev/ci/travis_install.sh b/scripts/dev/ci/travis_install.sh
index f5d75eed5..5317e2135 100644
--- a/scripts/dev/ci/travis_install.sh
+++ b/scripts/dev/ci/travis_install.sh
@@ -102,7 +102,7 @@ elif [[ $TRAVIS_OS_NAME == osx ]]; then
exit 0
fi
-pyqt_pkgs="python3-pyqt5 python3-pyqt5.qtwebkit"
+pyqt_pkgs="python3-pyqt5 python3-pyqt5.qtquick python3-pyqt5.qtwebkit"
pip_install pip
pip_install -r misc/requirements/requirements-tox.txt
diff --git a/tests/unit/browser/webkit/network/test_pac.py b/tests/unit/browser/webkit/network/test_pac.py
new file mode 100644
index 000000000..b99675b6a
--- /dev/null
+++ b/tests/unit/browser/webkit/network/test_pac.py
@@ -0,0 +1,173 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# This file is part of qutebrowser.
+#
+# qutebrowser is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# qutebrowser is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
+
+import http.server
+import threading
+import logging
+import sys
+import pytest
+
+from PyQt5.QtCore import QUrl, QT_VERSION_STR
+from PyQt5.QtNetwork import (QNetworkProxy, QNetworkProxyQuery, QHostInfo,
+ QHostAddress)
+
+from qutebrowser.browser.webkit.network import pac
+
+
+pytestmark = pytest.mark.usefixtures('qapp')
+
+
+def _pac_common_test(test_str):
+ fun_str_f = """
+ function FindProxyForURL(domain, host) {{
+ {}
+ return "DIRECT; PROXY 127.0.0.1:8080; SOCKS 192.168.1.1:4444";
+ }}
+ """
+
+ fun_str = fun_str_f.format(test_str)
+ res = pac.PACResolver(fun_str)
+ proxies = res.resolve(QNetworkProxyQuery(QUrl("https://example.com/test")))
+ assert len(proxies) == 3
+ assert proxies[0].type() == QNetworkProxy.NoProxy
+ assert proxies[1].type() == QNetworkProxy.HttpProxy
+ assert proxies[1].hostName() == "127.0.0.1"
+ assert proxies[1].port() == 8080
+ assert proxies[2].type() == QNetworkProxy.Socks5Proxy
+ assert proxies[2].hostName() == "192.168.1.1"
+ assert proxies[2].port() == 4444
+
+
+def _pac_equality_test(call, expected):
+ test_str_f = """
+ var res = ({0});
+ var expected = ({1});
+ if(res !== expected) {{
+ throw new Error("failed test {0}: got '" + res + "', expected '" + expected + "'");
+ }}
+ """
+ _pac_common_test(test_str_f.format(call, expected))
+
+
+def _pac_except_test(caplog, call):
+ test_str_f = """
+ var thrown = false;
+ try {{
+ var res = ({0});
+ }} catch(e) {{
+ thrown = true;
+ }}
+ if(!thrown) {{
+ throw new Error("failed test {0}: got '" + res + "', expected exception");
+ }}
+ """
+ with caplog.at_level(logging.ERROR):
+ _pac_common_test(test_str_f.format(call))
+
+
+def _pac_noexcept_test(call):
+ test_str_f = """
+ var res = ({0});
+ """
+ _pac_common_test(test_str_f.format(call))
+
+
+# pylint: disable=line-too-long, invalid-name
+
+
+@pytest.mark.parametrize("domain, expected", [
+ ("known.domain", "'1.2.3.4'"),
+ ("bogus.domain.foobar", "null")
+])
+def test_dnsResolve(monkeypatch, domain, expected):
+ def mock_fromName(host):
+ info = QHostInfo()
+ if host == "known.domain":
+ info.setAddresses([QHostAddress("1.2.3.4")])
+ return info
+ monkeypatch.setattr(QHostInfo, 'fromName', mock_fromName)
+ _pac_equality_test("dnsResolve('{}')".format(domain), expected)
+
+
+def test_myIpAddress():
+ _pac_equality_test("isResolvable(myIpAddress())", "true")
+
+
+def test_proxyBindings():
+ _pac_equality_test("JSON.stringify(ProxyConfig.bindings)", "'{}'")
+
+
+def test_invalid_port():
+ test_str = """
+ function FindProxyForURL(domain, host) {
+ return "PROXY 127.0.0.1:FOO";
+ }
+ """
+
+ res = pac.PACResolver(test_str)
+ with pytest.raises(pac.ParseProxyError):
+ res.resolve(QNetworkProxyQuery(QUrl("https://example.com/test")))
+
+
+# See https://github.com/The-Compiler/qutebrowser/pull/1891#issuecomment-259222615
+
+try:
+ from PyQt5 import QtWebEngineWidgets
+except ImportError:
+ QtWebEngineWidgets = None
+
+
+@pytest.mark.skipif(QT_VERSION_STR.startswith('5.7') and
+ QtWebEngineWidgets is not None and
+ sys.platform == "linux",
+ reason="Segfaults when run with QtWebEngine tests on Linux")
+def test_fetch():
+ test_str = """
+ function FindProxyForURL(domain, host) {
+ return "DIRECT; PROXY 127.0.0.1:8080; SOCKS 192.168.1.1:4444";
+ }
+ """
+
+ class PACHandler(http.server.BaseHTTPRequestHandler):
+ def do_GET(self):
+ self.send_response(200)
+
+ self.send_header('Content-type', 'application/x-ns-proxy-autoconfig')
+ self.end_headers()
+
+ self.wfile.write(test_str.encode("ascii"))
+
+ ready_event = threading.Event()
+
+ def serve():
+ httpd = http.server.HTTPServer(("127.0.0.1", 8081), PACHandler)
+ ready_event.set()
+ httpd.handle_request()
+ httpd.server_close()
+
+ serve_thread = threading.Thread(target=serve, daemon=True)
+ serve_thread.start()
+ try:
+ ready_event.wait()
+ res = pac.PACFetcher(QUrl("pac+http://127.0.0.1:8081"))
+ assert res.fetch_error() is None
+ finally:
+ serve_thread.join()
+ proxies = res.resolve(QNetworkProxyQuery(QUrl("https://example.com/test")))
+ assert len(proxies) == 3
diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py
index 77cfdfdee..37e763999 100644
--- a/tests/unit/config/test_configtypes.py
+++ b/tests/unit/config/test_configtypes.py
@@ -1492,6 +1492,8 @@ class TestProxy:
'http://user:pass@example.com:2323/',
'socks://user:pass@example.com:2323/',
'socks5://user:pass@example.com:2323/',
+ 'pac+http://example.com/proxy.pac',
+ 'pac+https://example.com/proxy.pac',
])
def test_validate_valid(self, klass, val):
klass(none_ok=True).validate(val)
diff --git a/tox.ini b/tox.ini
index 6c51695dd..b52550056 100644
--- a/tox.ini
+++ b/tox.ini
@@ -183,4 +183,5 @@ commands =
[testenv:eslint]
deps =
whitelist_externals = eslint
-commands = eslint --color qutebrowser/javascript
+changedir = {toxinidir}/qutebrowser/javascript
+commands = eslint --color .