diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 8f677377a..388f20fb1 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -198,6 +198,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Temporary state for AutoHSTS enhancement self._autohsts = {} # type: Dict[str, Dict[str, Union[int, float]]] self._ocsp_prefetch = {} # type: Dict[str, str] + self._ocsp_dbm_bsddb = False # These will be set in the prepare function self._prepared = False @@ -2561,15 +2562,53 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if handler.ocsp_request_to_file(ocsp_workfile): # Guaranteed good response cache_path = os.path.join(self.config.config_dir, "ocsp", "ocsp_cache") - # dbm.open automatically adds the file extension, it will be - db = dbm.open(cache_path, "c") + db = self._ocsp_dbm_open(cache_path) cert_sha = apache_util.certid_sha1(cert_path) db[cert_sha] = self._ocsp_response_dbm(ocsp_workfile) - db.close() + self._ocsp_dbm_close(db) else: logger.warning("Encountered an issue while trying to prefetch OCSP " "response for certificate: %s", cert_path) + def _ensure_ocsp_prefetch_compatibility(self): + """Make sure that the operating system supports the required libraries + to manage Apache DBM files. + + :raises: errors.NotSupportedError + """ + try: + import bsddb + except ImportError: + import dbm + if not hasattr(dbm, 'ndbm'): + msg = ("Unfortunately your operating system does not have a " + "compatible database module available for managing " + "Apache cache database.") + raise errors.NotSupportedError(msg) + + def _ocsp_dbm_open(self, filepath): + """Helper method to open an DBM file in a way that depends on the platform + that Certbot is run on. Returns an open database structure.""" + try: + import bsddb + self._ocsp_dbm_bsddb = True + cache_path = filepath + ".db" + database = bsddb.hashopen(cache_path, 'c') + except ImportError: + # Python3 doesn't have bsddb module, so we use dbm.ndbm instead + import dbm + database = dbm.ndbm.open(filepath, 'c') + return database + + def _ocsp_dbm_close(self, database): + """Helper method to sync and close a DBM file, in a way required by the + used dbm implementation.""" + if self._ocsp_dbm_bsddb: + database.sync() + database.close() + else: + database.close() + def _ocsp_response_dbm(self, workfile): """Creates a dbm entry for OCSP response data @@ -2645,6 +2684,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): TLS. i.e. clients would not have to query the OCSP responder. """ + # Fail early if we are not able to support this + self._ensure_ocsp_prefetch_compatibility() prefetch_vhosts = set() for domain in domains: matched_vhosts = self.choose_vhosts(domain, create_if_no_ssl=False) @@ -2658,6 +2699,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self._enable_ocsp_stapling(vh, None, prefetch=True) self._ocsp_prefetch_save(lineage.cert_path, lineage.chain_path) self.restart() + logger.warning(apache_util.certid_sha1_hex(lineage.cert_path)) self._ocsp_refresh(lineage.cert_path, lineage.chain_path) def update_ocsp_prefetch(self, _unused_lineage): diff --git a/certbot-apache/certbot_apache/tests/ocsp_prefetch_test.py b/certbot-apache/certbot_apache/tests/ocsp_prefetch_test.py index f8cf05b97..42178d146 100644 --- a/certbot-apache/certbot_apache/tests/ocsp_prefetch_test.py +++ b/certbot-apache/certbot_apache/tests/ocsp_prefetch_test.py @@ -12,19 +12,6 @@ class OCSPPrefetchTest(util.ApacheTest): """Tests for OCSP Prefetch feature""" # pylint: disable=protected-access - def openDBM(self, path): - self.config._ensure_ocsp_dirs() - try: - import dbm.ndbm as dbm # pragma: no cover - d = dbm.open(path, 'c') - except ImportError: # pragma: no cover - # dbm.ndbm only available on Python3 - import anydbm # pragma: no cover - anydbm._names = ["dbm"] - d = anydbm.open(path, 'c') - return d - - def setUp(self): # pylint: disable=arguments-differ super(OCSPPrefetchTest, self).setUp() @@ -73,6 +60,7 @@ class OCSPPrefetchTest(util.ApacheTest): @mock.patch("certbot_apache.constants.OCSP_INTERNAL_TTL", 0) @mock.patch("certbot_apache.configurator.ApacheConfigurator.restart") def test_ocsp_prefetch_refresh(self, _mock_restart): + db_path = os.path.join(self.config_dir, "ocsp", "ocsp_cache") def ocsp_req_mock(workfile): """Method to mock the OCSP request and write response to file""" with open(workfile, 'w') as fh: @@ -84,11 +72,12 @@ class OCSPPrefetchTest(util.ApacheTest): self.call_mocked(self.config.enable_ocsp_prefetch, self.lineage, ["ocspvhost.com"]) - odbm = dbm.open(os.path.join(self.config_dir, "ocsp", "ocsp_cache"), 'c') + + odbm = self.config._ocsp_dbm_open(db_path) self.assertEquals(len(odbm.keys()), 1) # The actual response data is prepended by Apache timestamp self.assertTrue(odbm[odbm.keys()[0]].endswith(b'MOCKRESPONSE')) - odbm.close() + self.config._ocsp_dbm_close(odbm) with mock.patch(ocsp_path, side_effect=ocsp_req_mock) as mock_ocsp: self.call_mocked(self.config.update_ocsp_prefetch, None) @@ -115,17 +104,18 @@ class OCSPPrefetchTest(util.ApacheTest): @mock.patch("certbot_apache.configurator.ApacheConfigurator.config_test") def test_ocsp_prefetch_backup_db(self, _mock_test): - db_path = os.path.join(self.config_dir, "ocsp", "ocsp_cache.db") + db_path = os.path.join(self.config_dir, "ocsp", "ocsp_cache") def ocsp_del_db(): """Side effect of _reload() that deletes the DBM file, like Apache does when restarting""" - os.remove(db_path) - self.assertFalse(os.path.isfile(db_path)) + full_db_path = db_path + ".db" + os.remove(full_db_path) + self.assertFalse(os.path.isfile(full_db_path)) self.config._ensure_ocsp_dirs() - odbm = dbm.open(db_path[:-3], 'c') + odbm = self.config._ocsp_dbm_open(db_path) odbm["mock_key"] = b'mock_value' - odbm.close() + self.config._ocsp_dbm_close(odbm) # Mock OCSP prefetch dict to signify that there should be a db self.config._ocsp_prefetch = {"mock": "value"} @@ -133,9 +123,9 @@ class OCSPPrefetchTest(util.ApacheTest): with mock.patch(rel_path, side_effect=ocsp_del_db) as mock_reload: self.config.restart() - odbm = dbm.open(db_path[:-3], 'c') + odbm = self.config._ocsp_dbm_open(db_path) self.assertEquals(odbm["mock_key"], b'mock_value') - odbm.close() + self.config._ocsp_dbm_close(odbm) @mock.patch("certbot_apache.configurator.ApacheConfigurator.config_test") @mock.patch("certbot_apache.configurator.ApacheConfigurator._reload") @@ -172,14 +162,6 @@ class OCSPPrefetchTest(util.ApacheTest): self.config.update_ocsp_prefetch(None) self.assertFalse(mock_refresh.called) - def test_dbm_format(self): - db_path = os.path.join(self.config_dir, "ocsp", "ocsp_cache.db") - dobject = self.openDBM(db_path) - dbm_dir = dir(dobject) - dbm_dir.append(dobject.__module__) - self.assertEquals(1, dbm_dir) - - if __name__ == "__main__": unittest.main() # pragma: no cover