From f1aadde13cd30ecf89afb8d8bd07ad9f4e0b5210 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Wed, 6 Mar 2013 21:53:29 +0000 Subject: [PATCH 01/22] Initial Cherrypy implementation --- openlp/core/ui/exceptionform.py | 6 + openlp/core/ui/slidecontroller.py | 7 +- openlp/core/utils/__init__.py | 7 +- openlp/plugins/remotes/html/stage.js | 6 +- openlp/plugins/remotes/lib/httpauth.py | 192 ++++++++++++++++ openlp/plugins/remotes/lib/httpserver.py | 281 +++++++++++------------ openlp/plugins/remotes/lib/remotetab.py | 271 ++++++++++++++-------- openlp/plugins/remotes/remoteplugin.py | 5 + scripts/check_dependencies.py | 1 + 9 files changed, 534 insertions(+), 242 deletions(-) create mode 100644 openlp/plugins/remotes/lib/httpauth.py diff --git a/openlp/core/ui/exceptionform.py b/openlp/core/ui/exceptionform.py index 50885b15b..c10f98d9b 100644 --- a/openlp/core/ui/exceptionform.py +++ b/openlp/core/ui/exceptionform.py @@ -69,6 +69,11 @@ try: MAKO_VERSION = mako.__version__ except ImportError: MAKO_VERSION = u'-' +try: + import cherrypy + CHERRYPY_VERSION = cherrypy.__version__ +except ImportError: + CHERRYPY_VERSION = u'-' try: import uno arg = uno.createUnoStruct(u'com.sun.star.beans.PropertyValue') @@ -138,6 +143,7 @@ class ExceptionForm(QtGui.QDialog, Ui_ExceptionDialog): u'PyEnchant: %s\n' % ENCHANT_VERSION + \ u'PySQLite: %s\n' % SQLITE_VERSION + \ u'Mako: %s\n' % MAKO_VERSION + \ + u'CherryPy: %s\n' % CHERRYPY_VERSION + \ u'pyUNO bridge: %s\n' % UNO_VERSION if platform.system() == u'Linux': if os.environ.get(u'KDE_FULL_SESSION') == u'true': diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 84ea296d2..3a2c0b582 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -362,8 +362,9 @@ class SlideController(DisplayController): # Signals QtCore.QObject.connect(self.previewListWidget, QtCore.SIGNAL(u'clicked(QModelIndex)'), self.onSlideSelected) if self.isLive: + # Need to use event as called across threads and UI is updated + QtCore.QObject.connect(self, QtCore.SIGNAL(u'slidecontroller_toggle_display'), self.toggle_display) Registry().register_function(u'slidecontroller_live_spin_delay', self.receive_spin_delay) - Registry().register_function(u'slidecontroller_toggle_display', self.toggle_display) self.toolbar.setWidgetVisible(self.loopList, False) self.toolbar.setWidgetVisible(self.wideMenu, False) else: @@ -867,9 +868,9 @@ class SlideController(DisplayController): """ Go to the requested slide """ - index = int(message[0]) - if not self.serviceItem: + if not self.serviceItem or not message[0]: return + index = int(message[0]) if self.serviceItem.is_command(): Registry().execute(u'%s_slide' % self.serviceItem.name.lower(), [self.serviceItem, self.isLive, index]) self.updatePreview() diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 104567039..dd611d303 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -90,6 +90,7 @@ class AppLocation(object): VersionDir = 5 CacheDir = 6 LanguageDir = 7 + SharedData = 8 # Base path where data/config/cache dir is located BaseDir = None @@ -150,18 +151,18 @@ def _get_os_dir_path(dir_type): if sys.platform == u'win32': if dir_type == AppLocation.DataDir: return os.path.join(unicode(os.getenv(u'APPDATA'), encoding), u'openlp', u'data') - elif dir_type == AppLocation.LanguageDir: + elif dir_type == AppLocation.LanguageDir or dir_type == AppLocation.SharedData: return os.path.split(openlp.__file__)[0] return os.path.join(unicode(os.getenv(u'APPDATA'), encoding), u'openlp') elif sys.platform == u'darwin': if dir_type == AppLocation.DataDir: return os.path.join(unicode(os.getenv(u'HOME'), encoding), u'Library', u'Application Support', u'openlp', u'Data') - elif dir_type == AppLocation.LanguageDir: + elif dir_type == AppLocation.LanguageDir or dir_type == AppLocation.SharedData: return os.path.split(openlp.__file__)[0] return os.path.join(unicode(os.getenv(u'HOME'), encoding), u'Library', u'Application Support', u'openlp') else: - if dir_type == AppLocation.LanguageDir: + if dir_type == AppLocation.LanguageDir or dir_type == AppLocation.SharedData: prefixes = [u'/usr/local', u'/usr'] for prefix in prefixes: directory = os.path.join(prefix, u'share', u'openlp') diff --git a/openlp/plugins/remotes/html/stage.js b/openlp/plugins/remotes/html/stage.js index dcc2e4b70..dff51537c 100644 --- a/openlp/plugins/remotes/html/stage.js +++ b/openlp/plugins/remotes/html/stage.js @@ -26,7 +26,7 @@ window.OpenLP = { loadService: function (event) { $.getJSON( - "/api/service/list", + "/stage/api/service/list", function (data, status) { OpenLP.nextSong = ""; $("#notes").html(""); @@ -46,7 +46,7 @@ window.OpenLP = { }, loadSlides: function (event) { $.getJSON( - "/api/controller/live/text", + "/stage/api/controller/live/text", function (data, status) { OpenLP.currentSlides = data.results.slides; OpenLP.currentSlide = 0; @@ -137,7 +137,7 @@ window.OpenLP = { }, pollServer: function () { $.getJSON( - "/api/poll", + "/stage/api/poll", function (data, status) { OpenLP.updateClock(data); if (OpenLP.currentItem != data.results.item || diff --git a/openlp/plugins/remotes/lib/httpauth.py b/openlp/plugins/remotes/lib/httpauth.py new file mode 100644 index 000000000..ce3ea091e --- /dev/null +++ b/openlp/plugins/remotes/lib/httpauth.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2013 Raoul Snyman # +# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program 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; version 2 of the License. # +# # +# This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### + +""" +The :mod:`http` module manages the HTTP authorisation logic. This code originates from +http://tools.cherrypy.org/wiki/AuthenticationAndAccessRestrictions + +""" + +import cherrypy +import urlparse + +SESSION_KEY = '_cp_openlp' + + +def check_credentials(user_name, password): + """ + Verifies credentials for username and password. + Returns None on success or a string describing the error on failure + """ + # @todo make from config + if user_name == 'openlp' and password == 'openlp': + return None + else: + return u"Incorrect username or password." + # if u.password != md5.new(password).hexdigest(): + # return u"Incorrect password" + + +def check_auth(*args, **kwargs): + """ + A tool that looks in config for 'auth.require'. If found and it + is not None, a login is required and the entry is evaluated as a list of + conditions that the user must fulfill + """ + print "check" + conditions = cherrypy.request.config.get('auth.require', None) + print conditions + print args, kwargs + print urlparse.urlparse(cherrypy.url()) + url = urlparse.urlparse(cherrypy.url()) + print urlparse.parse_qs(url.query) + if conditions is not None: + username = cherrypy.session.get(SESSION_KEY) + if username: + cherrypy.request.login = username + for condition in conditions: + # A condition is just a callable that returns true or false + if not condition(): + raise cherrypy.HTTPRedirect("/auth/login") + else: + raise cherrypy.HTTPRedirect("/auth/login") + +cherrypy.tools.auth = cherrypy.Tool('before_handler', check_auth) + + +def require_auth(*conditions): + """ + A decorator that appends conditions to the auth.require config variable. + """ + print conditions + def decorate(f): + if not hasattr(f, '_cp_config'): + f._cp_config = dict() + if 'auth.require' not in f._cp_config: + f._cp_config['auth.require'] = [] + f._cp_config['auth.require'].extend(conditions) + print "a ", [f] + return f + return decorate + + +# Conditions are callables that return True +# if the user fulfills the conditions they define, False otherwise +# +# They can access the current username as cherrypy.request.login +# +# Define those at will however suits the application. + +#def member_of(groupname): +# def check(): +# # replace with actual check if is in +# return cherrypy.request.login == 'joe' and groupname == 'admin' +# return check + + +#def name_is(reqd_username): +# return lambda: reqd_username == cherrypy.request.login + +#def any_of(*conditions): +# """ +# Returns True if any of the conditions match +# """ +# def check(): +# for c in conditions: +# if c(): +# return True +# return False +# return check + +# By default all conditions are required, but this might still be +# needed if you want to use it inside of an any_of(...) condition +#def all_of(*conditions): +# """ +# Returns True if all of the conditions match +# """ +# def check(): +# for c in conditions: +# if not c(): +# return False +# return True +# return check +# Controller to provide login and logout actions + + +class AuthController(object): + + def on_login(self, username): + """ + Called on successful login + """ + + def on_logout(self, username): + """ + Called on logout + """ + + def get_loginform(self, username, msg="Enter login information", from_page="/"): + """ + Provides a login form + """ + return """ +
+ + %(msg)s
+ Username:
+ Password:
+ + """ % locals() + + @cherrypy.expose + def login(self, username=None, password=None, from_page="/"): + """ + Provides the actual login control + """ + if username is None or password is None: + return self.get_loginform("", from_page=from_page) + + error_msg = check_credentials(username, password) + if error_msg: + return self.get_loginform(username, error_msg, from_page) + else: + cherrypy.session[SESSION_KEY] = cherrypy.request.login = username + self.on_login(username) + raise cherrypy.HTTPRedirect(from_page or "/") + + @cherrypy.expose + def logout(self, from_page="/"): + sess = cherrypy.session + username = sess.get(SESSION_KEY, None) + sess[SESSION_KEY] = None + if username: + cherrypy.request.login = None + self.on_logout(username) + raise cherrypy.HTTPRedirect(from_page or "/") + diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 3b2c7439a..f4dd633e8 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -119,40 +119,23 @@ import os import re import urllib import urlparse +import cherrypy -from PyQt4 import QtCore, QtNetwork from mako.template import Template +from PyQt4 import QtCore from openlp.core.lib import Registry, Settings, PluginStatus, StringContent - from openlp.core.utils import AppLocation, translate +from openlp.plugins.remotes.lib.httpauth import AuthController, require_auth log = logging.getLogger(__name__) -class HttpResponse(object): - """ - A simple object to encapsulate a pseudo-http response. - """ - code = '200 OK' - content = '' - headers = { - 'Content-Type': 'text/html; charset="utf-8"\r\n' - } - - def __init__(self, content='', headers=None, code=None): - if headers is None: - headers = {} - self.content = content - for key, value in headers.iteritems(): - self.headers[key] = value - if code: - self.code = code - class HttpServer(object): """ Ability to control OpenLP via a web browser. """ + def __init__(self, plugin): """ Initialise the httpserver, and start the server. @@ -163,22 +146,28 @@ class HttpServer(object): self.connections = [] self.current_item = None self.current_slide = None - self.start_tcp() + self.conf = {'/files': {u'tools.staticdir.on': True, + u'tools.staticdir.dir': self.html_dir}} + self.start_server() - def start_tcp(self): + def start_server(self): """ Start the http server, use the port in the settings default to 4316. Listen out for slide and song changes so they can be broadcast to clients. Listen out for socket connections. """ - log.debug(u'Start TCP server') + log.debug(u'Start CherryPy server') port = Settings().value(self.plugin.settingsSection + u'/port') address = Settings().value(self.plugin.settingsSection + u'/ip address') - self.server = QtNetwork.QTcpServer() - self.server.listen(QtNetwork.QHostAddress(address), port) + server_config = {u'server.socket_host': str(address), + u'server.socket_port': port} + cherrypy.config.update(server_config) + cherrypy.config.update({'environment': 'embedded'}) + cherrypy.config.update({'engine.autoreload_on': False}) + cherrypy.tree.mount(HttpConnection(self), '/', config=self.conf) + cherrypy.engine.start() Registry().register_function(u'slidecontroller_live_changed', self.slide_change) Registry().register_function(u'slidecontroller_live_started', self.item_change) - QtCore.QObject.connect(self.server, QtCore.SIGNAL(u'newConnection()'), self.new_connection) log.debug(u'TCP listening on port %d' % port) def slide_change(self, row): @@ -193,50 +182,41 @@ class HttpServer(object): """ self.current_item = items[0] - def new_connection(self): - """ - A new http connection has been made. Create a client object to handle - communication. - """ - log.debug(u'new http connection') - socket = self.server.nextPendingConnection() - if socket: - self.connections.append(HttpConnection(self, socket)) - - def close_connection(self, connection): - """ - The connection has been closed. Clean up - """ - log.debug(u'close http connection') - if connection in self.connections: - self.connections.remove(connection) - def close(self): """ Close down the http server. """ log.debug(u'close http server') - self.server.close() + cherrypy.engine.exit() + cherrypy.engine.stop() class HttpConnection(object): """ - A single connection, this handles communication between the server - and the client. + A single connection, this handles communication between the server and the client. """ - def __init__(self, parent, socket): + _cp_config = { + 'tools.sessions.on': True, + 'tools.auth.on': True + } + + auth = AuthController() + + def __init__(self, parent): """ Initialise the http connection. Listen out for socket signals. """ - log.debug(u'Initialise HttpConnection: %s' % socket.peerAddress()) - self.socket = socket + #log.debug(u'Initialise HttpConnection: %s' % socket.peerAddress()) + #self.socket = socket self.parent = parent self.routes = [ (u'^/$', self.serve_file), (u'^/(stage)$', self.serve_file), (r'^/files/(.*)$', self.serve_file), (r'^/api/poll$', self.poll), + (r'^/stage/api/poll$', self.poll), (r'^/api/controller/(live|preview)/(.*)$', self.controller), + (r'^/stage/api/controller/live/(.*)$', self.controller), (r'^/api/service/(.*)$', self.service), (r'^/api/display/(hide|show|blank|theme|desktop)$', self.display), (r'^/api/alert$', self.alert), @@ -245,17 +225,79 @@ class HttpConnection(object): (r'^/api/(.*)/live$', self.go_live), (r'^/api/(.*)/add$', self.add_to_service) ] - QtCore.QObject.connect(self.socket, QtCore.SIGNAL(u'readyRead()'), self.ready_read) - QtCore.QObject.connect(self.socket, QtCore.SIGNAL(u'disconnected()'), self.disconnected) self.translate() + @cherrypy.expose + #@require_auth(auth) + def default(self, *args, **kwargs): + """ + Handles the requests for the main url. This is secure depending on settings. + """ + # Loop through the routes we set up earlier and execute them + return self._process_http_request(args, kwargs) + + @cherrypy.expose + def stage(self, *args, **kwargs): + """ + Handles the requests for the stage url. This is not secure. + """ + print "Stage" + url = urlparse.urlparse(cherrypy.url()) + self.url_params = urlparse.parse_qs(url.query) + print url + print [self.url_params] + #return self.serve_file(u'stage') + return self._process_http_request(args, kwargs) + + @cherrypy.expose + def files(self, *args, **kwargs): + """ + Handles the requests for the stage url. This is not secure. + """ + print "files" + url = urlparse.urlparse(cherrypy.url()) + self.url_params = urlparse.parse_qs(url.query) + print url + print [self.url_params] + print args + #return self.serve_file(args) + return self._process_http_request(args, kwargs) + + def _process_http_request(self, args, kwargs): + """ + Common function to process HTTP requests where secure or insecure + """ + print "common handler" + url = urlparse.urlparse(cherrypy.url()) + self.url_params = urlparse.parse_qs(url.query) + print url + print [self.url_params] + response = None + for route, func in self.routes: + match = re.match(route, url.path) + if match: + print 'Route "%s" matched "%s"', route, url.path + log.debug('Route "%s" matched "%s"', route, url.path) + args = [] + for param in match.groups(): + args.append(param) + response = func(*args) + break + if response: + return response + else: + return self._http_not_found() + def _get_service_items(self): + """ + Read the service item in use and return the data as a json object + """ service_items = [] if self.parent.current_item: current_unique_identifier = self.parent.current_item.unique_identifier else: current_unique_identifier = None - for item in self.service_manager.serviceItems: + for item in self.service_manager.service_items: service_item = item[u'service_item'] service_items.append({ u'id': unicode(service_item.unique_identifier), @@ -296,40 +338,6 @@ class HttpConnection(object): 'slides': translate('RemotePlugin.Mobile', 'Slides') } - def ready_read(self): - """ - Data has been sent from the client. Respond to it - """ - log.debug(u'ready to read socket') - if self.socket.canReadLine(): - data = str(self.socket.readLine()) - try: - log.debug(u'received: ' + data) - except UnicodeDecodeError: - # Malicious request containing non-ASCII characters. - self.close() - return - words = data.split(' ') - response = None - if words[0] == u'GET': - url = urlparse.urlparse(words[1]) - self.url_params = urlparse.parse_qs(url.query) - # Loop through the routes we set up earlier and execute them - for route, func in self.routes: - match = re.match(route, url.path) - if match: - log.debug('Route "%s" matched "%s"', route, url.path) - args = [] - for param in match.groups(): - args.append(param) - response = func(*args) - break - if response: - self.send_response(response) - else: - self.send_response(HttpResponse(code='404 Not Found')) - self.close() - def serve_file(self, filename=None): """ Send a file to the socket. For now, just a subset of file types @@ -339,6 +347,7 @@ class HttpConnection(object): Ultimately for i18n, this could first look for xx/file.html before falling back to file.html... where xx is the language, e.g. 'en' """ + print "serve_file", filename log.debug(u'serve file request %s' % filename) if not filename: filename = u'index.html' @@ -346,7 +355,7 @@ class HttpConnection(object): filename = u'stage.html' path = os.path.normpath(os.path.join(self.parent.html_dir, filename)) if not path.startswith(self.parent.html_dir): - return HttpResponse(code=u'404 Not Found') + return self._http_not_found() ext = os.path.splitext(filename)[1] html = None if ext == u'.html': @@ -375,11 +384,12 @@ class HttpConnection(object): content = file_handle.read() except IOError: log.exception(u'Failed to open %s' % path) - return HttpResponse(code=u'404 Not Found') + return self._http_not_found() finally: if file_handle: file_handle.close() - return HttpResponse(content, {u'Content-Type': mimetype}) + cherrypy.response.headers['Content-Type'] = mimetype + return content def poll(self): """ @@ -389,24 +399,25 @@ class HttpConnection(object): u'service': self.service_manager.service_id, u'slide': self.parent.current_slide or 0, u'item': self.parent.current_item.unique_identifier if self.parent.current_item else u'', - u'twelve':Settings().value(u'remotes/twelve hour'), + u'twelve': Settings().value(u'remotes/twelve hour'), u'blank': self.live_controller.blankScreen.isChecked(), u'theme': self.live_controller.themeScreen.isChecked(), u'display': self.live_controller.desktopScreen.isChecked() } - return HttpResponse(json.dumps({u'results': result}), - {u'Content-Type': u'application/json'}) + cherrypy.response.headers['Content-Type'] = u'application/json' + return json.dumps({u'results': result}) def display(self, action): """ Hide or show the display screen. + This is a cross Thread call and UI is updated so Events need to be used. ``action`` This is the action, either ``hide`` or ``show``. """ - Registry().execute(u'slidecontroller_toggle_display', action) - return HttpResponse(json.dumps({u'results': {u'success': True}}), - {u'Content-Type': u'application/json'}) + self.live_controller.emit(QtCore.SIGNAL(u'slidecontroller_toggle_display'), action) + cherrypy.response.headers['Content-Type'] = u'application/json' + return json.dumps({u'results': {u'success': True}}) def alert(self): """ @@ -417,14 +428,14 @@ class HttpConnection(object): try: text = json.loads(self.url_params[u'data'][0])[u'request'][u'text'] except KeyError, ValueError: - return HttpResponse(code=u'400 Bad Request') + return self._http_bad_request() text = urllib.unquote(text) Registry().execute(u'alerts_text', [text]) success = True else: success = False - return HttpResponse(json.dumps({u'results': {u'success': success}}), - {u'Content-Type': u'application/json'}) + cherrypy.response.headers['Content-Type'] = u'application/json' + return json.dumps({u'results': {u'success': success}}) def controller(self, type, action): """ @@ -465,34 +476,37 @@ class HttpConnection(object): try: data = json.loads(self.url_params[u'data'][0]) except KeyError, ValueError: - return HttpResponse(code=u'400 Bad Request') + return self._http_bad_request() log.info(data) # This slot expects an int within a list. id = data[u'request'][u'id'] Registry().execute(event, [id]) else: - Registry().execute(event) + Registry().execute(event, [0]) json_data = {u'results': {u'success': True}} - return HttpResponse(json.dumps(json_data), - {u'Content-Type': u'application/json'}) + cherrypy.response.headers['Content-Type'] = u'application/json' + return json.dumps(json_data) def service(self, action): + """ + List details of the Service and update the UI + """ event = u'servicemanager_%s' % action if action == u'list': - return HttpResponse(json.dumps({u'results': {u'items': self._get_service_items()}}), - {u'Content-Type': u'application/json'}) + cherrypy.response.headers['Content-Type'] = u'application/json' + return json.dumps({u'results': {u'items': self._get_service_items()}}) else: event += u'_item' if self.url_params and self.url_params.get(u'data'): try: data = json.loads(self.url_params[u'data'][0]) except KeyError, ValueError: - return HttpResponse(code=u'400 Bad Request') + return self._http_bad_request() Registry().execute(event, data[u'request'][u'id']) else: Registry().execute(event) - return HttpResponse(json.dumps({u'results': {u'success': True}}), - {u'Content-Type': u'application/json'}) + cherrypy.response.headers['Content-Type'] = u'application/json' + return json.dumps({u'results': {u'success': True}}) def pluginInfo(self, action): """ @@ -507,9 +521,8 @@ class HttpConnection(object): for plugin in self.plugin_manager.plugins: if plugin.status == PluginStatus.Active and plugin.mediaItem and plugin.mediaItem.hasSearch: searches.append([plugin.name, unicode(plugin.textStrings[StringContent.Name][u'plural'])]) - return HttpResponse( - json.dumps({u'results': {u'items': searches}}), - {u'Content-Type': u'application/json'}) + cherrypy.response.headers['Content-Type'] = u'application/json' + return json.dumps({u'results': {u'items': searches}}) def search(self, type): """ @@ -521,15 +534,15 @@ class HttpConnection(object): try: text = json.loads(self.url_params[u'data'][0])[u'request'][u'text'] except KeyError, ValueError: - return HttpResponse(code=u'400 Bad Request') + return self._http_bad_request() text = urllib.unquote(text) plugin = self.plugin_manager.get_plugin_by_name(type) if plugin.status == PluginStatus.Active and plugin.mediaItem and plugin.mediaItem.hasSearch: results = plugin.mediaItem.search(text, False) else: results = [] - return HttpResponse(json.dumps({u'results': {u'items': results}}), - {u'Content-Type': u'application/json'}) + cherrypy.response.headers['Content-Type'] = u'application/json' + return json.dumps({u'results': {u'items': results}}) def go_live(self, type): """ @@ -538,11 +551,11 @@ class HttpConnection(object): try: id = json.loads(self.url_params[u'data'][0])[u'request'][u'id'] except KeyError, ValueError: - return HttpResponse(code=u'400 Bad Request') + return self._http_bad_request() plugin = self.plugin_manager.get_plugin_by_name(type) if plugin.status == PluginStatus.Active and plugin.mediaItem: plugin.mediaItem.goLive(id, remote=True) - return HttpResponse(code=u'200 OK') + return self._http_success() def add_to_service(self, type): """ @@ -551,38 +564,22 @@ class HttpConnection(object): try: id = json.loads(self.url_params[u'data'][0])[u'request'][u'id'] except KeyError, ValueError: - return HttpResponse(code=u'400 Bad Request') + return self._http_bad_request() plugin = self.plugin_manager.get_plugin_by_name(type) if plugin.status == PluginStatus.Active and plugin.mediaItem: item_id = plugin.mediaItem.createItemFromId(id) plugin.mediaItem.addToService(item_id, remote=True) - return HttpResponse(code=u'200 OK') + self._http_success() - def send_response(self, response): - http = u'HTTP/1.1 %s\r\n' % response.code - for header, value in response.headers.iteritems(): - http += '%s: %s\r\n' % (header, value) - http += '\r\n' - self.socket.write(http) - self.socket.write(response.content) + def _http_success(self): + cherrypy.response.status = 200 - def disconnected(self): - """ - The client has disconnected. Tidy up - """ - log.debug(u'socket disconnected') - self.close() + def _http_bad_request(self): + cherrypy.response.status = 400 - def close(self): - """ - The server has closed the connection. Tidy up - """ - if not self.socket: - return - log.debug(u'close socket') - self.socket.close() - self.socket = None - self.parent.close_connection(self) + def _http_not_found(self): + cherrypy.response.status = 404 + cherrypy.response.body = ["Sorry, an error occured"] def _get_service_manager(self): """ diff --git a/openlp/plugins/remotes/lib/remotetab.py b/openlp/plugins/remotes/lib/remotetab.py index 38b8753ab..7d23f500f 100644 --- a/openlp/plugins/remotes/lib/remotetab.py +++ b/openlp/plugins/remotes/lib/remotetab.py @@ -27,9 +27,12 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### +import os.path + from PyQt4 import QtCore, QtGui, QtNetwork from openlp.core.lib import Registry, Settings, SettingsTab, translate +from openlp.core.utils import AppLocation ZERO_URL = u'0.0.0.0' @@ -45,117 +48,203 @@ class RemoteTab(SettingsTab): def setupUi(self): self.setObjectName(u'RemoteTab') SettingsTab.setupUi(self) - self.serverSettingsGroupBox = QtGui.QGroupBox(self.leftColumn) - self.serverSettingsGroupBox.setObjectName(u'serverSettingsGroupBox') - self.serverSettingsLayout = QtGui.QFormLayout(self.serverSettingsGroupBox) - self.serverSettingsLayout.setObjectName(u'serverSettingsLayout') - self.addressLabel = QtGui.QLabel(self.serverSettingsGroupBox) - self.addressLabel.setObjectName(u'addressLabel') - self.addressEdit = QtGui.QLineEdit(self.serverSettingsGroupBox) - self.addressEdit.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) - self.addressEdit.setValidator(QtGui.QRegExpValidator(QtCore.QRegExp( - u'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'), self)) - self.addressEdit.setObjectName(u'addressEdit') - QtCore.QObject.connect(self.addressEdit, QtCore.SIGNAL(u'textChanged(const QString&)'), self.setUrls) - self.serverSettingsLayout.addRow(self.addressLabel, self.addressEdit) - self.twelveHourCheckBox = QtGui.QCheckBox(self.serverSettingsGroupBox) - self.twelveHourCheckBox.setObjectName(u'twelveHourCheckBox') - self.serverSettingsLayout.addRow(self.twelveHourCheckBox) - self.portLabel = QtGui.QLabel(self.serverSettingsGroupBox) - self.portLabel.setObjectName(u'portLabel') - self.portSpinBox = QtGui.QSpinBox(self.serverSettingsGroupBox) - self.portSpinBox.setMaximum(32767) - self.portSpinBox.setObjectName(u'portSpinBox') - QtCore.QObject.connect(self.portSpinBox, QtCore.SIGNAL(u'valueChanged(int)'), self.setUrls) - self.serverSettingsLayout.addRow(self.portLabel, self.portSpinBox) - self.remoteUrlLabel = QtGui.QLabel(self.serverSettingsGroupBox) - self.remoteUrlLabel.setObjectName(u'remoteUrlLabel') - self.remoteUrl = QtGui.QLabel(self.serverSettingsGroupBox) - self.remoteUrl.setObjectName(u'remoteUrl') - self.remoteUrl.setOpenExternalLinks(True) - self.serverSettingsLayout.addRow(self.remoteUrlLabel, self.remoteUrl) - self.stageUrlLabel = QtGui.QLabel(self.serverSettingsGroupBox) - self.stageUrlLabel.setObjectName(u'stageUrlLabel') - self.stageUrl = QtGui.QLabel(self.serverSettingsGroupBox) - self.stageUrl.setObjectName(u'stageUrl') - self.stageUrl.setOpenExternalLinks(True) - self.serverSettingsLayout.addRow(self.stageUrlLabel, self.stageUrl) - self.leftLayout.addWidget(self.serverSettingsGroupBox) - self.androidAppGroupBox = QtGui.QGroupBox(self.rightColumn) - self.androidAppGroupBox.setObjectName(u'androidAppGroupBox') - self.rightLayout.addWidget(self.androidAppGroupBox) - self.qrLayout = QtGui.QVBoxLayout(self.androidAppGroupBox) - self.qrLayout.setObjectName(u'qrLayout') - self.qrCodeLabel = QtGui.QLabel(self.androidAppGroupBox) - self.qrCodeLabel.setPixmap(QtGui.QPixmap(u':/remotes/android_app_qr.png')) - self.qrCodeLabel.setAlignment(QtCore.Qt.AlignCenter) - self.qrCodeLabel.setObjectName(u'qrCodeLabel') - self.qrLayout.addWidget(self.qrCodeLabel) - self.qrDescriptionLabel = QtGui.QLabel(self.androidAppGroupBox) - self.qrDescriptionLabel.setObjectName(u'qrDescriptionLabel') - self.qrDescriptionLabel.setOpenExternalLinks(True) - self.qrDescriptionLabel.setWordWrap(True) - self.qrLayout.addWidget(self.qrDescriptionLabel) + self.server_settings_group_box = QtGui.QGroupBox(self.leftColumn) + self.server_settings_group_box.setObjectName(u'server_settings_group_box') + self.server_settings_layout = QtGui.QFormLayout(self.server_settings_group_box) + self.server_settings_layout.setObjectName(u'server_settings_layout') + self.address_label = QtGui.QLabel(self.server_settings_group_box) + self.address_label.setObjectName(u'address_label') + self.address_edit = QtGui.QLineEdit(self.server_settings_group_box) + self.address_edit.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) + self.address_edit.setValidator(QtGui.QRegExpValidator(QtCore.QRegExp(u'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'), + self)) + self.address_edit.setObjectName(u'address_edit') + self.server_settings_layout.addRow(self.address_label, self.address_edit) + self.twelve_hour_check_box = QtGui.QCheckBox(self.server_settings_group_box) + self.twelve_hour_check_box.setObjectName(u'twelve_hour_check_box') + self.server_settings_layout.addRow(self.twelve_hour_check_box) + self.leftLayout.addWidget(self.server_settings_group_box) + self.http_settings_group_box = QtGui.QGroupBox(self.leftColumn) + self.http_settings_group_box.setObjectName(u'http_settings_group_box') + self.http_setting_layout = QtGui.QFormLayout(self.http_settings_group_box) + self.http_setting_layout.setObjectName(u'http_setting_layout') + self.port_label = QtGui.QLabel(self.http_settings_group_box) + self.port_label.setObjectName(u'port_label') + self.port_spin_box = QtGui.QSpinBox(self.http_settings_group_box) + self.port_spin_box.setMaximum(32767) + self.port_spin_box.setObjectName(u'port_spin_box') + self.http_setting_layout.addRow(self.port_label, self.port_spin_box) + self.remote_url_label = QtGui.QLabel(self.http_settings_group_box) + self.remote_url_label.setObjectName(u'remote_url_label') + self.remote_url = QtGui.QLabel(self.http_settings_group_box) + self.remote_url.setObjectName(u'remote_url') + self.remote_url.setOpenExternalLinks(True) + self.http_setting_layout.addRow(self.remote_url_label, self.remote_url) + self.stage_url_label = QtGui.QLabel(self.http_settings_group_box) + self.stage_url_label.setObjectName(u'stage_url_label') + self.stage_url = QtGui.QLabel(self.http_settings_group_box) + self.stage_url.setObjectName(u'stage_url') + self.stage_url.setOpenExternalLinks(True) + self.http_setting_layout.addRow(self.stage_url_label, self.stage_url) + self.leftLayout.addWidget(self.http_settings_group_box) + self.https_settings_group_box = QtGui.QGroupBox(self.leftColumn) + self.https_settings_group_box.setCheckable(True) + self.https_settings_group_box.setChecked(False) + self.https_settings_group_box.setObjectName(u'https_settings_group_box') + self.https_settings_layout = QtGui.QFormLayout(self.https_settings_group_box) + self.https_settings_layout.setObjectName(u'https_settings_layout') + self.https_error_label = QtGui.QLabel(self.https_settings_group_box) + self.https_error_label.setVisible(False) + self.https_error_label.setWordWrap(True) + self.https_error_label.setObjectName(u'https_error_label') + self.https_settings_layout.addRow(self.https_error_label) + self.https_port_label = QtGui.QLabel(self.https_settings_group_box) + self.https_port_label.setObjectName(u'https_port_label') + self.https_port_spin_box = QtGui.QSpinBox(self.https_settings_group_box) + self.https_port_spin_box.setMaximum(32767) + self.https_port_spin_box.setObjectName(u'https_port_spin_box') + self.https_settings_layout.addRow(self.https_port_label, self.https_port_spin_box) + self.remote_https_url = QtGui.QLabel(self.https_settings_group_box) + self.remote_https_url.setObjectName(u'remote_http_url') + self.remote_https_url.setOpenExternalLinks(True) + self.remote_https_url_label = QtGui.QLabel(self.https_settings_group_box) + self.remote_https_url_label.setObjectName(u'remote_http_url_label') + self.https_settings_layout.addRow(self.remote_https_url_label, self.remote_https_url) + self.stage_https_url_label = QtGui.QLabel(self.http_settings_group_box) + self.stage_https_url_label.setObjectName(u'stage_https_url_label') + self.stage_https_url = QtGui.QLabel(self.https_settings_group_box) + self.stage_https_url.setObjectName(u'stage_https_url') + self.stage_https_url.setOpenExternalLinks(True) + self.https_settings_layout.addRow(self.stage_https_url_label, self.stage_https_url) + self.leftLayout.addWidget(self.https_settings_group_box) + self.user_login_group_box = QtGui.QGroupBox(self.leftColumn) + self.user_login_group_box.setCheckable(True) + self.user_login_group_box.setChecked(False) + self.user_login_group_box.setObjectName(u'user_login_group_box') + self.user_login_layout = QtGui.QFormLayout(self.user_login_group_box) + self.user_login_layout.setObjectName(u'user_login_layout') + self.user_id_label = QtGui.QLabel(self.user_login_group_box) + self.user_id_label.setObjectName(u'user_id_label') + self.user_id = QtGui.QLineEdit(self.user_login_group_box) + self.user_id.setObjectName(u'user_id') + self.user_login_layout.addRow(self.user_id_label, self.user_id) + self.password_label = QtGui.QLabel(self.user_login_group_box) + self.password_label.setObjectName(u'password_label') + self.password = QtGui.QLineEdit(self.user_login_group_box) + self.password.setObjectName(u'password') + self.user_login_layout.addRow(self.password_label, self.password) + self.leftLayout.addWidget(self.user_login_group_box) + self.android_app_group_box = QtGui.QGroupBox(self.rightColumn) + self.android_app_group_box.setObjectName(u'android_app_group_box') + self.rightLayout.addWidget(self.android_app_group_box) + self.qr_layout = QtGui.QVBoxLayout(self.android_app_group_box) + self.qr_layout.setObjectName(u'qr_layout') + self.qr_code_label = QtGui.QLabel(self.android_app_group_box) + self.qr_code_label.setPixmap(QtGui.QPixmap(u':/remotes/android_app_qr.png')) + self.qr_code_label.setAlignment(QtCore.Qt.AlignCenter) + self.qr_code_label.setObjectName(u'qr_code_label') + self.qr_layout.addWidget(self.qr_code_label) + self.qr_description_label = QtGui.QLabel(self.android_app_group_box) + self.qr_description_label.setObjectName(u'qr_description_label') + self.qr_description_label.setOpenExternalLinks(True) + self.qr_description_label.setWordWrap(True) + self.qr_layout.addWidget(self.qr_description_label) self.leftLayout.addStretch() self.rightLayout.addStretch() - QtCore.QObject.connect(self.twelveHourCheckBox, QtCore.SIGNAL(u'stateChanged(int)'), - self.onTwelveHourCheckBoxChanged) + self.twelve_hour_check_box.stateChanged.connect(self.on_twelve_hour_check_box_changed) + self.address_edit.textChanged.connect(self.set_urls) + self.port_spin_box.valueChanged.connect(self.set_urls) + self.https_port_spin_box.valueChanged.connect(self.set_urls) def retranslateUi(self): - self.serverSettingsGroupBox.setTitle( - translate('RemotePlugin.RemoteTab', 'Server Settings')) - self.addressLabel.setText(translate('RemotePlugin.RemoteTab', 'Serve on IP address:')) - self.portLabel.setText(translate('RemotePlugin.RemoteTab', 'Port number:')) - self.remoteUrlLabel.setText(translate('RemotePlugin.RemoteTab', 'Remote URL:')) - self.stageUrlLabel.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:')) - self.twelveHourCheckBox.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format')) - self.androidAppGroupBox.setTitle(translate('RemotePlugin.RemoteTab', 'Android App')) - self.qrDescriptionLabel.setText(translate('RemotePlugin.RemoteTab', - 'Scan the QR code or click download to install the ' - 'Android app from Google Play.')) + self.server_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Server Settings')) + self.address_label.setText(translate('RemotePlugin.RemoteTab', 'Serve on IP address:')) + self.port_label.setText(translate('RemotePlugin.RemoteTab', 'Port number:')) + self.remote_url_label.setText(translate('RemotePlugin.RemoteTab', 'Remote URL:')) + self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:')) + self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format')) + self.android_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Android App')) + self.qr_description_label.setText(translate('RemotePlugin.RemoteTab', + 'Scan the QR code or click download to install the ' + 'Android app from Google Play.')) + self.https_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'HTTPS Server')) + self.https_error_label.setText(translate('RemotePlugin.RemoteTab', + 'Could not find an SSL certificate. The HTTPS server will not be available unless an SSL certificate ' + 'is found. Please see the manual for more information.')) + self.https_port_label.setText(self.port_label.text()) + self.remote_https_url_label.setText(self.remote_url_label.text()) + self.stage_https_url_label.setText(self.stage_url_label.text()) + self.user_login_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'User Authentication')) + self.user_id_label.setText(translate('RemotePlugin.RemoteTab', 'User id:')) + self.password_label.setText(translate('RemotePlugin.RemoteTab', 'Password:')) - def setUrls(self): + def set_urls(self): ipAddress = u'localhost' - if self.addressEdit.text() == ZERO_URL: - ifaces = QtNetwork.QNetworkInterface.allInterfaces() - for iface in ifaces: - if not iface.isValid(): + if self.address_edit.text() == ZERO_URL: + interfaces = QtNetwork.QNetworkInterface.allInterfaces() + for interface in interfaces: + if not interface.isValid(): continue - if not (iface.flags() & (QtNetwork.QNetworkInterface.IsUp | QtNetwork.QNetworkInterface.IsRunning)): + if not (interface.flags() & (QtNetwork.QNetworkInterface.IsUp | QtNetwork.QNetworkInterface.IsRunning)): continue - for addr in iface.addressEntries(): - ip = addr.ip() + for address in interface.addressEntries(): + ip = address.ip() if ip.protocol() == 0 and ip != QtNetwork.QHostAddress.LocalHost: ipAddress = ip break else: - ipAddress = self.addressEdit.text() - url = u'http://%s:%s/' % (ipAddress, self.portSpinBox.value()) - self.remoteUrl.setText(u'%s' % (url, url)) - url += u'stage' - self.stageUrl.setText(u'%s' % (url, url)) + ipAddress = self.address_edit.text() + http_url = u'http://%s:%s/' % (ipAddress, self.port_spin_box.value()) + https_url = u'https://%s:%s/' % (ipAddress, self.https_port_spin_box.value()) + self.remote_url.setText(u'%s' % (http_url, http_url)) + self.remote_https_url.setText(u'%s' % (https_url, https_url)) + http_url += u'stage' + https_url += u'stage' + self.stage_url.setText(u'%s' % (http_url, http_url)) + self.stage_https_url.setText(u'%s' % (https_url, https_url)) def load(self): - self.portSpinBox.setValue(Settings().value(self.settingsSection + u'/port')) - self.addressEdit.setText(Settings().value(self.settingsSection + u'/ip address')) - self.twelveHour = Settings().value(self.settingsSection + u'/twelve hour') - self.twelveHourCheckBox.setChecked(self.twelveHour) - self.setUrls() + self.port_spin_box.setValue(Settings().value(self.settingsSection + u'/port')) + self.https_port_spin_box.setValue(Settings().value(self.settingsSection + u'/https port')) + self.address_edit.setText(Settings().value(self.settingsSection + u'/ip address')) + self.twelve_hour = Settings().value(self.settingsSection + u'/twelve hour') + self.twelve_hour_check_box.setChecked(self.twelve_hour) + shared_data = AppLocation.get_directory(AppLocation.SharedData) + if not os.path.exists(os.path.join(shared_data, u'openlp.crt')) or \ + not os.path.exists(os.path.join(shared_data, u'openlp.key')): + self.https_settings_group_box.setChecked(False) + self.https_settings_group_box.setEnabled(False) + self.https_error_label.setVisible(True) + else: + self.https_settings_group_box.setChecked(Settings().value(self.settingsSection + u'/https enabled')) + self.https_settings_group_box.setEnabled(True) + self.https_error_label.setVisible(False) + self.user_login_group_box.setChecked(Settings().value(self.settingsSection + u'/authentication enabled')) + self.user_id.setText(Settings().value(self.settingsSection + u'/user id')) + self.password.setText(Settings().value(self.settingsSection + u'/password')) + self.set_urls() def save(self): changed = False - if Settings().value(self.settingsSection + u'/ip address') != self.addressEdit.text() or \ - Settings().value(self.settingsSection + u'/port') != self.portSpinBox.value(): + if Settings().value(self.settingsSection + u'/ip address') != self.address_edit.text() or \ + Settings().value(self.settingsSection + u'/port') != self.port_spin_box.value() or \ + Settings().value(self.settingsSection + u'/https port') != self.https_port_spin_box.value() or \ + Settings().value(self.settingsSection + u'/https enabled') != self.https_settings_group_box.isChecked(): changed = True - Settings().setValue(self.settingsSection + u'/port', self.portSpinBox.value()) - Settings().setValue(self.settingsSection + u'/ip address', self.addressEdit.text()) - Settings().setValue(self.settingsSection + u'/twelve hour', self.twelveHour) + Settings().setValue(self.settingsSection + u'/port', self.port_spin_box.value()) + Settings().setValue(self.settingsSection + u'/https port', self.https_port_spin_box.value()) + Settings().setValue(self.settingsSection + u'/https enabled', self.https_settings_group_box.isChecked()) + Settings().setValue(self.settingsSection + u'/ip address', self.address_edit.text()) + Settings().setValue(self.settingsSection + u'/twelve hour', self.twelve_hour) + Settings().setValue(self.settingsSection + u'/authentication enabled', self.user_login_group_box.isChecked()) + Settings().setValue(self.settingsSection + u'/user id', self.user_id.text()) + Settings().setValue(self.settingsSection + u'/password', self.password.text()) if changed: Registry().register_function(u'remotes_config_updated') - def onTwelveHourCheckBoxChanged(self, check_state): - self.twelveHour = False + def on_twelve_hour_check_box_changed(self, check_state): + self.twelve_hour = False # we have a set value convert to True/False if check_state == QtCore.Qt.Checked: - self.twelveHour = True + self.twelve_hour = True diff --git a/openlp/plugins/remotes/remoteplugin.py b/openlp/plugins/remotes/remoteplugin.py index e028dfcbb..7c1541ea6 100644 --- a/openlp/plugins/remotes/remoteplugin.py +++ b/openlp/plugins/remotes/remoteplugin.py @@ -37,6 +37,11 @@ log = logging.getLogger(__name__) __default_settings__ = { u'remotes/twelve hour': True, u'remotes/port': 4316, + u'remotes/https port': 4317, + u'remotes/https enabled': False, + u'remotes/user id': u'openlp', + u'remotes/password': u'password', + u'remotes/authentication enabled': False, u'remotes/ip address': u'0.0.0.0' } diff --git a/scripts/check_dependencies.py b/scripts/check_dependencies.py index 3485b8505..2ff62cf65 100755 --- a/scripts/check_dependencies.py +++ b/scripts/check_dependencies.py @@ -81,6 +81,7 @@ MODULES = [ 'enchant', 'BeautifulSoup', 'mako', + 'cherrypy', 'migrate', 'uno', ] From c62dab074ecaa31790089d809eb76a4530de53b3 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 8 Mar 2013 19:15:49 +0000 Subject: [PATCH 02/22] Basic Authentication now working --- openlp/core/ui/slidecontroller.py | 2 +- openlp/plugins/remotes/lib/httpauth.py | 18 ++++++++++------ openlp/plugins/remotes/lib/httpserver.py | 27 +++++++++++------------- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 98868bbcb..5fdd58aae 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -860,7 +860,7 @@ class SlideController(DisplayController): """ Go to the requested slide """ - if not self.serviceItem or not message[0]: + if not self.service_item or not message[0]: return index = int(message[0]) if not self.service_item: diff --git a/openlp/plugins/remotes/lib/httpauth.py b/openlp/plugins/remotes/lib/httpauth.py index ce3ea091e..6ed42cebd 100644 --- a/openlp/plugins/remotes/lib/httpauth.py +++ b/openlp/plugins/remotes/lib/httpauth.py @@ -45,6 +45,7 @@ def check_credentials(user_name, password): Returns None on success or a string describing the error on failure """ # @todo make from config + print "check_credentials" if user_name == 'openlp' and password == 'openlp': return None else: @@ -59,18 +60,17 @@ def check_auth(*args, **kwargs): is not None, a login is required and the entry is evaluated as a list of conditions that the user must fulfill """ - print "check" + print "check_auth" conditions = cherrypy.request.config.get('auth.require', None) + print urlparse.urlparse(cherrypy.url()), conditions print conditions - print args, kwargs - print urlparse.urlparse(cherrypy.url()) - url = urlparse.urlparse(cherrypy.url()) - print urlparse.parse_qs(url.query) if conditions is not None: username = cherrypy.session.get(SESSION_KEY) + print username if username: cherrypy.request.login = username for condition in conditions: + print "c ", condition # A condition is just a callable that returns true or false if not condition(): raise cherrypy.HTTPRedirect("/auth/login") @@ -84,14 +84,15 @@ def require_auth(*conditions): """ A decorator that appends conditions to the auth.require config variable. """ - print conditions def decorate(f): + """ + Lets process a decoration. + """ if not hasattr(f, '_cp_config'): f._cp_config = dict() if 'auth.require' not in f._cp_config: f._cp_config['auth.require'] = [] f._cp_config['auth.require'].extend(conditions) - print "a ", [f] return f return decorate @@ -182,6 +183,9 @@ class AuthController(object): @cherrypy.expose def logout(self, from_page="/"): + """ + Provides the actual logout functions + """ sess = cherrypy.session username = sess.get(SESSION_KEY, None) sess[SESSION_KEY] = None diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index dc9422932..345c0b1b7 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -205,8 +205,6 @@ class HttpConnection(object): """ Initialise the http connection. Listen out for socket signals. """ - #log.debug(u'Initialise HttpConnection: %s' % socket.peerAddress()) - #self.socket = socket self.parent = parent self.routes = [ (u'^/$', self.serve_file), @@ -215,8 +213,9 @@ class HttpConnection(object): (r'^/api/poll$', self.poll), (r'^/stage/api/poll$', self.poll), (r'^/api/controller/(live|preview)/(.*)$', self.controller), - (r'^/stage/api/controller/live/(.*)$', self.controller), + (r'^/stage/api/controller/(live|preview)/(.*)$', self.controller), (r'^/api/service/(.*)$', self.service), + (r'^/stage/api/service/(.*)$', self.service), (r'^/api/display/(hide|show|blank|theme|desktop)$', self.display), (r'^/api/alert$', self.alert), (r'^/api/plugin/(search)$', self.pluginInfo), @@ -227,11 +226,15 @@ class HttpConnection(object): self.translate() @cherrypy.expose - #@require_auth(auth) + @require_auth() def default(self, *args, **kwargs): """ - Handles the requests for the main url. This is secure depending on settings. + Handles the requests for the main url. This is secure depending on settings in config. """ + print "default" + url = urlparse.urlparse(cherrypy.url()) + self.url_params = urlparse.parse_qs(url.query) + print url # Loop through the routes we set up earlier and execute them return self._process_http_request(args, kwargs) @@ -244,22 +247,17 @@ class HttpConnection(object): url = urlparse.urlparse(cherrypy.url()) self.url_params = urlparse.parse_qs(url.query) print url - print [self.url_params] - #return self.serve_file(u'stage') return self._process_http_request(args, kwargs) @cherrypy.expose def files(self, *args, **kwargs): """ - Handles the requests for the stage url. This is not secure. + Handles the requests for the files url. This is not secure. """ print "files" url = urlparse.urlparse(cherrypy.url()) self.url_params = urlparse.parse_qs(url.query) print url - print [self.url_params] - print args - #return self.serve_file(args) return self._process_http_request(args, kwargs) def _process_http_request(self, args, kwargs): @@ -269,13 +267,11 @@ class HttpConnection(object): print "common handler" url = urlparse.urlparse(cherrypy.url()) self.url_params = urlparse.parse_qs(url.query) - print url - print [self.url_params] response = None for route, func in self.routes: match = re.match(route, url.path) if match: - print 'Route "%s" matched "%s"', route, url.path + print 'Route "%s" matched "%s"', route, url.path, func log.debug('Route "%s" matched "%s"', route, url.path) args = [] for param in match.groups(): @@ -346,7 +342,6 @@ class HttpConnection(object): Ultimately for i18n, this could first look for xx/file.html before falling back to file.html... where xx is the language, e.g. 'en' """ - print "serve_file", filename log.debug(u'serve file request %s' % filename) if not filename: filename = u'index.html' @@ -483,6 +478,7 @@ class HttpConnection(object): Registry().execute(event, [0]) json_data = {u'results': {u'success': True}} cherrypy.response.headers['Content-Type'] = u'application/json' + print json.dumps(json_data) return json.dumps(json_data) def service(self, action): @@ -549,6 +545,7 @@ class HttpConnection(object): """ Go live on an item of type ``plugin``. """ + print "go_live" try: id = json.loads(self.url_params[u'data'][0])[u'request'][u'id'] except KeyError, ValueError: From 5c79832bcc8f1448292c7ec5c1bfe5775ea75fd9 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Mon, 11 Mar 2013 21:00:00 +0000 Subject: [PATCH 03/22] More updates to authentication --- openlp/core/lib/settingstab.py | 1 + openlp/core/ui/slidecontroller.py | 2 - openlp/plugins/remotes/lib/httpauth.py | 90 ++++++++---------------- openlp/plugins/remotes/lib/httpserver.py | 32 ++++----- openlp/plugins/remotes/lib/remotetab.py | 7 +- 5 files changed, 46 insertions(+), 86 deletions(-) diff --git a/openlp/core/lib/settingstab.py b/openlp/core/lib/settingstab.py index 5b8a01fc6..51abfbe03 100644 --- a/openlp/core/lib/settingstab.py +++ b/openlp/core/lib/settingstab.py @@ -36,6 +36,7 @@ from PyQt4 import QtGui from openlp.core.lib import Registry + class SettingsTab(QtGui.QWidget): """ SettingsTab is a helper widget for plugins to define Tabs for the settings diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 5fdd58aae..44ae023b6 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -860,8 +860,6 @@ class SlideController(DisplayController): """ Go to the requested slide """ - if not self.service_item or not message[0]: - return index = int(message[0]) if not self.service_item: return diff --git a/openlp/plugins/remotes/lib/httpauth.py b/openlp/plugins/remotes/lib/httpauth.py index 6ed42cebd..7e0e2ebe5 100644 --- a/openlp/plugins/remotes/lib/httpauth.py +++ b/openlp/plugins/remotes/lib/httpauth.py @@ -34,24 +34,24 @@ http://tools.cherrypy.org/wiki/AuthenticationAndAccessRestrictions """ import cherrypy -import urlparse +import logging + +from openlp.core.lib import Settings SESSION_KEY = '_cp_openlp' +log = logging.getLogger(__name__) + def check_credentials(user_name, password): """ Verifies credentials for username and password. Returns None on success or a string describing the error on failure """ - # @todo make from config - print "check_credentials" - if user_name == 'openlp' and password == 'openlp': + if user_name == Settings().value(u'remotes/user id') and password == Settings().value(u'remotes/password'): return None else: return u"Incorrect username or password." - # if u.password != md5.new(password).hexdigest(): - # return u"Incorrect password" def check_auth(*args, **kwargs): @@ -60,17 +60,14 @@ def check_auth(*args, **kwargs): is not None, a login is required and the entry is evaluated as a list of conditions that the user must fulfill """ - print "check_auth" conditions = cherrypy.request.config.get('auth.require', None) - print urlparse.urlparse(cherrypy.url()), conditions - print conditions + if not Settings().value(u'remotes/authentication enabled'): + return None if conditions is not None: username = cherrypy.session.get(SESSION_KEY) - print username if username: cherrypy.request.login = username for condition in conditions: - print "c ", condition # A condition is just a callable that returns true or false if not condition(): raise cherrypy.HTTPRedirect("/auth/login") @@ -97,49 +94,6 @@ def require_auth(*conditions): return decorate -# Conditions are callables that return True -# if the user fulfills the conditions they define, False otherwise -# -# They can access the current username as cherrypy.request.login -# -# Define those at will however suits the application. - -#def member_of(groupname): -# def check(): -# # replace with actual check if is in -# return cherrypy.request.login == 'joe' and groupname == 'admin' -# return check - - -#def name_is(reqd_username): -# return lambda: reqd_username == cherrypy.request.login - -#def any_of(*conditions): -# """ -# Returns True if any of the conditions match -# """ -# def check(): -# for c in conditions: -# if c(): -# return True -# return False -# return check - -# By default all conditions are required, but this might still be -# needed if you want to use it inside of an any_of(...) condition -#def all_of(*conditions): -# """ -# Returns True if all of the conditions match -# """ -# def check(): -# for c in conditions: -# if not c(): -# return False -# return True -# return check -# Controller to provide login and logout actions - - class AuthController(object): def on_login(self, username): @@ -156,14 +110,26 @@ class AuthController(object): """ Provides a login form """ - return """ - - - %(msg)s
- Username:
- Password:
- - """ % locals() + return """ + + + + User Login + + + + + + + + + + + %(msg)s
+ Username:
+ Password:
+ + """ % locals() @cherrypy.expose def login(self, username=None, password=None, from_page="/"): diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 345c0b1b7..211608858 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -130,6 +130,7 @@ from openlp.plugins.remotes.lib.httpauth import AuthController, require_auth log = logging.getLogger(__name__) + class HttpServer(object): """ Ability to control OpenLP via a web browser. @@ -156,10 +157,19 @@ class HttpServer(object): clients. Listen out for socket connections. """ log.debug(u'Start CherryPy server') - port = Settings().value(self.plugin.settingsSection + u'/port') - address = Settings().value(self.plugin.settingsSection + u'/ip address') - server_config = {u'server.socket_host': str(address), - u'server.socket_port': port} + if Settings().value(self.plugin.settingsSection + u'/https enabled'): + port = Settings().value(self.plugin.settingsSection + u'/https port') + address = Settings().value(self.plugin.settingsSection + u'/ip address') + shared_data = AppLocation.get_directory(AppLocation.SharedData) + server_config = {u'server.socket_host': str(address), + u'server.socket_port': port, + u'server.ssl_certificate': os.path.join(shared_data, u'openlp.crt'), + u'server.ssl_private_key': os.path.join(shared_data, u'openlp.key')} + else: + port = Settings().value(self.plugin.settingsSection + u'/port') + address = Settings().value(self.plugin.settingsSection + u'/ip address') + server_config = {u'server.socket_host': str(address), + u'server.socket_port': port} cherrypy.config.update(server_config) cherrypy.config.update({'environment': 'embedded'}) cherrypy.config.update({'engine.autoreload_on': False}) @@ -231,10 +241,8 @@ class HttpConnection(object): """ Handles the requests for the main url. This is secure depending on settings in config. """ - print "default" url = urlparse.urlparse(cherrypy.url()) self.url_params = urlparse.parse_qs(url.query) - print url # Loop through the routes we set up earlier and execute them return self._process_http_request(args, kwargs) @@ -243,10 +251,8 @@ class HttpConnection(object): """ Handles the requests for the stage url. This is not secure. """ - print "Stage" url = urlparse.urlparse(cherrypy.url()) self.url_params = urlparse.parse_qs(url.query) - print url return self._process_http_request(args, kwargs) @cherrypy.expose @@ -254,24 +260,20 @@ class HttpConnection(object): """ Handles the requests for the files url. This is not secure. """ - print "files" url = urlparse.urlparse(cherrypy.url()) self.url_params = urlparse.parse_qs(url.query) - print url return self._process_http_request(args, kwargs) def _process_http_request(self, args, kwargs): """ Common function to process HTTP requests where secure or insecure """ - print "common handler" url = urlparse.urlparse(cherrypy.url()) self.url_params = urlparse.parse_qs(url.query) response = None for route, func in self.routes: match = re.match(route, url.path) if match: - print 'Route "%s" matched "%s"', route, url.path, func log.debug('Route "%s" matched "%s"', route, url.path) args = [] for param in match.groups(): @@ -474,11 +476,8 @@ class HttpConnection(object): # This slot expects an int within a list. id = data[u'request'][u'id'] Registry().execute(event, [id]) - else: - Registry().execute(event, [0]) json_data = {u'results': {u'success': True}} cherrypy.response.headers['Content-Type'] = u'application/json' - print json.dumps(json_data) return json.dumps(json_data) def service(self, action): @@ -545,7 +544,6 @@ class HttpConnection(object): """ Go live on an item of type ``plugin``. """ - print "go_live" try: id = json.loads(self.url_params[u'data'][0])[u'request'][u'id'] except KeyError, ValueError: @@ -577,7 +575,7 @@ class HttpConnection(object): def _http_not_found(self): cherrypy.response.status = 404 - cherrypy.response.body = ["Sorry, an error occured"] + cherrypy.response.body = ["Sorry, an error occurred "] def _get_service_manager(self): """ diff --git a/openlp/plugins/remotes/lib/remotetab.py b/openlp/plugins/remotes/lib/remotetab.py index b5c911257..7d47988fc 100644 --- a/openlp/plugins/remotes/lib/remotetab.py +++ b/openlp/plugins/remotes/lib/remotetab.py @@ -56,15 +56,13 @@ class RemoteTab(SettingsTab): self.address_label.setObjectName(u'address_label') self.address_edit = QtGui.QLineEdit(self.server_settings_group_box) self.address_edit.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) - - self.address_edit.setValidator(QtGui.QRegExpValidator(QtCore.QRegExp(u'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'), + self.address_edit.setValidator(QtGui.QRegExpValidator(QtCore.QRegExp(u'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'), self)) self.address_edit.setObjectName(u'address_edit') self.server_settings_layout.addRow(self.address_label, self.address_edit) self.twelve_hour_check_box = QtGui.QCheckBox(self.server_settings_group_box) self.twelve_hour_check_box.setObjectName(u'twelve_hour_check_box') self.server_settings_layout.addRow(self.twelve_hour_check_box) - self.leftLayout.addWidget(self.server_settings_group_box) self.http_settings_group_box = QtGui.QGroupBox(self.leftColumn) self.http_settings_group_box.setObjectName(u'http_settings_group_box') @@ -75,7 +73,6 @@ class RemoteTab(SettingsTab): self.port_spin_box = QtGui.QSpinBox(self.http_settings_group_box) self.port_spin_box.setMaximum(32767) self.port_spin_box.setObjectName(u'port_spin_box') - self.http_setting_layout.addRow(self.port_label, self.port_spin_box) self.remote_url_label = QtGui.QLabel(self.http_settings_group_box) self.remote_url_label.setObjectName(u'remote_url_label') @@ -244,7 +241,7 @@ class RemoteTab(SettingsTab): Settings().setValue(self.settingsSection + u'/user id', self.user_id.text()) Settings().setValue(self.settingsSection + u'/password', self.password.text()) if changed: - Registry().register_function(u'remotes_config_updated') + Registry().execute(u'remotes_config_updated') def on_twelve_hour_check_box_changed(self, check_state): From c580aac61be53f0de54667d1daa06b9ebacabd55 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Wed, 13 Mar 2013 19:09:26 +0000 Subject: [PATCH 04/22] custom login page --- openlp/plugins/remotes/html/login.html | 51 ++++++++++++++++++++++++ openlp/plugins/remotes/html/openlp.css | 8 ++++ openlp/plugins/remotes/lib/httpauth.py | 51 +++++++++++++----------- openlp/plugins/remotes/lib/httpserver.py | 2 + 4 files changed, 88 insertions(+), 24 deletions(-) create mode 100644 openlp/plugins/remotes/html/login.html diff --git a/openlp/plugins/remotes/html/login.html b/openlp/plugins/remotes/html/login.html new file mode 100644 index 000000000..4441958de --- /dev/null +++ b/openlp/plugins/remotes/html/login.html @@ -0,0 +1,51 @@ + + + + + + + + ${title} + + + + + + + + + + +

${message}

+

+ User name:
+ Password:
+ + + \ No newline at end of file diff --git a/openlp/plugins/remotes/html/openlp.css b/openlp/plugins/remotes/html/openlp.css index 4bc1bf907..60a8fe625 100644 --- a/openlp/plugins/remotes/html/openlp.css +++ b/openlp/plugins/remotes/html/openlp.css @@ -36,3 +36,11 @@ .ui-li .ui-btn-text a.ui-link-inherit{ white-space: normal; } + +.ui-page{ + padding: 100px 100px 100px 100px; + width: 300px; +} +.ui-input-text{ + width: 30px; +} \ No newline at end of file diff --git a/openlp/plugins/remotes/lib/httpauth.py b/openlp/plugins/remotes/lib/httpauth.py index 7e0e2ebe5..d46620855 100644 --- a/openlp/plugins/remotes/lib/httpauth.py +++ b/openlp/plugins/remotes/lib/httpauth.py @@ -35,8 +35,12 @@ http://tools.cherrypy.org/wiki/AuthenticationAndAccessRestrictions import cherrypy import logging +import os + +from mako.template import Template from openlp.core.lib import Settings +from openlp.core.utils import AppLocation, translate SESSION_KEY = '_cp_openlp' @@ -48,6 +52,7 @@ def check_credentials(user_name, password): Verifies credentials for username and password. Returns None on success or a string describing the error on failure """ + print "check" if user_name == Settings().value(u'remotes/user id') and password == Settings().value(u'remotes/password'): return None else: @@ -70,9 +75,12 @@ def check_auth(*args, **kwargs): for condition in conditions: # A condition is just a callable that returns true or false if not condition(): + print "r1" raise cherrypy.HTTPRedirect("/auth/login") else: + print "r2" raise cherrypy.HTTPRedirect("/auth/login") + print "r3" cherrypy.tools.auth = cherrypy.Tool('before_handler', check_auth) @@ -100,36 +108,31 @@ class AuthController(object): """ Called on successful login """ + pass def on_logout(self, username): """ Called on logout """ + pass - def get_loginform(self, username, msg="Enter login information", from_page="/"): + def get_login_form(self, username, message=None, from_page="/"): """ Provides a login form """ - return """ - - - - User Login - - - - - - - - -
- - %(msg)s
- Username:
- Password:
- - """ % locals() + if not message: + message = translate('RemotePlugin.Mobile', 'Enter login information') + variables = { + 'title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 User Login'), + 'from_page': from_page, + 'message': message, + 'username': username + } + directory = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), u'remotes', u'html') + login_html = os.path.normpath(os.path.join(directory, u'login.html')) + html = Template(filename=login_html, input_encoding=u'utf-8', output_encoding=u'utf-8').render(**variables) + cherrypy.response.headers['Content-Type'] = u'text/html' + return html @cherrypy.expose def login(self, username=None, password=None, from_page="/"): @@ -137,14 +140,14 @@ class AuthController(object): Provides the actual login control """ if username is None or password is None: - return self.get_loginform("", from_page=from_page) - + return self.get_login_form("", from_page=from_page) error_msg = check_credentials(username, password) if error_msg: - return self.get_loginform(username, error_msg, from_page) + return self.get_login_form(username, from_page, error_msg,) else: cherrypy.session[SESSION_KEY] = cherrypy.request.login = username self.on_login(username) + print from_page raise cherrypy.HTTPRedirect(from_page or "/") @cherrypy.expose diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 211608858..db6143eb0 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -174,6 +174,8 @@ class HttpServer(object): cherrypy.config.update({'environment': 'embedded'}) cherrypy.config.update({'engine.autoreload_on': False}) cherrypy.tree.mount(HttpConnection(self), '/', config=self.conf) + # Turn off the flood of access messages cause by poll + cherrypy.log.access_log.propagate = False cherrypy.engine.start() Registry().register_function(u'slidecontroller_live_changed', self.slide_change) Registry().register_function(u'slidecontroller_live_started', self.item_change) From 3c32bc75011eac3b7ed3d07d90c8f818a6dab80d Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Wed, 13 Mar 2013 19:51:56 +0000 Subject: [PATCH 05/22] Added tests --- openlp/plugins/remotes/lib/httpauth.py | 2 +- .../openlp_core_lib/test_settings.py | 4 +- .../openlp_plugins/remotes/__init__.py | 1 + .../openlp_plugins/remotes/test_auth.py | 65 +++++++++++++++++++ 4 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 tests/functional/openlp_plugins/remotes/__init__.py create mode 100644 tests/functional/openlp_plugins/remotes/test_auth.py diff --git a/openlp/plugins/remotes/lib/httpauth.py b/openlp/plugins/remotes/lib/httpauth.py index d46620855..6fe4197e2 100644 --- a/openlp/plugins/remotes/lib/httpauth.py +++ b/openlp/plugins/remotes/lib/httpauth.py @@ -56,7 +56,7 @@ def check_credentials(user_name, password): if user_name == Settings().value(u'remotes/user id') and password == Settings().value(u'remotes/password'): return None else: - return u"Incorrect username or password." + return translate('RemotePlugin.Mobile', 'Incorrect username or password.') def check_auth(*args, **kwargs): diff --git a/tests/functional/openlp_core_lib/test_settings.py b/tests/functional/openlp_core_lib/test_settings.py index 827bfa156..b06bb4eac 100644 --- a/tests/functional/openlp_core_lib/test_settings.py +++ b/tests/functional/openlp_core_lib/test_settings.py @@ -11,7 +11,9 @@ from PyQt4 import QtGui class TestSettings(TestCase): - + """ + Test the functions in the Settings module + """ def setUp(self): """ Create the UI diff --git a/tests/functional/openlp_plugins/remotes/__init__.py b/tests/functional/openlp_plugins/remotes/__init__.py new file mode 100644 index 000000000..f87606f07 --- /dev/null +++ b/tests/functional/openlp_plugins/remotes/__init__.py @@ -0,0 +1 @@ +__author__ = 'tim' diff --git a/tests/functional/openlp_plugins/remotes/test_auth.py b/tests/functional/openlp_plugins/remotes/test_auth.py new file mode 100644 index 000000000..a300c0127 --- /dev/null +++ b/tests/functional/openlp_plugins/remotes/test_auth.py @@ -0,0 +1,65 @@ +""" +This module contains tests for the lib submodule of the Remotes plugin. +""" +import os +from unittest import TestCase +from tempfile import mkstemp +from mock import patch + +from openlp.core.lib import Settings +from openlp.plugins.remotes.lib.httpauth import check_credentials +from PyQt4 import QtGui + +__default_settings__ = { + u'remotes/twelve hour': True, + u'remotes/port': 4316, + u'remotes/https port': 4317, + u'remotes/https enabled': False, + u'remotes/user id': u'openlp', + u'remotes/password': u'password', + u'remotes/authentication enabled': False, + u'remotes/ip address': u'0.0.0.0' +} + + +class TestLib(TestCase): + """ + Test the functions in the :mod:`lib` module. + """ + def setUp(self): + """ + Create the UI + """ + fd, self.ini_file = mkstemp(u'.ini') + Settings().set_filename(self.ini_file) + self.application = QtGui.QApplication.instance() + Settings().extend_default_settings(__default_settings__) + + def tearDown(self): + """ + Delete all the C++ objects at the end so that we don't have a segfault + """ + del self.application + os.unlink(self.ini_file) + os.unlink(Settings().fileName()) + + def check_credentials_test(self): + """ + Test the clean_string() function + """ + # GIVEN: A user and password + Settings().setValue(u'remotes/user id', u'twinkle') + Settings().setValue(u'remotes/password', u'mongoose') + + # WHEN: We run the string through the function + authenticated = check_credentials(u'', u'') + + # THEN: The string should be cleaned up and lower-cased + self.assertEqual(authenticated, u'Incorrect username or password.', + u'The return should be a error message string') + + # WHEN: We run the string through the function + authenticated = check_credentials(u'twinkle', u'mongoose') + + # THEN: The string should be cleaned up and lower-cased + self.assertEqual(authenticated, None, u'The return should be a None string') From d04dbd791f9ef141f947ca555fbc014cf84fd614 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 15 Mar 2013 08:40:00 +0000 Subject: [PATCH 06/22] more changes --- openlp/plugins/remotes/html/login.html | 2 +- openlp/plugins/remotes/html/openlp.css | 8 -------- openlp/plugins/remotes/html/openlp.js | 2 +- openlp/plugins/remotes/lib/httpauth.py | 3 --- openlp/plugins/remotes/lib/httpserver.py | 7 +------ 5 files changed, 3 insertions(+), 19 deletions(-) diff --git a/openlp/plugins/remotes/html/login.html b/openlp/plugins/remotes/html/login.html index 4441958de..736d3f0ab 100644 --- a/openlp/plugins/remotes/html/login.html +++ b/openlp/plugins/remotes/html/login.html @@ -33,7 +33,7 @@ ${title} - + diff --git a/openlp/plugins/remotes/html/openlp.css b/openlp/plugins/remotes/html/openlp.css index 60a8fe625..4bc1bf907 100644 --- a/openlp/plugins/remotes/html/openlp.css +++ b/openlp/plugins/remotes/html/openlp.css @@ -36,11 +36,3 @@ .ui-li .ui-btn-text a.ui-link-inherit{ white-space: normal; } - -.ui-page{ - padding: 100px 100px 100px 100px; - width: 300px; -} -.ui-input-text{ - width: 30px; -} \ No newline at end of file diff --git a/openlp/plugins/remotes/html/openlp.js b/openlp/plugins/remotes/html/openlp.js index 00877e332..7c5c19e32 100644 --- a/openlp/plugins/remotes/html/openlp.js +++ b/openlp/plugins/remotes/html/openlp.js @@ -359,5 +359,5 @@ $.ajaxSetup({cache: false}); $("#search").live("pageinit", function (event) { OpenLP.getSearchablePlugins(); }); -setInterval("OpenLP.pollServer();", 500); +setInterval("OpenLP.pollServer();", 5000); OpenLP.pollServer(); diff --git a/openlp/plugins/remotes/lib/httpauth.py b/openlp/plugins/remotes/lib/httpauth.py index 6fe4197e2..bd3c1f911 100644 --- a/openlp/plugins/remotes/lib/httpauth.py +++ b/openlp/plugins/remotes/lib/httpauth.py @@ -75,12 +75,9 @@ def check_auth(*args, **kwargs): for condition in conditions: # A condition is just a callable that returns true or false if not condition(): - print "r1" raise cherrypy.HTTPRedirect("/auth/login") else: - print "r2" raise cherrypy.HTTPRedirect("/auth/login") - print "r3" cherrypy.tools.auth = cherrypy.Tool('before_handler', check_auth) diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index b8acc574f..308533b9d 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -144,8 +144,6 @@ class HttpServer(object): self.plugin = plugin self.html_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), u'remotes', u'html') self.connections = [] - self.current_item = None - self.current_slide = None self.conf = {'/files': {u'tools.staticdir.on': True, u'tools.staticdir.dir': self.html_dir}} self.start_server() @@ -177,8 +175,6 @@ class HttpServer(object): # Turn off the flood of access messages cause by poll cherrypy.log.access_log.propagate = False cherrypy.engine.start() - Registry().register_function(u'slidecontroller_live_changed', self.slide_change) - Registry().register_function(u'slidecontroller_live_started', self.item_change) log.debug(u'TCP listening on port %d' % port) def close(self): @@ -481,8 +477,7 @@ class HttpConnection(object): if action == u'list': cherrypy.response.headers['Content-Type'] = u'application/json' return json.dumps({u'results': {u'items': self._get_service_items()}}) - else: - event += u'_item' + event += u'_item' if self.url_params and self.url_params.get(u'data'): try: data = json.loads(self.url_params[u'data'][0]) From 0f6216d653f1df071556e6e57867d8c2c477ff84 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sun, 17 Mar 2013 21:20:40 +0000 Subject: [PATCH 07/22] More changes --- openlp/core/ui/servicemanager.py | 5 ++++- openlp/core/ui/slidecontroller.py | 2 +- openlp/plugins/remotes/html/openlp.js | 4 ++-- openlp/plugins/remotes/lib/httpserver.py | 17 +++++++++++------ 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 9e99e2303..c7ab8dd77 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -270,7 +270,6 @@ class ServiceManagerDialog(object): Registry().register_function(u'config_screen_changed', self.regenerate_service_Items) Registry().register_function(u'theme_update_global', self.theme_change) Registry().register_function(u'mediaitem_suffix_reset', self.reset_supported_suffixes) - Registry().register_function(u'servicemanager_set_item', self.on_set_item) def drag_enter_event(self, event): """ @@ -313,6 +312,9 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog): self.layout.setSpacing(0) self.layout.setMargin(0) self.setup_ui(self) + # Need to use event as called across threads and UI is updated + print self + QtCore.QObject.connect(self, QtCore.SIGNAL(u'servicemanager_set_item'), self.on_set_item) def set_modified(self, modified=True): """ @@ -1008,6 +1010,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog): """ Called by a signal to select a specific item. """ + print "hello", message self.set_item(int(message)) def set_item(self, index): diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 0c22a6353..3b2824fda 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -99,7 +99,7 @@ class SlideController(DisplayController): u'delay_spin_box' ] self.audio_list = [ - u'audio_pause_item', + u'audioPauseItem', u'audio_time_label' ] self.wide_menu = [ diff --git a/openlp/plugins/remotes/html/openlp.js b/openlp/plugins/remotes/html/openlp.js index 7c5c19e32..3cbe65366 100644 --- a/openlp/plugins/remotes/html/openlp.js +++ b/openlp/plugins/remotes/html/openlp.js @@ -147,7 +147,7 @@ window.OpenLP = { }, pollServer: function () { $.getJSON( - "/api/poll", + "/stage/api/poll", function (data, status) { var prevItem = OpenLP.currentItem; OpenLP.currentSlide = data.results.slide; @@ -359,5 +359,5 @@ $.ajaxSetup({cache: false}); $("#search").live("pageinit", function (event) { OpenLP.getSearchablePlugins(); }); -setInterval("OpenLP.pollServer();", 5000); +setInterval("OpenLP.pollServer();", 500); OpenLP.pollServer(); diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 308533b9d..a594c1bca 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -199,7 +199,7 @@ class HttpConnection(object): def __init__(self, parent): """ - Initialise the http connection. Listen out for socket signals. + Initialise the CherryPy Server """ self.parent = parent self.routes = [ @@ -229,6 +229,9 @@ class HttpConnection(object): """ url = urlparse.urlparse(cherrypy.url()) self.url_params = urlparse.parse_qs(url.query) + self.request_data = None + if isinstance(kwargs, dict): + self.request_data = kwargs.get(u'data', None) # Loop through the routes we set up earlier and execute them return self._process_http_request(args, kwargs) @@ -255,7 +258,7 @@ class HttpConnection(object): Common function to process HTTP requests where secure or insecure """ url = urlparse.urlparse(cherrypy.url()) - self.url_params = urlparse.parse_qs(url.query) + self.url_params = kwargs response = None for route, func in self.routes: match = re.match(route, url.path) @@ -478,13 +481,15 @@ class HttpConnection(object): cherrypy.response.headers['Content-Type'] = u'application/json' return json.dumps({u'results': {u'items': self._get_service_items()}}) event += u'_item' - if self.url_params and self.url_params.get(u'data'): + if self.request_data: try: - data = json.loads(self.url_params[u'data'][0]) - except KeyError, ValueError: + data = json.loads(self.request_data)[u'request'][u'id'] + except KeyError: return self._http_bad_request() - Registry().execute(event, data[u'request'][u'id']) + print "A", event , data + self.service_manager.emit(QtCore.SIGNAL(event, data)) else: + print "B", event Registry().execute(event) cherrypy.response.headers['Content-Type'] = u'application/json' return json.dumps({u'results': {u'success': True}}) From 729c93b70b6a154dfc723405dd40c75410a57c8b Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Wed, 20 Mar 2013 20:17:00 +0000 Subject: [PATCH 08/22] More changes --- openlp/core/ui/servicemanager.py | 2 +- openlp/plugins/remotes/lib/httpserver.py | 40 +++++++++++------------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 97c6ca2db..7c3745299 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -1008,7 +1008,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog): def on_set_item(self, message): """ - Called by a signal to select a specific item. + Called by a signal to select a specific item and make it live usually from remote. """ print "hello", message self.set_item(int(message)) diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index a594c1bca..368bf0192 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -155,17 +155,17 @@ class HttpServer(object): clients. Listen out for socket connections. """ log.debug(u'Start CherryPy server') - if Settings().value(self.plugin.settingsSection + u'/https enabled'): - port = Settings().value(self.plugin.settingsSection + u'/https port') - address = Settings().value(self.plugin.settingsSection + u'/ip address') + if Settings().value(self.plugin.settings_section + u'/https enabled'): + port = Settings().value(self.plugin.settings_section + u'/https port') + address = Settings().value(self.plugin.settings_section + u'/ip address') shared_data = AppLocation.get_directory(AppLocation.SharedData) server_config = {u'server.socket_host': str(address), u'server.socket_port': port, u'server.ssl_certificate': os.path.join(shared_data, u'openlp.crt'), u'server.ssl_private_key': os.path.join(shared_data, u'openlp.key')} else: - port = Settings().value(self.plugin.settingsSection + u'/port') - address = Settings().value(self.plugin.settingsSection + u'/ip address') + port = Settings().value(self.plugin.settings_section + u'/port') + address = Settings().value(self.plugin.settings_section + u'/ip address') server_config = {u'server.socket_host': str(address), u'server.socket_port': port} cherrypy.config.update(server_config) @@ -214,7 +214,7 @@ class HttpConnection(object): (r'^/stage/api/service/(.*)$', self.service), (r'^/api/display/(hide|show|blank|theme|desktop)$', self.display), (r'^/api/alert$', self.alert), - (r'^/api/plugin/(search)$', self.pluginInfo), + (r'^/api/plugin/(search)$', self.plugin_info), (r'^/api/(.*)/search$', self.search), (r'^/api/(.*)/live$', self.go_live), (r'^/api/(.*)/add$', self.add_to_service) @@ -456,9 +456,9 @@ class HttpConnection(object): if current_item: json_data[u'results'][u'item'] = self.live_controller.service_item.unique_identifier else: - if self.url_params and self.url_params.get(u'data'): + if self.request_data: try: - data = json.loads(self.url_params[u'data'][0]) + data = json.loads(self.request_data)[u'request'][u'id'] except KeyError, ValueError: return self._http_bad_request() log.info(data) @@ -486,15 +486,13 @@ class HttpConnection(object): data = json.loads(self.request_data)[u'request'][u'id'] except KeyError: return self._http_bad_request() - print "A", event , data - self.service_manager.emit(QtCore.SIGNAL(event, data)) + self.service_manager.emit(QtCore.SIGNAL(event), data) else: - print "B", event Registry().execute(event) cherrypy.response.headers['Content-Type'] = u'application/json' return json.dumps({u'results': {u'success': True}}) - def pluginInfo(self, action): + def plugin_info(self, action): """ Return plugin related information, based on the action. @@ -505,8 +503,8 @@ class HttpConnection(object): if action == u'search': searches = [] for plugin in self.plugin_manager.plugins: - if plugin.status == PluginStatus.Active and plugin.mediaItem and plugin.mediaItem.hasSearch: - searches.append([plugin.name, unicode(plugin.textStrings[StringContent.Name][u'plural'])]) + if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.hasSearch: + searches.append([plugin.name, unicode(plugin.text_strings[StringContent.Name][u'plural'])]) cherrypy.response.headers['Content-Type'] = u'application/json' return json.dumps({u'results': {u'items': searches}}) @@ -523,8 +521,8 @@ class HttpConnection(object): return self._http_bad_request() text = urllib.unquote(text) plugin = self.plugin_manager.get_plugin_by_name(plugin_name) - if plugin.status == PluginStatus.Active and plugin.mediaItem and plugin.mediaItem.hasSearch: - results = plugin.mediaItem.search(text, False) + if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search: + results = plugin.media_item.search(text, False) else: results = [] cherrypy.response.headers['Content-Type'] = u'application/json' @@ -539,8 +537,8 @@ class HttpConnection(object): except KeyError, ValueError: return self._http_bad_request() plugin = self.plugin_manager.get_plugin_by_name(type) - if plugin.status == PluginStatus.Active and plugin.mediaItem: - plugin.mediaItem.goLive(id, remote=True) + if plugin.status == PluginStatus.Active and plugin.media_item: + plugin.media_item.go_live(id, remote=True) return self._http_success() def add_to_service(self, plugin_name): @@ -552,9 +550,9 @@ class HttpConnection(object): except KeyError, ValueError: return self._http_bad_request() plugin = self.plugin_manager.get_plugin_by_name(type) - if plugin.status == PluginStatus.Active and plugin.mediaItem: - item_id = plugin.mediaItem.createItemFromId(id) - plugin.mediaItem.addToService(item_id, remote=True) + if plugin.status == PluginStatus.Active and plugin.media_item: + item_id = plugin.media_item.create_item_from_id(id) + plugin.media_item.add_to_service(item_id, remote=True) self._http_success() def _http_success(self): From 401f5ac2be3b087abfecf4bf6fbbcbca06289d7e Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 26 Mar 2013 08:55:05 +0000 Subject: [PATCH 09/22] More updates --- openlp/core/ui/slidecontroller.py | 2 - openlp/plugins/remotes/html/login.html | 3 +- openlp/plugins/remotes/lib/httpauth.py | 10 ++--- openlp/plugins/remotes/lib/httpserver.py | 14 ++----- .../openlp_plugins/remotes/test_auth.py | 38 +++++++++++++++---- 5 files changed, 40 insertions(+), 27 deletions(-) diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 518e2a715..a66d0057b 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -1080,7 +1080,6 @@ class SlideController(DisplayController): """ Go to the next slide. """ - print "next" if not self.service_item: return Registry().execute(u'%s_next' % self.service_item.name.lower(), [self.service_item, self.is_live]) @@ -1108,7 +1107,6 @@ class SlideController(DisplayController): """ Go to the previous slide. """ - print "prev" if not self.service_item: return Registry().execute(u'%s_previous' % self.service_item.name.lower(), [self.service_item, self.is_live]) diff --git a/openlp/plugins/remotes/html/login.html b/openlp/plugins/remotes/html/login.html index 736d3f0ab..5d649629b 100644 --- a/openlp/plugins/remotes/html/login.html +++ b/openlp/plugins/remotes/html/login.html @@ -36,7 +36,6 @@ - @@ -48,4 +47,4 @@ Password:
- \ No newline at end of file + diff --git a/openlp/plugins/remotes/lib/httpauth.py b/openlp/plugins/remotes/lib/httpauth.py index bd3c1f911..e74dc9c04 100644 --- a/openlp/plugins/remotes/lib/httpauth.py +++ b/openlp/plugins/remotes/lib/httpauth.py @@ -59,11 +59,10 @@ def check_credentials(user_name, password): return translate('RemotePlugin.Mobile', 'Incorrect username or password.') -def check_auth(*args, **kwargs): +def check_authentication(*args, **kwargs): """ - A tool that looks in config for 'auth.require'. If found and it - is not None, a login is required and the entry is evaluated as a list of - conditions that the user must fulfill + A tool that looks in config for 'auth.require'. If found and it is not None, a login is required and the entry is + evaluated as a list of conditions that the user must fulfill """ conditions = cherrypy.request.config.get('auth.require', None) if not Settings().value(u'remotes/authentication enabled'): @@ -79,7 +78,7 @@ def check_auth(*args, **kwargs): else: raise cherrypy.HTTPRedirect("/auth/login") -cherrypy.tools.auth = cherrypy.Tool('before_handler', check_auth) +cherrypy.tools.auth = cherrypy.Tool('before_handler', check_authentication) def require_auth(*conditions): @@ -136,6 +135,7 @@ class AuthController(object): """ Provides the actual login control """ + print "login", from_page if username is None or password is None: return self.get_login_form("", from_page=from_page) error_msg = check_credentials(username, password) diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 485a28240..e397d821f 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -43,7 +43,7 @@ the remotes. ``/files/{filename}`` Serve a static file. -``/api/poll`` +``/stage/api/poll`` Poll to see if there are any changes. Returns a JSON-encoded dict of any changes that occurred:: @@ -227,30 +227,24 @@ class HttpConnection(object): """ Handles the requests for the main url. This is secure depending on settings in config. """ - #url = urlparse.urlparse(cherrypy.url()) - #self.url_params = urlparse.parse_qs(url.query) + print "default" self.request_data = None if isinstance(kwargs, dict): self.request_data = kwargs.get(u'data', None) - # Loop through the routes we set up earlier and execute them return self._process_http_request(args, kwargs) @cherrypy.expose def stage(self, *args, **kwargs): """ - Handles the requests for the stage url. This is not secure. + Handles the requests for the /stage url. This is not secure. """ - #url = urlparse.urlparse(cherrypy.url()) - #self.url_params = urlparse.parse_qs(url.query) return self._process_http_request(args, kwargs) @cherrypy.expose def files(self, *args, **kwargs): """ - Handles the requests for the files url. This is not secure. + Handles the requests for the /files url. This is not secure. """ - #url = urlparse.urlparse(cherrypy.url()) - #self.url_params = urlparse.parse_qs(url.query) return self._process_http_request(args, kwargs) def _process_http_request(self, args, kwargs): diff --git a/tests/functional/openlp_plugins/remotes/test_auth.py b/tests/functional/openlp_plugins/remotes/test_auth.py index a300c0127..8afabd0c9 100644 --- a/tests/functional/openlp_plugins/remotes/test_auth.py +++ b/tests/functional/openlp_plugins/remotes/test_auth.py @@ -4,10 +4,11 @@ This module contains tests for the lib submodule of the Remotes plugin. import os from unittest import TestCase from tempfile import mkstemp -from mock import patch +from mock import patch, MagicMock +import cherrypy from openlp.core.lib import Settings -from openlp.plugins.remotes.lib.httpauth import check_credentials +from openlp.plugins.remotes.lib.httpauth import check_credentials, check_authentication from PyQt4 import QtGui __default_settings__ = { @@ -21,6 +22,8 @@ __default_settings__ = { u'remotes/ip address': u'0.0.0.0' } +SESSION_KEY = '_cp_openlp' + class TestLib(TestCase): """ @@ -34,6 +37,10 @@ class TestLib(TestCase): Settings().set_filename(self.ini_file) self.application = QtGui.QApplication.instance() Settings().extend_default_settings(__default_settings__) + cherrypy.config.update({'environment': "test_suite"}) + # prevent the HTTP server from ever starting + cherrypy.server.unsubscribe() + cherrypy.engine.start() def tearDown(self): """ @@ -42,24 +49,39 @@ class TestLib(TestCase): del self.application os.unlink(self.ini_file) os.unlink(Settings().fileName()) + cherrypy.engine.exit() def check_credentials_test(self): """ - Test the clean_string() function + Test the Authentication check routine. """ - # GIVEN: A user and password + # GIVEN: A user and password in settings Settings().setValue(u'remotes/user id', u'twinkle') Settings().setValue(u'remotes/password', u'mongoose') - # WHEN: We run the string through the function + # WHEN: We run the function with no input authenticated = check_credentials(u'', u'') - # THEN: The string should be cleaned up and lower-cased + # THEN: The authentication will fail with an error message self.assertEqual(authenticated, u'Incorrect username or password.', u'The return should be a error message string') - # WHEN: We run the string through the function + # WHEN: We run the function with the correct input authenticated = check_credentials(u'twinkle', u'mongoose') - # THEN: The string should be cleaned up and lower-cased + # THEN: The authentication will pass. self.assertEqual(authenticated, None, u'The return should be a None string') + + def check_auth_inactive_test(self): + """ + Test the Authentication check routine. + """ + # GIVEN: A access which is secure + Settings().setValue(u'remotes/authentication enabled', False) + + # WHEN: We run the function with no input + with patch(u'cherrypy.request.config'): + authenticated = check_authentication(None, None) + + # THEN: The authentication will fail with an error message + self.assertEqual(authenticated, None, u'The authentication should return None as not required') From 451189b22202a5075dcadcf714e4d3c6b9067630 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 26 Mar 2013 11:39:32 +0000 Subject: [PATCH 10/22] Fix up login page to handle redirection correctly --- openlp/plugins/remotes/html/login.html | 2 -- openlp/plugins/remotes/lib/httpauth.py | 4 +--- openlp/plugins/remotes/lib/httpserver.py | 8 +++++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/openlp/plugins/remotes/html/login.html b/openlp/plugins/remotes/html/login.html index 5d649629b..60c71166c 100644 --- a/openlp/plugins/remotes/html/login.html +++ b/openlp/plugins/remotes/html/login.html @@ -35,8 +35,6 @@ - -
diff --git a/openlp/plugins/remotes/lib/httpauth.py b/openlp/plugins/remotes/lib/httpauth.py index e74dc9c04..cfa14a4a1 100644 --- a/openlp/plugins/remotes/lib/httpauth.py +++ b/openlp/plugins/remotes/lib/httpauth.py @@ -52,7 +52,6 @@ def check_credentials(user_name, password): Verifies credentials for username and password. Returns None on success or a string describing the error on failure """ - print "check" if user_name == Settings().value(u'remotes/user id') and password == Settings().value(u'remotes/password'): return None else: @@ -128,6 +127,7 @@ class AuthController(object): login_html = os.path.normpath(os.path.join(directory, u'login.html')) html = Template(filename=login_html, input_encoding=u'utf-8', output_encoding=u'utf-8').render(**variables) cherrypy.response.headers['Content-Type'] = u'text/html' + cherrypy.response.status = 200 return html @cherrypy.expose @@ -135,7 +135,6 @@ class AuthController(object): """ Provides the actual login control """ - print "login", from_page if username is None or password is None: return self.get_login_form("", from_page=from_page) error_msg = check_credentials(username, password) @@ -144,7 +143,6 @@ class AuthController(object): else: cherrypy.session[SESSION_KEY] = cherrypy.request.login = username self.on_login(username) - print from_page raise cherrypy.HTTPRedirect(from_page or "/") @cherrypy.expose diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index e397d821f..6759d10f3 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -227,7 +227,6 @@ class HttpConnection(object): """ Handles the requests for the main url. This is secure depending on settings in config. """ - print "default" self.request_data = None if isinstance(kwargs, dict): self.request_data = kwargs.get(u'data', None) @@ -252,7 +251,6 @@ class HttpConnection(object): Common function to process HTTP requests where secure or insecure """ url = urlparse.urlparse(cherrypy.url()) - #self.url_params = kwargs response = None for route, func in self.routes: match = re.match(route, url.path) @@ -315,7 +313,11 @@ class HttpConnection(object): 'no_results': translate('RemotePlugin.Mobile', 'No Results'), 'options': translate('RemotePlugin.Mobile', 'Options'), 'service': translate('RemotePlugin.Mobile', 'Service'), - 'slides': translate('RemotePlugin.Mobile', 'Slides') + 'slides': translate('RemotePlugin.Mobile', 'Slides'), + 'title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 User Login'), + 'from_page': "", + 'message': "", + 'username': "username" } def serve_file(self, filename=None): From ab011a22c195ec0781e57b38f97de3ab3538d3e1 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Thu, 28 Mar 2013 14:45:47 +0000 Subject: [PATCH 11/22] last version before big refactor --- openlp/plugins/remotes/lib/httpauth.py | 2 ++ openlp/plugins/remotes/lib/httpserver.py | 6 ++---- openlp/plugins/remotes/remoteplugin.py | 1 + tests/functional/openlp_plugins/remotes/test_auth.py | 12 +++--------- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/openlp/plugins/remotes/lib/httpauth.py b/openlp/plugins/remotes/lib/httpauth.py index cfa14a4a1..f760bfc8c 100644 --- a/openlp/plugins/remotes/lib/httpauth.py +++ b/openlp/plugins/remotes/lib/httpauth.py @@ -64,6 +64,8 @@ def check_authentication(*args, **kwargs): evaluated as a list of conditions that the user must fulfill """ conditions = cherrypy.request.config.get('auth.require', None) + a = cherrypy.request + print a if not Settings().value(u'remotes/authentication enabled'): return None if conditions is not None: diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 6759d10f3..22a75e49e 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -138,7 +138,7 @@ class HttpServer(object): def __init__(self, plugin): """ - Initialise the httpserver, and start the server. + Initialise the http server, and start the server. """ log.debug(u'Initialise httpserver') self.plugin = plugin @@ -146,13 +146,11 @@ class HttpServer(object): self.connections = [] self.conf = {'/files': {u'tools.staticdir.on': True, u'tools.staticdir.dir': self.html_dir}} - self.start_server() def start_server(self): """ Start the http server, use the port in the settings default to 4316. - Listen out for slide and song changes so they can be broadcast to - clients. Listen out for socket connections. + Listen out for slide and song changes so they can be broadcast to clients. Listen out for socket connections. """ log.debug(u'Start CherryPy server') if Settings().value(self.plugin.settings_section + u'/https enabled'): diff --git a/openlp/plugins/remotes/remoteplugin.py b/openlp/plugins/remotes/remoteplugin.py index 4e37f9853..5be537b60 100644 --- a/openlp/plugins/remotes/remoteplugin.py +++ b/openlp/plugins/remotes/remoteplugin.py @@ -66,6 +66,7 @@ class RemotesPlugin(Plugin): log.debug(u'initialise') Plugin.initialise(self) self.server = HttpServer(self) + self.server.start_server() def finalise(self): """ diff --git a/tests/functional/openlp_plugins/remotes/test_auth.py b/tests/functional/openlp_plugins/remotes/test_auth.py index 8afabd0c9..32622af9c 100644 --- a/tests/functional/openlp_plugins/remotes/test_auth.py +++ b/tests/functional/openlp_plugins/remotes/test_auth.py @@ -5,7 +5,6 @@ import os from unittest import TestCase from tempfile import mkstemp from mock import patch, MagicMock -import cherrypy from openlp.core.lib import Settings from openlp.plugins.remotes.lib.httpauth import check_credentials, check_authentication @@ -25,7 +24,7 @@ __default_settings__ = { SESSION_KEY = '_cp_openlp' -class TestLib(TestCase): +class TestAuth(TestCase): """ Test the functions in the :mod:`lib` module. """ @@ -37,10 +36,6 @@ class TestLib(TestCase): Settings().set_filename(self.ini_file) self.application = QtGui.QApplication.instance() Settings().extend_default_settings(__default_settings__) - cherrypy.config.update({'environment': "test_suite"}) - # prevent the HTTP server from ever starting - cherrypy.server.unsubscribe() - cherrypy.engine.start() def tearDown(self): """ @@ -49,11 +44,10 @@ class TestLib(TestCase): del self.application os.unlink(self.ini_file) os.unlink(Settings().fileName()) - cherrypy.engine.exit() def check_credentials_test(self): """ - Test the Authentication check routine. + Test the Authentication check routine with credentials. """ # GIVEN: A user and password in settings Settings().setValue(u'remotes/user id', u'twinkle') @@ -74,7 +68,7 @@ class TestLib(TestCase): def check_auth_inactive_test(self): """ - Test the Authentication check routine. + Test the Authentication check routine when inactive. """ # GIVEN: A access which is secure Settings().setValue(u'remotes/authentication enabled', False) From 8cf886971c1b5f1c9007a5e92d0cdd44c4897878 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 29 Mar 2013 07:48:55 +0000 Subject: [PATCH 12/22] Refactor http server --- openlp/core/ui/settingsform.py | 1 + openlp/plugins/remotes/lib/httpserver.py | 150 ++++++++++++++--------- openlp/plugins/remotes/lib/remotetab.py | 24 +++- openlp/plugins/remotes/remoteplugin.py | 8 +- 4 files changed, 114 insertions(+), 69 deletions(-) diff --git a/openlp/core/ui/settingsform.py b/openlp/core/ui/settingsform.py index eeb85fa66..bc40539cf 100644 --- a/openlp/core/ui/settingsform.py +++ b/openlp/core/ui/settingsform.py @@ -96,6 +96,7 @@ class SettingsForm(QtGui.QDialog, Ui_SettingsDialog): """ Process the form saving the settings """ + log.debug(u'Processing settings exit') for tabIndex in range(self.stacked_layout.count()): self.stacked_layout.widget(tabIndex).save() # if the display of image background are changing we need to regenerate the image cache diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 22a75e49e..b660ec094 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -126,54 +126,115 @@ from PyQt4 import QtCore from openlp.core.lib import Registry, Settings, PluginStatus, StringContent from openlp.core.utils import AppLocation, translate -from openlp.plugins.remotes.lib.httpauth import AuthController, require_auth + +from cherrypy._cpcompat import md5, sha, ntob log = logging.getLogger(__name__) +def sha_password_encrypter(password): + + return sha(ntob(password)).hexdigest() + + +def fetch_password(username): + if username != Settings().value(u'remotes/user id'): + return None + print "fetch password", username + return sha(ntob(Settings().value(u'remotes/password'))).hexdigest() + + class HttpServer(object): """ Ability to control OpenLP via a web browser. """ + _cp_config = { + 'tools.sessions.on': True, + 'tools.auth.on': True + } + def __init__(self, plugin): """ Initialise the http server, and start the server. """ log.debug(u'Initialise httpserver') self.plugin = plugin - self.html_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), u'remotes', u'html') - self.connections = [] - self.conf = {'/files': {u'tools.staticdir.on': True, - u'tools.staticdir.dir': self.html_dir}} + self.router = HttpRouter() def start_server(self): """ - Start the http server, use the port in the settings default to 4316. - Listen out for slide and song changes so they can be broadcast to clients. Listen out for socket connections. + Start the http server based on configuration. """ log.debug(u'Start CherryPy server') + # Define to security levels and add the router code + self.root = self.Public() + self.root.files = self.Files() + self.root.stage = self.Stage() + self.root.router = self.router + self.root.files.router = self.router + self.root.stage.router = self.router + cherrypy.tree.mount(self.root, '/', config=self.define_config()) + # Turn off the flood of access messages cause by poll + cherrypy.log.access_log.propagate = False + cherrypy.engine.start() + + def define_config(self): if Settings().value(self.plugin.settings_section + u'/https enabled'): port = Settings().value(self.plugin.settings_section + u'/https port') address = Settings().value(self.plugin.settings_section + u'/ip address') shared_data = AppLocation.get_directory(AppLocation.SharedData) - server_config = {u'server.socket_host': str(address), - u'server.socket_port': port, - u'server.ssl_certificate': os.path.join(shared_data, u'openlp.crt'), - u'server.ssl_private_key': os.path.join(shared_data, u'openlp.key')} + cherrypy.config.update({u'server.socket_host': str(address), + u'server.socket_port': port, + u'server.ssl_certificate': os.path.join(shared_data, u'openlp.crt'), + u'server.ssl_private_key': os.path.join(shared_data, u'openlp.key')}) else: port = Settings().value(self.plugin.settings_section + u'/port') address = Settings().value(self.plugin.settings_section + u'/ip address') - server_config = {u'server.socket_host': str(address), - u'server.socket_port': port} - cherrypy.config.update(server_config) - cherrypy.config.update({'environment': 'embedded'}) - cherrypy.config.update({'engine.autoreload_on': False}) - cherrypy.tree.mount(HttpConnection(self), '/', config=self.conf) - # Turn off the flood of access messages cause by poll - cherrypy.log.access_log.propagate = False - cherrypy.engine.start() - log.debug(u'TCP listening on port %d' % port) + cherrypy.config.update({u'server.socket_host': str(address)}) + cherrypy.config.update({u'server.socket_port': port}) + cherrypy.config.update({u'environment': u'embedded'}) + cherrypy.config.update({u'engine.autoreload_on': False}) + directory_config = {u'/': {u'tools.staticdir.on': True, + u'tools.staticdir.dir': self.router.html_dir, + u'tools.basic_auth.on': Settings().value(u'remotes/authentication enabled'), + u'tools.basic_auth.realm': u'OpenLP Remote Login', + u'tools.basic_auth.users': fetch_password, + u'tools.basic_auth.encrypt': sha_password_encrypter}, + u'/files': {u'tools.staticdir.on': True, + u'tools.staticdir.dir': self.router.html_dir, + u'tools.basic_auth.on': False}, + u'/stage': {u'tools.staticdir.on': True, + u'tools.staticdir.dir': self.router.html_dir, + u'tools.basic_auth.on': False}} + return directory_config + + def reload_config(self): + cherrypy.tree.mount(self.root, '/', config=self.define_config()) + cherrypy.config.reset() + + class Public: + @cherrypy.expose + def default(self, *args, **kwargs): + print "public" + self.router.request_data = None + if isinstance(kwargs, dict): + self.router.request_data = kwargs.get(u'data', None) + url = urlparse.urlparse(cherrypy.url()) + return self.router.process_http_request(url.path, *args) + + class Files: + @cherrypy.expose + def default(self, *args, **kwargs): + print "files" + url = urlparse.urlparse(cherrypy.url()) + return self.router.process_http_request(url.path, *args) + + class Stage: + @cherrypy.expose + def default(self, *args, **kwargs): + url = urlparse.urlparse(cherrypy.url()) + return self.router.process_http_request(url.path, *args) def close(self): """ @@ -181,25 +242,17 @@ class HttpServer(object): """ log.debug(u'close http server') cherrypy.engine.exit() - cherrypy.engine.stop() -class HttpConnection(object): +class HttpRouter(object): """ A single connection, this handles communication between the server and the client. """ - _cp_config = { - 'tools.sessions.on': True, - 'tools.auth.on': True - } - auth = AuthController() - - def __init__(self, parent): + def __init__(self): """ Initialise the CherryPy Server """ - self.parent = parent self.routes = [ (u'^/$', self.serve_file), (u'^/(stage)$', self.serve_file), @@ -218,33 +271,9 @@ class HttpConnection(object): (r'^/api/(.*)/add$', self.add_to_service) ] self.translate() + self.html_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), u'remotes', u'html') - @cherrypy.expose - @require_auth() - def default(self, *args, **kwargs): - """ - Handles the requests for the main url. This is secure depending on settings in config. - """ - self.request_data = None - if isinstance(kwargs, dict): - self.request_data = kwargs.get(u'data', None) - return self._process_http_request(args, kwargs) - - @cherrypy.expose - def stage(self, *args, **kwargs): - """ - Handles the requests for the /stage url. This is not secure. - """ - return self._process_http_request(args, kwargs) - - @cherrypy.expose - def files(self, *args, **kwargs): - """ - Handles the requests for the /files url. This is not secure. - """ - return self._process_http_request(args, kwargs) - - def _process_http_request(self, args, kwargs): + def process_http_request(self, url_path, *args): """ Common function to process HTTP requests where secure or insecure """ @@ -253,7 +282,7 @@ class HttpConnection(object): for route, func in self.routes: match = re.match(route, url.path) if match: - log.debug('Route "%s" matched "%s"', route, url.path) + log.debug('Route "%s" matched "%s"', route, url_path) args = [] for param in match.groups(): args.append(param) @@ -332,8 +361,8 @@ class HttpConnection(object): filename = u'index.html' elif filename == u'stage': filename = u'stage.html' - path = os.path.normpath(os.path.join(self.parent.html_dir, filename)) - if not path.startswith(self.parent.html_dir): + path = os.path.normpath(os.path.join(self.html_dir, filename)) + if not path.startswith(self.html_dir): return self._http_not_found() ext = os.path.splitext(filename)[1] html = None @@ -450,7 +479,6 @@ class HttpConnection(object): if current_item: json_data[u'results'][u'item'] = self.live_controller.service_item.unique_identifier else: - print event if self.request_data: try: data = json.loads(self.request_data)[u'request'][u'id'] diff --git a/openlp/plugins/remotes/lib/remotetab.py b/openlp/plugins/remotes/lib/remotetab.py index 7c76516dc..77baf9116 100644 --- a/openlp/plugins/remotes/lib/remotetab.py +++ b/openlp/plugins/remotes/lib/remotetab.py @@ -155,6 +155,7 @@ class RemoteTab(SettingsTab): self.address_edit.textChanged.connect(self.set_urls) self.port_spin_box.valueChanged.connect(self.set_urls) self.https_port_spin_box.valueChanged.connect(self.set_urls) + self.https_settings_group_box.clicked.connect(self.https_changed) def retranslateUi(self): self.server_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Server Settings')) @@ -224,15 +225,22 @@ class RemoteTab(SettingsTab): self.user_id.setText(Settings().value(self.settings_section + u'/user id')) self.password.setText(Settings().value(self.settings_section + u'/password')) self.set_urls() + self.https_changed() def save(self): - changed = False + """ + Save the configuration and update the server configuration if necessary + """ if Settings().value(self.settings_section + u'/ip address') != self.address_edit.text() or \ Settings().value(self.settings_section + u'/port') != self.port_spin_box.value() or \ Settings().value(self.settings_section + u'/https port') != self.https_port_spin_box.value() or \ Settings().value(self.settings_section + u'/https enabled') != \ - self.https_settings_group_box.isChecked(): - changed = True + self.https_settings_group_box.isChecked() or \ + Settings().value(self.settings_section + u'/authentication enabled') != \ + self.user_login_group_box.isChecked() or \ + Settings().value(self.settings_section + u'/user id') != self.user_id.text() or \ + Settings().value(self.settings_section + u'/password') != self.password.text(): + self.settings_form.register_post_process(u'remotes_config_updated') Settings().setValue(self.settings_section + u'/port', self.port_spin_box.value()) Settings().setValue(self.settings_section + u'/https port', self.https_port_spin_box.value()) Settings().setValue(self.settings_section + u'/https enabled', self.https_settings_group_box.isChecked()) @@ -241,12 +249,16 @@ class RemoteTab(SettingsTab): Settings().setValue(self.settings_section + u'/authentication enabled', self.user_login_group_box.isChecked()) Settings().setValue(self.settings_section + u'/user id', self.user_id.text()) Settings().setValue(self.settings_section + u'/password', self.password.text()) - if changed: - Registry().execute(u'remotes_config_updated') - def on_twelve_hour_check_box_changed(self, check_state): self.twelve_hour = False # we have a set value convert to True/False if check_state == QtCore.Qt.Checked: self.twelve_hour = True + + def https_changed(self): + """ + Invert the HTTP group box based on Https group settings + """ + self.http_settings_group_box.setEnabled(not self.https_settings_group_box.isChecked()) + diff --git a/openlp/plugins/remotes/remoteplugin.py b/openlp/plugins/remotes/remoteplugin.py index 5be537b60..fd2906feb 100644 --- a/openlp/plugins/remotes/remoteplugin.py +++ b/openlp/plugins/remotes/remoteplugin.py @@ -29,6 +29,8 @@ import logging +from PyQt4 import QtGui + from openlp.core.lib import Plugin, StringContent, translate, build_icon from openlp.plugins.remotes.lib import RemoteTab, HttpServer @@ -76,6 +78,7 @@ class RemotesPlugin(Plugin): Plugin.finalise(self) if self.server: self.server.close() + self.server = None def about(self): """ @@ -105,5 +108,6 @@ class RemotesPlugin(Plugin): """ Called when Config is changed to restart the server on new address or port """ - self.finalise() - self.initialise() + log.debug(u'remote config changed') + self.main_window.information_message(translate('RemotePlugin', 'Configuration Change'), + translate('RemotePlugin', 'OpenLP will need to be restarted for the Remote changes to become active.')) From 347636d0787240d54a72705a6cdbcabdcc0fd434 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 29 Mar 2013 08:33:10 +0000 Subject: [PATCH 13/22] Move userid and password change out of restart scope --- openlp/plugins/remotes/lib/remotetab.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openlp/plugins/remotes/lib/remotetab.py b/openlp/plugins/remotes/lib/remotetab.py index 77baf9116..658f5cfcf 100644 --- a/openlp/plugins/remotes/lib/remotetab.py +++ b/openlp/plugins/remotes/lib/remotetab.py @@ -31,7 +31,7 @@ import os.path from PyQt4 import QtCore, QtGui, QtNetwork -from openlp.core.lib import Registry, Settings, SettingsTab, translate +from openlp.core.lib import Settings, SettingsTab, translate from openlp.core.utils import AppLocation @@ -237,9 +237,7 @@ class RemoteTab(SettingsTab): Settings().value(self.settings_section + u'/https enabled') != \ self.https_settings_group_box.isChecked() or \ Settings().value(self.settings_section + u'/authentication enabled') != \ - self.user_login_group_box.isChecked() or \ - Settings().value(self.settings_section + u'/user id') != self.user_id.text() or \ - Settings().value(self.settings_section + u'/password') != self.password.text(): + self.user_login_group_box.isChecked(): self.settings_form.register_post_process(u'remotes_config_updated') Settings().setValue(self.settings_section + u'/port', self.port_spin_box.value()) Settings().setValue(self.settings_section + u'/https port', self.https_port_spin_box.value()) From ced77e4ec3f236779fe0b9cd17e79700a0cafae2 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 29 Mar 2013 09:06:43 +0000 Subject: [PATCH 14/22] Add some proper tests --- openlp/plugins/remotes/lib/httpauth.py | 162 ------------------ openlp/plugins/remotes/lib/httpserver.py | 3 +- .../openlp_plugins/remotes/test_auth.py | 81 --------- .../{test_server.py => test_router.py} | 32 ++-- 4 files changed, 18 insertions(+), 260 deletions(-) delete mode 100644 openlp/plugins/remotes/lib/httpauth.py delete mode 100644 tests/functional/openlp_plugins/remotes/test_auth.py rename tests/functional/openlp_plugins/remotes/{test_server.py => test_router.py} (71%) diff --git a/openlp/plugins/remotes/lib/httpauth.py b/openlp/plugins/remotes/lib/httpauth.py deleted file mode 100644 index f760bfc8c..000000000 --- a/openlp/plugins/remotes/lib/httpauth.py +++ /dev/null @@ -1,162 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2013 Raoul Snyman # -# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan # -# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # -# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # -# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # -# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # -# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # -# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # -# --------------------------------------------------------------------------- # -# This program 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; version 2 of the License. # -# # -# This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### - -""" -The :mod:`http` module manages the HTTP authorisation logic. This code originates from -http://tools.cherrypy.org/wiki/AuthenticationAndAccessRestrictions - -""" - -import cherrypy -import logging -import os - -from mako.template import Template - -from openlp.core.lib import Settings -from openlp.core.utils import AppLocation, translate - -SESSION_KEY = '_cp_openlp' - -log = logging.getLogger(__name__) - - -def check_credentials(user_name, password): - """ - Verifies credentials for username and password. - Returns None on success or a string describing the error on failure - """ - if user_name == Settings().value(u'remotes/user id') and password == Settings().value(u'remotes/password'): - return None - else: - return translate('RemotePlugin.Mobile', 'Incorrect username or password.') - - -def check_authentication(*args, **kwargs): - """ - A tool that looks in config for 'auth.require'. If found and it is not None, a login is required and the entry is - evaluated as a list of conditions that the user must fulfill - """ - conditions = cherrypy.request.config.get('auth.require', None) - a = cherrypy.request - print a - if not Settings().value(u'remotes/authentication enabled'): - return None - if conditions is not None: - username = cherrypy.session.get(SESSION_KEY) - if username: - cherrypy.request.login = username - for condition in conditions: - # A condition is just a callable that returns true or false - if not condition(): - raise cherrypy.HTTPRedirect("/auth/login") - else: - raise cherrypy.HTTPRedirect("/auth/login") - -cherrypy.tools.auth = cherrypy.Tool('before_handler', check_authentication) - - -def require_auth(*conditions): - """ - A decorator that appends conditions to the auth.require config variable. - """ - def decorate(f): - """ - Lets process a decoration. - """ - if not hasattr(f, '_cp_config'): - f._cp_config = dict() - if 'auth.require' not in f._cp_config: - f._cp_config['auth.require'] = [] - f._cp_config['auth.require'].extend(conditions) - return f - return decorate - - -class AuthController(object): - - def on_login(self, username): - """ - Called on successful login - """ - pass - - def on_logout(self, username): - """ - Called on logout - """ - pass - - def get_login_form(self, username, message=None, from_page="/"): - """ - Provides a login form - """ - if not message: - message = translate('RemotePlugin.Mobile', 'Enter login information') - variables = { - 'title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 User Login'), - 'from_page': from_page, - 'message': message, - 'username': username - } - directory = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), u'remotes', u'html') - login_html = os.path.normpath(os.path.join(directory, u'login.html')) - html = Template(filename=login_html, input_encoding=u'utf-8', output_encoding=u'utf-8').render(**variables) - cherrypy.response.headers['Content-Type'] = u'text/html' - cherrypy.response.status = 200 - return html - - @cherrypy.expose - def login(self, username=None, password=None, from_page="/"): - """ - Provides the actual login control - """ - if username is None or password is None: - return self.get_login_form("", from_page=from_page) - error_msg = check_credentials(username, password) - if error_msg: - return self.get_login_form(username, from_page, error_msg,) - else: - cherrypy.session[SESSION_KEY] = cherrypy.request.login = username - self.on_login(username) - raise cherrypy.HTTPRedirect(from_page or "/") - - @cherrypy.expose - def logout(self, from_page="/"): - """ - Provides the actual logout functions - """ - sess = cherrypy.session - username = sess.get(SESSION_KEY, None) - sess[SESSION_KEY] = None - if username: - cherrypy.request.login = None - self.on_logout(username) - raise cherrypy.HTTPRedirect(from_page or "/") - diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index b660ec094..f8a79899e 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -277,10 +277,9 @@ class HttpRouter(object): """ Common function to process HTTP requests where secure or insecure """ - url = urlparse.urlparse(cherrypy.url()) response = None for route, func in self.routes: - match = re.match(route, url.path) + match = re.match(route, url_path) if match: log.debug('Route "%s" matched "%s"', route, url_path) args = [] diff --git a/tests/functional/openlp_plugins/remotes/test_auth.py b/tests/functional/openlp_plugins/remotes/test_auth.py deleted file mode 100644 index 32622af9c..000000000 --- a/tests/functional/openlp_plugins/remotes/test_auth.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -This module contains tests for the lib submodule of the Remotes plugin. -""" -import os -from unittest import TestCase -from tempfile import mkstemp -from mock import patch, MagicMock - -from openlp.core.lib import Settings -from openlp.plugins.remotes.lib.httpauth import check_credentials, check_authentication -from PyQt4 import QtGui - -__default_settings__ = { - u'remotes/twelve hour': True, - u'remotes/port': 4316, - u'remotes/https port': 4317, - u'remotes/https enabled': False, - u'remotes/user id': u'openlp', - u'remotes/password': u'password', - u'remotes/authentication enabled': False, - u'remotes/ip address': u'0.0.0.0' -} - -SESSION_KEY = '_cp_openlp' - - -class TestAuth(TestCase): - """ - Test the functions in the :mod:`lib` module. - """ - def setUp(self): - """ - Create the UI - """ - fd, self.ini_file = mkstemp(u'.ini') - Settings().set_filename(self.ini_file) - self.application = QtGui.QApplication.instance() - Settings().extend_default_settings(__default_settings__) - - def tearDown(self): - """ - Delete all the C++ objects at the end so that we don't have a segfault - """ - del self.application - os.unlink(self.ini_file) - os.unlink(Settings().fileName()) - - def check_credentials_test(self): - """ - Test the Authentication check routine with credentials. - """ - # GIVEN: A user and password in settings - Settings().setValue(u'remotes/user id', u'twinkle') - Settings().setValue(u'remotes/password', u'mongoose') - - # WHEN: We run the function with no input - authenticated = check_credentials(u'', u'') - - # THEN: The authentication will fail with an error message - self.assertEqual(authenticated, u'Incorrect username or password.', - u'The return should be a error message string') - - # WHEN: We run the function with the correct input - authenticated = check_credentials(u'twinkle', u'mongoose') - - # THEN: The authentication will pass. - self.assertEqual(authenticated, None, u'The return should be a None string') - - def check_auth_inactive_test(self): - """ - Test the Authentication check routine when inactive. - """ - # GIVEN: A access which is secure - Settings().setValue(u'remotes/authentication enabled', False) - - # WHEN: We run the function with no input - with patch(u'cherrypy.request.config'): - authenticated = check_authentication(None, None) - - # THEN: The authentication will fail with an error message - self.assertEqual(authenticated, None, u'The authentication should return None as not required') diff --git a/tests/functional/openlp_plugins/remotes/test_server.py b/tests/functional/openlp_plugins/remotes/test_router.py similarity index 71% rename from tests/functional/openlp_plugins/remotes/test_server.py rename to tests/functional/openlp_plugins/remotes/test_router.py index d574a8542..9604bc747 100644 --- a/tests/functional/openlp_plugins/remotes/test_server.py +++ b/tests/functional/openlp_plugins/remotes/test_router.py @@ -6,10 +6,9 @@ import os from unittest import TestCase from tempfile import mkstemp from mock import patch, MagicMock -import cherrypy from openlp.core.lib import Settings -from openlp.plugins.remotes.lib.httpserver import HttpConnection +from openlp.plugins.remotes.lib.httpserver import HttpRouter from PyQt4 import QtGui __default_settings__ = { @@ -23,10 +22,8 @@ __default_settings__ = { u'remotes/ip address': u'0.0.0.0' } -SESSION_KEY = '_cp_openlp' - -class TestAuth(TestCase): +class TestRouter(TestCase): """ Test the functions in the :mod:`lib` module. """ @@ -38,7 +35,7 @@ class TestAuth(TestCase): Settings().set_filename(self.ini_file) self.application = QtGui.QApplication.instance() Settings().extend_default_settings(__default_settings__) - self.server = HttpConnection(None) + self.router = HttpRouter() def tearDown(self): """ @@ -49,19 +46,24 @@ class TestAuth(TestCase): def process_http_request_test(self): """ - Test the Authentication check routine with credentials. + Test the router control functionality """ - # GIVEN: A user and password in settings - cherrypy = MagicMock() - cherrypy.url.return_value = "nosetest/apl/poll" + # GIVEN: A testing set of Routes + mocked_function = MagicMock() + test_route = [ + (r'^/stage/api/poll$', mocked_function), + ] + self.router.routes = test_route - print cherrypy.url() + # WHEN: called with a poll route + self.router.process_http_request(u'/stage/api/poll', None) - with patch(u'url.path') as mocked_url: - mocked_url.return_value = "nosetest/apl/poll" - self.server._process_http_request(None, None) + # THEN: the function should have been called only once + assert mocked_function.call_count == 1, \ + u'The mocked function should have been matched and called once.' - self.assertFalse() + + #self.assertFalse() # WHEN: We run the function with no input #authenticated = check_credentials(u'', u'') From 6cd384e02824111db4a72a4fe7275193c52beec6 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 29 Mar 2013 09:32:46 +0000 Subject: [PATCH 15/22] Fix up tests --- openlp/plugins/remotes/lib/httpserver.py | 41 +++++---- .../openlp_plugins/remotes/test_router.py | 58 ++++++++----- .../remotes/test_remoteserver.py | 85 ------------------- 3 files changed, 63 insertions(+), 121 deletions(-) delete mode 100644 tests/interfaces/openlp_plugins/remotes/test_remoteserver.py diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index f8a79899e..5a9e05042 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -127,28 +127,32 @@ from PyQt4 import QtCore from openlp.core.lib import Registry, Settings, PluginStatus, StringContent from openlp.core.utils import AppLocation, translate -from cherrypy._cpcompat import md5, sha, ntob +from cherrypy._cpcompat import sha, ntob log = logging.getLogger(__name__) def sha_password_encrypter(password): - + """ + Create an encrypted password for the given password. + """ return sha(ntob(password)).hexdigest() def fetch_password(username): + """ + Fetch the password for a provided user. + """ if username != Settings().value(u'remotes/user id'): return None - print "fetch password", username - return sha(ntob(Settings().value(u'remotes/password'))).hexdigest() + return sha_password_encrypter(Settings().value(u'remotes/password')) class HttpServer(object): """ Ability to control OpenLP via a web browser. + This class controls the Cherrypy server and configuration. """ - _cp_config = { 'tools.sessions.on': True, 'tools.auth.on': True @@ -167,7 +171,7 @@ class HttpServer(object): Start the http server based on configuration. """ log.debug(u'Start CherryPy server') - # Define to security levels and add the router code + # Define to security levels and inject the router code self.root = self.Public() self.root.files = self.Files() self.root.stage = self.Stage() @@ -180,6 +184,9 @@ class HttpServer(object): cherrypy.engine.start() def define_config(self): + """ + Define the configuration of the server. + """ if Settings().value(self.plugin.settings_section + u'/https enabled'): port = Settings().value(self.plugin.settings_section + u'/https port') address = Settings().value(self.plugin.settings_section + u'/ip address') @@ -209,14 +216,12 @@ class HttpServer(object): u'tools.basic_auth.on': False}} return directory_config - def reload_config(self): - cherrypy.tree.mount(self.root, '/', config=self.define_config()) - cherrypy.config.reset() - class Public: + """ + Main access class with may have security enabled on it. + """ @cherrypy.expose def default(self, *args, **kwargs): - print "public" self.router.request_data = None if isinstance(kwargs, dict): self.router.request_data = kwargs.get(u'data', None) @@ -224,13 +229,18 @@ class HttpServer(object): return self.router.process_http_request(url.path, *args) class Files: + """ + Provides access to files and has no security available. These are read only accesses + """ @cherrypy.expose def default(self, *args, **kwargs): - print "files" url = urlparse.urlparse(cherrypy.url()) return self.router.process_http_request(url.path, *args) class Stage: + """ + Stageview is read only so security is not relevant and would reduce it's usability + """ @cherrypy.expose def default(self, *args, **kwargs): url = urlparse.urlparse(cherrypy.url()) @@ -246,12 +256,11 @@ class HttpServer(object): class HttpRouter(object): """ - A single connection, this handles communication between the server and the client. + This code is called by the HttpServer upon a request and it processes it based on the routing table. """ - def __init__(self): """ - Initialise the CherryPy Server + Initialise the router """ self.routes = [ (u'^/$', self.serve_file), @@ -275,7 +284,7 @@ class HttpRouter(object): def process_http_request(self, url_path, *args): """ - Common function to process HTTP requests where secure or insecure + Common function to process HTTP requests """ response = None for route, func in self.routes: diff --git a/tests/functional/openlp_plugins/remotes/test_router.py b/tests/functional/openlp_plugins/remotes/test_router.py index 9604bc747..f86b69612 100644 --- a/tests/functional/openlp_plugins/remotes/test_router.py +++ b/tests/functional/openlp_plugins/remotes/test_router.py @@ -8,7 +8,7 @@ from tempfile import mkstemp from mock import patch, MagicMock from openlp.core.lib import Settings -from openlp.plugins.remotes.lib.httpserver import HttpRouter +from openlp.plugins.remotes.lib.httpserver import HttpRouter, fetch_password, sha_password_encrypter from PyQt4 import QtGui __default_settings__ = { @@ -44,6 +44,43 @@ class TestRouter(TestCase): del self.application os.unlink(self.ini_file) + def fetch_password_unknown_test(self): + """ + Test the fetch password code with an unknown userid + """ + # GIVEN: A default configuration + # WHEN: called with the defined userid + password = fetch_password(u'itwinkle') + print password + + # THEN: the function should return None + self.assertEqual(password, None, u'The result for fetch_password should be None') + + def fetch_password_known_test(self): + """ + Test the fetch password code with the defined userid + """ + # GIVEN: A default configuration + # WHEN: called with the defined userid + password = fetch_password(u'openlp') + required_password = sha_password_encrypter(u'password') + + # THEN: the function should return the correct password + self.assertEqual(password, required_password, u'The result for fetch_password should be the defined password') + + def sha_password_encrypter_test(self): + """ + Test hash password function + """ + # GIVEN: A default configuration + # WHEN: called with the defined userid + required_password = sha_password_encrypter(u'password') + test_value = '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8' + + # THEN: the function should return the correct password + self.assertEqual(required_password, test_value, + u'The result for sha_password_encrypter should return the correct encrypted password') + def process_http_request_test(self): """ Test the router control functionality @@ -61,22 +98,3 @@ class TestRouter(TestCase): # THEN: the function should have been called only once assert mocked_function.call_count == 1, \ u'The mocked function should have been matched and called once.' - - - #self.assertFalse() - - # WHEN: We run the function with no input - #authenticated = check_credentials(u'', u'') - - # THEN: The authentication will fail with an error message - #self.assertEqual(authenticated, u'Incorrect username or password.', - # u'The return should be a error message string') - - # WHEN: We run the function with the correct input - #authenticated = check_credentials(u'twinkle', u'mongoose') - - # THEN: The authentication will pass. - #self.assertEqual(authenticated, None, u'The return should be a None string') - - - diff --git a/tests/interfaces/openlp_plugins/remotes/test_remoteserver.py b/tests/interfaces/openlp_plugins/remotes/test_remoteserver.py deleted file mode 100644 index ded7ca4c9..000000000 --- a/tests/interfaces/openlp_plugins/remotes/test_remoteserver.py +++ /dev/null @@ -1,85 +0,0 @@ -""" -This module contains tests for the lib submodule of the Remotes plugin. -""" -import os -from unittest import TestCase -from tempfile import mkstemp -from mock import patch, MagicMock - - -import urllib -from BeautifulSoup import BeautifulSoup, NavigableString, Tag - -from openlp.core.lib import Settings -from openlp.plugins.remotes.lib import HttpServer -from PyQt4 import QtGui - -__default_settings__ = { - u'remotes/twelve hour': True, - u'remotes/port': 4316, - u'remotes/https port': 4317, - u'remotes/https enabled': False, - u'remotes/user id': u'openlp', - u'remotes/password': u'password', - u'remotes/authentication enabled': False, - u'remotes/ip address': u'0.0.0.0' -} - -SESSION_KEY = '_cp_openlp' - - -class TestRemoteServer(TestCase): - """ - Test the functions in the :mod:`lib` module. - """ - def setUp(self): - """ - Create the UI - """ - fd, self.ini_file = mkstemp(u'.ini') - Settings().set_filename(self.ini_file) - self.application = QtGui.QApplication.instance() - Settings().extend_default_settings(__default_settings__) - self.server = HttpServer(self) - - def tearDown(self): - """ - Delete all the C++ objects at the end so that we don't have a segfault - """ - del self.application - os.unlink(self.ini_file) - os.unlink(Settings().fileName()) - self.server.close() - - def check_access_test(self): - """ - Test the Authentication check routine. - """ - # GIVEN: A user and password in settings - Settings().setValue(u'remotes/user id', u'twinkle') - Settings().setValue(u'remotes/password', u'mongoose') - - # WHEN: We run the function with no input - authenticated = check_credentials(u'', u'') - - # THEN: The authentication will fail with an error message - self.assertEqual(authenticated, u'Incorrect username or password.', - u'The return should be a error message string') - - # WHEN: We run the function with the correct input - authenticated = check_credentials(u'twinkle', u'mongoose') - - # THEN: The authentication will pass. - self.assertEqual(authenticated, None, u'The return should be a None string') - - def check_auth_inactive_test(self): - """ - Test the Authentication check routine. - """ - # GIVEN: A access which is secure - Settings().setValue(u'remotes/authentication enabled', True) - - # WHEN: We run the function with no input - f = urllib.urlopen("http://localhost:4316") - soup = BeautifulSoup(f.read()) - print soup.title.string From eb51590becf22cb7930fb6ebef049bcaa817697a Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 29 Mar 2013 09:39:41 +0000 Subject: [PATCH 16/22] Code cleanup ready for reviews --- openlp/plugins/remotes/html/login.css | 46 ----------------------- openlp/plugins/remotes/html/login.html | 48 ------------------------ openlp/plugins/remotes/lib/httpserver.py | 15 +++++--- openlp/plugins/remotes/lib/remotetab.py | 9 +++++ 4 files changed, 19 insertions(+), 99 deletions(-) delete mode 100644 openlp/plugins/remotes/html/login.css delete mode 100644 openlp/plugins/remotes/html/login.html diff --git a/openlp/plugins/remotes/html/login.css b/openlp/plugins/remotes/html/login.css deleted file mode 100644 index 3881b55ee..000000000 --- a/openlp/plugins/remotes/html/login.css +++ /dev/null @@ -1,46 +0,0 @@ -/****************************************************************************** -* OpenLP - Open Source Lyrics Projection * -* --------------------------------------------------------------------------- * -* Copyright (c) 2008-2013 Raoul Snyman * -* Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan * -* Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, * -* Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. * -* Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, * -* Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, * -* Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, * -* Frode Woldsund, Martin Zibricky * -* --------------------------------------------------------------------------- * -* This program 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; version 2 of the License. * -* * -* This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 * -* Temple Place, Suite 330, Boston, MA 02111-1307 USA * -******************************************************************************/ - -.ui-icon-blank { - background-image: url(images/ui-icon-blank.png); -} - -.ui-icon-unblank { - background-image: url(images/ui-icon-unblank.png); -} - -/* Overwrite style from jquery-mobile.css */ -.ui-li .ui-btn-text a.ui-link-inherit{ - white-space: normal; -} - -.ui-page{ - padding: 100px 100px 100px 100px; - width: 300px; -} -.ui-input-text{ - width: 30px; -} \ No newline at end of file diff --git a/openlp/plugins/remotes/html/login.html b/openlp/plugins/remotes/html/login.html deleted file mode 100644 index 60c71166c..000000000 --- a/openlp/plugins/remotes/html/login.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - ${title} - - - - - - - -

${message}

-

- User name:
- Password:
- - - diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 5a9e05042..677ea3146 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -348,11 +348,7 @@ class HttpRouter(object): 'no_results': translate('RemotePlugin.Mobile', 'No Results'), 'options': translate('RemotePlugin.Mobile', 'Options'), 'service': translate('RemotePlugin.Mobile', 'Service'), - 'slides': translate('RemotePlugin.Mobile', 'Slides'), - 'title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 User Login'), - 'from_page': "", - 'message': "", - 'username': "username" + 'slides': translate('RemotePlugin.Mobile', 'Slides') } def serve_file(self, filename=None): @@ -588,12 +584,21 @@ class HttpRouter(object): self._http_success() def _http_success(self): + """ + Set the HTTP success return code. + """ cherrypy.response.status = 200 def _http_bad_request(self): + """ + Set the HTTP bad response return code. + """ cherrypy.response.status = 400 def _http_not_found(self): + """ + Set the HTTP not found return code. + """ cherrypy.response.status = 404 cherrypy.response.body = ["Sorry, an error occurred "] diff --git a/openlp/plugins/remotes/lib/remotetab.py b/openlp/plugins/remotes/lib/remotetab.py index 658f5cfcf..685cd5882 100644 --- a/openlp/plugins/remotes/lib/remotetab.py +++ b/openlp/plugins/remotes/lib/remotetab.py @@ -181,6 +181,9 @@ class RemoteTab(SettingsTab): self.password_label.setText(translate('RemotePlugin.RemoteTab', 'Password:')) def set_urls(self): + """ + Update the display based on the data input on the screen + """ ip_address = u'localhost' if self.address_edit.text() == ZERO_URL: interfaces = QtNetwork.QNetworkInterface.allInterfaces() @@ -206,6 +209,9 @@ class RemoteTab(SettingsTab): self.stage_https_url.setText(u'%s' % (https_url, https_url)) def load(self): + """ + Load the configuration and update the server configuration if necessary + """ self.port_spin_box.setValue(Settings().value(self.settings_section + u'/port')) self.https_port_spin_box.setValue(Settings().value(self.settings_section + u'/https port')) self.address_edit.setText(Settings().value(self.settings_section + u'/ip address')) @@ -249,6 +255,9 @@ class RemoteTab(SettingsTab): Settings().setValue(self.settings_section + u'/password', self.password.text()) def on_twelve_hour_check_box_changed(self, check_state): + """ + Toggle the 12 hour check box. + """ self.twelve_hour = False # we have a set value convert to True/False if check_state == QtCore.Qt.Checked: From efa7d8c8c0039ba6e4c5e777398f214f8b686adb Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 29 Mar 2013 20:58:06 +0000 Subject: [PATCH 17/22] minor fixes and move certificate location --- openlp/core/utils/applocation.py | 7 +++---- openlp/plugins/remotes/lib/httpserver.py | 6 +++--- openlp/plugins/remotes/lib/remotetab.py | 6 +++--- tests/functional/openlp_plugins/remotes/__init__.py | 1 - tests/functional/openlp_plugins/remotes/test_router.py | 5 ++--- 5 files changed, 11 insertions(+), 14 deletions(-) diff --git a/openlp/core/utils/applocation.py b/openlp/core/utils/applocation.py index 4215cad22..2f1cb45ba 100644 --- a/openlp/core/utils/applocation.py +++ b/openlp/core/utils/applocation.py @@ -63,7 +63,6 @@ class AppLocation(object): VersionDir = 5 CacheDir = 6 LanguageDir = 7 - SharedData = 8 # Base path where data/config/cache dir is located BaseDir = None @@ -150,18 +149,18 @@ def _get_os_dir_path(dir_type): if sys.platform == u'win32': if dir_type == AppLocation.DataDir: return os.path.join(unicode(os.getenv(u'APPDATA'), encoding), u'openlp', u'data') - elif dir_type == AppLocation.LanguageDir or dir_type == AppLocation.SharedData: + elif dir_type == AppLocation.LanguageDir: return os.path.split(openlp.__file__)[0] return os.path.join(unicode(os.getenv(u'APPDATA'), encoding), u'openlp') elif sys.platform == u'darwin': if dir_type == AppLocation.DataDir: return os.path.join(unicode(os.getenv(u'HOME'), encoding), u'Library', u'Application Support', u'openlp', u'Data') - elif dir_type == AppLocation.LanguageDir or dir_type == AppLocation.SharedData: + elif dir_type == AppLocation.LanguageDir: return os.path.split(openlp.__file__)[0] return os.path.join(unicode(os.getenv(u'HOME'), encoding), u'Library', u'Application Support', u'openlp') else: - if dir_type == AppLocation.LanguageDir or dir_type == AppLocation.SharedData: + if dir_type == AppLocation.LanguageDir: prefixes = [u'/usr/local', u'/usr'] for prefix in prefixes: directory = os.path.join(prefix, u'share', u'openlp') diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 677ea3146..51490bb38 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -190,11 +190,11 @@ class HttpServer(object): if Settings().value(self.plugin.settings_section + u'/https enabled'): port = Settings().value(self.plugin.settings_section + u'/https port') address = Settings().value(self.plugin.settings_section + u'/ip address') - shared_data = AppLocation.get_directory(AppLocation.SharedData) + local_data = AppLocation.get_directory(AppLocation.DataDir) cherrypy.config.update({u'server.socket_host': str(address), u'server.socket_port': port, - u'server.ssl_certificate': os.path.join(shared_data, u'openlp.crt'), - u'server.ssl_private_key': os.path.join(shared_data, u'openlp.key')}) + u'server.ssl_certificate': os.path.join(local_data, u'remotes', u'openlp.crt'), + u'server.ssl_private_key': os.path.join(local_data, u'remotes', u'openlp.key')}) else: port = Settings().value(self.plugin.settings_section + u'/port') address = Settings().value(self.plugin.settings_section + u'/ip address') diff --git a/openlp/plugins/remotes/lib/remotetab.py b/openlp/plugins/remotes/lib/remotetab.py index 685cd5882..09934b58c 100644 --- a/openlp/plugins/remotes/lib/remotetab.py +++ b/openlp/plugins/remotes/lib/remotetab.py @@ -217,9 +217,9 @@ class RemoteTab(SettingsTab): self.address_edit.setText(Settings().value(self.settings_section + u'/ip address')) self.twelve_hour = Settings().value(self.settings_section + u'/twelve hour') self.twelve_hour_check_box.setChecked(self.twelve_hour) - shared_data = AppLocation.get_directory(AppLocation.SharedData) - if not os.path.exists(os.path.join(shared_data, u'openlp.crt')) or \ - not os.path.exists(os.path.join(shared_data, u'openlp.key')): + local_data = AppLocation.get_directory(AppLocation.DataDir) + if not os.path.exists(os.path.join(local_data, u'remotes', u'openlp.crt')) or \ + not os.path.exists(os.path.join(local_data, u'remotes', u'openlp.key')): self.https_settings_group_box.setChecked(False) self.https_settings_group_box.setEnabled(False) self.https_error_label.setVisible(True) diff --git a/tests/functional/openlp_plugins/remotes/__init__.py b/tests/functional/openlp_plugins/remotes/__init__.py index f87606f07..e69de29bb 100644 --- a/tests/functional/openlp_plugins/remotes/__init__.py +++ b/tests/functional/openlp_plugins/remotes/__init__.py @@ -1 +0,0 @@ -__author__ = 'tim' diff --git a/tests/functional/openlp_plugins/remotes/test_router.py b/tests/functional/openlp_plugins/remotes/test_router.py index f86b69612..3b344c3b2 100644 --- a/tests/functional/openlp_plugins/remotes/test_router.py +++ b/tests/functional/openlp_plugins/remotes/test_router.py @@ -5,7 +5,7 @@ import os from unittest import TestCase from tempfile import mkstemp -from mock import patch, MagicMock +from mock import MagicMock from openlp.core.lib import Settings from openlp.plugins.remotes.lib.httpserver import HttpRouter, fetch_password, sha_password_encrypter @@ -51,7 +51,6 @@ class TestRouter(TestCase): # GIVEN: A default configuration # WHEN: called with the defined userid password = fetch_password(u'itwinkle') - print password # THEN: the function should return None self.assertEqual(password, None, u'The result for fetch_password should be None') @@ -75,7 +74,7 @@ class TestRouter(TestCase): # GIVEN: A default configuration # WHEN: called with the defined userid required_password = sha_password_encrypter(u'password') - test_value = '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8' + test_value = u'5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8' # THEN: the function should return the correct password self.assertEqual(required_password, test_value, From 25d74260ce72f659edb5a6504d29b684f793e79b Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sat, 30 Mar 2013 06:56:28 +0000 Subject: [PATCH 18/22] Add tests for the server --- openlp/plugins/bibles/lib/http.py | 3 + openlp/plugins/remotes/lib/httpserver.py | 14 +- openlp/plugins/remotes/remoteplugin.py | 2 +- .../openlp_core_lib/test_uistrings.py | 1 + tests/interfaces/openlp_plugins/__init__.pyc | Bin 184 -> 0 bytes .../openlp_plugins/remotes/test_server.py | 120 ++++++++++++++++++ 6 files changed, 132 insertions(+), 8 deletions(-) delete mode 100644 tests/interfaces/openlp_plugins/__init__.pyc create mode 100644 tests/interfaces/openlp_plugins/remotes/test_server.py diff --git a/openlp/plugins/bibles/lib/http.py b/openlp/plugins/bibles/lib/http.py index b01377a05..736727e20 100644 --- a/openlp/plugins/bibles/lib/http.py +++ b/openlp/plugins/bibles/lib/http.py @@ -55,6 +55,7 @@ UGLY_CHARS = { log = logging.getLogger(__name__) + class BGExtract(object): """ Extract verses from BibleGateway @@ -671,6 +672,7 @@ class HTTPBible(BibleDB): application = property(_get_application) + def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, pre_parse_substitute=None, cleaner=None): """ @@ -715,6 +717,7 @@ def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, Registry().get(u'application').process_events() return soup + def send_error_message(error_type): """ Send a standard error message informing the user of an issue. diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 51490bb38..0489428ac 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -158,12 +158,12 @@ class HttpServer(object): 'tools.auth.on': True } - def __init__(self, plugin): + def __init__(self): """ Initialise the http server, and start the server. """ log.debug(u'Initialise httpserver') - self.plugin = plugin + self.settings_section = u'remotes' self.router = HttpRouter() def start_server(self): @@ -187,17 +187,17 @@ class HttpServer(object): """ Define the configuration of the server. """ - if Settings().value(self.plugin.settings_section + u'/https enabled'): - port = Settings().value(self.plugin.settings_section + u'/https port') - address = Settings().value(self.plugin.settings_section + u'/ip address') + if Settings().value(self.settings_section + u'/https enabled'): + port = Settings().value(self.settings_section + u'/https port') + address = Settings().value(self.settings_section + u'/ip address') local_data = AppLocation.get_directory(AppLocation.DataDir) cherrypy.config.update({u'server.socket_host': str(address), u'server.socket_port': port, u'server.ssl_certificate': os.path.join(local_data, u'remotes', u'openlp.crt'), u'server.ssl_private_key': os.path.join(local_data, u'remotes', u'openlp.key')}) else: - port = Settings().value(self.plugin.settings_section + u'/port') - address = Settings().value(self.plugin.settings_section + u'/ip address') + port = Settings().value(self.settings_section + u'/port') + address = Settings().value(self.settings_section + u'/ip address') cherrypy.config.update({u'server.socket_host': str(address)}) cherrypy.config.update({u'server.socket_port': port}) cherrypy.config.update({u'environment': u'embedded'}) diff --git a/openlp/plugins/remotes/remoteplugin.py b/openlp/plugins/remotes/remoteplugin.py index fd2906feb..f443fbda4 100644 --- a/openlp/plugins/remotes/remoteplugin.py +++ b/openlp/plugins/remotes/remoteplugin.py @@ -67,7 +67,7 @@ class RemotesPlugin(Plugin): """ log.debug(u'initialise') Plugin.initialise(self) - self.server = HttpServer(self) + self.server = HttpServer() self.server.start_server() def finalise(self): diff --git a/tests/functional/openlp_core_lib/test_uistrings.py b/tests/functional/openlp_core_lib/test_uistrings.py index 3351657d1..0070533db 100644 --- a/tests/functional/openlp_core_lib/test_uistrings.py +++ b/tests/functional/openlp_core_lib/test_uistrings.py @@ -6,6 +6,7 @@ from unittest import TestCase from openlp.core.lib import UiStrings + class TestUiStrings(TestCase): def check_same_instance_test(self): diff --git a/tests/interfaces/openlp_plugins/__init__.pyc b/tests/interfaces/openlp_plugins/__init__.pyc deleted file mode 100644 index 0d24c9eff54ac7d86f14523ef46194c4bbc24cad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 184 zcmZ9GO$q`r423JY5W#!QX3Pa-@BpGC9-x#qGe-JH(*ZrLhY(zw0apgz3w(L-vV3nh zpI3LW>NgA72NAEtoKn|jCZ|SB{TUl!a7zKfL|4!-^d;TVR)%xNc Date: Fri, 5 Apr 2013 20:37:15 +0100 Subject: [PATCH 19/22] Cleanups and more tests --- openlp/core/lib/plugin.py | 4 +- openlp/core/lib/pluginmanager.py | 2 +- openlp/plugins/media/mediaplugin.py | 2 +- .../presentations/presentationplugin.py | 2 +- .../openlp_core_lib/test_pluginmanager.py | 12 +- .../openlp_core_utils/test_applocation.py | 10 ++ .../openlp_plugins/remotes/test_remotetab.py | 108 ++++++++++++++++++ tests/resources/remotes/openlp.crt | 0 tests/resources/remotes/openlp.key | 0 9 files changed, 129 insertions(+), 11 deletions(-) create mode 100644 tests/functional/openlp_plugins/remotes/test_remotetab.py create mode 100644 tests/resources/remotes/openlp.crt create mode 100644 tests/resources/remotes/openlp.key diff --git a/openlp/core/lib/plugin.py b/openlp/core/lib/plugin.py index dd9843930..b4f851b24 100644 --- a/openlp/core/lib/plugin.py +++ b/openlp/core/lib/plugin.py @@ -103,7 +103,7 @@ class Plugin(QtCore.QObject): ``add_export_menu_Item(export_menu)`` Add an item to the Export menu. - ``create_settings_Tab()`` + ``create_settings_tab()`` Creates a new instance of SettingsTabItem to be used in the Settings dialog. @@ -252,7 +252,7 @@ class Plugin(QtCore.QObject): """ pass - def create_settings_Tab(self, parent): + def create_settings_tab(self, parent): """ Create a tab for the settings window to display the configurable options for this plugin to the user. diff --git a/openlp/core/lib/pluginmanager.py b/openlp/core/lib/pluginmanager.py index 8fc294ea6..db96e3fa7 100644 --- a/openlp/core/lib/pluginmanager.py +++ b/openlp/core/lib/pluginmanager.py @@ -153,7 +153,7 @@ class PluginManager(object): """ for plugin in self.plugins: if plugin.status is not PluginStatus.Disabled: - plugin.create_settings_Tab(self.settings_form) + plugin.create_settings_tab(self.settings_form) def hook_import_menu(self): """ diff --git a/openlp/plugins/media/mediaplugin.py b/openlp/plugins/media/mediaplugin.py index 3e685f4c6..4bc5314ff 100644 --- a/openlp/plugins/media/mediaplugin.py +++ b/openlp/plugins/media/mediaplugin.py @@ -54,7 +54,7 @@ class MediaPlugin(Plugin): # passed with drag and drop messages self.dnd_id = u'Media' - def create_settings_Tab(self, parent): + def create_settings_tab(self, parent): """ Create the settings Tab """ diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py index 7872c25b7..5bc95e388 100644 --- a/openlp/plugins/presentations/presentationplugin.py +++ b/openlp/plugins/presentations/presentationplugin.py @@ -69,7 +69,7 @@ class PresentationPlugin(Plugin): self.icon_path = u':/plugins/plugin_presentations.png' self.icon = build_icon(self.icon_path) - def create_settings_Tab(self, parent): + def create_settings_tab(self, parent): """ Create the settings Tab """ diff --git a/tests/functional/openlp_core_lib/test_pluginmanager.py b/tests/functional/openlp_core_lib/test_pluginmanager.py index 9d6c30f8e..8317e78dc 100644 --- a/tests/functional/openlp_core_lib/test_pluginmanager.py +++ b/tests/functional/openlp_core_lib/test_pluginmanager.py @@ -74,7 +74,7 @@ class TestPluginManager(TestCase): # WHEN: We run hook_settings_tabs() plugin_manager.hook_settings_tabs() - # THEN: The create_settings_Tab() method should have been called + # THEN: The hook_settings_tabs() method should have been called assert mocked_plugin.create_media_manager_item.call_count == 0, \ u'The create_media_manager_item() method should not have been called.' @@ -94,8 +94,8 @@ class TestPluginManager(TestCase): # WHEN: We run hook_settings_tabs() plugin_manager.hook_settings_tabs() - # THEN: The create_settings_Tab() method should not have been called, but the plugins lists should be the same - assert mocked_plugin.create_settings_Tab.call_count == 0, \ + # THEN: The create_settings_tab() method should not have been called, but the plugins lists should be the same + assert mocked_plugin.create_settings_tab.call_count == 0, \ u'The create_media_manager_item() method should not have been called.' self.assertEqual(mocked_settings_form.plugin_manager.plugins, plugin_manager.plugins, u'The plugins on the settings form should be the same as the plugins in the plugin manager') @@ -117,7 +117,7 @@ class TestPluginManager(TestCase): plugin_manager.hook_settings_tabs() # THEN: The create_media_manager_item() method should have been called with the mocked settings form - assert mocked_plugin.create_settings_Tab.call_count == 1, \ + assert mocked_plugin.create_settings_tab.call_count == 1, \ u'The create_media_manager_item() method should have been called once.' self.assertEqual(mocked_settings_form.plugin_manager.plugins, plugin_manager.plugins, u'The plugins on the settings form should be the same as the plugins in the plugin manager') @@ -135,8 +135,8 @@ class TestPluginManager(TestCase): # WHEN: We run hook_settings_tabs() plugin_manager.hook_settings_tabs() - # THEN: The create_settings_Tab() method should have been called - mocked_plugin.create_settings_Tab.assert_called_with(self.mocked_settings_form) + # THEN: The create_settings_tab() method should have been called + mocked_plugin.create_settings_tab.assert_called_with(self.mocked_settings_form) def hook_import_menu_with_disabled_plugin_test(self): """ diff --git a/tests/functional/openlp_core_utils/test_applocation.py b/tests/functional/openlp_core_utils/test_applocation.py index 5473da8c0..b59f41f37 100644 --- a/tests/functional/openlp_core_utils/test_applocation.py +++ b/tests/functional/openlp_core_utils/test_applocation.py @@ -30,8 +30,10 @@ class TestAppLocation(TestCase): mocked_get_directory.return_value = u'test/dir' mocked_check_directory_exists.return_value = True mocked_os.path.normpath.return_value = u'test/dir' + # WHEN: we call AppLocation.get_data_path() data_path = AppLocation.get_data_path() + # THEN: check that all the correct methods were called, and the result is correct mocked_settings.contains.assert_called_with(u'advanced/data path') mocked_get_directory.assert_called_with(AppLocation.DataDir) @@ -49,8 +51,10 @@ class TestAppLocation(TestCase): mocked_settings.contains.return_value = True mocked_settings.value.return_value.toString.return_value = u'custom/dir' mocked_os.path.normpath.return_value = u'custom/dir' + # WHEN: we call AppLocation.get_data_path() data_path = AppLocation.get_data_path() + # THEN: the mocked Settings methods were called and the value returned was our set up value mocked_settings.contains.assert_called_with(u'advanced/data path') mocked_settings.value.assert_called_with(u'advanced/data path') @@ -100,8 +104,10 @@ class TestAppLocation(TestCase): # GIVEN: A mocked out AppLocation.get_data_path() mocked_get_data_path.return_value = u'test/dir' mocked_check_directory_exists.return_value = True + # WHEN: we call AppLocation.get_data_path() data_path = AppLocation.get_section_data_path(u'section') + # THEN: check that all the correct methods were called, and the result is correct mocked_check_directory_exists.assert_called_with(u'test/dir/section') assert data_path == u'test/dir/section', u'Result should be "test/dir/section"' @@ -112,8 +118,10 @@ class TestAppLocation(TestCase): """ with patch(u'openlp.core.utils.applocation._get_frozen_path') as mocked_get_frozen_path: mocked_get_frozen_path.return_value = u'app/dir' + # WHEN: We call AppLocation.get_directory directory = AppLocation.get_directory(AppLocation.AppDir) + # THEN: assert directory == u'app/dir', u'Directory should be "app/dir"' @@ -130,8 +138,10 @@ class TestAppLocation(TestCase): mocked_get_frozen_path.return_value = u'plugins/dir' mocked_sys.frozen = 1 mocked_sys.argv = ['openlp'] + # WHEN: We call AppLocation.get_directory directory = AppLocation.get_directory(AppLocation.PluginsDir) + # THEN: assert directory == u'plugins/dir', u'Directory should be "plugins/dir"' diff --git a/tests/functional/openlp_plugins/remotes/test_remotetab.py b/tests/functional/openlp_plugins/remotes/test_remotetab.py new file mode 100644 index 000000000..22bee8139 --- /dev/null +++ b/tests/functional/openlp_plugins/remotes/test_remotetab.py @@ -0,0 +1,108 @@ +""" +This module contains tests for the lib submodule of the Remotes plugin. +""" +import os + +from unittest import TestCase +from tempfile import mkstemp +from mock import patch + +from openlp.core.lib import Settings +from openlp.plugins.remotes.lib.remotetab import RemoteTab + +from PyQt4 import QtGui + +__default_settings__ = { + u'remotes/twelve hour': True, + u'remotes/port': 4316, + u'remotes/https port': 4317, + u'remotes/https enabled': False, + u'remotes/user id': u'openlp', + u'remotes/password': u'password', + u'remotes/authentication enabled': False, + u'remotes/ip address': u'0.0.0.0' +} + +ZERO_URL = u'0.0.0.0' + +TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'..', u'..', u'..', u'resources')) + + +class TestRemoteTab(TestCase): + """ + Test the functions in the :mod:`lib` module. + """ + def setUp(self): + """ + Create the UI + """ + fd, self.ini_file = mkstemp(u'.ini') + Settings().set_filename(self.ini_file) + self.application = QtGui.QApplication.instance() + Settings().extend_default_settings(__default_settings__) + self.parent = QtGui.QMainWindow() + self.form = RemoteTab(self.parent, u'Remotes', None, None) + + def tearDown(self): + """ + Delete all the C++ objects at the end so that we don't have a segfault + """ + del self.application + del self.parent + del self.form + os.unlink(self.ini_file) + + def set_basic_urls_test(self): + """ + Test the set_urls function with standard defaults + """ + # GIVEN: A mocked location + with patch(u'openlp.core.utils.applocation.Settings') as mocked_class, \ + patch(u'openlp.core.utils.AppLocation.get_directory') as mocked_get_directory, \ + patch(u'openlp.core.utils.applocation.check_directory_exists') as mocked_check_directory_exists, \ + patch(u'openlp.core.utils.applocation.os') as mocked_os: + # GIVEN: A mocked out Settings class and a mocked out AppLocation.get_directory() + mocked_settings = mocked_class.return_value + mocked_settings.contains.return_value = False + mocked_get_directory.return_value = u'test/dir' + mocked_check_directory_exists.return_value = True + mocked_os.path.normpath.return_value = u'test/dir' + + # WHEN: when the set_urls is called having reloaded the form. + self.form.load() + self.form.set_urls() + # THEN: the following screen values should be set + self.assertEqual(self.form.address_edit.text(), ZERO_URL, u'The default URL should be set on the screen') + self.assertEqual(self.form.https_settings_group_box.isEnabled(), False, + u'The Https box should not be enabled') + self.assertEqual(self.form.https_settings_group_box.isChecked(), False, + u'The Https checked box should note be Checked') + self.assertEqual(self.form.user_login_group_box.isChecked(), False, + u'The authentication box should not be enabled') + + def set_certificate_urls_test(self): + """ + Test the set_urls function with certificate available + """ + # GIVEN: A mocked location + with patch(u'openlp.core.utils.applocation.Settings') as mocked_class, \ + patch(u'openlp.core.utils.AppLocation.get_directory') as mocked_get_directory, \ + patch(u'openlp.core.utils.applocation.check_directory_exists') as mocked_check_directory_exists, \ + patch(u'openlp.core.utils.applocation.os') as mocked_os: + # GIVEN: A mocked out Settings class and a mocked out AppLocation.get_directory() + mocked_settings = mocked_class.return_value + mocked_settings.contains.return_value = False + mocked_get_directory.return_value = TEST_PATH + mocked_check_directory_exists.return_value = True + mocked_os.path.normpath.return_value = TEST_PATH + + # WHEN: when the set_urls is called having reloaded the form. + self.form.load() + self.form.set_urls() + # THEN: the following screen values should be set + self.assertEqual(self.form.http_settings_group_box.isEnabled(), True, + u'The Http group box should be enabled') + self.assertEqual(self.form.https_settings_group_box.isChecked(), False, + u'The Https checked box should be Checked') + self.assertEqual(self.form.https_settings_group_box.isEnabled(), True, + u'The Https box should be enabled') diff --git a/tests/resources/remotes/openlp.crt b/tests/resources/remotes/openlp.crt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/resources/remotes/openlp.key b/tests/resources/remotes/openlp.key new file mode 100644 index 000000000..e69de29bb From 31b4e561b4ef54e40b795ce0e4274f22994eb11e Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Mon, 8 Apr 2013 17:53:11 +0100 Subject: [PATCH 20/22] Fix up code review change --- openlp/core/lib/serviceitem.py | 6 ++---- openlp/plugins/remotes/lib/httpserver.py | 6 +++--- tests/functional/openlp_plugins/remotes/test_router.py | 8 ++++---- tests/interfaces/openlp_plugins/remotes/test_server.py | 4 ++-- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index f57243818..c4ac846c9 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -62,12 +62,10 @@ class ItemCapabilities(object): tab when making the previous item live. ``CanEdit`` - The capability to allow the ServiceManager to allow the item to be - edited + The capability to allow the ServiceManager to allow the item to be edited ``CanMaintain`` - The capability to allow the ServiceManager to allow the item to be - reordered. + The capability to allow the ServiceManager to allow the item to be reordered. ``RequiresMedia`` Determines is the service_item needs a Media Player diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 0489428ac..083f36db0 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -132,7 +132,7 @@ from cherrypy._cpcompat import sha, ntob log = logging.getLogger(__name__) -def sha_password_encrypter(password): +def make_sha_hash(password): """ Create an encrypted password for the given password. """ @@ -145,7 +145,7 @@ def fetch_password(username): """ if username != Settings().value(u'remotes/user id'): return None - return sha_password_encrypter(Settings().value(u'remotes/password')) + return make_sha_hash(Settings().value(u'remotes/password')) class HttpServer(object): @@ -207,7 +207,7 @@ class HttpServer(object): u'tools.basic_auth.on': Settings().value(u'remotes/authentication enabled'), u'tools.basic_auth.realm': u'OpenLP Remote Login', u'tools.basic_auth.users': fetch_password, - u'tools.basic_auth.encrypt': sha_password_encrypter}, + u'tools.basic_auth.encrypt': make_sha_hash}, u'/files': {u'tools.staticdir.on': True, u'tools.staticdir.dir': self.router.html_dir, u'tools.basic_auth.on': False}, diff --git a/tests/functional/openlp_plugins/remotes/test_router.py b/tests/functional/openlp_plugins/remotes/test_router.py index 3b344c3b2..2980a339b 100644 --- a/tests/functional/openlp_plugins/remotes/test_router.py +++ b/tests/functional/openlp_plugins/remotes/test_router.py @@ -8,7 +8,7 @@ from tempfile import mkstemp from mock import MagicMock from openlp.core.lib import Settings -from openlp.plugins.remotes.lib.httpserver import HttpRouter, fetch_password, sha_password_encrypter +from openlp.plugins.remotes.lib.httpserver import HttpRouter, fetch_password, make_sha_hash from PyQt4 import QtGui __default_settings__ = { @@ -62,7 +62,7 @@ class TestRouter(TestCase): # GIVEN: A default configuration # WHEN: called with the defined userid password = fetch_password(u'openlp') - required_password = sha_password_encrypter(u'password') + required_password = make_sha_hash(u'password') # THEN: the function should return the correct password self.assertEqual(password, required_password, u'The result for fetch_password should be the defined password') @@ -73,12 +73,12 @@ class TestRouter(TestCase): """ # GIVEN: A default configuration # WHEN: called with the defined userid - required_password = sha_password_encrypter(u'password') + required_password = make_sha_hash(u'password') test_value = u'5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8' # THEN: the function should return the correct password self.assertEqual(required_password, test_value, - u'The result for sha_password_encrypter should return the correct encrypted password') + u'The result for make_sha_hash should return the correct encrypted password') def process_http_request_test(self): """ diff --git a/tests/interfaces/openlp_plugins/remotes/test_server.py b/tests/interfaces/openlp_plugins/remotes/test_server.py index d7ce88010..9ae544d21 100644 --- a/tests/interfaces/openlp_plugins/remotes/test_server.py +++ b/tests/interfaces/openlp_plugins/remotes/test_server.py @@ -9,10 +9,10 @@ from mock import MagicMock import urllib2 import cherrypy -from BeautifulSoup import BeautifulSoup, NavigableString, Tag +from BeautifulSoup import BeautifulSoup from openlp.core.lib import Settings -from openlp.plugins.remotes.lib.httpserver import HttpServer, fetch_password, sha_password_encrypter +from openlp.plugins.remotes.lib.httpserver import HttpServer from PyQt4 import QtGui __default_settings__ = { From 220681c900981c0aad6c7b42e476aba227ea38e4 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sun, 14 Apr 2013 16:59:57 +0100 Subject: [PATCH 21/22] Comment fixes --- openlp/core/lib/mediamanageritem.py | 6 ++++++ openlp/plugins/remotes/lib/httpserver.py | 6 ++++++ .../openlp_plugins/remotes/test_server.py | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/openlp/core/lib/mediamanageritem.py b/openlp/core/lib/mediamanageritem.py index d27ef4041..01329b842 100644 --- a/openlp/core/lib/mediamanageritem.py +++ b/openlp/core/lib/mediamanageritem.py @@ -487,6 +487,9 @@ class MediaManagerItem(QtGui.QWidget): def go_live_remote(self, message): """ Remote Call wrapper + + ``message`` + The passed data item_id:Remote. """ self.go_live(message[0], remote=message[1]) @@ -535,6 +538,9 @@ class MediaManagerItem(QtGui.QWidget): def add_to_service_remote(self, message): """ Remote Call wrapper + + ``message`` + The passed data item:Remote. """ self.add_to_service(message[0], remote=message[1]) diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 083f36db0..203b67205 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -285,6 +285,12 @@ class HttpRouter(object): def process_http_request(self, url_path, *args): """ Common function to process HTTP requests + + ``url_path`` + The requested URL. + + ``*args`` + Any passed data. """ response = None for route, func in self.routes: diff --git a/tests/interfaces/openlp_plugins/remotes/test_server.py b/tests/interfaces/openlp_plugins/remotes/test_server.py index 9ae544d21..8795eeaf3 100644 --- a/tests/interfaces/openlp_plugins/remotes/test_server.py +++ b/tests/interfaces/openlp_plugins/remotes/test_server.py @@ -101,6 +101,15 @@ class TestRouter(TestCase): def call_remote_server(url, username=None, password=None): + """ + Helper function + + ``username`` + The username. + + ``password`` + The password. + """ if username: passman = urllib2.HTTPPasswordMgrWithDefaultRealm() passman.add_password(None, url, username, password) @@ -115,6 +124,15 @@ def call_remote_server(url, username=None, password=None): def process_http_request(url_path, *args): + """ + Override function to make the Mock work but does nothing. + + ``Url_path`` + The url_path. + + ``*args`` + Some args. + """ cherrypy.response.status = 200 return None From 7f78250bb2798cea89349febf332c5fcf1c1edfb Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Mon, 15 Apr 2013 18:55:50 +0100 Subject: [PATCH 22/22] Make object --- openlp/plugins/remotes/lib/httpserver.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 203b67205..878b197b3 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -216,7 +216,7 @@ class HttpServer(object): u'tools.basic_auth.on': False}} return directory_config - class Public: + class Public(object): """ Main access class with may have security enabled on it. """ @@ -228,7 +228,7 @@ class HttpServer(object): url = urlparse.urlparse(cherrypy.url()) return self.router.process_http_request(url.path, *args) - class Files: + class Files(object): """ Provides access to files and has no security available. These are read only accesses """ @@ -237,7 +237,7 @@ class HttpServer(object): url = urlparse.urlparse(cherrypy.url()) return self.router.process_http_request(url.path, *args) - class Stage: + class Stage(object): """ Stageview is read only so security is not relevant and would reduce it's usability """