diff --git a/src/txacme/endpoint.py b/src/txacme/endpoint.py index 023b2ef..8824951 100644 --- a/src/txacme/endpoint.py +++ b/src/txacme/endpoint.py @@ -138,6 +138,7 @@ def _parse(reactor, directory, pemdir, *args, **kwargs): for issuing certs. :param str pemdir: The path to the certificate directory to use. """ + onstore_scripts = kwargs.pop('onstore', 'false').lower().startswith('t') def colon_join(items): return ':'.join([item.replace(':', '\\:') for item in items]) sub = colon_join(list(args) + ['='.join(item) for item in kwargs.items()]) @@ -160,7 +161,7 @@ def colon_join(items): reactor=reactor, directory=directory, client_creator=partial(Client.from_url, key=acme_key, alg=RS256), - cert_store=DirectoryStore(pem_path), + cert_store=DirectoryStore(pem_path, onstore_scripts=onstore_scripts), cert_mapping=HostDirectoryMap(pem_path), sub_endpoint=serverFromString(reactor, sub)) diff --git a/src/txacme/store.py b/src/txacme/store.py index 55462b3..c7af924 100644 --- a/src/txacme/store.py +++ b/src/txacme/store.py @@ -1,9 +1,12 @@ """ ``txacme.interfaces.ICertificateStore`` implementations. """ +import os + import attr from pem import parse from twisted.internet.defer import maybeDeferred, succeed +from twisted.internet.utils import getProcessValue from zope.interface import implementer from txacme.interfaces import ICertificateStore @@ -16,6 +19,8 @@ class DirectoryStore(object): A certificate store that keeps certificates in a directory on disk. """ path = attr.ib() + onstore_scripts = attr.ib(default=False) + reactor = attr.ib(default=None) def _get(self, server_name): """ @@ -33,7 +38,16 @@ def get(self, server_name): def store(self, server_name, pem_objects): p = self.path.child(server_name + u'.pem') p.setContent(b''.join(o.as_bytes() for o in pem_objects)) - return succeed(None) + if not self.onstore_scripts: + return succeed(None) + onstore_script = self.path.child(server_name + u'.onstore') + if not onstore_script.exists(): + return succeed(None) + d = getProcessValue( + onstore_script.path.encode(), args=[server_name.encode()], + env=os.environ, path=self.path.path.encode(), reactor=self.reactor) + d.addCallback(lambda ign: None) + return d def as_dict(self): return succeed( diff --git a/src/txacme/test/test_store.py b/src/txacme/test/test_store.py index 7fe873d..7a5c6de 100644 --- a/src/txacme/test/test_store.py +++ b/src/txacme/test/test_store.py @@ -1,9 +1,12 @@ import pem from fixtures import TempDir +from hypothesis import strategies as s from hypothesis import example, given from testtools import TestCase -from testtools.matchers import ContainsDict, Equals, Is, IsInstance -from testtools.twistedsupport import succeeded +from testtools.matchers import ( + ContainsDict, Equals, FileExists, Is, IsInstance, Not) +from testtools.twistedsupport import ( + AsynchronousDeferredRunTestForBrokenTwisted, succeeded) from twisted.python.filepath import FilePath from txacme.store import DirectoryStore @@ -91,14 +94,72 @@ def test_get_missing(self, server_name): failed_with(IsInstance(KeyError))) -class DirectoryStoreTests(_StoreTestsMixin, TestCase): +class _DirectoryStoreTestsMixin(object): + def setUp(self): + super(_DirectoryStoreTestsMixin, self).setUp() + self.temp_dir = FilePath(self.useFixture(TempDir()).path) + + @given(ts.dns_names(), ts.pem_objects(), s.integers()) + def test_onstore_script(self, server_name, pem_objects, nonce): + """ + .onstore scripts will be run after something is stored, but only if the + setting is enabled. + """ + script = self.temp_dir.child(server_name + '.onstore') + script.setContent("""\ +#!/bin/sh +echo >output "%s" "$1" + """.strip(' ') % nonce) + script.chmod(0o700) + d = self.cert_store.store(server_name, pem_objects) + return self._check_onstore_script(d, server_name, nonce) + + +class DirectoryStoreTests( + _StoreTestsMixin, _DirectoryStoreTestsMixin, TestCase): """ Tests for `txacme.store.DirectoryStore`. """ def setUp(self): super(DirectoryStoreTests, self).setUp() - temp_dir = self.useFixture(TempDir()) - self.cert_store = DirectoryStore(FilePath(temp_dir.path)) + self.cert_store = DirectoryStore(self.temp_dir) + + def _check_onstore_script(self, d, server_name, nonce): + self.expectThat(d, succeeded(Is(None))) + self.expectThat(self.temp_dir.child('output').path, Not(FileExists())) + + +class DirectoryStoreWithOnstoreScriptsTests( + _StoreTestsMixin, _DirectoryStoreTestsMixin, TestCase): + """ + Tests for `txacme.store.DirectoryStore` with onstore_scripts=True. + """ + def setUp(self): + super(DirectoryStoreWithOnstoreScriptsTests, self).setUp() + self.cert_store = DirectoryStore(self.temp_dir, onstore_scripts=True) + self.example_result = self.defaultTestResult() + + def execute_example(self, f): + runtest_fac = AsynchronousDeferredRunTestForBrokenTwisted.make_factory( + timeout=2) + + class Case(TestCase): + def test_example(self): + result = f() + if callable(result): + result = result() + return result + + runtest_fac(Case('test_example')).run(self.example_result) + + def _check_output(self, ign, expected_content): + self.assertThat( + self.temp_dir.child('output').getContent(), + Equals(expected_content)) + + def _check_onstore_script(self, d, server_name, nonce): + d.addCallback(self._check_output, '%s %s\n' % (nonce, server_name)) + return d class MemoryStoreTests(_StoreTestsMixin, TestCase):