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', ]