# HG changeset patch # User souliane # Date 1395154778 -3600 # Node ID fadbba1d793f26fbc3fea3933cc37b385b5b660f # Parent ae598511850d284da968e26dd5bdeb3e3c0b2c0f server_side: added support for SSL and related parameters: Full parameters list: -t, --connection_type= 'http', 'https' or 'both' (to launch both servers). [default: both] -p, --port= The port number to listen HTTP on. [default: 8080] -s, --port_https= The port number to listen HTTPS on. [default: 8443] -c, --ssl_certificate= PEM certificate with both private and public parts. [default: libervia.pem] -r, --redirect_to_https= automatically redirect from HTTP to HTTPS. [default: 0] -w, --security_warning= warn user that he is about to connect on HTTP. [default: 1] diff -r ae598511850d -r fadbba1d793f libervia.py --- a/libervia.py Fri Mar 21 09:20:27 2014 +0100 +++ b/libervia.py Tue Mar 18 15:59:38 2014 +0100 @@ -309,12 +309,15 @@ def displayNotification(self, title, body): self.notification.notify(title, body) - def _isRegisteredCB(self, registered): + def _isRegisteredCB(self, result): + registered, warning = result if not registered: self._register_box = RegisterBox(self.logged) self._register_box.centerBox() self._register_box.show() - self._tryAutoConnect() + if warning: + dialog.InfoDialog(_('Security warning'), warning).show() + self._tryAutoConnect(skip_validation=not not warning) else: self._register.call('isConnected', self._isConnectedCB) @@ -357,8 +360,10 @@ self.bridge.call('asyncGetParamA', lambda value: params_ui_cb(self.params_ui[param], value), self.params_ui[param]['name'], self.params_ui[param]['category']) - def _tryAutoConnect(self): - """This method retrieve the eventual URL parameters to auto-connect the user.""" + def _tryAutoConnect(self, skip_validation=False): + """This method retrieve the eventual URL parameters to auto-connect the user. + @param skip_validation: if True, set the form values but do not validate it + """ params = getURLParams(Window.getLocation().getSearch()) if "login" in params: self._register_box._form.login_box.setText(params["login"]) @@ -366,7 +371,8 @@ if "passwd" in params: # try to connect self._register_box._form.login_pass_box.setText(params["passwd"]) - self._register_box._form.onLogin(None) + if not skip_validation: + self._register_box._form.onLogin(None) return True else: # this would eventually set the browser saved password diff -r ae598511850d -r fadbba1d793f libervia_server/__init__.py --- a/libervia_server/__init__.py Fri Mar 21 09:20:27 2014 +0100 +++ b/libervia_server/__init__.py Tue Mar 18 15:59:38 2014 +0100 @@ -25,7 +25,7 @@ from twisted.web import error as weberror from twisted.web.static import File from twisted.web.resource import Resource, NoResource -from twisted.web.util import Redirect +from twisted.web.util import Redirect, redirectTo from twisted.python.components import registerAdapter from twisted.python.failure import Failure from twisted.words.protocols.jabber.jid import JID @@ -33,17 +33,28 @@ from txjsonrpc import jsonrpclib from logging import debug, info, warning, error -import re, glob -import os.path, sys -import tempfile, shutil, uuid +import re +import glob +import os.path +import sys +import tempfile +import shutil +import uuid from zope.interface import Interface, Attribute, implements from xml.dom import minidom +from httplib import HTTPS_PORT from constants import Const from libervia_server.blog import MicroBlog from sat_frontends.bridge.DBus import DBusBridgeFrontend, BridgeExceptionNoService from sat.core.i18n import _, D_ from sat.tools.xml_tools import paramsXML2XMLUI +try: + import OpenSSL + from twisted.internet import ssl + ssl_available = True +except: + ssl_available = False class ISATSession(Interface): @@ -553,14 +564,14 @@ Render method with some hacks: - if login is requested, try to login with form data - except login, every method is jsonrpc - - user doesn't need to be authentified for isRegistered or registerParams, but must be for all other methods + - user doesn't need to be authentified for explicitely listed methods, but must be for all others """ if request.postpath==['login']: return self.login(request) _session = request.getSession() parsed = jsonrpclib.loads(request.content.read()) method = parsed.get("method") - if method not in ['isRegistered', 'registerParams', 'getMenus']: + if method not in ['isRegistered', 'registerParams', 'getMenus']: #if we don't call these methods, we need to be identified profile = ISATSession(_session).profile if not profile: @@ -737,10 +748,16 @@ return server.NOT_DONE_YET def jsonrpc_isRegistered(self): - """Tell if the user is already registered""" + """ + @return: a couple (registered, message) with: + - registered: True if the user is already registered, False otherwise + - message: a security warning message if registered is False *and* the connection is unsecure, None otherwise + """ _session = self.request.getSession() profile = ISATSession(_session).profile - return bool(profile) + if bool(profile): + return (True, None) + return (False, self.__getSecurityWarning()) def jsonrpc_registerParams(self): """Register the frontend specific parameters""" @@ -766,6 +783,17 @@ # XXX: we put this method in Register because we get menus before being logged return self.sat_host.bridge.getMenus('', Const.SECURITY_LIMIT) + def __getSecurityWarning(self): + """@return: a security warning message, or None if the connection is secure""" + if self.request.URLPath().scheme == 'https' or not self.sat_host.security_warning: + return None + text = D_("You are about to connect to an unsecured service.") + if self.sat_host.connection_type == 'both': + new_port = (':%s' % self.sat_host.port_https) if self.sat_host.port_https != HTTPS_PORT else '' + url = "https://%s" % self.request.URLPath().netloc.replace(':%s' % self.sat_host.port, new_port) + text += D_('
Secure version of this website: %(url)s') % {'url': url} + return text + class SignalHandler(jsonrpc.JSONRPC): @@ -957,11 +985,37 @@ return ("setAvatar", filepath, profile) +def coerceConnectionType(value): # called from Libervia.OPT_PARAMETERS + allowed_values = ('http', 'https', 'both') + if value not in allowed_values: + raise ValueError("Invalid parameter value, not in %s" % str(allowed_values)) + return value + + class Libervia(service.Service): - def __init__(self, port=8080): + OPT_PARAMETERS = [['connection_type', 't', 'https', "'http', 'https' or 'both' (to launch both servers).", coerceConnectionType], + ['port', 'p', 8080, 'The port number to listen HTTP on.', int], + ['port_https', 's', 8443, 'The port number to listen HTTPS on.', int], + ['ssl_certificate', 'c', 'libervia.pem', 'PEM certificate with both private and public parts.', str], + ['redirect_to_https', 'r', 1, 'automatically redirect from HTTP to HTTPS.', int], + ['security_warning', 'w', 1, 'warn user that he is about to connect on HTTP.', int], + ] + + def __init__(self, *args, **kwargs): + if not kwargs: + # During the loading of the twisted plugins, we just need the default values. + # This part is not executed when the plugin is actually started. + for name, value in [(option[0], option[2]) for option in self.OPT_PARAMETERS]: + kwargs[name] = value + + self.connection_type = kwargs['connection_type'] + self.port = kwargs['port'] + self.port_https = kwargs['port_https'] + self.ssl_certificate = kwargs['ssl_certificate'] + self.redirect_to_https = kwargs['redirect_to_https'] + self.security_warning = kwargs['security_warning'] self._cleanup = [] - self.port = port root = ProtectedFile(Const.LIBERVIA_DIR) self.signal_handler = SignalHandler(self) _register = Register(self) @@ -1015,7 +1069,25 @@ self._cleanup.insert(0, (callback, args, kwargs)) def startService(self): - reactor.listenTCP(self.port, self.site) + if self.connection_type in ('https', 'both'): + if not ssl_available: + raise(ImportError(_("Python module pyOpenSSL is not installed!"))) + try: + with open(self.ssl_certificate) as keyAndCert: + try: + cert = ssl.PrivateCertificate.loadPEM(keyAndCert.read()) + except OpenSSL.crypto.Error as e: + error(_("The file '%s' must contain both private and public parts of the certificate") % self.ssl_certificate) + raise e + except IOError as e: + error(_("The file '%s' doesn't exist") % self.ssl_certificate) + raise e + reactor.listenSSL(self.port_https, self.site, cert.options()) + if self.connection_type in ('http', 'both'): + if self.connection_type == 'both' and self.redirect_to_https: + reactor.listenTCP(self.port, server.Site(RedirectToHTTPS(self.port, self.port_https))) + else: + reactor.listenTCP(self.port, self.site) def stopService(self): print "launching cleaning methods" @@ -1028,6 +1100,21 @@ def stop(self): reactor.stop() + +class RedirectToHTTPS(Resource): + + def __init__(self, old_port, new_port): + Resource.__init__(self) + self.isLeaf = True + self.old_port = old_port + self.new_port = new_port + + def render(self, request): + netloc = request.URLPath().netloc.replace(':%s' % self.old_port, ':%s' % self.new_port) + url = "https://" + netloc + request.uri + return redirectTo(url, request) + + registerAdapter(SATSession, server.Session, ISATSession) application = service.Application(Const.APP_NAME) service = Libervia() diff -r ae598511850d -r fadbba1d793f twisted/plugins/libervia.py --- a/twisted/plugins/libervia.py Fri Mar 21 09:20:27 2014 +0100 +++ b/twisted/plugins/libervia.py Tue Mar 18 15:59:38 2014 +0100 @@ -22,38 +22,51 @@ from twisted.python import usage from twisted.plugin import IPlugin from twisted.application.service import IServiceMaker -from twisted.application import internet from xdg.BaseDirectory import save_config_path -from ConfigParser import SafeConfigParser, NoSectionError +from ConfigParser import SafeConfigParser, NoSectionError, NoOptionError from os.path import expanduser try: from libervia_server import Libervia + opt_params = Libervia.OPT_PARAMETERS except (ImportError, SystemExit): - pass # avoid raising an error when you call twisted and sat is not launched + # avoid raising an error when you call twisted and sat is not launched + opt_params = [] class Options(usage.Options): - optParameters = [['port', 'p', 8080, 'The port number to listen on.']] + + # optArgs is not really useful in our case, we need more than a flag + optParameters = opt_params + + def __init__(self): + """You want to read SàT configuration file now in order to overwrite the hard-coded default values. + This is because the priority is (form lowest to highest): + - use hard-coded default values + - use the values from SàT configuration files + - use the values passes on the command line + If you do it later: after the command line options have been parsed, there's no good way to know + if the options values are the hard-coded ones or if they have been passed on the command line. + """ + for index in xrange(0, len(self.optParameters)): + name = self.optParameters[index][0] + try: + value = config.get('libervia', name) + self.optParameters[index][2] = self.optParameters[index][4](value) + except (NoSectionError, NoOptionError): + pass + usage.Options.__init__(self) class LiberviaMaker(object): implements(IServiceMaker, IPlugin) tapname = 'libervia' - description = 'The web frontend of Salut à Toi' + description = u'The web frontend of Salut à Toi' options = Options def makeService(self, options): - if not isinstance(options['port'], int): - port = int(options['port']) - else: - try: - port = config.getint('libervia', 'port') - except NoSectionError: - port = 8080 - return Libervia(port=port) - + return Libervia(dict(options)) # get rid of the usage.Option surcharge config_path = save_config_path('sat') config = SafeConfigParser()