Merge branch 'abbradar-pac'
This commit is contained in:
commit
c1c184645d
@ -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
|
||||
~~~~~~~
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
305
qutebrowser/browser/webkit/network/pac.py
Normal file
305
qutebrowser/browser/webkit/network/pac.py
Normal 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)
|
@ -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:
|
||||
|
@ -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):
|
||||
|
2
qutebrowser/javascript/.eslintignore
Normal file
2
qutebrowser/javascript/.eslintignore
Normal file
@ -0,0 +1,2 @@
|
||||
# Upstream Mozilla's code
|
||||
pac_utils.js
|
257
qutebrowser/javascript/pac_utils.js
Normal file
257
qutebrowser/javascript/pac_utils.js
Normal 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));
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
173
tests/unit/browser/webkit/network/test_pac.py
Normal file
173
tests/unit/browser/webkit/network/test_pac.py
Normal 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
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user