changeset 415:fadbba1d793f

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]
author souliane <souliane@mailoo.org>
date Tue, 18 Mar 2014 15:59:38 +0100
parents ae598511850d
children e9bc7854bce6
files libervia.py libervia_server/__init__.py twisted/plugins/libervia.py
diffstat 3 files changed, 136 insertions(+), 30 deletions(-) [+]
line wrap: on
line diff
--- 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
--- 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_('<br />Secure version of this website: <a href="%(url)s">%(url)s</a>') % {'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()
--- 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()