From b220b5438f1f49a26ff742c8d313f062e7e68b1d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 22 Dec 2016 13:51:27 +0100 Subject: [PATCH] Add urlutils.proxy_for_url --- qutebrowser/config/configtypes.py | 55 +++++++-------------------- qutebrowser/utils/urlutils.py | 47 ++++++++++++++++++++++- tests/unit/config/test_configtypes.py | 32 +++------------- tests/unit/utils/test_urlutils.py | 42 ++++++++++++++++++++ 4 files changed, 106 insertions(+), 70 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 7c5f9721c..b0399fcac 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -28,17 +28,14 @@ import itertools import collections import warnings import datetime -import functools from PyQt5.QtCore import QUrl, Qt from PyQt5.QtGui import QColor, QFont -from PyQt5.QtNetwork import QNetworkProxy 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 @@ -1016,38 +1013,10 @@ 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': 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): super().__init__(none_ok) self.valid_values = ValidValues( @@ -1055,19 +1024,18 @@ class Proxy(BaseType): ('none', "Don't use any proxy")) def validate(self, value): + from qutebrowser.utils import urlutils self._basic_validation(value) if not value: return elif value in self.valid_values: return url = QUrl(value) - if not url.isValid(): - raise configexc.ValidationError( - value, "invalid url, {}".format(url.errorString())) - elif url.scheme() not in self.PROXY_TYPES: - raise configexc.ValidationError(value, "must be a proxy URL " - "(http://... or socks://...) or " - "system/none!") + + try: + self.transform(value) + except (urlutils.InvalidUrlError, urlutils.InvalidProxyTypeError) as e: + raise configexc.ValidationError(value, e) def complete(self): out = [] @@ -1081,14 +1049,17 @@ class Proxy(BaseType): return out def transform(self, value): + from qutebrowser.utils import urlutils if not value: return None elif value == 'system': return SYSTEM_PROXY - elif value == 'none': - return QNetworkProxy(QNetworkProxy.NoProxy) - url = QUrl(value) - return self.PROXY_TYPES[url.scheme()](url) + + if value == 'none': + url = QUrl('direct://') + else: + url = QUrl(value) + return urlutils.proxy_from_url(url) class SearchEngineName(BaseType): diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 6a24ceb82..ae5733bc6 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -27,11 +27,12 @@ import posixpath import urllib.parse from PyQt5.QtCore import QUrl -from PyQt5.QtNetwork import QHostInfo, QHostAddress +from PyQt5.QtNetwork import QHostInfo, QHostAddress, QNetworkProxy from qutebrowser.config import config, configexc from qutebrowser.utils import log, qtutils, message, utils from qutebrowser.commands import cmdexc +from qutebrowser.browser.webkit.network import pac # FIXME: we probably could raise some exceptions on invalid URLs @@ -589,3 +590,47 @@ def data_url(mimetype, data): url = QUrl('data:{};base64,{}'.format(mimetype, b64)) qtutils.ensure_valid(url) return url + + +class InvalidProxyTypeError(Exception): + + """Error raised when proxy_from_url gets an unknown proxy type.""" + + def __init__(self, typ): + super().__init__("Invalid proxy type {}!".format(typ)) + + +def proxy_from_url(url): + """Create a QNetworkProxy from QUrl and a proxy type. + + Args: + url: URL of a proxy (possibly with credentials). + + Return: + New QNetworkProxy. + """ + if not url.isValid(): + raise InvalidUrlError(url) + + scheme = url.scheme() + if scheme in ['pac+http', 'pac+https']: + return pac.PACFetcher + + types = { + 'http': QNetworkProxy.HttpProxy, + 'socks': QNetworkProxy.Socks5Proxy, + 'socks5': QNetworkProxy.Socks5Proxy, + 'direct': QNetworkProxy.NoProxy, + } + if scheme not in types: + raise InvalidProxyTypeError(scheme) + + proxy = QNetworkProxy(types[scheme], 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 diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index 37e763999..f2e18c459 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -59,16 +59,6 @@ class Font(QFont): return f -class NetworkProxy(QNetworkProxy): - - """A QNetworkProxy with a nicer repr().""" - - def __repr__(self): - return utils.get_repr(self, type=self.type(), hostName=self.hostName(), - port=self.port(), user=self.user(), - password=self.password()) - - class RegexEq: """A class to compare regex objects.""" @@ -1490,10 +1480,7 @@ class TestProxy: 'system', 'none', '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) @@ -1519,27 +1506,18 @@ class TestProxy: @pytest.mark.parametrize('val, expected', [ ('', None), ('system', configtypes.SYSTEM_PROXY), - ('none', NetworkProxy(QNetworkProxy.NoProxy)), + ('none', QNetworkProxy(QNetworkProxy.NoProxy)), ('socks://example.com/', - NetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com')), - ('socks5://example.com', - NetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com')), - ('socks5://example.com:2342', - NetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 2342)), - ('socks5://foo@example.com', - NetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 0, 'foo')), - ('socks5://foo:bar@example.com', - NetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 0, 'foo', - 'bar')), + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com')), ('socks5://foo:bar@example.com:2323', - NetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 2323, 'foo', - 'bar')), + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 2323, + 'foo', 'bar')), ]) def test_transform(self, klass, val, expected): """Test transform with an empty value.""" actual = klass().transform(val) if isinstance(actual, QNetworkProxy): - actual = NetworkProxy(actual) + actual = QNetworkProxy(actual) assert actual == expected diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py index 984971b3c..06196f5d2 100644 --- a/tests/unit/utils/test_urlutils.py +++ b/tests/unit/utils/test_urlutils.py @@ -24,9 +24,11 @@ import collections import logging from PyQt5.QtCore import QUrl +from PyQt5.QtNetwork import QNetworkProxy import pytest from qutebrowser.commands import cmdexc +from qutebrowser.browser.webkit.network import pac from qutebrowser.utils import utils, urlutils, qtutils, usertypes @@ -736,3 +738,43 @@ def test_file_url(): def test_data_url(): url = urlutils.data_url('text/plain', b'foo') assert url == QUrl('data:text/plain;base64,Zm9v') + + + +class TestProxyFromUrl: + + @pytest.mark.parametrize('url, expected', [ + ('socks://example.com/', + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com')), + ('socks5://example.com', + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com')), + ('socks5://example.com:2342', + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 2342)), + ('socks5://foo@example.com', + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 0, 'foo')), + ('socks5://foo:bar@example.com', + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 0, 'foo', + 'bar')), + ('socks5://foo:bar@example.com:2323', + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 2323, + 'foo', 'bar')), + ('direct://', QNetworkProxy(QNetworkProxy.NoProxy)), + ]) + def test_proxy_from_url_valid(self, url, expected): + assert urlutils.proxy_from_url(QUrl(url)) == expected + + @pytest.mark.parametrize('scheme', ['pac+http', 'pac+https']) + def test_proxy_from_url_pac(self, scheme): + fetcher = urlutils.proxy_from_url(QUrl('{}://foo'.format(scheme))) + assert fetcher is pac.PACFetcher + + @pytest.mark.parametrize('url, exception', [ + ('blah', urlutils.InvalidProxyTypeError), + (':', urlutils.InvalidUrlError), # invalid URL + # Invalid/unsupported scheme + ('ftp://example.com/', urlutils.InvalidProxyTypeError), + ('socks4://example.com/', urlutils.InvalidProxyTypeError), + ]) + def test_invalid(self, url, exception): + with pytest.raises(exception): + urlutils.proxy_from_url(QUrl(url))