Merge remote-tracking branch 'github/letsencrypt/master' into test-mode

Conflicts:
	letsencrypt/network2.py
This commit is contained in:
Jakub Warmuz 2015-05-22 06:26:16 +00:00
commit 7495145563
No known key found for this signature in database
GPG key ID: 2A7BAD3A489B52EA
57 changed files with 401 additions and 147 deletions

11
.dockerignore Normal file
View file

@ -0,0 +1,11 @@
# this file uses slightly different syntax than .gitignore,
# e.g. ".tox/" will not ignore .tox directory
# well, official docker build should be done on clean git checkout
# anyway, so .tox should be empty... But I'm sure people will try to
# test docker on their git working directories.
.git
.tox
venv
docs

61
Dockerfile Normal file
View file

@ -0,0 +1,61 @@
# https://github.com/letsencrypt/lets-encrypt-preview/pull/431#issuecomment-103659297
# it is more likely developers will already have ubuntu:trusty rather
# than e.g. debian:jessie and image size differences are negligible
FROM ubuntu:trusty
MAINTAINER Jakub Warmuz <jakub@warmuz.org>
MAINTAINER William Budington <bill@eff.org>
# Note: this only exposes the port to other docker containers. You
# still have to bind to 443@host at runtime, as per the ACME spec.
EXPOSE 443
# TODO: make sure --config-dir and --work-dir cannot be changed
# through the CLI (letsencrypt-docker wrapper that uses standalone
# authenticator and text mode only?)
VOLUME /etc/letsencrypt /var/lib/letsencrypt
WORKDIR /opt/letsencrypt
# no need to mkdir anything:
# https://docs.docker.com/reference/builder/#copy
# If <dest> doesn't exist, it is created along with all missing
# directories in its path.
COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/
RUN /opt/letsencrypt/src/ubuntu.sh && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* \
/tmp/* \
/var/tmp/*
# the above is not likely to change, so by putting it further up the
# Dockerfile we make sure we cache as much as possible
COPY setup.py README.rst CHANGES.rst MANIFEST.in /opt/letsencrypt/src/
# all above files are necessary for setup.py, however, package source
# code directory has to be copied separately to a subdirectory...
# https://docs.docker.com/reference/builder/#copy: "If <src> is a
# directory, the entire contents of the directory are copied,
# including filesystem metadata. Note: The directory itself is not
# copied, just its contents." Order again matters, three files are far
# more likely to be cached than the whole project directory
COPY letsencrypt /opt/letsencrypt/src/letsencrypt/
COPY acme /opt/letsencrypt/src/acme/
COPY letsencrypt_apache /opt/letsencrypt/src/letsencrypt_apache/
COPY letsencrypt_nginx /opt/letsencrypt/src/letsencrypt_nginx/
RUN virtualenv --no-site-packages -p python2 /opt/letsencrypt/venv && \
/opt/letsencrypt/venv/bin/pip install -e /opt/letsencrypt/src
# install in editable mode (-e) to save space: it's not possible to
# "rm -rf /opt/letsencrypt/src" (it's stays in the underlaying image);
# this might also help in debugging: you can "docker run --entrypoint
# bash" and investigate, apply patches, etc.
ENV PATH /opt/letsencrypt/venv/bin:$PATH
# TODO: is --text really necessary?
ENTRYPOINT [ "letsencrypt", "--text" ]

View file

@ -3,8 +3,7 @@ include CHANGES.rst
include CONTRIBUTING.md
include linter_plugin.py
include letsencrypt/EULA
recursive-include letsencrypt/client/tests/testdata *
recursive-include letsencrypt/tests/testdata *
recursive-include acme/schemata *.json
recursive-include acme/jose/testdata *

View file

@ -21,7 +21,7 @@ All you need to do is::
user@www:~$ sudo letsencrypt -d www.example.org auth
and if you have a compatbile web server (Apache or Nginx), Let's Encrypt can
and if you have a compatible web server (Apache or Nginx), Let's Encrypt can
not only get a new certificate, but also deploy it and configure your
server automatically!::

View file

@ -18,11 +18,9 @@ class Error(jose.JSONObjectWithFields, Exception):
'badCSR': 'The CSR is unacceptable (e.g., due to a short key)',
}
# TODO: Boulder omits 'type' and 'instance', spec requires, boulder#128
typ = jose.Field('type', omitempty=True)
typ = jose.Field('type')
title = jose.Field('title', omitempty=True)
detail = jose.Field('detail')
instance = jose.Field('instance', omitempty=True)
@typ.encoder
def typ(value): # pylint: disable=missing-docstring,no-self-argument
@ -227,10 +225,6 @@ class Authorization(ResourceBody):
challenges = jose.Field('challenges', omitempty=True)
combinations = jose.Field('combinations', omitempty=True)
# TODO: acme-spec #92, #98
key = Registration._fields['key']
contact = Registration._fields['contact']
status = jose.Field('status', omitempty=True, decoder=Status.from_json)
# TODO: 'expires' is allowed for Authorization Resources in
# general, but for Key Authorization '[t]he "expires" field MUST

View file

@ -21,7 +21,8 @@ class ErrorTest(unittest.TestCase):
def setUp(self):
from acme.messages2 import Error
self.error = Error(detail='foo', typ='malformed')
self.error = Error(detail='foo', typ='malformed', title='title')
self.jobj = {'detail': 'foo', 'title': 'some title'}
def test_typ_prefix(self):
self.assertEqual('malformed', self.error.typ)
@ -32,15 +33,15 @@ class ErrorTest(unittest.TestCase):
def test_typ_decoder_missing_prefix(self):
from acme.messages2 import Error
self.assertRaises(jose.DeserializationError, Error.from_json,
{'detail': 'foo', 'type': 'malformed'})
self.assertRaises(jose.DeserializationError, Error.from_json,
{'detail': 'foo', 'type': 'not valid bare type'})
self.jobj['type'] = 'malformed'
self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj)
self.jobj['type'] = 'not valid bare type'
self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj)
def test_typ_decoder_not_recognized(self):
from acme.messages2 import Error
self.assertRaises(jose.DeserializationError, Error.from_json,
{'detail': 'foo', 'type': 'urn:acme:error:baz'})
self.jobj['type'] = 'urn:acme:error:baz'
self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj)
def test_description(self):
self.assertEqual(

View file

@ -10,21 +10,35 @@
# - 7.8 "wheezy" (x64)
# - 8.0 "jessie" (x64)
# virtualenv binary can be found in different packages depending on
# distro version (#346)
distro=$(lsb_release -si)
# 6.0.10 => 60, 14.04 => 1404
version=$(lsb_release -sr | awk -F '.' '{print $1 $2}')
if [ "$distro" = "Ubuntu" -a "$version" -ge 1410 ]
then
virtualenv="virtualenv"
elif [ "$distro" = "Debian" -a "$version" -ge 80 ]
newer () {
distro=$(lsb_release -si)
# 6.0.10 => 60, 14.04 => 1404
# TODO: in sid version==unstable
version=$(lsb_release -sr | awk -F '.' '{print $1 $2}')
if [ "$distro" = "Ubuntu" -a "$version" -ge 1410 ]
then
return 0;
elif [ "$distro" = "Debian" -a "$version" -ge 80 ]
then
return 0;
else
return 1;
fi
}
# you can force newer if lsb_release is not available (e.g. Docker
# debian:jessie base image)
if [ "$1" = "newer" ] || newer
then
virtualenv="virtualenv"
else
virtualenv="python-virtualenv"
fi
# dpkg-dev: dpkg-architecture binary necessary to compile M2Crypto, c.f.
# #276, https://github.com/martinpaljak/M2Crypto/issues/62,
# M2Crypto setup.py:add_multiarch_paths

14
docker-compose.yml Normal file
View file

@ -0,0 +1,14 @@
production:
build: .
ports:
- "443:443"
# For development, mount git root to /opt/letsencrypt/src in order to
# make the dev workflow more vagrant-like.
development:
build: .
ports:
- "443:443"
volumes:
- .:/opt/letsencrypt/src
- /opt/letsencrypt/venv

View file

@ -1,5 +0,0 @@
:mod:`letsencrypt.client.proof_of_possession`
--------------------------------------------------
.. automodule:: letsencrypt.client.proof_of_possession
:members:

View file

@ -0,0 +1,5 @@
:mod:`letsencrypt.proof_of_possession`
--------------------------------------
.. automodule:: letsencrypt.proof_of_possession
:members:

View file

@ -2,6 +2,24 @@
Using the Let's Encrypt client
==============================
Quick start
===========
Using docker you can quickly get yourself a testing cert. From the
server that the domain your requesting a cert for resolves to,
download docker, and issue the following command
.. code-block:: shell
sudo docker run -it --rm -p 443:443 --name letsencrypt \
-v "/etc/letsencrypt:/etc/letsencrypt" \
-v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
quay.io/letsencrypt/lets-encrypt-preview:latest
And follow the instructions. Your new cert will be available in
``/etc/letsencrypt/certs``.
Prerequisites
=============
@ -31,7 +49,7 @@ Debian
sudo ./bootstrap/debian.sh
For squezze you will need to:
For squeeze you will need to:
- Use ``virtualenv --no-site-packages -p python`` instead of ``-p python2``.

View file

@ -20,7 +20,7 @@ class Authenticator(common.Plugin):
# "self" as first argument, e.g. def prepare(self)...
class Installer(common.Plugins):
class Installer(common.Plugin):
"""Example Installer."""
zope.interface.implements(interfaces.IInstaller)
zope.interface.classProvides(interfaces.IPluginFactory)

View file

@ -19,7 +19,7 @@ class ContinuityAuthenticator(object):
:ivar proof_of_pos: Performs "proofOfPossession" challenges.
:type proof_of_pos:
:class:`letsencrypt.client.proof_of_possession.Proof_of_Possession`
:class:`letsencrypt.proof_of_possession.Proof_of_Possession`
"""
zope.interface.implements(interfaces.IAuthenticator)
@ -32,7 +32,7 @@ class ContinuityAuthenticator(object):
:type config: :class:`letsencrypt.interfaces.IConfig`
:param installer: Let's Encrypt Installer.
:type installer: :class:`letsencrypt.client.interfaces.IInstaller`
:type installer: :class:`letsencrypt.interfaces.IInstaller`
"""
self.rec_token = recovery_token.RecoveryToken(

View file

@ -1,4 +1,4 @@
"""Let's Encrypt client crypto utility functions
"""Let's Encrypt client crypto utility functions.
.. todo:: Make the transition to use PSS rather than PKCS1_v1_5 when the server
is capable of handling the signatures.
@ -13,6 +13,7 @@ import Crypto.PublicKey.RSA
import Crypto.Signature.PKCS1_v1_5
import M2Crypto
import OpenSSL
from letsencrypt import le_util
@ -231,3 +232,44 @@ def make_ss_cert(key_str, domains, not_before=None,
assert cert.verify()
# print check_purpose(,0
return cert.as_pem()
def _request_san(req): # TODO: implement directly in PyOpenSSL!
# constants based on implementation of
# OpenSSL.crypto.X509Error._subjectAltNameString
parts_separator = ", "
part_separator = ":"
extension_short_name = "subjectAltName"
# pylint: disable=protected-access,no-member
label = OpenSSL.crypto.X509Extension._prefixes[OpenSSL.crypto._lib.GEN_DNS]
assert parts_separator not in label
prefix = label + part_separator
extensions = [ext._subjectAltNameString().split(parts_separator)
for ext in req.get_extensions()
if ext.get_short_name() == extension_short_name]
# WARNING: this function assumes that no SAN can include
# parts_separator, hence the split!
return [part.split(part_separator)[1] for parts in extensions
for part in parts if part.startswith(prefix)]
def get_sans_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM):
"""Get list of Subject Alternative Names from signing request.
:param str csr: Certificate Signing Request in PEM format (must contain
one or more subjectAlternativeNames, or the function will fail,
raising ValueError)
:returns: List of referenced subject alternative names
:rtype: list
"""
try:
request = OpenSSL.crypto.load_certificate_request(typ, csr)
except OpenSSL.crypto.Error as error:
logging.exception(error)
raise
return _request_san(request)

View file

@ -117,6 +117,7 @@ class Network(object):
:rtype: `requests.Response`
"""
logging.debug('Sending GET request to %s', uri)
kwargs.setdefault('verify', self.verify_ssl)
try:
response = requests.get(uri, **kwargs)
@ -136,13 +137,13 @@ class Network(object):
:rtype: `requests.Response`
"""
logging.debug('Sending POST data: %s', data)
logging.debug('Sending POST data to %s: %s', uri, data)
kwargs.setdefault('verify', self.verify_ssl)
try:
response = requests.post(uri, data=data, **kwargs)
except requests.exceptions.RequestException as error:
raise errors.NetworkError(error)
logging.debug('Received response %s: %s', response, response.text)
logging.debug('Received response %s: %r', response, response.text)
self._check_response(response, content_type=content_type)
return response
@ -251,6 +252,7 @@ class Network(object):
def _authzr_from_response(self, response, identifier,
uri=None, new_cert_uri=None):
# pylint: disable=no-self-use
if new_cert_uri is None:
try:
new_cert_uri = response.links['next']['url']
@ -261,8 +263,7 @@ class Network(object):
body=messages2.Authorization.from_json(response.json()),
uri=response.headers.get('Location', uri),
new_cert_uri=new_cert_uri)
if (authzr.body.key != self.key.public()
or authzr.body.identifier != identifier):
if authzr.body.identifier != identifier:
raise errors.UnexpectedUpdate(authzr)
return authzr

View file

@ -215,8 +215,9 @@ class PluginsRegistry(collections.Mapping):
return None
def __repr__(self):
return "{0}({1!r})".format(
self.__class__.__name__, set(self._plugins.itervalues()))
return "{0}({1})".format(
self.__class__.__name__, ','.join(
repr(p_ep) for p_ep in self._plugins.itervalues()))
def __str__(self):
if not self._plugins:

View file

@ -154,6 +154,7 @@ class PluginsRegistryTest(unittest.TestCase):
def setUp(self):
from letsencrypt.plugins.disco import PluginsRegistry
self.plugin_ep = mock.MagicMock(name="mock")
self.plugin_ep.__hash__.side_effect = TypeError
self.plugins = {"mock": self.plugin_ep}
self.reg = PluginsRegistry(self.plugins)
@ -227,7 +228,7 @@ class PluginsRegistryTest(unittest.TestCase):
def test_repr(self):
self.plugin_ep.__repr__ = lambda _: "PluginEntryPoint#mock"
self.assertEqual("PluginsRegistry(set([PluginEntryPoint#mock]))",
self.assertEqual("PluginsRegistry(PluginEntryPoint#mock)",
repr(self.reg))
def test_str(self):

View file

@ -152,9 +152,6 @@ class StandaloneAuthenticator(common.Plugin):
:rtype: bool
"""
signal.signal(signal.SIGIO, self.client_signal_handler)
signal.signal(signal.SIGUSR1, self.client_signal_handler)
signal.signal(signal.SIGUSR2, self.client_signal_handler)
display = zope.component.getUtility(interfaces.IDisplay)
@ -259,6 +256,16 @@ class StandaloneAuthenticator(common.Plugin):
:rtype: bool
"""
# In order to avoid a race condition, we set the signal handler
# that will be needed by the parent process now, and undo this
# action if we turn out to be the child process. (This needs
# to happen before the fork because the child will send one of
# these signals to the parent almost immediately after the
# fork, and the parent must already be ready to receive it.)
signal.signal(signal.SIGIO, self.client_signal_handler)
signal.signal(signal.SIGUSR1, self.client_signal_handler)
signal.signal(signal.SIGUSR2, self.client_signal_handler)
fork_result = os.fork()
Crypto.Random.atfork()
if fork_result:
@ -269,6 +276,12 @@ class StandaloneAuthenticator(common.Plugin):
return self.do_parent_process(port)
else:
# CHILD process (the TCP listener subprocess)
# Undo the parent's signal handler settings, which aren't
# applicable to us.
signal.signal(signal.SIGIO, signal.SIG_DFL)
signal.signal(signal.SIGUSR1, signal.SIG_DFL)
signal.signal(signal.SIGUSR2, signal.SIG_DFL)
self.child_pid = os.getpid()
# do_child_process() is normally not expected to return but
# should terminate via sys.exit().

View file

@ -404,47 +404,39 @@ class DoParentProcessTest(unittest.TestCase):
StandaloneAuthenticator
self.authenticator = StandaloneAuthenticator(config=None, name=None)
@mock.patch("letsencrypt.plugins.standalone.authenticator.signal.signal")
@mock.patch("letsencrypt.plugins.standalone.authenticator."
"zope.component.getUtility")
def test_do_parent_process_ok(self, mock_get_utility, mock_signal):
def test_do_parent_process_ok(self, mock_get_utility):
self.authenticator.subproc_state = "ready"
result = self.authenticator.do_parent_process(1717)
self.assertTrue(result)
self.assertEqual(mock_get_utility.call_count, 1)
self.assertEqual(mock_signal.call_count, 3)
@mock.patch("letsencrypt.plugins.standalone.authenticator.signal.signal")
@mock.patch("letsencrypt.plugins.standalone.authenticator."
"zope.component.getUtility")
def test_do_parent_process_inuse(self, mock_get_utility, mock_signal):
def test_do_parent_process_inuse(self, mock_get_utility):
self.authenticator.subproc_state = "inuse"
result = self.authenticator.do_parent_process(1717)
self.assertFalse(result)
self.assertEqual(mock_get_utility.call_count, 1)
self.assertEqual(mock_signal.call_count, 3)
@mock.patch("letsencrypt.plugins.standalone.authenticator.signal.signal")
@mock.patch("letsencrypt.plugins.standalone.authenticator."
"zope.component.getUtility")
def test_do_parent_process_cantbind(self, mock_get_utility, mock_signal):
def test_do_parent_process_cantbind(self, mock_get_utility):
self.authenticator.subproc_state = "cantbind"
result = self.authenticator.do_parent_process(1717)
self.assertFalse(result)
self.assertEqual(mock_get_utility.call_count, 1)
self.assertEqual(mock_signal.call_count, 3)
@mock.patch("letsencrypt.plugins.standalone.authenticator.signal.signal")
@mock.patch("letsencrypt.plugins.standalone.authenticator."
"zope.component.getUtility")
def test_do_parent_process_timeout(self, mock_get_utility, mock_signal):
def test_do_parent_process_timeout(self, mock_get_utility):
# Normally times out in 5 seconds and returns False. We can
# now set delay_amount to a lower value so that it times out
# faster than it would under normal use.
result = self.authenticator.do_parent_process(1717, delay_amount=1)
self.assertFalse(result)
self.assertEqual(mock_get_utility.call_count, 1)
self.assertEqual(mock_signal.call_count, 3)
class DoChildProcessTest(unittest.TestCase):

View file

@ -156,14 +156,6 @@ class GetAuthorizationsTest(unittest.TestCase):
self.assertRaises(errors.AuthorizationError,
self.handler.get_authorizations, ["0"])
def _get_exp_response(self, domain, path, challs):
# pylint: disable=no-self-use
exp_resp = [None] * len(challs)
for i in path:
exp_resp[i] = TRANSLATE[challs[i].typ] + str(domain)
return exp_resp
def _validate_all(self, unused_1, unused_2):
for dom in self.handler.authzr.keys():
azr = self.handler.authzr[dom]
@ -284,8 +276,6 @@ class PollChallengesTest(unittest.TestCase):
identifier=authzr.body.identifier,
challenges=new_challbs,
combinations=authzr.body.combinations,
key=authzr.body.key,
contact=authzr.body.contact,
status=status_,
),
)
@ -443,19 +433,5 @@ def gen_dom_authzr(domain, unused_new_authzr_uri, challs):
[messages2.STATUS_PENDING]*len(challs))
def gen_path(required, challs):
"""Generate a combination by picking ``required`` from ``challs``.
:param required: Required types of challenges (subclasses of
:class:`~acme.challenges.Challenge`).
:param challs: Sequence of ACME challenge messages, corresponding to
:attr:`acme.messages.Challenge.challenges`.
:return: :class:`list` of :class:`int`
"""
return [challs.index(chall) for chall in required]
if __name__ == "__main__":
unittest.main() # pragma: no cover

View file

@ -12,10 +12,11 @@ class CLITest(unittest.TestCase):
def _call(cls, args):
from letsencrypt import cli
args = ['--text'] + args
with mock.patch("letsencrypt.cli.sys.stdout") as stdout:
with mock.patch("letsencrypt.cli.sys.stderr") as stderr:
ret = cli.main(args)
return ret, stdout, stderr
with mock.patch('letsencrypt.cli.sys.stdout') as stdout:
with mock.patch('letsencrypt.cli.sys.stderr') as stderr:
with mock.patch('letsencrypt.cli.client') as client:
ret = cli.main(args)
return ret, stdout, stderr, client
def test_no_flags(self):
self.assertRaises(SystemExit, self._call, [])
@ -23,6 +24,18 @@ class CLITest(unittest.TestCase):
def test_help(self):
self.assertRaises(SystemExit, self._call, ['--help'])
def test_rollback(self):
_, _, _, client = self._call(['rollback'])
client.rollback.assert_called_once()
_, _, _, client = self._call(['rollback', '--checkpoints', '123'])
client.rollback.assert_called_once_with(
mock.ANY, 123, mock.ANY, mock.ANY)
def test_config_changes(self):
_, _, _, client = self._call(['config_changes'])
client.view_config_changes.assert_called_once()
def test_plugins(self):
flags = ['--init', '--prepare', '--authenticators', '--installers']
for args in itertools.chain(

View file

@ -56,8 +56,7 @@ class DetermineAccountTest(unittest.TestCase):
class RollbackTest(unittest.TestCase):
"""Test the rollback function."""
def setUp(self):
from letsencrypt_apache.configurator import ApacheConfigurator
self.m_install = mock.MagicMock(spec=ApacheConfigurator)
self.m_install = mock.MagicMock()
@classmethod
def _call(cls, checkpoints, side_effect):

View file

@ -7,6 +7,7 @@ import tempfile
import unittest
import M2Crypto
import OpenSSL
import mock
@ -150,5 +151,41 @@ class MakeSSCertTest(unittest.TestCase):
make_ss_cert(RSA512_KEY, ['example.com', 'www.example.com'])
class GetSansFromCsrTest(unittest.TestCase):
"""Tests for letsencrypt.crypto_util.get_sans_from_csr."""
def test_extract_one_san(self):
from letsencrypt.crypto_util import get_sans_from_csr
csr = pkg_resources.resource_string(
__name__, os.path.join('testdata', 'csr.pem'))
self.assertEqual(get_sans_from_csr(csr), ['example.com'])
def test_extract_two_sans(self):
from letsencrypt.crypto_util import get_sans_from_csr
csr = pkg_resources.resource_string(
__name__, os.path.join('testdata', 'csr-san.pem'))
self.assertEqual(get_sans_from_csr(csr), ['example.com',
'www.example.com'])
def test_extract_six_sans(self):
from letsencrypt.crypto_util import get_sans_from_csr
csr = pkg_resources.resource_string(
__name__, os.path.join('testdata', 'csr-6sans.pem'))
self.assertEqual(get_sans_from_csr(csr),
["example.com", "example.org", "example.net",
"example.info", "subdomain.example.com",
"other.subdomain.example.com"])
def test_parse_non_csr(self):
from letsencrypt.crypto_util import get_sans_from_csr
self.assertRaises(OpenSSL.crypto.Error, get_sans_from_csr,
"hello there")
def test_parse_no_sans(self):
from letsencrypt.crypto_util import get_sans_from_csr
csr = pkg_resources.resource_string(
__name__, os.path.join('testdata', 'csr-nosans.pem'))
self.assertEqual([], get_sans_from_csr(csr))
if __name__ == '__main__':
unittest.main() # pragma: no cover

View file

@ -3,6 +3,8 @@ import datetime
import httplib
import os
import pkg_resources
import shutil
import tempfile
import unittest
import M2Crypto
@ -50,6 +52,8 @@ class NetworkTest(unittest.TestCase):
self.identifier = messages2.Identifier(
typ=messages2.IDENTIFIER_FQDN, value='example.com')
self.config = mock.Mock(accounts_dir=tempfile.mkdtemp())
# Registration
self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212')
reg = messages2.Registration(
@ -69,7 +73,7 @@ class NetworkTest(unittest.TestCase):
self.authz = messages2.Authorization(
identifier=messages2.Identifier(
typ=messages2.IDENTIFIER_FQDN, value='example.com'),
challenges=(challb,), combinations=None, key=KEY.public())
challenges=(challb,), combinations=None)
self.authzr = messages2.AuthorizationResource(
body=self.authz, uri=authzr_uri,
new_cert_uri='https://www.letsencrypt-demo.org/acme/new-cert')
@ -80,6 +84,9 @@ class NetworkTest(unittest.TestCase):
uri='https://www.letsencrypt-demo.org/acme/cert/1',
cert_chain_uri='https://www.letsencrypt-demo.org/ca')
def tearDown(self):
shutil.rmtree(self.config.accounts_dir)
def _mock_post_get(self):
# pylint: disable=protected-access
self.net._post = mock.MagicMock(return_value=self.response)
@ -97,7 +104,7 @@ class NetworkTest(unittest.TestCase):
return self.value
@classmethod
def from_json(cls, value):
return cls(value)
pass # pragma: no cover
# pylint: disable=protected-access
jws = self.net._wrap_in_jws(MockJSONDeSerializable('foo'))
self.assertEqual(jose.JWS.json_loads(jws).payload, '"foo"')
@ -111,7 +118,8 @@ class NetworkTest(unittest.TestCase):
def test_check_response_not_ok_jobj_error(self):
self.response.ok = False
self.response.json.return_value = messages2.Error(detail='foo')
self.response.json.return_value = messages2.Error(
detail='foo', typ='serverInternal', title='some title').to_json()
# pylint: disable=protected-access
self.assertRaises(
messages2.Error, self.net._check_response, self.response)
@ -218,8 +226,8 @@ class NetworkTest(unittest.TestCase):
def test_register_from_account(self):
self.net.register = mock.Mock()
acc = account.Account(
mock.Mock(accounts_dir='mock_dir'), 'key',
email='cert-admin@example.com', phone='+12025551212')
self.config, 'key', email='cert-admin@example.com',
phone='+12025551212')
self.net.register_from_account(acc)
@ -228,9 +236,8 @@ class NetworkTest(unittest.TestCase):
def test_register_from_account_partial_info(self):
self.net.register = mock.Mock()
acc = account.Account(
mock.Mock(accounts_dir='mock_dir'), 'key',
email='cert-admin@example.com')
acc2 = account.Account(mock.Mock(accounts_dir='mock_dir'), 'key')
self.config, 'key', email='cert-admin@example.com')
acc2 = account.Account(self.config, 'key')
self.net.register_from_account(acc)
self.net.register.assert_called_with(
@ -270,11 +277,10 @@ class NetworkTest(unittest.TestCase):
# TODO: test POST call arguments
# TODO: split here and separate test
authz_wrong_key = self.authz.update(key=KEY2.public())
self.response.json.return_value = authz_wrong_key.to_json()
self.assertRaises(
errors.UnexpectedUpdate, self.net.request_challenges,
self.identifier, self.regr)
self.response.json.return_value = self.authz.update(
identifier=self.identifier.update(value='foo')).to_json()
self.assertRaises(errors.UnexpectedUpdate, self.net.request_challenges,
self.identifier, self.authzr.uri)
def test_request_challenges_missing_next(self):
self.response.status_code = httplib.CREATED
@ -348,6 +354,11 @@ class NetworkTest(unittest.TestCase):
self.assertEqual((self.authzr, self.response),
self.net.poll(self.authzr))
# TODO: split here and separate test
self.response.json.return_value = self.authz.update(
identifier=self.identifier.update(value='foo')).to_json()
self.assertRaises(errors.UnexpectedUpdate, self.net.poll, self.authzr)
def test_request_issuance(self):
self.response.content = CERT.as_der()
self.response.headers['Location'] = self.certr.uri

View file

@ -12,8 +12,6 @@ from letsencrypt import errors
from letsencrypt import le_util
from letsencrypt.display import util as display_util
from letsencrypt_apache import configurator
class RevokerBase(unittest.TestCase): # pylint: disable=too-few-public-methods
"""Base Class for Revoker Tests."""
@ -60,8 +58,7 @@ class RevokerTest(RevokerBase):
self._store_certs()
self.revoker = Revoker(
mock.MagicMock(spec=configurator.ApacheConfigurator),
self.mock_config)
installer=mock.MagicMock(), config=self.mock_config)
def tearDown(self):
shutil.rmtree(self.backup_dir)

View file

@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBuzCCAWUCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgTCE1pY2hpZ2FuMRIw
EAYDVQQHEwlBbm4gQXJib3IxDDAKBgNVBAoTA0VGRjEfMB0GA1UECxMWVW5pdmVy
c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wXDANBgkqhkiG
9w0BAQEFAANLADBIAkEA9LYRcVE3Nr+qleecEcX8JwVDnjeG1X7ucsCasuuZM0e0
9cmYuUzxIkMjO/9x4AVcvXXRXPEV+LzWWkfkTlzRMwIDAQABoIGGMIGDBgkqhkiG
9w0BCQ4xdjB0MHIGA1UdEQRrMGmCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZ4IL
ZXhhbXBsZS5uZXSCDGV4YW1wbGUuaW5mb4IVc3ViZG9tYWluLmV4YW1wbGUuY29t
ghtvdGhlci5zdWJkb21haW4uZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADQQBd
k4BE5qvEvkYoZM/2++Xd9RrQ6wsdj0QiJQCozfsI4lQx6ZJnbtNc7HpDrX4W6XIv
IvzVBz/nD11drfz/RNuX
-----END CERTIFICATE REQUEST-----

View file

@ -0,0 +1,8 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBFTCBwAIBADBbMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh
MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRQwEgYDVQQDDAtleGFt
cGxlLm9yZzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQD0thFxUTc2v6qV55wRxfwn
BUOeN4bVfu5ywJqy65kzR7T1yZi5TPEiQyM7/3HgBVy9ddFc8RX4vNZaR+ROXNEz
AgMBAAGgADANBgkqhkiG9w0BAQsFAANBAMikGL8Ch7hQCStXH7chhDp6+pt2+VSo
wgsrPQ2Bw4veDMlSemUrH+4e0TwbbntHfvXTDHWs9P3BiIDJLxFrjuA=
-----END CERTIFICATE REQUEST-----

View file

@ -7,6 +7,8 @@ import unittest
import mock
from letsencrypt import constants as core_constants
from letsencrypt_apache import configurator
from letsencrypt_apache import constants
from letsencrypt_apache import obj
@ -20,7 +22,7 @@ class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods
self.temp_dir, self.config_dir, self.work_dir = dir_setup(
"debian_apache_2_4/two_vhost_80")
self.ssl_options = setup_apache_ssl_options(self.config_dir)
self.ssl_options = setup_ssl_options(self.config_dir)
self.config_path = os.path.join(
self.temp_dir, "debian_apache_2_4/two_vhost_80/apache2")
@ -31,14 +33,19 @@ class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods
"acme.jose", "testdata/rsa256_key.pem")
def dir_setup(test_dir="debian_apache_2_4/two_vhost_80"):
def dir_setup(test_dir="debian_apache_2_4/two_vhost_80",
pkg="letsencrypt_apache.tests"):
"""Setup the directories necessary for the configurator."""
temp_dir = tempfile.mkdtemp("temp")
config_dir = tempfile.mkdtemp("config")
work_dir = tempfile.mkdtemp("work")
os.chmod(temp_dir, core_constants.CONFIG_DIRS_MODE)
os.chmod(config_dir, core_constants.CONFIG_DIRS_MODE)
os.chmod(work_dir, core_constants.CONFIG_DIRS_MODE)
test_configs = pkg_resources.resource_filename(
"letsencrypt_apache.tests", "testdata/%s" % test_dir)
pkg, os.path.join("testdata", test_dir))
shutil.copytree(
test_configs, os.path.join(temp_dir, test_dir), symlinks=True)
@ -46,10 +53,11 @@ def dir_setup(test_dir="debian_apache_2_4/two_vhost_80"):
return temp_dir, config_dir, work_dir
def setup_apache_ssl_options(config_dir):
def setup_ssl_options(
config_dir, mod_ssl_conf=constants.MOD_SSL_CONF):
"""Move the ssl_options into position and return the path."""
option_path = os.path.join(config_dir, "options-ssl.conf")
shutil.copyfile(constants.MOD_SSL_CONF, option_path)
shutil.copyfile(mod_ssl_conf, option_path)
return option_path

View file

@ -271,7 +271,7 @@ class NginxConfigurator(common.Plugin):
the existing one?
:param vhost: The vhost to add SSL to.
:type vhost: :class:`~letsencrypt.client.plugins.nginx.obj.VirtualHost`
:type vhost: :class:`~letsencrypt_nginx.obj.VirtualHost`
"""
ssl_block = [['listen', '443 ssl'],

View file

@ -78,7 +78,7 @@ class NginxDvsni(ApacheDvsni):
"""Modifies Nginx config to include challenge server blocks.
:param list ll_addrs: list of lists of
:class:`letsencrypt.client.plugins.apache.obj.Addr` to apply
:class:`letsencrypt_nginx.obj.Addr` to apply
:raises errors.LetsEncryptMisconfigurationError:
Unable to find a suitable HTTP block to include DVSNI hosts.
@ -115,7 +115,7 @@ class NginxDvsni(ApacheDvsni):
"""Creates a server block for a DVSNI challenge.
:param achall: Annotated DVSNI challenge.
:type achall: :class:`letsencrypt.client.achallenges.DVSNI`
:type achall: :class:`letsencrypt.achallenges.DVSNI`
:param list addrs: addresses of challenged domain
:class:`list` of type :class:`~nginx.obj.Addr`

View file

@ -3,7 +3,7 @@ import string
from pyparsing import (
Literal, White, Word, alphanums, CharsNotIn, Forward, Group,
Optional, OneOrMore, ZeroOrMore, pythonStyleComment)
Optional, OneOrMore, Regex, ZeroOrMore, pythonStyleComment)
class RawNginxParser(object):
@ -16,17 +16,21 @@ class RawNginxParser(object):
semicolon = Literal(";").suppress()
space = White().suppress()
key = Word(alphanums + "_/")
value = CharsNotIn("{};,")
# Matches anything that is not a special character AND any chars in single
# or double quotes
value = Regex(r"((\".*\")?(\'.*\')?[^\{\};,]?)+")
location = CharsNotIn("{};," + string.whitespace)
# modifier for location uri [ = | ~ | ~* | ^~ ]
modifier = Literal("=") | Literal("~*") | Literal("~") | Literal("^~")
# rules
assignment = (key + Optional(space + value) + semicolon)
location_statement = Optional(space + modifier) + Optional(space + location)
if_statement = Literal("if") + space + Regex(r"\(.+\)") + space
block = Forward()
block << Group(
Group(key + Optional(space + modifier) + Optional(space + location))
(Group(key + location_statement) ^ Group(if_statement))
+ left_bracket
+ Group(ZeroOrMore(Group(assignment) | block))
+ right_bracket)

View file

@ -84,6 +84,26 @@ class TestRawNginxParser(unittest.TestCase):
]]]]]
)
def test_parse_from_file2(self):
parsed = load(open(util.get_data_filename('edge_cases.conf')))
self.assertEqual(
parsed,
[[['server'], [['server_name', 'simple']]],
[['server'],
[['server_name', 'with.if'],
[['location', '~', '^/services/.+$'],
[[['if', '($request_filename ~* \\.(ttf|woff)$)'],
[['add_header', 'Access-Control-Allow-Origin "*"']]]]]]],
[['server'],
[['server_name', 'with.complicated.headers'],
[['location', '~*', '\\.(?:gif|jpe?g|png)$'],
[['add_header', 'Pragma public'],
['add_header',
'Cache-Control \'public, must-revalidate, proxy-revalidate\''
' "test,;{}" foo'],
['blah', '"hello;world"'],
['try_files', '$uri @rewrites']]]]]])
def test_dump_as_file(self):
parsed = load(open(util.get_data_filename('nginx.conf')))
parsed[-1][-1].append([['server'],

View file

@ -25,8 +25,8 @@ class NginxParserTest(util.NginxTest):
shutil.rmtree(self.work_dir)
def test_root_normalized(self):
path = os.path.join(self.temp_dir, "foo/////"
"bar/../../testdata")
path = os.path.join(self.temp_dir, "etc_nginx/////"
"ubuntu_nginx/../../etc_nginx")
nparser = parser.NginxParser(path, None)
self.assertEqual(nparser.root, self.config_path)

View file

@ -0,0 +1,27 @@
# This is not a valid nginx config file but it tests edge cases in valid nginx syntax
server {
server_name simple;
}
server {
server_name with.if;
location ~ ^/services/.+$ {
if ($request_filename ~* \.(ttf|woff)$) {
add_header Access-Control-Allow-Origin "*";
}
}
}
server {
server_name with.complicated.headers;
location ~* \.(?:gif|jpe?g|png)$ {
add_header Pragma public;
add_header Cache-Control 'public, must-revalidate, proxy-revalidate' "test,;{}" foo;
blah "hello;world";
try_files $uri @rewrites;
}
}

View file

@ -1,12 +1,12 @@
"""Common utilities for letsencrypt_nginx."""
import os
import pkg_resources
import shutil
import tempfile
import unittest
import mock
from letsencrypt_apache.tests import util as apache_util
from letsencrypt_nginx import constants
from letsencrypt_nginx import configurator
@ -16,13 +16,13 @@ class NginxTest(unittest.TestCase): # pylint: disable=too-few-public-methods
def setUp(self):
super(NginxTest, self).setUp()
self.temp_dir, self.config_dir, self.work_dir = dir_setup(
"testdata")
self.temp_dir, self.config_dir, self.work_dir = apache_util.dir_setup(
"etc_nginx", "letsencrypt_nginx.tests")
self.ssl_options = setup_nginx_ssl_options(self.config_dir)
self.ssl_options = apache_util.setup_ssl_options(
self.config_dir, constants.MOD_SSL_CONF)
self.config_path = os.path.join(
self.temp_dir, "testdata")
self.config_path = os.path.join(self.temp_dir, "etc_nginx")
self.rsa256_file = pkg_resources.resource_filename(
"acme.jose", "testdata/rsa256_key.pem")
@ -33,29 +33,8 @@ class NginxTest(unittest.TestCase): # pylint: disable=too-few-public-methods
def get_data_filename(filename):
"""Gets the filename of a test data file."""
return pkg_resources.resource_filename(
"letsencrypt_nginx.tests", "testdata/%s" % filename)
def dir_setup(test_dir="debian_nginx/two_vhost_80"):
"""Setup the directories necessary for the configurator."""
temp_dir = tempfile.mkdtemp("temp")
config_dir = tempfile.mkdtemp("config")
work_dir = tempfile.mkdtemp("work")
test_configs = pkg_resources.resource_filename(
"letsencrypt_nginx.tests", test_dir)
shutil.copytree(
test_configs, os.path.join(temp_dir, test_dir), symlinks=True)
return temp_dir, config_dir, work_dir
def setup_nginx_ssl_options(config_dir):
"""Move the ssl_options into position and return the path."""
option_path = os.path.join(config_dir, "options-ssl.conf")
shutil.copyfile(constants.MOD_SSL_CONF, option_path)
return option_path
"letsencrypt_nginx.tests", os.path.join(
"testdata", "etc_nginx", filename))
def get_nginx_configurator(

View file

@ -38,7 +38,8 @@ install_requires = [
'psutil>=2.1.0', # net_connections introduced in 2.1.0
'pyasn1', # urllib3 InsecurePlatformWarning (#304)
'pycrypto',
'PyOpenSSL',
# https://pyopenssl.readthedocs.org/en/latest/api/crypto.html#OpenSSL.crypto.X509Req.get_extensions
'PyOpenSSL>=0.15',
'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary?
'pyrfc3339',
'python-augeas',