Start on implementing global OpenLP proxying

This commit is contained in:
Phill Ridout 2018-06-07 18:44:35 +01:00
parent b0c31b00f2
commit 6f34f8f2b8
4 changed files with 198 additions and 31 deletions

View File

@ -32,6 +32,7 @@ import requests
from openlp.core.common import trace_error_handler
from openlp.core.common.registry import Registry
from openlp.core.common.settings import ProxyMode, Settings
log = logging.getLogger(__name__ + '.__init__')
@ -64,6 +65,39 @@ CONNECTION_TIMEOUT = 30
CONNECTION_RETRIES = 2
def get_proxy_settings(mode=None):
"""
Create a dictionary containing the proxy settings.
:param ProxyMode | None mode: Specify the source of the proxy settings
:return: A dict using the format expected by the requests library.
:rtype: dict | None
"""
settings = Settings()
if mode is None:
mode = settings.value('advanced/proxy mode')
if mode == ProxyMode.NO_PROXY:
return {'http': None, 'https': None}
elif mode == ProxyMode.SYSTEM_PROXY:
# The requests library defaults to using the proxy settings in the environment variables
return
elif mode == ProxyMode.MANUAL_PROXY:
http_addr = settings.value('advanced/proxy http')
https_addr = settings.value('advanced/proxy https')
username = settings.value('advanced/proxy username')
password = settings.value('advanced/proxy password')
basic_auth = ''
if username:
basic_auth = '{username}:{password}@'.format(username=username, password=password)
http_value = None
https_value = None
if http_addr is not None:
http_value = 'http://{basic_auth}{http_addr}'.format(basic_auth=basic_auth, http_addr=http_addr)
if https_addr is not None:
https_value = 'https://{basic_auth}{https_addr}'.format(basic_auth=basic_auth, https_addr=https_addr)
return {'http': http_value, 'https': https_value}
def get_user_agent():
"""
Return a user agent customised for the platform the user is on.
@ -75,7 +109,7 @@ def get_user_agent():
return browser_list[random_index]
def get_web_page(url, headers=None, update_openlp=False, proxies=None):
def get_web_page(url, headers=None, update_openlp=False, proxy_mode=None):
"""
Attempts to download the webpage at url and returns that page or None.
@ -90,6 +124,8 @@ def get_web_page(url, headers=None, update_openlp=False, proxies=None):
headers = {}
if 'user-agent' not in [key.lower() for key in headers.keys()]:
headers['User-Agent'] = get_user_agent()
if proxy_mode is None:
proxies = get_proxy_settings(mode=proxy_mode)
log.debug('Downloading URL = %s' % url)
retries = 0
while retries < CONNECTION_RETRIES:

View File

@ -26,6 +26,7 @@ import datetime
import json
import logging
import os
from enum import IntEnum
from tempfile import gettempdir
from PyQt5 import QtCore, QtGui
@ -38,6 +39,13 @@ log = logging.getLogger(__name__)
__version__ = 2
class ProxyMode(IntEnum):
NO_PROXY = 1
SYSTEM_PROXY = 2
MANUAL_PROXY = 3
# Fix for bug #1014422.
X11_BYPASS_DEFAULT = True
if is_linux(): # pragma: no cover
@ -116,6 +124,11 @@ class Settings(QtCore.QSettings):
'advanced/print file meta data': False,
'advanced/print notes': False,
'advanced/print slide text': False,
'advanced/proxy mode': ProxyMode.SYSTEM_PROXY,
'advanced/proxy http': '',
'advanced/proxy https': '',
'advanced/proxy username': '',
'advanced/proxy password': '',
'advanced/recent file count': 4,
'advanced/save current plugin': False,
'advanced/slide limits': SlideLimits.End,

View File

@ -35,6 +35,7 @@ from openlp.core.lib import SettingsTab, build_icon
from openlp.core.ui.style import HAS_DARK_STYLE
from openlp.core.widgets.edits import PathEdit
from openlp.core.widgets.enums import PathEditType
from openlp.core.widgets.widgets import ProxyWidget
log = logging.getLogger(__name__)
@ -76,6 +77,9 @@ class AdvancedTab(SettingsTab):
self.media_plugin_check_box = QtWidgets.QCheckBox(self.ui_group_box)
self.media_plugin_check_box.setObjectName('media_plugin_check_box')
self.ui_layout.addRow(self.media_plugin_check_box)
self.hide_mouse_check_box = QtWidgets.QCheckBox(self.ui_group_box)
self.hide_mouse_check_box.setObjectName('hide_mouse_check_box')
self.ui_layout.addWidget(self.hide_mouse_check_box)
self.double_click_live_check_box = QtWidgets.QCheckBox(self.ui_group_box)
self.double_click_live_check_box.setObjectName('double_click_live_check_box')
self.ui_layout.addRow(self.double_click_live_check_box)
@ -116,6 +120,24 @@ class AdvancedTab(SettingsTab):
self.use_dark_style_checkbox = QtWidgets.QCheckBox(self.ui_group_box)
self.use_dark_style_checkbox.setObjectName('use_dark_style_checkbox')
self.ui_layout.addRow(self.use_dark_style_checkbox)
# Service Item Slide Limits
self.slide_group_box = QtWidgets.QGroupBox(self.left_column)
self.slide_group_box.setObjectName('slide_group_box')
self.slide_layout = QtWidgets.QVBoxLayout(self.slide_group_box)
self.slide_layout.setObjectName('slide_layout')
self.slide_label = QtWidgets.QLabel(self.slide_group_box)
self.slide_label.setWordWrap(True)
self.slide_layout.addWidget(self.slide_label)
self.end_slide_radio_button = QtWidgets.QRadioButton(self.slide_group_box)
self.end_slide_radio_button.setObjectName('end_slide_radio_button')
self.slide_layout.addWidget(self.end_slide_radio_button)
self.wrap_slide_radio_button = QtWidgets.QRadioButton(self.slide_group_box)
self.wrap_slide_radio_button.setObjectName('wrap_slide_radio_button')
self.slide_layout.addWidget(self.wrap_slide_radio_button)
self.next_item_radio_button = QtWidgets.QRadioButton(self.slide_group_box)
self.next_item_radio_button.setObjectName('next_item_radio_button')
self.slide_layout.addWidget(self.next_item_radio_button)
self.left_layout.addWidget(self.slide_group_box)
# Data Directory
self.data_directory_group_box = QtWidgets.QGroupBox(self.left_column)
self.data_directory_group_box.setObjectName('data_directory_group_box')
@ -142,33 +164,6 @@ class AdvancedTab(SettingsTab):
self.data_directory_layout.addRow(self.data_directory_copy_check_layout)
self.data_directory_layout.addRow(self.new_data_directory_has_files_label)
self.left_layout.addWidget(self.data_directory_group_box)
# Hide mouse
self.hide_mouse_group_box = QtWidgets.QGroupBox(self.right_column)
self.hide_mouse_group_box.setObjectName('hide_mouse_group_box')
self.hide_mouse_layout = QtWidgets.QVBoxLayout(self.hide_mouse_group_box)
self.hide_mouse_layout.setObjectName('hide_mouse_layout')
self.hide_mouse_check_box = QtWidgets.QCheckBox(self.hide_mouse_group_box)
self.hide_mouse_check_box.setObjectName('hide_mouse_check_box')
self.hide_mouse_layout.addWidget(self.hide_mouse_check_box)
self.right_layout.addWidget(self.hide_mouse_group_box)
# Service Item Slide Limits
self.slide_group_box = QtWidgets.QGroupBox(self.right_column)
self.slide_group_box.setObjectName('slide_group_box')
self.slide_layout = QtWidgets.QVBoxLayout(self.slide_group_box)
self.slide_layout.setObjectName('slide_layout')
self.slide_label = QtWidgets.QLabel(self.slide_group_box)
self.slide_label.setWordWrap(True)
self.slide_layout.addWidget(self.slide_label)
self.end_slide_radio_button = QtWidgets.QRadioButton(self.slide_group_box)
self.end_slide_radio_button.setObjectName('end_slide_radio_button')
self.slide_layout.addWidget(self.end_slide_radio_button)
self.wrap_slide_radio_button = QtWidgets.QRadioButton(self.slide_group_box)
self.wrap_slide_radio_button.setObjectName('wrap_slide_radio_button')
self.slide_layout.addWidget(self.wrap_slide_radio_button)
self.next_item_radio_button = QtWidgets.QRadioButton(self.slide_group_box)
self.next_item_radio_button.setObjectName('next_item_radio_button')
self.slide_layout.addWidget(self.next_item_radio_button)
self.right_layout.addWidget(self.slide_group_box)
# Display Workarounds
self.display_workaround_group_box = QtWidgets.QGroupBox(self.right_column)
self.display_workaround_group_box.setObjectName('display_workaround_group_box')
@ -223,6 +218,9 @@ class AdvancedTab(SettingsTab):
self.service_name_example.setObjectName('service_name_example')
self.service_name_layout.addRow(self.service_name_example_label, self.service_name_example)
self.right_layout.addWidget(self.service_name_group_box)
# Proxies
self.proxy_widget = ProxyWidget(self.right_column)
self.right_layout.addWidget(self.proxy_widget)
# After the last item on each side, add some spacing
self.left_layout.addStretch()
self.right_layout.addStretch()
@ -311,7 +309,6 @@ class AdvancedTab(SettingsTab):
translate('OpenLP.AdvancedTab',
'Revert to the default service name "{name}".').format(name=UiStrings().DefaultServiceName))
self.service_name_example_label.setText(translate('OpenLP.AdvancedTab', 'Example:'))
self.hide_mouse_group_box.setTitle(translate('OpenLP.AdvancedTab', 'Mouse Cursor'))
self.hide_mouse_check_box.setText(translate('OpenLP.AdvancedTab', 'Hide mouse cursor when over display window'))
self.data_directory_new_label.setText(translate('OpenLP.AdvancedTab', 'Path:'))
self.data_directory_cancel_button.setText(translate('OpenLP.AdvancedTab', 'Cancel'))
@ -334,6 +331,7 @@ class AdvancedTab(SettingsTab):
self.wrap_slide_radio_button.setText(translate('OpenLP.GeneralTab', '&Wrap around'))
self.next_item_radio_button.setText(translate('OpenLP.GeneralTab', '&Move to next/previous service item'))
self.search_as_type_check_box.setText(translate('SongsPlugin.GeneralTab', 'Enable search as you type'))
self.proxy_widget.retranslate_ui()
def load(self):
"""
@ -436,6 +434,7 @@ class AdvancedTab(SettingsTab):
if HAS_DARK_STYLE:
settings.setValue('use_dark_style', self.use_dark_style_checkbox.isChecked())
settings.endGroup()
self.proxy_widget.save()
def on_search_as_type_check_box_changed(self, check_state):
self.is_search_as_you_type_enabled = (check_state == QtCore.Qt.Checked)

View File

@ -27,13 +27,14 @@ import tempfile
from unittest import TestCase
from unittest.mock import MagicMock, patch
from openlp.core.common.httputils import get_user_agent, get_web_page, get_url_file_size, download_file
from openlp.core.common.httputils import ProxyMode, download_file, get_proxy_settings, get_url_file_size, \
get_user_agent, get_web_page
from openlp.core.common.path import Path
from openlp.core.common.settings import Settings
from tests.helpers.testmixin import TestMixin
class TestHttpUtils(TestCase, TestMixin):
"""
A test suite to test out various http helper functions.
"""
@ -240,3 +241,121 @@ class TestHttpUtils(TestCase, TestMixin):
# THEN: socket.timeout should have been caught
# NOTE: Test is if $tmpdir/tempfile is still there, then test fails since ftw deletes bad downloaded files
assert os.path.exists(self.tempfile) is False, 'tempfile should have been deleted'
class TestGetProxySettings(TestCase, TestMixin):
def setUp(self):
self.build_settings()
self.addCleanup(self.destroy_settings)
@patch('openlp.core.common.httputils.Settings')
def test_mode_arg_specified(self, MockSettings):
"""
Test that the argument is used rather than reading the 'advanced/proxy mode' setting
"""
# GIVEN: Mocked settings
mocked_settings = MagicMock()
MockSettings.return_value = mocked_settings
# WHEN: Calling `get_proxy_settings` with the mode arg specified
get_proxy_settings(mode=ProxyMode.NO_PROXY)
# THEN: The mode arg should have been used rather than looking it up in the settings
mocked_settings.value.assert_not_called()
@patch('openlp.core.common.httputils.Settings')
def test_mode_incorrect_arg_specified(self, MockSettings):
"""
Test that the system settings are used when the mode arg specieied is invalid
"""
# GIVEN: Mocked settings
mocked_settings = MagicMock()
MockSettings.return_value = mocked_settings
# WHEN: Calling `get_proxy_settings` with an invalid mode arg specified
result = get_proxy_settings(mode='qwerty')
# THEN: An None should be returned
mocked_settings.value.assert_not_called()
assert result is None
def test_no_proxy_mode(self):
"""
Test that a dictionary with http and https values are set to None is returned, when `NO_PROXY` mode is specified
"""
# GIVEN: A `proxy mode` setting of NO_PROXY
Settings().setValue('advanced/proxy mode', ProxyMode.NO_PROXY)
# WHEN: Calling `get_proxy_settings`
result = get_proxy_settings()
# THEN: The returned value should be a dictionary with http and https values set to None
assert result == {'http': None, 'https': None}
def test_system_proxy_mode(self):
"""
Test that None is returned, when `SYSTEM_PROXY` mode is specified
"""
# GIVEN: A `proxy mode` setting of SYSTEM_PROXY
Settings().setValue('advanced/proxy mode', ProxyMode.SYSTEM_PROXY)
# WHEN: Calling `get_proxy_settings`
result = get_proxy_settings()
# THEN: The returned value should be None
assert result is None
def test_manual_proxy_mode_no_auth(self):
"""
Test that the correct proxy addresses are returned when basic authentication is not used
"""
# GIVEN: A `proxy mode` setting of MANUAL_PROXY with proxy servers, but no auth credentials are supplied
Settings().setValue('advanced/proxy mode', ProxyMode.MANUAL_PROXY)
Settings().setValue('advanced/proxy http', 'testhttp.server:port')
Settings().setValue('advanced/proxy https', 'testhttps.server:port')
Settings().setValue('advanced/proxy username', '')
Settings().setValue('advanced/proxy password', '')
# WHEN: Calling `get_proxy_settings`
result = get_proxy_settings()
# THEN: The returned value should be the proxy servers without authentication
assert result == {'http': 'http://testhttp.server:port', 'https': 'https://testhttps.server:port'}
def test_manual_proxy_mode_auth(self):
"""
Test that the correct proxy addresses are returned when basic authentication is used
"""
# GIVEN: A `proxy mode` setting of MANUAL_PROXY with proxy servers and auth credentials supplied
Settings().setValue('advanced/proxy mode', ProxyMode.MANUAL_PROXY)
Settings().setValue('advanced/proxy http', 'testhttp.server:port')
Settings().setValue('advanced/proxy https', 'testhttps.server:port')
Settings().setValue('advanced/proxy username', 'user')
Settings().setValue('advanced/proxy password', 'pass')
# WHEN: Calling `get_proxy_settings`
result = get_proxy_settings()
# THEN: The returned value should be the proxy servers with the authentication credentials
assert result == {'http': 'http://user:pass@testhttp.server:port',
'https': 'https://user:pass@testhttps.server:port'}
def test_manual_proxy_mode_no_servers(self):
"""
Test that the system proxies are overidden when the MANUAL_PROXY mode is specified, but no server addresses are
supplied
"""
# GIVEN: A `proxy mode` setting of MANUAL_PROXY with no servers specified
Settings().setValue('advanced/proxy mode', ProxyMode.MANUAL_PROXY)
Settings().setValue('advanced/proxy http', None)
Settings().setValue('advanced/proxy https', None)
Settings().setValue('advanced/proxy username', 'user')
Settings().setValue('advanced/proxy password', 'pass')
# WHEN: Calling `get_proxy_settings`
result = get_proxy_settings()
# THEN: The returned value should be the proxy servers set to None
assert result == {'http': None, 'https': None}