Merge branch 'pac' of https://github.com/abbradar/qutebrowser into abbradar-pac
This commit is contained in:
commit
5a89ee96d9
@ -27,3 +27,4 @@ import sys
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
sys.exit(qutebrowser.qutebrowser.main())
|
sys.exit(qutebrowser.qutebrowser.main())
|
||||||
|
|
||||||
|
@ -396,6 +396,14 @@ class NetworkManager(QNetworkAccessManager):
|
|||||||
Return:
|
Return:
|
||||||
A QNetworkReply.
|
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()
|
scheme = req.url().scheme()
|
||||||
if scheme in self._scheme_handlers:
|
if scheme in self._scheme_handlers:
|
||||||
result = self._scheme_handlers[scheme].createRequest(
|
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 PyQt5.QtNetwork import QNetworkProxy, QNetworkProxyFactory
|
||||||
|
|
||||||
from qutebrowser.config import config, configtypes
|
from qutebrowser.config import config, configtypes
|
||||||
|
from qutebrowser.utils import objreg
|
||||||
|
from qutebrowser.browser.webkit.network import pac
|
||||||
|
|
||||||
|
|
||||||
def init():
|
def init():
|
||||||
"""Set the application wide proxy factory."""
|
"""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):
|
class ProxyFactory(QNetworkProxyFactory):
|
||||||
|
|
||||||
"""Factory for proxies to be used by qutebrowser."""
|
"""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):
|
def queryProxy(self, query):
|
||||||
"""Get the QNetworkProxies for a query.
|
"""Get the QNetworkProxies for a query.
|
||||||
|
|
||||||
@ -46,6 +62,8 @@ class ProxyFactory(QNetworkProxyFactory):
|
|||||||
proxy = config.get('network', 'proxy')
|
proxy = config.get('network', 'proxy')
|
||||||
if proxy is configtypes.SYSTEM_PROXY:
|
if proxy is configtypes.SYSTEM_PROXY:
|
||||||
proxies = QNetworkProxyFactory.systemProxyForQuery(query)
|
proxies = QNetworkProxyFactory.systemProxyForQuery(query)
|
||||||
|
elif isinstance(proxy, pac.PACFetcher):
|
||||||
|
proxies = proxy.resolve(query)
|
||||||
else:
|
else:
|
||||||
proxies = [proxy]
|
proxies = [proxy]
|
||||||
for p in proxies:
|
for p in proxies:
|
||||||
|
@ -28,6 +28,7 @@ import itertools
|
|||||||
import collections
|
import collections
|
||||||
import warnings
|
import warnings
|
||||||
import datetime
|
import datetime
|
||||||
|
import functools
|
||||||
|
|
||||||
from PyQt5.QtCore import QUrl, Qt
|
from PyQt5.QtCore import QUrl, Qt
|
||||||
from PyQt5.QtGui import QColor, QFont
|
from PyQt5.QtGui import QColor, QFont
|
||||||
@ -37,6 +38,7 @@ from PyQt5.QtWidgets import QTabWidget, QTabBar
|
|||||||
from qutebrowser.commands import cmdutils
|
from qutebrowser.commands import cmdutils
|
||||||
from qutebrowser.config import configexc
|
from qutebrowser.config import configexc
|
||||||
from qutebrowser.utils import standarddir, utils
|
from qutebrowser.utils import standarddir, utils
|
||||||
|
from qutebrowser.browser.webkit.network import pac
|
||||||
|
|
||||||
|
|
||||||
SYSTEM_PROXY = object() # Return value for Proxy type
|
SYSTEM_PROXY = object() # Return value for Proxy type
|
||||||
@ -1014,14 +1016,36 @@ class ShellCommand(BaseType):
|
|||||||
return shlex.split(value)
|
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):
|
class Proxy(BaseType):
|
||||||
|
|
||||||
"""A proxy URL or special value."""
|
"""A proxy URL or special value."""
|
||||||
|
|
||||||
PROXY_TYPES = {
|
PROXY_TYPES = {
|
||||||
'http': QNetworkProxy.HttpProxy,
|
'http': functools.partial(proxy_from_url, QNetworkProxy.HttpProxy),
|
||||||
'socks': QNetworkProxy.Socks5Proxy,
|
'pac+http': pac.PACFetcher,
|
||||||
'socks5': QNetworkProxy.Socks5Proxy,
|
'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):
|
def __init__(self, none_ok=False):
|
||||||
@ -1053,6 +1077,7 @@ class Proxy(BaseType):
|
|||||||
out.append(('socks://', 'SOCKS proxy URL'))
|
out.append(('socks://', 'SOCKS proxy URL'))
|
||||||
out.append(('socks://localhost:9050/', 'Tor via SOCKS'))
|
out.append(('socks://localhost:9050/', 'Tor via SOCKS'))
|
||||||
out.append(('http://localhost:8080/', 'Local HTTP proxy'))
|
out.append(('http://localhost:8080/', 'Local HTTP proxy'))
|
||||||
|
out.append(('pac+https://example.com/proxy.pac', 'Proxy autoconfiguration file URL'))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def transform(self, value):
|
def transform(self, value):
|
||||||
@ -1063,15 +1088,7 @@ class Proxy(BaseType):
|
|||||||
elif value == 'none':
|
elif value == 'none':
|
||||||
return QNetworkProxy(QNetworkProxy.NoProxy)
|
return QNetworkProxy(QNetworkProxy.NoProxy)
|
||||||
url = QUrl(value)
|
url = QUrl(value)
|
||||||
typ = self.PROXY_TYPES[url.scheme()]
|
return self.PROXY_TYPES[url.scheme()](url)
|
||||||
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 SearchEngineName(BaseType):
|
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',
|
'commands', 'signals', 'downloads',
|
||||||
'js', 'qt', 'rfc6266', 'ipc', 'shlexer',
|
'js', 'qt', 'rfc6266', 'ipc', 'shlexer',
|
||||||
'save', 'message', 'config', 'sessions',
|
'save', 'message', 'config', 'sessions',
|
||||||
'webelem', 'prompt'
|
'webelem', 'prompt', 'network'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -140,6 +140,7 @@ config = logging.getLogger('config')
|
|||||||
sessions = logging.getLogger('sessions')
|
sessions = logging.getLogger('sessions')
|
||||||
webelem = logging.getLogger('webelem')
|
webelem = logging.getLogger('webelem')
|
||||||
prompt = logging.getLogger('prompt')
|
prompt = logging.getLogger('prompt')
|
||||||
|
network = logging.getLogger('network')
|
||||||
|
|
||||||
|
|
||||||
ram_handler = None
|
ram_handler = None
|
||||||
|
@ -102,7 +102,7 @@ elif [[ $TRAVIS_OS_NAME == osx ]]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
pyqt_pkgs="python3-pyqt5 python3-pyqt5.qtwebkit"
|
pyqt_pkgs="python3-pyqt5 python3-pyqt5.qtquick python3-pyqt5.qtwebkit"
|
||||||
|
|
||||||
pip_install pip
|
pip_install pip
|
||||||
pip_install -r misc/requirements/requirements-tox.txt
|
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 == "5.7.0" 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/',
|
'http://user:pass@example.com:2323/',
|
||||||
'socks://user:pass@example.com:2323/',
|
'socks://user:pass@example.com:2323/',
|
||||||
'socks5://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):
|
def test_validate_valid(self, klass, val):
|
||||||
klass(none_ok=True).validate(val)
|
klass(none_ok=True).validate(val)
|
||||||
|
Loading…
Reference in New Issue
Block a user