Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add .onstore scripts #27

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/txacme/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()])
Expand All @@ -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))

Expand Down
16 changes: 15 additions & 1 deletion src/txacme/store.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
"""
Expand All @@ -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(
Expand Down
71 changes: 66 additions & 5 deletions src/txacme/test/test_store.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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):
Expand Down