changeset 813:6e27604ec95a

server: added --tls_private_key and --tls_chain options. --tls_certificate .pem file will be used for private_key if --tls_private_key is not specified.
author Goffi <goffi@goffi.org>
date Sun, 20 Dec 2015 20:01:42 +0100
parents fd6965c16e7e
children e8c0c2b5e2d5
files src/server/server.py src/twisted/plugins/libervia_server.py
diffstat 2 files changed, 98 insertions(+), 16 deletions(-) [+]
line wrap: on
line diff
--- a/src/server/server.py	Sun Dec 20 20:01:40 2015 +0100
+++ b/src/server/server.py	Sun Dec 20 20:01:42 2015 +0100
@@ -51,9 +51,8 @@
 try:
     import OpenSSL
     from twisted.internet import ssl
-    ssl_available = True
-except:
-    ssl_available = False
+except ImportError:
+    ssl = None
 
 from libervia.server.constants import Const as C
 from libervia.server.blog import MicroBlog
@@ -1343,25 +1342,104 @@
 
         self.initialised.addCallback(initOk)
 
+    ## TLS related methods ##
+
+    def _TLSOptionsCheck(self):
+        """Check options coherence if TLS is activated, and update missing values
+
+        Must be called only if TLS is activated
+        """
+        if not self.options['tls_certificate']:
+            log.error(u"a TLS certificate is needed to activate HTTPS connection")
+            self.quit(1)
+        if not self.options['tls_private_key']:
+            self.options['tls_private_key'] = self.options['tls_certificate']
+
+
+        if not self.options['tls_private_key']:
+            self.options['tls_private_key'] = self.options['tls_certificate']
+
+    def _loadCertificates(self, f):
+        """Read a .pem file with a list of certificates
+
+        @param f (file): file obj (opened .pem file)
+        @return (list[OpenSSL.crypto.X509]): list of certificates
+        @raise OpenSSL.crypto.Error: error while parsing the file
+        """
+        # XXX: didn't found any method to load a .pem file with several certificates
+        #      so the certificates split is done here
+        certificates = []
+        buf = []
+        while True:
+            line = f.readline()
+            buf.append(line)
+            if '-----END CERTIFICATE-----' in line:
+                certificates.append(OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, ''.join(buf)))
+                buf=[]
+            elif not line:
+                log.debug(u"{} certificate(s) found".format(len(certificates)))
+                return certificates
+
+    def _loadPKey(self, f):
+        """Read a private key from a .pem file
+
+        @param f (file): file obj (opened .pem file)
+        @return (list[OpenSSL.crypto.PKey]): private key object
+        @raise OpenSSL.crypto.Error: error while parsing the file
+        """
+        return OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, f.read())
+
+    def _loadCertificate(self, f):
+        """Read a public certificate from a .pem file
+
+        @param f (file): file obj (opened .pem file)
+        @return (list[OpenSSL.crypto.X509]): public certificate
+        @raise OpenSSL.crypto.Error: error while parsing the file
+        """
+        return OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, f.read())
+
+    def _getTLSContextFactory(self):
+        """Load TLS certificate and build the context factory needed for listenSSL"""
+        if ssl is None:
+            raise ImportError(u"Python module pyOpenSSL is not installed!")
+
+        cert_options = {}
+
+        for name, option, method in [('privateKey', 'tls_private_key', self._loadPKey),
+                                    ('certificate', 'tls_certificate', self._loadCertificate),
+                                    ('extraCertChain', 'tls_chain', self._loadCertificates)]:
+            path = self.options[option]
+            if not path:
+                assert option=='tls_chain'
+                continue
+            log.debug(u"loading {option} from {path}".format(option=option, path=path))
+            try:
+                with open(path) as f:
+                    cert_options[name] = method(f)
+            except IOError as e:
+                log.error(u"Error while reading file {path} for option {option}: {error}".format(path=path, option=option, error=e))
+                self.quit(2)
+            except OpenSSL.crypto.Error:
+                log.error(u"Error while parsing file {path} for option {option}, are you sure it is a valid .pem file?".format(path=path, option=option))
+                if option=='tls_private_key' and self.options['tls_certificate'] == path:
+                    log.error(u"You are using the same file for private key and public certificate, make sure that both a in {path} or use --tls_private_key option".format(path=path))
+                self.quit(2)
+
+        return ssl.CertificateOptions(**cert_options)
+
+    ## service management ##
+
     def _startService(self, dummy=None):
         """Actually start the HTTP(S) server(s) after the profile for Libervia is connected.
+
+        @raise ImportError: OpenSSL is not available
         @raise IOError: the certificate file doesn't exist
         @raise OpenSSL.crypto.Error: the certificate file is invalid
         """
         if self.options['connection_type'] in ('https', 'both'):
-            if ssl is None:
-                raise ImportError(u"Python module pyOpenSSL is not installed!")
-            try:
-                with open(os.path.expanduser(self.options['ssl_certificate'])) as keyAndCert:
-                    try:
-                        cert = ssl.PrivateCertificate.loadPEM(keyAndCert.read())
-                    except OpenSSL.crypto.Error as e:
-                        log.error(_(u"The file '%s' must contain both private and public parts of the certificate") % self.options['ssl_certificate'])
-                        raise e
-            except IOError as e:
-                log.error(_(u"The file '%s' doesn't exist") % self.options['ssl_certificate'])
-                raise e
-            reactor.listenSSL(self.options['port_https'], self.site, cert.options())
+            self._TLSOptionsCheck()
+            context_factory = self._getTLSContextFactory()
+            reactor.listenSSL(self.options['port_https'], self.site, context_factory)
         if self.options['connection_type'] in ('http', 'both'):
             if self.options['connection_type'] == 'both' and self.options['redirect_to_https']:
                 reactor.listenTCP(self.options['port'], server.Site(RedirectToHTTPS(self.options['port'], self.options['port_https_ext'])))
--- a/src/twisted/plugins/libervia_server.py	Sun Dec 20 20:01:40 2015 +0100
+++ b/src/twisted/plugins/libervia_server.py	Sun Dec 20 20:01:42 2015 +0100
@@ -2,6 +2,8 @@
 # -*- coding: utf-8 -*-
 
 # Libervia: a Salut à Toi frontend
+# Copyright (C) 2013, 2014, 2015 Jérôme Poisson <goffi@goffi.org>
+# Copyright (C) 2013, 2014, 2015 Adrien Cossa <souliane@mailoo.org>
 # Copyright (C) 2013  Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
 
 # This program is free software: you can redistribute it and/or modify
@@ -66,7 +68,9 @@
                        ['port', 'p', 8080, _(u'The port number to listen HTTP on.').encode('utf-8'), int],
                        ['port_https', 's', 8443, _(u'The port number to listen HTTPS on.').encode('utf-8'), int],
                        ['port_https_ext', 'e', 0, _(u'The external port number used for HTTPS (0 means port_https value).').encode('utf-8'), int],
+                       ['tls_private_key', '', '', _(u'TLS certificate private key (PEM format)').encode('utf-8'), str],
                        ['tls_certificate', 'c', 'libervia.pem', _(u'TLS certificate public certificate or private key and public certificate combined (PEM format)').encode('utf-8'), str],
+                       ['tls_chain', '', '', _(u'TLS certificate intermediate chain (PEM format)').encode('utf-8'), str],
                        ['redirect_to_https', 'r', 1, _(u'Automatically redirect from HTTP to HTTPS.').encode('utf-8'), int],
                        ['security_warning', 'w', 1, _(u'Warn user that he is about to connect on HTTP.').encode('utf-8'), int],
                        ['passphrase', 'k', '', (_(u"Passphrase for the SàT profile named '%s'") % C.SERVICE_PROFILE).encode('utf-8'), str],