From 85fa4c91448e36658679cbac982ee00b18f95daa Mon Sep 17 00:00:00 2001
From: drebs <drebs@leap.se>
Date: Mon, 13 May 2013 18:12:53 -0300
Subject: Refactor ssl monkey patching and fix https tests.

---
 src/leap/soledad/__init__.py                    | 44 ++++++++++++++-
 src/leap/soledad/server.py                      |  1 -
 src/leap/soledad/shared_db.py                   | 29 +---------
 src/leap/soledad/tests/test_leap_backend.py     | 75 ++++++++++++++++---------
 src/leap/soledad/tests/u1db_tests/test_https.py | 10 +++-
 5 files changed, 101 insertions(+), 58 deletions(-)

diff --git a/src/leap/soledad/__init__.py b/src/leap/soledad/__init__.py
index 4373c53b..b41987cf 100644
--- a/src/leap/soledad/__init__.py
+++ b/src/leap/soledad/__init__.py
@@ -33,10 +33,18 @@ import logging
 import urlparse
 import simplejson as json
 import scrypt
+import httplib
+import socket
+import ssl
 
 
 from xdg import BaseDirectory
 from hashlib import sha256
+from u1db.remote import http_client
+from u1db.remote.ssl_match_hostname import (  # noqa
+    CertificateError,
+    match_hostname,
+)
 
 
 from leap.common import events
@@ -58,6 +66,13 @@ from leap.soledad.crypto import SoledadCrypto
 logger = logging.getLogger(name=__name__)
 
 
+SOLEDAD_CERT = None
+"""
+Path to the certificate file used to certify the SSL connection between
+Soledad client and server.
+"""
+
+
 #
 # Exceptions
 #
@@ -100,7 +115,6 @@ def base64_encode(data):
 # Soledad: local encrypted storage and remote encrypted sync.
 #
 
-
 class Soledad(object):
     """
     Soledad provides encrypted data storage and sync.
@@ -204,7 +218,7 @@ class Soledad(object):
         self._init_config(secrets_path, local_db_path, server_url)
         self._set_token(auth_token)
         # configure SSL certificate
-        shared_db.SOLEDAD_CERT = cert_file
+        SOLEDAD_CERT = cert_file
         # initiate bootstrap sequence
         self._bootstrap()
 
@@ -959,3 +973,29 @@ class Soledad(object):
     server_url = property(
         _get_server_url,
         doc='The URL of the Soledad server.')
+
+
+#-----------------------------------------------------------------------------
+# Monkey patching u1db to be able to provide a custom SSL cert
+#-----------------------------------------------------------------------------
+
+class VerifiedHTTPSConnection(httplib.HTTPSConnection):
+    """HTTPSConnection verifying server side certificates."""
+    # derived from httplib.py
+
+    def connect(self):
+        "Connect to a host on a given (SSL) port."
+        sock = socket.create_connection((self.host, self.port),
+                                        self.timeout, self.source_address)
+        if self._tunnel_host:
+            self.sock = sock
+            self._tunnel()
+        self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
+                                    ssl_version=ssl.PROTOCOL_SSLv3,
+                                    cert_reqs=ssl.CERT_REQUIRED,
+                                    ca_certs=SOLEDAD_CERT)
+        match_hostname(self.sock.getpeercert(), self.host)
+
+
+old__VerifiedHTTPSConnection = http_client._VerifiedHTTPSConnection
+http_client._VerifiedHTTPSConnection = VerifiedHTTPSConnection
diff --git a/src/leap/soledad/server.py b/src/leap/soledad/server.py
index b39b90f8..e7b55a3e 100644
--- a/src/leap/soledad/server.py
+++ b/src/leap/soledad/server.py
@@ -121,7 +121,6 @@ class SoledadAuthMiddleware(object):
         error message otherwise.
         @rtype: list
         """
-
         unauth_err = lambda msg: self._error(start_response,
                                              401,
                                              "unauthorized",
diff --git a/src/leap/soledad/shared_db.py b/src/leap/soledad/shared_db.py
index 3929e828..c954dbea 100644
--- a/src/leap/soledad/shared_db.py
+++ b/src/leap/soledad/shared_db.py
@@ -26,38 +26,11 @@ except ImportError:
     import json  # noqa
 
 
-from u1db.remote import http_database, http_client
+from u1db.remote import http_database
 
 
 from leap.soledad.auth import TokenBasedAuth
 
-SOLEDAD_CERT = None
-
-#-----------------------------------------------------------------------------
-# Monkey patching u1db to be able to provide a custom SSL cert
-#-----------------------------------------------------------------------------
-
-import httplib
-import socket
-import ssl
-
-class VerifiedHTTPSConnection(httplib.HTTPSConnection):
-    """HTTPSConnection verifying server side certificates."""
-    # derived from httplib.py
-
-    def connect(self):
-        "Connect to a host on a given (SSL) port."
-        sock = socket.create_connection((self.host, self.port),
-                                        self.timeout, self.source_address)
-        if self._tunnel_host:
-            self.sock = sock
-            self._tunnel()
-        self.sock = ssl.wrap_socket(sock, self.key_file, SOLEDAD_CERT,
-                                    ssl_version=ssl.PROTOCOL_SSLv3,
-                                    cert_reqs=ssl.CERT_REQUIRED,
-                                    ca_certs=SOLEDAD_CERT)
-
-http_client._VerifiedHTTPSConnection = VerifiedHTTPSConnection
 
 #-----------------------------------------------------------------------------
 # Soledad shared database
diff --git a/src/leap/soledad/tests/test_leap_backend.py b/src/leap/soledad/tests/test_leap_backend.py
index b58a5473..dbebadb5 100644
--- a/src/leap/soledad/tests/test_leap_backend.py
+++ b/src/leap/soledad/tests/test_leap_backend.py
@@ -22,6 +22,7 @@ Test Leap backend bits.
 
 import u1db
 import os
+import ssl
 try:
     import simplejson as json
 except ImportError:
@@ -35,7 +36,7 @@ from u1db.remote import (
     http_target,
 )
 
-
+from leap import soledad
 from leap.soledad.backends import leap_backend
 from leap.soledad.server import (
     SoledadApp,
@@ -509,39 +510,63 @@ class TestLeapSyncTarget(
 # The following tests come from `u1db.tests.test_https`.
 #-----------------------------------------------------------------------------
 
-def oauth_https_sync_target(test, host, path):
-    _, port = test.server.server_address
-    st = leap_backend.LeapSyncTarget(
-        'https://%s:%d/~/%s' % (host, port, path),
-        crypto=test._soledad._crypto)
-    st.set_oauth_credentials(tests.consumer1.key, tests.consumer1.secret,
-                             tests.token1.key, tests.token1.secret)
-    return st
-
 def token_leap_https_sync_target(test, host, path):
     _, port = test.server.server_address
     st = leap_backend.LeapSyncTarget(
-        'https://%s:%d/~/%s' % (host, port, path),
+        'https://%s:%d/%s' % (host, port, path),
         crypto=test._soledad._crypto)
     st.set_token_credentials('user-uuid', 'auth-token')
     return st
 
 
-#class TestLeapSyncTargetHttpsSupport(test_https.TestHttpSyncTargetHttpsSupport,
-#                                     BaseSoledadTest):
-#
-#    scenarios = [
-#        ('oauth_https', {'server_def': test_https.https_server_def,
-#                         'make_app_with_state': make_oauth_http_app,
-#                         'make_document_for_test': make_leap_document_for_test,
-#                         'sync_target': oauth_https_sync_target,
-#                         }),
-#        ('token_soledad_https', {'server_def': test_https.https_server_def,
-#                        'make_app_with_state': make_token_soledad_app,
-#                        'make_document_for_test': make_leap_document_for_test,
-#                        'sync_target': token_leap_https_sync_target}),
-#    ]
+class TestLeapSyncTargetHttpsSupport(test_https.TestHttpSyncTargetHttpsSupport,
+                                     BaseSoledadTest):
+
+    scenarios = [
+        ('token_soledad_https', {'server_def': test_https.https_server_def,
+                        'make_app_with_state': make_token_soledad_app,
+                        'make_document_for_test': make_leap_document_for_test,
+                        'sync_target': token_leap_https_sync_target}),
+    ]
 
+    def setUp(self):
+        # the parent constructor undoes our SSL monkey patch to ensure tests
+        # run smoothly with standard u1db.
+        test_https.TestHttpSyncTargetHttpsSupport.setUp(self)
+        # so here monkey patch again to test our functionality.
+        http_client._VerifiedHTTPSConnection = soledad.VerifiedHTTPSConnection
+        soledad.SOLEDAD_CERT = http_client.CA_CERTS
+
+    def test_working(self):
+        """
+        Test that SSL connections work well.
+
+        This test was adapted to patch Soledad's HTTPS connection custom class
+        with the intended CA certificates.
+        """
+        self.startServer()
+        db = self.request_state._create_database('test')
+        self.patch(soledad, 'SOLEDAD_CERT', self.cacert_pem)
+        remote_target = self.getSyncTarget('localhost', 'test')
+        remote_target.record_sync_info('other-id', 2, 'T-id')
+        self.assertEqual(
+            (2, 'T-id'), db._get_replica_gen_and_trans_id('other-id'))
+
+    def test_host_mismatch(self):
+        """
+        Test that SSL connections to a hostname different than the one in the
+        certificate raise CertificateError.
+
+        This test was adapted to patch Soledad's HTTPS connection custom class
+        with the intended CA certificates.
+        """
+        self.startServer()
+        self.request_state._create_database('test')
+        self.patch(soledad, 'SOLEDAD_CERT', self.cacert_pem)
+        remote_target = self.getSyncTarget('127.0.0.1', 'test')
+        self.assertRaises(
+            http_client.CertificateError, remote_target.record_sync_info,
+            'other-id', 2, 'T-id')
 
 #-----------------------------------------------------------------------------
 # The following tests come from `u1db.tests.test_http_database`.
diff --git a/src/leap/soledad/tests/u1db_tests/test_https.py b/src/leap/soledad/tests/u1db_tests/test_https.py
index 3f8797d8..b4b14722 100644
--- a/src/leap/soledad/tests/u1db_tests/test_https.py
+++ b/src/leap/soledad/tests/u1db_tests/test_https.py
@@ -6,13 +6,13 @@ import sys
 
 from paste import httpserver
 
-from leap.soledad.tests import u1db_tests as tests
-
 from u1db.remote import (
     http_client,
     http_target,
 )
 
+from leap import soledad
+from leap.soledad.tests import u1db_tests as tests
 from leap.soledad.tests.u1db_tests.test_remote_sync_target import (
     make_oauth_http_app,
 )
@@ -69,6 +69,12 @@ class TestHttpSyncTargetHttpsSupport(tests.TestCaseWithServer):
             self.skipTest("Requires pyOpenSSL")
         self.cacert_pem = os.path.join(os.path.dirname(__file__),
                                        'testing-certs', 'cacert.pem')
+        # The default u1db http_client class for doing HTTPS only does HTTPS
+        # if the platform is linux. Because of this, soledad replaces that
+        # class with one that will do HTTPS independent of the platform. In
+        # order to maintain the compatibility with u1db default tests, we undo
+        # that replacement here.
+        http_client._VerifiedHTTPSConnection = soledad.old__VerifiedHTTPSConnection
         super(TestHttpSyncTargetHttpsSupport, self).setUp()
 
     def getSyncTarget(self, host, path=None):
-- 
cgit v1.2.3