Skip to content

Commit

Permalink
Merge branch 'simplefs'
Browse files Browse the repository at this point in the history
  • Loading branch information
Brad Warren committed Oct 17, 2015
2 parents a107ceb + 63c080b commit 69711e4
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 0 deletions.
5 changes: 5 additions & 0 deletions docs/api/plugins/webroot.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
:mod:`letsencrypt.plugins.webroot`
----------------------------------

.. automodule:: letsencrypt.plugins.webroot
:members:
4 changes: 4 additions & 0 deletions letsencrypt/plugins/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ def option_namespace(self):
"""ArgumentParser options namespace (prefix of all options)."""
return option_namespace(self.name)

def option_name(self, name):
"""Option name (include plugin namespace)."""
return self.option_namespace + name

@property
def dest_namespace(self):
"""ArgumentParser dest namespace (prefix of all destinations)."""
Expand Down
3 changes: 3 additions & 0 deletions letsencrypt/plugins/common_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ def test_init(self):
def test_option_namespace(self):
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):
self.assertEqual("mock_", self.plugin.dest_namespace)

Expand Down
87 changes: 87 additions & 0 deletions letsencrypt/plugins/webroot.py
Original file line number Diff line number Diff line change
@@ -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 changes: 82 additions & 0 deletions letsencrypt/plugins/webroot_test.py
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ def read_file(filename, encoding='utf8'):
'manual = letsencrypt.plugins.manual:Authenticator',
'null = letsencrypt.plugins.null:Installer',
'standalone = letsencrypt.plugins.standalone:Authenticator',
'webroot = letsencrypt.plugins.webroot:Authenticator',
],
},
)

0 comments on commit 69711e4

Please sign in to comment.