pvl.login.server: separate redirect/refresh'd step for cert download to display html first; fix set-cookie quoting for werkzeug 0.9
authorTero Marttila <terom@paivola.fi>
Tue, 14 Jan 2014 23:15:36 +0200
changeset 375 df3bf49634a1
parent 374 d2426cebb46a
child 376 31d9ae0c1dab
pvl.login.server: separate redirect/refresh'd step for cert download to display html first; fix set-cookie quoting for werkzeug 0.9
pvl/login/server.py
pvl/login/ssl.py
--- a/pvl/login/server.py	Tue Jan 14 23:14:53 2014 +0200
+++ b/pvl/login/server.py	Tue Jan 14 23:15:36 2014 +0200
@@ -371,20 +371,21 @@
                 set_pubtkt = self.app.renew(self.pubtkt)
             
         if set_pubtkt :
-            # browsers seem to be very particular about quoting ;'s in cookie values...
-            # this follows PHP's setcookie() encoding...
-            cookie = werkzeug.urls.url_quote(self.app.sign(set_pubtkt))
+            signed = self.app.sign(set_pubtkt)
             
             self.pubtkt = set_pubtkt
+            
+            # browsers and mod_pubtkt seem to be very particular about quoting ;'s in cookie values...
+            # this follows PHP's setcookie() encoding, without any quoting of the value..
+            cookie = '{cookie}={value}; Domain={domain}; Secure; HttpOnly'.format(
+                    cookie  = self.app.cookie_name,
+                    value   = werkzeug.urls.url_quote(signed),
+                    domain  = self.app.cookie_domain,
+            )
 
             # redirect with cookie
             response = pvl.web.response.redirect(self.back)
-
-            response.set_cookie(self.app.cookie_name, cookie,
-                domain      = self.app.cookie_domain,
-                secure      = self.app.cookie_secure,
-                httponly    = self.app.cookie_httponly,
-            )
+            response.headers.add('Set-Cookie', cookie)
 
             return response
 
@@ -486,23 +487,72 @@
 
     OUT = 'tmp/spkac'
 
+    def render_cert (self) :
+        return html.div(class_='container')(
+            self.render_alerts(),
+            html.div(class_='alert alert-success')(
+                "Your new SSL client cert has been signed, and should shortly be installed within your browser."
+            )
+        )
+
+    def respond_cert (self, cert) :
+        """
+            Generate a response for a signed cert, showing the user an informational page, and redirecting to the cert itself..
+        """
+
+        location = self.url(SSL, cert=cert)
+
+        return pvl.web.Response(
+                self.render_html(
+                    body        = self.render_cert(),
+                    extrahead   = html.meta(http_equiv='refresh', content='0;{location}'.format(location=location)),
+                ),
+                status      = 200,
+                #headers     = {
+                #    'Location': location
+                #},
+                mimetype    = 'text/html',
+        )
+
     def process_spkac (self, spkac) :
         log.info("SPKAC: %s", spkac)
-
-        cert = self.app.sign_ssl(self.pubtkt, spkac)
-
-        file = self.response_file(open(cert))
+        
+        try :
+            cert = self.app.ssl_sign(self.pubtkt, spkac)
+        except pvl.login.ssl.Error as ex :
+            self.alert('danger', ex)
+            return
+        
+        log.info("Redirecting to client cert: %s", cert)
+        return self.respond_cert(cert)
 
-        log.info("Returning client cert: %s", cert)
+    def process_cert (self, cert) :
+        """
+            Return user cert as download.
+
+            Uses the application/x-x509-user-cert mimetype per
+                https://developer.mozilla.org/en-US/docs/NSS_Certificate_Download_Specification
+        """
         
-        return pvl.web.Response(file, mimetype='application/x-x509-user-cert')
+        try :
+            file = self.app.ssl_open(self.pubtkt, cert)
+        except pvl.login.ssl.Error as ex :
+            self.alert('danger', ex)
+            return
+        
+        log.info("Returning client cert: %s", file)
 
-    def process (self) :
+        return pvl.web.Response(self.response_file(file), mimetype='application/x-x509-user-cert')
+
+    def process (self, cert=None) :
         if not self.process_cookie() :
             return self.redirect(Login, back=self.url())
 
         self.sslcert_dn = self.request.headers.get('X-Forwarded-SSL-DN')
 
+        if cert :
+            return self.process_cert(cert)
+
         if self.request.method == 'POST' :
             spkac = self.request.form.get('spkac')
 
@@ -536,6 +586,7 @@
 
         # proto
         urls.rule('/ssl',           SSL),
+        urls.rule('/ssl/<cert>',    SSL),
     ))
 
     PUBLIC_KEY = 'etc/login/public.pem'
@@ -624,9 +675,13 @@
                 udata   = self._auth.userdata(auth),
         )
 
-    def sign_ssl (self, pubtkt, spkac) :
+    def ssl_sign (self, pubtkt, spkac) :
         """
             Generate a SSL client cert for the given user.
+
+            Returns the redirect token for downloading it.
+            
+            Raises pvl.login.ssl.Error
         """
 
         if not self._ssl :
@@ -635,3 +690,12 @@
         return self._ssl.sign_user(pubtkt.uid, spkac,
                 userinfo    = pubtkt.udata,
         )
+    
+    def ssl_open (self, pubtkt, cert) :
+        """
+            Open and return an SSL cert file.
+
+            Raises pvl.login.ssl.Error
+        """
+
+        return self._ssl.open_cert(pubtkt.uid, cert)
--- a/pvl/login/ssl.py	Tue Jan 14 23:14:53 2014 +0200
+++ b/pvl/login/ssl.py	Tue Jan 14 23:15:36 2014 +0200
@@ -93,7 +93,7 @@
         """
             Sign given spkac string (base64-encoded) for given user.
 
-            Returns path to the signed cert.
+            Returns a name for the signed cert.
         """
 
         if not set(user).issubset(self.VALID_USER) :
@@ -119,7 +119,7 @@
         # sign it
         if os.path.exists(cert_file) :
             log.warning("cert already exists: %s", cert_file)
-            return cert_file
+            return name
         
         if os.path.exists(tmp_file) :
             log.warning("cleaning out previous tmp file: %s", tmp_file)
@@ -131,4 +131,19 @@
         log.debug("%s: rename %s -> %s", user, tmp_file, cert_file)
         os.rename(tmp_file, cert_file)
 
-        return cert_file
+        return name
+
+    def open_cert (self, user, name) :
+        """
+            Return an opened cert file by username / cert name.
+        """
+
+        if not set(user).issubset(self.VALID_USER) :
+            raise Error("Invalid username: {user}".format(user=user))
+
+        path = os.path.join(self.users, user, name)
+
+        if not os.path.exists(path) :
+            raise Error("No cert found on server")
+
+        return open(path)