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 import trace_error_handler
from openlp.core.common.registry import Registry from openlp.core.common.registry import Registry
from openlp.core.common.settings import ProxyMode, Settings
log = logging.getLogger(__name__ + '.__init__') log = logging.getLogger(__name__ + '.__init__')
@ -64,6 +65,39 @@ CONNECTION_TIMEOUT = 30
CONNECTION_RETRIES = 2 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(): def get_user_agent():
""" """
Return a user agent customised for the platform the user is on. 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] 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. 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 = {} headers = {}
if 'user-agent' not in [key.lower() for key in headers.keys()]: if 'user-agent' not in [key.lower() for key in headers.keys()]:
headers['User-Agent'] = get_user_agent() headers['User-Agent'] = get_user_agent()
if proxy_mode is None:
proxies = get_proxy_settings(mode=proxy_mode)
log.debug('Downloading URL = %s' % url) log.debug('Downloading URL = %s' % url)
retries = 0 retries = 0
while retries < CONNECTION_RETRIES: while retries < CONNECTION_RETRIES:

View File

@ -26,6 +26,7 @@ import datetime
import json import json
import logging import logging
import os import os
from enum import IntEnum
from tempfile import gettempdir from tempfile import gettempdir
from PyQt5 import QtCore, QtGui from PyQt5 import QtCore, QtGui
@ -38,6 +39,13 @@ log = logging.getLogger(__name__)
__version__ = 2 __version__ = 2
class ProxyMode(IntEnum):
NO_PROXY = 1
SYSTEM_PROXY = 2
MANUAL_PROXY = 3
# Fix for bug #1014422. # Fix for bug #1014422.
X11_BYPASS_DEFAULT = True X11_BYPASS_DEFAULT = True
if is_linux(): # pragma: no cover if is_linux(): # pragma: no cover
@ -116,6 +124,11 @@ class Settings(QtCore.QSettings):
'advanced/print file meta data': False, 'advanced/print file meta data': False,
'advanced/print notes': False, 'advanced/print notes': False,
'advanced/print slide text': 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/recent file count': 4,
'advanced/save current plugin': False, 'advanced/save current plugin': False,
'advanced/slide limits': SlideLimits.End, '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.ui.style import HAS_DARK_STYLE
from openlp.core.widgets.edits import PathEdit from openlp.core.widgets.edits import PathEdit
from openlp.core.widgets.enums import PathEditType from openlp.core.widgets.enums import PathEditType
from openlp.core.widgets.widgets import ProxyWidget
log = logging.getLogger(__name__) 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 = QtWidgets.QCheckBox(self.ui_group_box)
self.media_plugin_check_box.setObjectName('media_plugin_check_box') self.media_plugin_check_box.setObjectName('media_plugin_check_box')
self.ui_layout.addRow(self.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 = QtWidgets.QCheckBox(self.ui_group_box)
self.double_click_live_check_box.setObjectName('double_click_live_check_box') self.double_click_live_check_box.setObjectName('double_click_live_check_box')
self.ui_layout.addRow(self.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 = QtWidgets.QCheckBox(self.ui_group_box)
self.use_dark_style_checkbox.setObjectName('use_dark_style_checkbox') self.use_dark_style_checkbox.setObjectName('use_dark_style_checkbox')
self.ui_layout.addRow(self.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 # Data Directory
self.data_directory_group_box = QtWidgets.QGroupBox(self.left_column) self.data_directory_group_box = QtWidgets.QGroupBox(self.left_column)
self.data_directory_group_box.setObjectName('data_directory_group_box') 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.data_directory_copy_check_layout)
self.data_directory_layout.addRow(self.new_data_directory_has_files_label) self.data_directory_layout.addRow(self.new_data_directory_has_files_label)
self.left_layout.addWidget(self.data_directory_group_box) 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 # Display Workarounds
self.display_workaround_group_box = QtWidgets.QGroupBox(self.right_column) self.display_workaround_group_box = QtWidgets.QGroupBox(self.right_column)
self.display_workaround_group_box.setObjectName('display_workaround_group_box') 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_example.setObjectName('service_name_example')
self.service_name_layout.addRow(self.service_name_example_label, self.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) 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 # After the last item on each side, add some spacing
self.left_layout.addStretch() self.left_layout.addStretch()
self.right_layout.addStretch() self.right_layout.addStretch()
@ -311,7 +309,6 @@ class AdvancedTab(SettingsTab):
translate('OpenLP.AdvancedTab', translate('OpenLP.AdvancedTab',
'Revert to the default service name "{name}".').format(name=UiStrings().DefaultServiceName)) 'Revert to the default service name "{name}".').format(name=UiStrings().DefaultServiceName))
self.service_name_example_label.setText(translate('OpenLP.AdvancedTab', 'Example:')) 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.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_new_label.setText(translate('OpenLP.AdvancedTab', 'Path:'))
self.data_directory_cancel_button.setText(translate('OpenLP.AdvancedTab', 'Cancel')) 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.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.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.search_as_type_check_box.setText(translate('SongsPlugin.GeneralTab', 'Enable search as you type'))
self.proxy_widget.retranslate_ui()
def load(self): def load(self):
""" """
@ -436,6 +434,7 @@ class AdvancedTab(SettingsTab):
if HAS_DARK_STYLE: if HAS_DARK_STYLE:
settings.setValue('use_dark_style', self.use_dark_style_checkbox.isChecked()) settings.setValue('use_dark_style', self.use_dark_style_checkbox.isChecked())
settings.endGroup() settings.endGroup()
self.proxy_widget.save()
def on_search_as_type_check_box_changed(self, check_state): def on_search_as_type_check_box_changed(self, check_state):
self.is_search_as_you_type_enabled = (check_state == QtCore.Qt.Checked) 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 import TestCase
from unittest.mock import MagicMock, patch 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.path import Path
from openlp.core.common.settings import Settings
from tests.helpers.testmixin import TestMixin from tests.helpers.testmixin import TestMixin
class TestHttpUtils(TestCase, TestMixin): class TestHttpUtils(TestCase, TestMixin):
""" """
A test suite to test out various http helper functions. 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 # 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 # 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' 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}