From 96024d724438102661138fe2773f4c0c31fb671f Mon Sep 17 00:00:00 2001
From: Daira Hopwood <daira@jacaranda.org>
Date: Tue, 2 Jun 2015 18:07:20 +0100
Subject: [PATCH] Add OpenSSL version check and tests. refs ticket:2215

Signed-off-by: Daira Hopwood <daira@jacaranda.org>
---
 src/allmydata/__init__.py          | 26 +++++++++++++++++++++++++-
 src/allmydata/_auto_deps.py        |  4 +++-
 src/allmydata/test/test_version.py | 29 ++++++++++++++++++++++++++++-
 3 files changed, 56 insertions(+), 3 deletions(-)

diff --git a/src/allmydata/__init__.py b/src/allmydata/__init__.py
index 44f971f8..5f6cd8c1 100644
--- a/src/allmydata/__init__.py
+++ b/src/allmydata/__init__.py
@@ -152,6 +152,28 @@ def normalized_version(verstr, what=None):
         raise PackagingError, ("could not parse %s due to %s: %s"
                                % (what or repr(verstr), cls.__name__, value)), trace
 
+def get_openssl_version():
+    try:
+        from OpenSSL import SSL
+        return extract_openssl_version(SSL)
+    except Exception:
+        return ("unknown", None, None)
+
+def extract_openssl_version(ssl_module):
+    openssl_version = ssl_module.SSLeay_version(ssl_module.SSLEAY_VERSION)
+    if openssl_version.startswith('OpenSSL '):
+        openssl_version = openssl_version[8 :]
+
+    (version, _, comment) = openssl_version.partition(' ')
+
+    try:
+        openssl_cflags = ssl_module.SSLeay_version(ssl_module.SSLEAY_CFLAGS)
+        if '-DOPENSSL_NO_HEARTBEATS' in openssl_cflags.split(' '):
+            comment += ", no heartbeats"
+    except Exception:
+        pass
+
+    return (version, None, comment if comment else None)
 
 def get_package_versions_and_locations():
     import warnings
@@ -221,6 +243,8 @@ def get_package_versions_and_locations():
             packages.append( (pkgname, (platform.python_version(), sys.executable, None)) )
         elif pkgname == 'platform':
             packages.append( (pkgname, (get_platform(), None, None)) )
+        elif pkgname == 'OpenSSL':
+            packages.append( (pkgname, get_openssl_version()) )
 
     return packages
 
@@ -297,7 +321,7 @@ def cross_check(pkg_resources_vers_and_locs, imported_vers_and_locs_list):
     from _auto_deps import not_import_versionable, ignorable
 
     errors = []
-    not_pkg_resourceable = ['python', 'platform', __appname__.lower()]
+    not_pkg_resourceable = ['python', 'platform', __appname__.lower(), 'openssl']
 
     for name, (imp_ver, imp_loc, imp_comment) in imported_vers_and_locs_list:
         name = name.lower()
diff --git a/src/allmydata/_auto_deps.py b/src/allmydata/_auto_deps.py
index 69a60815..18faacc1 100644
--- a/src/allmydata/_auto_deps.py
+++ b/src/allmydata/_auto_deps.py
@@ -69,6 +69,7 @@ package_imports = [
     ('python',           None),
     ('platform',         None),
     ('pyOpenSSL',        'OpenSSL'),
+    ('OpenSSL',          None),
     ('simplejson',       'simplejson'),
     ('pycrypto',         'Crypto'),
     ('pyasn1',           'pyasn1'),
@@ -169,7 +170,8 @@ else:
 #   not *directly* depend on pyOpenSSL.
 #
 # * pyOpenSSL >= 0.13 is needed in order to avoid
-#   <https://tahoe-lafs.org/trac/tahoe-lafs/ticket/2005>.
+#   <https://tahoe-lafs.org/trac/tahoe-lafs/ticket/2005>, and also to check the
+#   version of OpenSSL that pyOpenSSL is using.
 #
 # * pyOpenSSL >= 0.14 is built on the 'cryptography' package which depends
 #   on 'cffi' (and indirectly several other packages). Unfortunately cffi
diff --git a/src/allmydata/test/test_version.py b/src/allmydata/test/test_version.py
index fc642777..9433e365 100644
--- a/src/allmydata/test/test_version.py
+++ b/src/allmydata/test/test_version.py
@@ -3,12 +3,27 @@ from pkg_resources import Requirement
 
 from twisted.trial import unittest
 
-from allmydata import check_requirement, cross_check, PackagingError
+from allmydata import check_requirement, cross_check, extract_openssl_version, PackagingError
 from allmydata.util.verlib import NormalizedVersion as V, \
                                   IrrationalVersionError, \
                                   suggest_normalized_version as suggest
 
 
+class MockSSL(object):
+    SSLEAY_VERSION = 0
+    SSLEAY_CFLAGS = 2
+
+    def __init__(self, version, compiled_without_heartbeats=False):
+        self.opts = {
+            self.SSLEAY_VERSION: version,
+            self.SSLEAY_CFLAGS: compiled_without_heartbeats and 'compiler: gcc -DOPENSSL_NO_HEARTBEATS'
+                                                             or 'compiler: gcc',
+        }
+
+    def SSLeay_version(self, which):
+        return self.opts[which]
+
+
 class CheckRequirement(unittest.TestCase):
     def test_check_requirement(self):
         self._check_success("setuptools >= 0.6c6", {"setuptools": ("0.6", "", None)})
@@ -118,6 +133,18 @@ class CheckRequirement(unittest.TestCase):
         self.failUnlessEqual(len(res), 1)
         self.failUnlessIn("but version '2.0'", res[0])
 
+    def test_extract_openssl_version(self):
+        self.failUnlessEqual(extract_openssl_version(MockSSL("")),
+                                                     ("", None, None))
+        self.failUnlessEqual(extract_openssl_version(MockSSL("NotOpenSSL a.b.c foo")),
+                                                     ("NotOpenSSL", None, "a.b.c foo"))
+        self.failUnlessEqual(extract_openssl_version(MockSSL("OpenSSL a.b.c")),
+                                                     ("a.b.c", None, None))
+        self.failUnlessEqual(extract_openssl_version(MockSSL("OpenSSL 1.0.1e 11 Feb 2013")),
+                                                     ("1.0.1e", None, "11 Feb 2013"))
+        self.failUnlessEqual(extract_openssl_version(MockSSL("OpenSSL 1.0.1e 11 Feb 2013", compiled_without_heartbeats=True)),
+                                                     ("1.0.1e", None, "11 Feb 2013, no heartbeats"))
+
 
 # based on https://bitbucket.org/tarek/distutilsversion/src/17df9a7d96ef/test_verlib.py
 
-- 
2.45.2