Merge branch 'simplefs'

This commit is contained in:
Brad Warren
2015-10-16 17:37:43 -07:00
6 changed files with 182 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
:mod:`letsencrypt.plugins.webroot`
----------------------------------
.. automodule:: letsencrypt.plugins.webroot
:members:
+4
View File
@@ -46,6 +46,10 @@ class Plugin(object):
"""ArgumentParser options namespace (prefix of all options).""" """ArgumentParser options namespace (prefix of all options)."""
return option_namespace(self.name) return option_namespace(self.name)
def option_name(self, name):
"""Option name (include plugin namespace)."""
return self.option_namespace + name
@property @property
def dest_namespace(self): def dest_namespace(self):
"""ArgumentParser dest namespace (prefix of all destinations).""" """ArgumentParser dest namespace (prefix of all destinations)."""
+3
View File
@@ -51,6 +51,9 @@ class PluginTest(unittest.TestCase):
def test_option_namespace(self): def test_option_namespace(self):
self.assertEqual("mock-", self.plugin.option_namespace) self.assertEqual("mock-", self.plugin.option_namespace)
def test_option_name(self):
self.assertEqual("mock-foo_bar", self.plugin.option_name("foo_bar"))
def test_dest_namespace(self): def test_dest_namespace(self):
self.assertEqual("mock_", self.plugin.dest_namespace) self.assertEqual("mock_", self.plugin.dest_namespace)
+87
View File
@@ -0,0 +1,87 @@
"""Webroot plugin."""
import errno
import logging
import os
import zope.interface
from acme import challenges
from letsencrypt import errors
from letsencrypt import interfaces
from letsencrypt.plugins import common
logger = logging.getLogger(__name__)
class Authenticator(common.Plugin):
"""Webroot Authenticator."""
zope.interface.implements(interfaces.IAuthenticator)
zope.interface.classProvides(interfaces.IPluginFactory)
description = "Webroot Authenticator"
MORE_INFO = """\
Authenticator plugin that performs SimpleHTTP challenge by saving
necessary validation resources to appropriate paths on the file
system. It expects that there is some other HTTP server configured
to serve all files under specified web root ({0})."""
def more_info(self): # pylint: disable=missing-docstring,no-self-use
return self.MORE_INFO.format(self.conf("path"))
@classmethod
def add_parser_arguments(cls, add):
add("path", help="public_html / webroot path")
def get_chall_pref(self, domain): # pragma: no cover
# pylint: disable=missing-docstring,no-self-use,unused-argument
return [challenges.SimpleHTTP]
def __init__(self, *args, **kwargs):
super(Authenticator, self).__init__(*args, **kwargs)
self.full_root = None
def prepare(self): # pylint: disable=missing-docstring
path = self.conf("path")
if path is None:
raise errors.PluginError("--{0} must be set".format(
self.option_name("path")))
if not os.path.isdir(path):
raise errors.PluginError(
path + " does not exist or is not a directory")
self.full_root = os.path.join(
path, challenges.SimpleHTTPResponse.URI_ROOT_PATH)
logger.debug("Creating root challenges validation dir at %s",
self.full_root)
try:
os.makedirs(self.full_root)
except OSError as exception:
if exception.errno != errno.EEXIST:
raise errors.PluginError(
"Couldn't create root for SimpleHTTP "
"challenge responses: {0}", exception)
def perform(self, achalls): # pylint: disable=missing-docstring
assert self.full_root is not None
return [self._perform_single(achall) for achall in achalls]
def _path_for_achall(self, achall):
return os.path.join(self.full_root, achall.chall.encode("token"))
def _perform_single(self, achall):
response, validation = achall.gen_response_and_validation(
tls=(not self.config.no_simple_http_tls))
path = self._path_for_achall(achall)
logger.debug("Attempting to save validation to %s", path)
with open(path, "w") as validation_file:
validation_file.write(validation.json_dumps())
return response
def cleanup(self, achalls): # pylint: disable=missing-docstring
for achall in achalls:
path = self._path_for_achall(achall)
logger.debug("Removing %s", path)
os.remove(path)
+82
View File
@@ -0,0 +1,82 @@
"""Tests for letsencrypt.plugins.webroot."""
import os
import shutil
import tempfile
import unittest
import mock
from acme import jose
from letsencrypt import achallenges
from letsencrypt import errors
from letsencrypt.tests import acme_util
from letsencrypt.tests import test_util
KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
class AuthenticatorTest(unittest.TestCase):
"""Tests for letsencrypt.plugins.webroot.Authenticator."""
achall = achallenges.SimpleHTTP(
challb=acme_util.SIMPLE_HTTP_P, domain=None, account_key=KEY)
def setUp(self):
from letsencrypt.plugins.webroot import Authenticator
self.path = tempfile.mkdtemp()
self.validation_path = os.path.join(
self.path, ".well-known", "acme-challenge",
"ZXZhR3hmQURzNnBTUmIyTEF2OUlaZjE3RHQzanV4R0orUEN0OTJ3citvQQ")
self.config = mock.MagicMock(webroot_path=self.path)
self.auth = Authenticator(self.config, "webroot")
self.auth.prepare()
def tearDown(self):
shutil.rmtree(self.path)
def test_more_info(self):
more_info = self.auth.more_info()
self.assertTrue(isinstance(more_info, str))
self.assertTrue(self.path in more_info)
def test_add_parser_arguments(self):
add = mock.MagicMock()
self.auth.add_parser_arguments(add)
self.assertEqual(1, add.call_count)
def test_prepare_bad_root(self):
self.config.webroot_path = os.path.join(self.path, "null")
self.assertRaises(errors.PluginError, self.auth.prepare)
def test_prepare_missing_root(self):
self.config.webroot_path = None
self.assertRaises(errors.PluginError, self.auth.prepare)
def test_prepare_full_root_exists(self):
# prepare() has already been called once in setUp()
self.auth.prepare() # shouldn't raise any exceptions
def test_prepare_reraises_other_errors(self):
self.auth.full_path = os.path.join(self.path, "null")
os.chmod(self.path, 0o000)
self.assertRaises(errors.PluginError, self.auth.prepare)
os.chmod(self.path, 0o700)
def test_perform_cleanup(self):
responses = self.auth.perform([self.achall])
self.assertEqual(1, len(responses))
self.assertTrue(os.path.exists(self.validation_path))
with open(self.validation_path) as validation_f:
validation = jose.JWS.json_loads(validation_f.read())
self.assertTrue(responses[0].check_validation(
validation, self.achall.chall, KEY.public_key()))
self.auth.cleanup([self.achall])
self.assertFalse(os.path.exists(self.validation_path))
if __name__ == "__main__":
unittest.main() # pragma: no cover
+1
View File
@@ -132,6 +132,7 @@ setup(
'manual = letsencrypt.plugins.manual:Authenticator', 'manual = letsencrypt.plugins.manual:Authenticator',
'null = letsencrypt.plugins.null:Installer', 'null = letsencrypt.plugins.null:Installer',
'standalone = letsencrypt.plugins.standalone:Authenticator', 'standalone = letsencrypt.plugins.standalone:Authenticator',
'webroot = letsencrypt.plugins.webroot:Authenticator',
], ],
}, },
) )