Merge branch 'abbradar-pac'

This commit is contained in:
Florian Bruhin 2016-12-22 09:13:04 +01:00
commit c1c184645d
13 changed files with 804 additions and 16 deletions

View File

@ -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
~~~~~~~

View File

@ -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

View File

@ -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(

View File

@ -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)

View File

@ -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:

View File

@ -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):

View File

@ -0,0 +1,2 @@
# Upstream Mozilla's code
pac_utils.js

View File

@ -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));
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -183,4 +183,5 @@ commands =
[testenv:eslint]
deps =
whitelist_externals = eslint
commands = eslint --color qutebrowser/javascript
changedir = {toxinidir}/qutebrowser/javascript
commands = eslint --color .