mirror of
https://github.com/certbot/certbot.git
synced 2026-01-21 19:01:07 +03:00
Merge remote-tracking branch 'github/letsencrypt/master' into bugs/302
This commit is contained in:
@@ -19,4 +19,10 @@ env:
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
irc: "chat.freenode.net#letsencrypt"
|
||||
irc:
|
||||
channels:
|
||||
- "chat.freenode.net#letsencrypt"
|
||||
on_success: never
|
||||
on_failure: always
|
||||
use_notice: true
|
||||
skip_join: true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
include README.rst
|
||||
include CHANGES.rst
|
||||
include CONTRIBUTING.rst
|
||||
include CONTRIBUTING.md
|
||||
include linter_plugin.py
|
||||
include letsencrypt/EULA
|
||||
recursive-include letsencrypt *.json
|
||||
|
||||
@@ -80,7 +80,7 @@ Documentation: https://letsencrypt.readthedocs.org/
|
||||
|
||||
Software project: https://github.com/letsencrypt/lets-encrypt-preview
|
||||
|
||||
Notes for developers: CONTRIBUTING.rst_
|
||||
Notes for developers: CONTRIBUTING.md_
|
||||
|
||||
Main Website: https://letsencrypt.org/
|
||||
|
||||
@@ -91,4 +91,4 @@ email to client-dev+subscribe@letsencrypt.org)
|
||||
|
||||
.. _Freenode: https://freenode.net
|
||||
.. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev
|
||||
.. _CONTRIBUTING.rst: https://github.com/letsencrypt/lets-encrypt-preview/blob/master/CONTRIBUTING.rst
|
||||
.. _CONTRIBUTING.md: https://github.com/letsencrypt/lets-encrypt-preview/blob/master/CONTRIBUTING.md
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
:mod:`letsencrypt.acme`
|
||||
=======================
|
||||
|
||||
.. contents::
|
||||
|
||||
.. automodule:: letsencrypt.acme
|
||||
:members:
|
||||
|
||||
@@ -8,9 +10,18 @@
|
||||
Messages
|
||||
--------
|
||||
|
||||
v00
|
||||
~~~
|
||||
|
||||
.. automodule:: letsencrypt.acme.messages
|
||||
:members:
|
||||
|
||||
v02
|
||||
~~~
|
||||
|
||||
.. automodule:: letsencrypt.acme.messages2
|
||||
:members:
|
||||
|
||||
|
||||
Challenges
|
||||
----------
|
||||
@@ -21,10 +32,18 @@ Challenges
|
||||
|
||||
Other ACME objects
|
||||
------------------
|
||||
|
||||
.. automodule:: letsencrypt.acme.other
|
||||
:members:
|
||||
|
||||
|
||||
Fields
|
||||
------
|
||||
|
||||
.. automodule:: letsencrypt.acme.fields
|
||||
:members:
|
||||
|
||||
|
||||
Errors
|
||||
------
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
:mod:`letsencrypt.client.apache`
|
||||
--------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.apache
|
||||
:members:
|
||||
|
||||
:mod:`letsencrypt.client.apache.configurator`
|
||||
=============================================
|
||||
|
||||
.. automodule:: letsencrypt.client.apache.configurator
|
||||
:members:
|
||||
|
||||
:mod:`letsencrypt.client.apache.dvsni`
|
||||
=============================================
|
||||
|
||||
.. automodule:: letsencrypt.client.apache.dvsni
|
||||
:members:
|
||||
|
||||
:mod:`letsencrypt.client.apache.obj`
|
||||
====================================
|
||||
|
||||
.. automodule:: letsencrypt.client.apache.obj
|
||||
:members:
|
||||
|
||||
:mod:`letsencrypt.client.apache.parser`
|
||||
=======================================
|
||||
|
||||
.. automodule:: letsencrypt.client.apache.parser
|
||||
:members:
|
||||
@@ -1,5 +0,0 @@
|
||||
:mod:`letsencrypt.client.client_authenticator`
|
||||
----------------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.client_authenticator
|
||||
:members:
|
||||
5
docs/api/client/continuity_auth.rst
Normal file
5
docs/api/client/continuity_auth.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
:mod:`letsencrypt.client.continuity_auth`
|
||||
-----------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.continuity_auth
|
||||
:members:
|
||||
5
docs/api/client/network2.rst
Normal file
5
docs/api/client/network2.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
:mod:`letsencrypt.client.network2`
|
||||
----------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.network2
|
||||
:members:
|
||||
29
docs/api/client/plugins/apache.rst
Normal file
29
docs/api/client/plugins/apache.rst
Normal file
@@ -0,0 +1,29 @@
|
||||
:mod:`letsencrypt.client.plugins.apache`
|
||||
----------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.plugins.apache
|
||||
:members:
|
||||
|
||||
:mod:`letsencrypt.client.plugins.apache.configurator`
|
||||
=====================================================
|
||||
|
||||
.. automodule:: letsencrypt.client.plugins.apache.configurator
|
||||
:members:
|
||||
|
||||
:mod:`letsencrypt.client.plugins.apache.dvsni`
|
||||
==============================================
|
||||
|
||||
.. automodule:: letsencrypt.client.plugins.apache.dvsni
|
||||
:members:
|
||||
|
||||
:mod:`letsencrypt.client.plugins.apache.obj`
|
||||
============================================
|
||||
|
||||
.. automodule:: letsencrypt.client.plugins.apache.obj
|
||||
:members:
|
||||
|
||||
:mod:`letsencrypt.client.plugins.apache.parser`
|
||||
===============================================
|
||||
|
||||
.. automodule:: letsencrypt.client.plugins.apache.parser
|
||||
:members:
|
||||
11
docs/api/client/plugins/standalone.rst
Normal file
11
docs/api/client/plugins/standalone.rst
Normal file
@@ -0,0 +1,11 @@
|
||||
:mod:`letsencrypt.client.plugins.standalone`
|
||||
--------------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.plugins.standalone
|
||||
:members:
|
||||
|
||||
:mod:`letsencrypt.client.plugins.standalone.authenticator`
|
||||
==========================================================
|
||||
|
||||
.. automodule:: letsencrypt.client.plugins.standalone.authenticator
|
||||
:members:
|
||||
@@ -1,5 +0,0 @@
|
||||
:mod:`letsencrypt.client.standalone_authenticator`
|
||||
--------------------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.standalone_authenticator
|
||||
:members:
|
||||
@@ -98,15 +98,16 @@ the ACME server. From the protocol, there are essentially two
|
||||
different types of challenges. Challenges that must be solved by
|
||||
individual plugins in order to satisfy domain validation (subclasses
|
||||
of `~.DVChallenge`, i.e. `~.challenges.DVSNI`,
|
||||
`~.challenges.SimpleHTTPS`, `~.challenges.DNS`) and client specific
|
||||
challenges (subclasses of `~.ClientChallenge`,
|
||||
`~.challenges.SimpleHTTPS`, `~.challenges.DNS`) and continuity specific
|
||||
challenges (subclasses of `~.ContinuityChallenge`,
|
||||
i.e. `~.challenges.RecoveryToken`, `~.challenges.RecoveryContact`,
|
||||
`~.challenges.ProofOfPossession`). Client specific challenges are
|
||||
always handled by the `~.ClientAuthenticator`. Right now we have two
|
||||
DV Authenticators, `~.ApacheConfigurator` and the
|
||||
`~.StandaloneAuthenticator`. The Standalone and Apache authenticators
|
||||
only solve the `~.challenges.DVSNI` challenge currently. (You can set
|
||||
which challenges your authenticator can handle through the
|
||||
`~.challenges.ProofOfPossession`). Continuity challenges are
|
||||
always handled by the `~.ContinuityAuthenticator`, while plugins are
|
||||
expected to handle `~.DVChallenge` types.
|
||||
Right now, we have two authenticator plugins, the `~.ApacheConfigurator`
|
||||
and the `~.StandaloneAuthenticator`. The Standalone and Apache
|
||||
authenticators only solve the `~.challenges.DVSNI` challenge currently.
|
||||
(You can set which challenges your authenticator can handle through the
|
||||
:meth:`~.IAuthenticator.get_chall_pref`.
|
||||
|
||||
(FYI: We also have a partial implementation for a `~.DNSAuthenticator`
|
||||
@@ -126,26 +127,27 @@ Installers and Authenticators will oftentimes be the same
|
||||
class/object. Installers and Authenticators are kept separate because
|
||||
it should be possible to use the `~.StandaloneAuthenticator` (it sets
|
||||
up its own Python server to perform challenges) with a program that
|
||||
cannot solve challenges itself. (I am imagining MTA installers).
|
||||
cannot solve challenges itself. (Imagine MTA installers).
|
||||
|
||||
|
||||
Installer Development
|
||||
---------------------
|
||||
|
||||
There are a few existing classes that may be beneficial while
|
||||
developing a new `~letsencrypt.client.interfaces.IInstaller`.
|
||||
Installers aimed to reconfigure UNIX servers may use Augeas for
|
||||
configuration parsing and can inherit from `~.AugeasConfigurator` class
|
||||
to handle much of the interface. Installers that are unable to use
|
||||
Augeas may still find the `~.Reverter` class helpful in handling
|
||||
configuration checkpoints and rollback.
|
||||
|
||||
|
||||
Display
|
||||
~~~~~~~
|
||||
|
||||
We currently offer a pythondialog and "text" mode for displays. I have
|
||||
rewritten the interface which should be merged within the next day
|
||||
(the rewrite is in the revoker branch of the repo and should be merged
|
||||
within the next day). Display plugins implement
|
||||
`~letsencrypt.client.interfaces.IDisplay` interface.
|
||||
|
||||
|
||||
Augeas
|
||||
------
|
||||
|
||||
Some plugins, especially those designed to reconfigure UNIX servers,
|
||||
can take inherit from `~.AugeasConfigurator` class in order to more
|
||||
efficiently handle common operations on UNIX server configuration
|
||||
files.
|
||||
We currently offer a pythondialog and "text" mode for displays. Display
|
||||
plugins implement the `~letsencrypt.client.interfaces.IDisplay`
|
||||
interface.
|
||||
|
||||
|
||||
.. _coding-style:
|
||||
|
||||
@@ -7,6 +7,7 @@ Welcome to the Let's Encrypt client documentation!
|
||||
intro
|
||||
using
|
||||
contributing
|
||||
plugins
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
19
docs/plugins.rst
Normal file
19
docs/plugins.rst
Normal file
@@ -0,0 +1,19 @@
|
||||
=======
|
||||
Plugins
|
||||
=======
|
||||
|
||||
Let's Encrypt client supports dynamic discovery of plugins through the
|
||||
`setuptools entry points`_. This way you can, for example, create a
|
||||
custom implementation of
|
||||
`~letsencrypt.client.interfaces.IAuthenticator` or the
|
||||
'~letsencrypt.client.interfaces.IInstaller' without having to
|
||||
merge it with the core upstream source code. An example is provided in
|
||||
``examples/plugins/`` directory.
|
||||
|
||||
Please be aware though that as this client is still in a developer-preview
|
||||
stage, the API may undergo a few changes. If you believe the plugin will be
|
||||
beneficial to the community, please consider submitting a pull request to the
|
||||
repo and we will update it with any necessary API changes.
|
||||
|
||||
.. _`setuptools entry points`:
|
||||
https://pythonhosted.org/setuptools/setuptools.html#dynamic-discovery-of-services-and-plugins
|
||||
18
examples/plugins/letsencrypt_example_plugins.py
Normal file
18
examples/plugins/letsencrypt_example_plugins.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Example Let's Encrypt plugins."""
|
||||
import zope.interface
|
||||
|
||||
from letsencrypt.client import interfaces
|
||||
|
||||
|
||||
class Authenticator(object):
|
||||
zope.interface.implements(interfaces.IAuthenticator)
|
||||
|
||||
description = 'Example Authenticator plugin'
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
|
||||
# Implement all methods from IAuthenticator, remembering to add
|
||||
# "self" as first argument, e.g. def prepare(self)...
|
||||
|
||||
# For full examples, see letsencrypt.client.plugins
|
||||
16
examples/plugins/setup.py
Normal file
16
examples/plugins/setup.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from setuptools import setup
|
||||
|
||||
|
||||
setup(
|
||||
name='letsencrypt-example-plugins',
|
||||
package='letsencrypt_example_plugins.py',
|
||||
install_requires=[
|
||||
'letsencrypt',
|
||||
'zope.interface',
|
||||
],
|
||||
entry_points={
|
||||
'letsencrypt.authenticators': [
|
||||
'example = letsencrypt_example_plugins:Authenticator',
|
||||
],
|
||||
},
|
||||
)
|
||||
42
examples/restified.py
Normal file
42
examples/restified.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import logging
|
||||
import os
|
||||
import pkg_resources
|
||||
|
||||
import M2Crypto
|
||||
|
||||
from letsencrypt.acme import messages2
|
||||
from letsencrypt.acme import jose
|
||||
|
||||
from letsencrypt.client import network2
|
||||
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
NEW_REG_URL = 'https://www.letsencrypt-demo.org/acme/new-reg'
|
||||
|
||||
key = jose.JWKRSA.load(pkg_resources.resource_string(
|
||||
'letsencrypt.acme.jose', os.path.join('testdata', 'rsa512_key.pem')))
|
||||
net = network2.Network(NEW_REG_URL, key)
|
||||
|
||||
regr = net.register(contact=(
|
||||
'mailto:cert-admin@example.com', 'tel:+12025551212'))
|
||||
logging.info('Auto-accepting TOS: %s', regr.terms_of_service)
|
||||
net.update_registration(regr.update(
|
||||
body=regr.body.update(agreement=regr.terms_of_service)))
|
||||
logging.debug(regr)
|
||||
|
||||
authzr = net.request_challenges(
|
||||
identifier=messages2.Identifier(
|
||||
typ=messages2.IDENTIFIER_FQDN, value='example1.com'),
|
||||
regr=regr)
|
||||
logging.debug(authzr)
|
||||
|
||||
authzr, authzr_response = net.poll(authzr)
|
||||
|
||||
csr = M2Crypto.X509.load_request_string(pkg_resources.resource_string(
|
||||
'letsencrypt.client.tests', os.path.join('testdata', 'csr.pem')))
|
||||
try:
|
||||
net.request_issuance(csr, (authzr,))
|
||||
except messages2.Error as error:
|
||||
print error.detail
|
||||
@@ -1 +0,0 @@
|
||||
letsencrypt/scripts/main.py
|
||||
@@ -18,7 +18,7 @@ class Challenge(jose.TypedJSONObjectWithFields):
|
||||
TYPES = {}
|
||||
|
||||
|
||||
class ClientChallenge(Challenge): # pylint: disable=abstract-method
|
||||
class ContinuityChallenge(Challenge): # pylint: disable=abstract-method
|
||||
"""Client validation challenges."""
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ class DVSNIResponse(ChallengeResponse):
|
||||
return self.z(chall) + self.DOMAIN_SUFFIX
|
||||
|
||||
@Challenge.register
|
||||
class RecoveryContact(ClientChallenge):
|
||||
class RecoveryContact(ContinuityChallenge):
|
||||
"""ACME "recoveryContact" challenge."""
|
||||
typ = "recoveryContact"
|
||||
|
||||
@@ -156,7 +156,7 @@ class RecoveryContactResponse(ChallengeResponse):
|
||||
|
||||
|
||||
@Challenge.register
|
||||
class RecoveryToken(ClientChallenge):
|
||||
class RecoveryToken(ContinuityChallenge):
|
||||
"""ACME "recoveryToken" challenge."""
|
||||
typ = "recoveryToken"
|
||||
|
||||
@@ -169,7 +169,7 @@ class RecoveryTokenResponse(ChallengeResponse):
|
||||
|
||||
|
||||
@Challenge.register
|
||||
class ProofOfPossession(ClientChallenge):
|
||||
class ProofOfPossession(ContinuityChallenge):
|
||||
"""ACME "proofOfPossession" challenge.
|
||||
|
||||
:ivar str nonce: Random data, **not** base64-encoded.
|
||||
@@ -184,7 +184,8 @@ class ProofOfPossession(ClientChallenge):
|
||||
"""Hints for "proofOfPossession" challenge.
|
||||
|
||||
:ivar jwk: JSON Web Key (:class:`letsencrypt.acme.jose.JWK`)
|
||||
:ivar list certs: List of :class:`M2Crypto.X509.X509` cetificates.
|
||||
:ivar list certs: List of :class:`letsencrypt.acme.jose.ComparableX509`
|
||||
certificates.
|
||||
|
||||
"""
|
||||
jwk = jose.Field("jwk", decoder=jose.JWK.from_json)
|
||||
|
||||
@@ -13,8 +13,10 @@ from letsencrypt.acme import other
|
||||
CERT = jose.ComparableX509(M2Crypto.X509.load_cert(
|
||||
pkg_resources.resource_filename(
|
||||
'letsencrypt.client.tests', 'testdata/cert.pem')))
|
||||
KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string(
|
||||
'letsencrypt.client.tests', os.path.join('testdata', 'rsa256_key.pem')))
|
||||
KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
|
||||
pkg_resources.resource_string(
|
||||
'letsencrypt.client.tests',
|
||||
os.path.join('testdata', 'rsa256_key.pem'))))
|
||||
|
||||
|
||||
class SimpleHTTPSTest(unittest.TestCase):
|
||||
|
||||
25
letsencrypt/acme/fields.py
Normal file
25
letsencrypt/acme/fields.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""ACME JSON fields."""
|
||||
import pyrfc3339
|
||||
|
||||
from letsencrypt.acme import jose
|
||||
|
||||
|
||||
class RFC3339Field(jose.Field):
|
||||
"""RFC3339 field encoder/decoder.
|
||||
|
||||
Handles decoding/encoding between RFC3339 strings and aware (not
|
||||
naive) `datetime.datetime` objects
|
||||
(e.g. ``datetime.datetime.now(pytz.utc)``).
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def default_encoder(cls, value):
|
||||
return pyrfc3339.generate(value)
|
||||
|
||||
@classmethod
|
||||
def default_decoder(cls, value):
|
||||
try:
|
||||
return pyrfc3339.parse(value)
|
||||
except ValueError as error:
|
||||
raise jose.DeserializationError(error)
|
||||
35
letsencrypt/acme/fields_test.py
Normal file
35
letsencrypt/acme/fields_test.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Tests for letsencrypt.acme.fields."""
|
||||
import datetime
|
||||
import unittest
|
||||
|
||||
import pytz
|
||||
|
||||
from letsencrypt.acme import jose
|
||||
|
||||
|
||||
class RFC3339FieldTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.acme.fields.RFC3339Field."""
|
||||
|
||||
def setUp(self):
|
||||
self.decoded = datetime.datetime(2015, 3, 27, tzinfo=pytz.utc)
|
||||
self.encoded = '2015-03-27T00:00:00Z'
|
||||
|
||||
def test_default_encoder(self):
|
||||
from letsencrypt.acme.fields import RFC3339Field
|
||||
self.assertEqual(
|
||||
self.encoded, RFC3339Field.default_encoder(self.decoded))
|
||||
|
||||
def test_default_encoder_naive_fails(self):
|
||||
from letsencrypt.acme.fields import RFC3339Field
|
||||
self.assertRaises(
|
||||
ValueError, RFC3339Field.default_encoder, datetime.datetime.now())
|
||||
|
||||
def test_default_decoder(self):
|
||||
from letsencrypt.acme.fields import RFC3339Field
|
||||
self.assertEqual(
|
||||
self.decoded, RFC3339Field.default_decoder(self.encoded))
|
||||
|
||||
def test_default_decoder_raises_deserialization_error(self):
|
||||
from letsencrypt.acme.fields import RFC3339Field
|
||||
self.assertRaises(
|
||||
jose.DeserializationError, RFC3339Field.default_decoder, '')
|
||||
@@ -70,5 +70,6 @@ from letsencrypt.acme.jose.jws import JWS
|
||||
|
||||
from letsencrypt.acme.jose.util import (
|
||||
ComparableX509,
|
||||
HashableRSAKey,
|
||||
ImmutableMap,
|
||||
)
|
||||
|
||||
@@ -129,18 +129,24 @@ class JSONDeSerializable(object):
|
||||
:returns: Fully serialized object.
|
||||
|
||||
"""
|
||||
partial = self.to_json()
|
||||
try_serialize = (lambda x: x.fully_serialize()
|
||||
if isinstance(x, JSONDeSerializable) else x)
|
||||
if isinstance(partial, basestring): # strings are sequences
|
||||
return partial
|
||||
if isinstance(partial, collections.Sequence):
|
||||
return [try_serialize(elem) for elem in partial]
|
||||
elif isinstance(partial, collections.Mapping):
|
||||
return dict([(try_serialize(key), try_serialize(value))
|
||||
for key, value in partial.iteritems()])
|
||||
else:
|
||||
return partial
|
||||
def _serialize(obj):
|
||||
if isinstance(obj, JSONDeSerializable):
|
||||
return _serialize(obj.to_json())
|
||||
if isinstance(obj, basestring): # strings are sequence
|
||||
return obj
|
||||
elif isinstance(obj, list):
|
||||
return [_serialize(subobj) for subobj in obj]
|
||||
elif isinstance(obj, collections.Sequence):
|
||||
# default to tuple, otherwise Mapping could get
|
||||
# unhashable list
|
||||
return tuple(_serialize(subobj) for subobj in obj)
|
||||
elif isinstance(obj, collections.Mapping):
|
||||
return dict((_serialize(key), _serialize(value))
|
||||
for key, value in obj.iteritems())
|
||||
else:
|
||||
return obj
|
||||
|
||||
return _serialize(self)
|
||||
|
||||
@util.abstractclassmethod
|
||||
def from_json(cls, unused_jobj):
|
||||
|
||||
@@ -3,6 +3,7 @@ import unittest
|
||||
|
||||
|
||||
class JSONDeSerializableTest(unittest.TestCase):
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.acme.jose.interfaces import JSONDeSerializable
|
||||
@@ -50,6 +51,8 @@ class JSONDeSerializableTest(unittest.TestCase):
|
||||
self.basic2 = Basic('foo2')
|
||||
self.seq = Sequence(self.basic1, self.basic2)
|
||||
self.mapping = Mapping(self.basic1, self.basic2)
|
||||
self.nested = Basic([[self.basic1]])
|
||||
self.tuple = Basic(('foo',))
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
self.Basic = Basic
|
||||
@@ -66,6 +69,12 @@ class JSONDeSerializableTest(unittest.TestCase):
|
||||
mock_value = object()
|
||||
self.assertTrue(self.Basic(mock_value).fully_serialize() is mock_value)
|
||||
|
||||
def test_fully_serialize_nested(self):
|
||||
self.assertEqual(self.nested.fully_serialize(), [['foo1']])
|
||||
|
||||
def test_fully_serialize(self):
|
||||
self.assertEqual(self.tuple.fully_serialize(), (('foo', )))
|
||||
|
||||
def test_from_json_not_implemented(self):
|
||||
from letsencrypt.acme.jose.interfaces import JSONDeSerializable
|
||||
self.assertRaises(TypeError, JSONDeSerializable.from_json, 'xxx')
|
||||
|
||||
@@ -83,7 +83,11 @@ class JWKOct(JWK):
|
||||
|
||||
@JWK.register
|
||||
class JWKRSA(JWK):
|
||||
"""RSA JWK."""
|
||||
"""RSA JWK.
|
||||
|
||||
:ivar key: `Crypto.PublicKey.RSA` wrapped in `.HashableRSAKey`
|
||||
|
||||
"""
|
||||
typ = 'RSA'
|
||||
__slots__ = ('key',)
|
||||
|
||||
@@ -114,7 +118,8 @@ class JWKRSA(JWK):
|
||||
:rtype: :class:`JWKRSA`
|
||||
|
||||
"""
|
||||
return cls(key=Crypto.PublicKey.RSA.importKey(string))
|
||||
return cls(key=util.HashableRSAKey(
|
||||
Crypto.PublicKey.RSA.importKey(string)))
|
||||
|
||||
def public(self):
|
||||
return type(self)(key=self.key.publickey())
|
||||
|
||||
@@ -3,7 +3,7 @@ import argparse
|
||||
import base64
|
||||
import sys
|
||||
|
||||
import M2Crypto.X509
|
||||
import M2Crypto
|
||||
|
||||
from letsencrypt.acme.jose import b64
|
||||
from letsencrypt.acme.jose import errors
|
||||
|
||||
@@ -5,7 +5,7 @@ import pkg_resources
|
||||
import unittest
|
||||
|
||||
import Crypto.PublicKey.RSA
|
||||
import M2Crypto.X509
|
||||
import M2Crypto
|
||||
import mock
|
||||
|
||||
from letsencrypt.acme.jose import b64
|
||||
|
||||
@@ -41,6 +41,26 @@ class ComparableX509(object): # pylint: disable=too-few-public-methods
|
||||
return self.as_der() == other.as_der()
|
||||
|
||||
|
||||
class HashableRSAKey(object): # pylint: disable=too-few-public-methods
|
||||
"""Wrapper for `Crypto.PublicKey.RSA` objects that supports hashing."""
|
||||
|
||||
def __init__(self, wrapped):
|
||||
self._wrapped = wrapped
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._wrapped, name)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self._wrapped == other
|
||||
|
||||
def __hash__(self):
|
||||
return hash((type(self), self.exportKey(format='DER')))
|
||||
|
||||
def publickey(self):
|
||||
"""Get wrapped public key."""
|
||||
return type(self)(self._wrapped.publickey())
|
||||
|
||||
|
||||
class ImmutableMap(collections.Mapping, collections.Hashable):
|
||||
# pylint: disable=too-few-public-methods
|
||||
"""Immutable key to value mapping with attribute access."""
|
||||
@@ -57,6 +77,12 @@ class ImmutableMap(collections.Mapping, collections.Hashable):
|
||||
for slot in self.__slots__:
|
||||
object.__setattr__(self, slot, kwargs.pop(slot))
|
||||
|
||||
def update(self, **kwargs):
|
||||
"""Return updated map."""
|
||||
items = dict(self)
|
||||
items.update(kwargs)
|
||||
return type(self)(**items) # pylint: disable=star-args
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return getattr(self, key)
|
||||
|
||||
@@ -1,7 +1,36 @@
|
||||
"""Tests for letsencrypt.acme.jose.util."""
|
||||
import functools
|
||||
import os
|
||||
import pkg_resources
|
||||
import unittest
|
||||
|
||||
import Crypto.PublicKey.RSA
|
||||
|
||||
|
||||
class HashableRSAKeyTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.acme.jose.util.HashableRSAKey."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.acme.jose.util import HashableRSAKey
|
||||
self.key = HashableRSAKey(Crypto.PublicKey.RSA.importKey(
|
||||
pkg_resources.resource_string(
|
||||
__name__, os.path.join('testdata', 'rsa256_key.pem'))))
|
||||
self.key_same = HashableRSAKey(Crypto.PublicKey.RSA.importKey(
|
||||
pkg_resources.resource_string(
|
||||
__name__, os.path.join('testdata', 'rsa256_key.pem'))))
|
||||
|
||||
def test_eq(self):
|
||||
# if __eq__ is not defined, then two HashableRSAKeys with same
|
||||
# _wrapped do not equate
|
||||
self.assertEqual(self.key, self.key_same)
|
||||
|
||||
def test_hash(self):
|
||||
self.assertTrue(isinstance(hash(self.key), int))
|
||||
|
||||
def test_publickey(self):
|
||||
from letsencrypt.acme.jose.util import HashableRSAKey
|
||||
self.assertTrue(isinstance(self.key.publickey(), HashableRSAKey))
|
||||
|
||||
|
||||
class ImmutableMapTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.acme.jose.util.ImmutableMap."""
|
||||
@@ -25,6 +54,10 @@ class ImmutableMapTest(unittest.TestCase):
|
||||
self.a2 = self.A(x=3, y=4)
|
||||
self.b = self.B(x=1, y=2)
|
||||
|
||||
def test_update(self):
|
||||
self.assertEqual(self.A(x=2, y=2), self.a1.update(x=2))
|
||||
self.assertEqual(self.a2, self.a1.update(x=3, y=4))
|
||||
|
||||
def test_get_missing_item_raises_key_error(self):
|
||||
self.assertRaises(KeyError, self.a1.__getitem__, 'z')
|
||||
|
||||
|
||||
298
letsencrypt/acme/messages2.py
Normal file
298
letsencrypt/acme/messages2.py
Normal file
@@ -0,0 +1,298 @@
|
||||
"""ACME protocol v02 messages."""
|
||||
from letsencrypt.acme import challenges
|
||||
from letsencrypt.acme import fields
|
||||
from letsencrypt.acme import jose
|
||||
|
||||
|
||||
class Error(jose.JSONObjectWithFields, Exception):
|
||||
"""ACME error.
|
||||
|
||||
https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00
|
||||
|
||||
"""
|
||||
|
||||
ERROR_TYPE_NAMESPACE = 'urn:acme:error:'
|
||||
ERROR_TYPE_DESCRIPTIONS = {
|
||||
'malformed': 'The request message was malformed',
|
||||
'unauthorized': 'The client lacks sufficient authorization',
|
||||
'serverInternal': 'The server experienced an internal error',
|
||||
'badCSR': 'The CSR is unacceptable (e.g., due to a short key)',
|
||||
}
|
||||
|
||||
# TODO: Boulder omits 'type' and 'instance', spec requires
|
||||
typ = jose.Field('type', omitempty=True)
|
||||
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
|
||||
return Error.ERROR_TYPE_NAMESPACE + value
|
||||
|
||||
@typ.decoder
|
||||
def typ(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
# pylint thinks isinstance(value, Error), so startswith is not found
|
||||
# pylint: disable=no-member
|
||||
if not value.startswith(Error.ERROR_TYPE_NAMESPACE):
|
||||
raise jose.DeserializationError('Missing error type prefix')
|
||||
|
||||
without_prefix = value[len(Error.ERROR_TYPE_NAMESPACE):]
|
||||
if without_prefix not in Error.ERROR_TYPE_DESCRIPTIONS:
|
||||
raise jose.DeserializationError('Error type not recognized')
|
||||
|
||||
return without_prefix
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
"""Hardcoded error description based on its type."""
|
||||
return self.ERROR_TYPE_DESCRIPTIONS[self.typ]
|
||||
|
||||
|
||||
class _Constant(jose.JSONDeSerializable):
|
||||
"""ACME constant."""
|
||||
__slots__ = ('name',)
|
||||
POSSIBLE_NAMES = NotImplemented
|
||||
|
||||
def __init__(self, name):
|
||||
self.POSSIBLE_NAMES[name] = self
|
||||
self.name = name
|
||||
|
||||
def to_json(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, value):
|
||||
if value not in cls.POSSIBLE_NAMES:
|
||||
raise jose.DeserializationError(
|
||||
'{0} not recognized'.format(cls.__name__))
|
||||
return cls.POSSIBLE_NAMES[value]
|
||||
|
||||
def __repr__(self):
|
||||
return '{0}({1})'.format(self.__class__.__name__, self.name)
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, type(self)) and other.name == self.name
|
||||
|
||||
|
||||
class Status(_Constant):
|
||||
"""ACME "status" field."""
|
||||
POSSIBLE_NAMES = {}
|
||||
STATUS_UNKNOWN = Status('unknown')
|
||||
STATUS_PENDING = Status('pending')
|
||||
STATUS_PROCESSING = Status('processing')
|
||||
STATUS_VALID = Status('valid')
|
||||
STATUS_INVALID = Status('invalid')
|
||||
STATUS_REVOKED = Status('revoked')
|
||||
|
||||
|
||||
class IdentifierType(_Constant):
|
||||
"""ACME identifier type."""
|
||||
POSSIBLE_NAMES = {}
|
||||
IDENTIFIER_FQDN = IdentifierType('dns') # IdentifierDNS in Boulder
|
||||
|
||||
|
||||
class Identifier(jose.JSONObjectWithFields):
|
||||
"""ACME identifier.
|
||||
|
||||
:ivar letsencrypt.acme.messages2.IdentifierType typ:
|
||||
|
||||
"""
|
||||
typ = jose.Field('type', decoder=IdentifierType.from_json)
|
||||
value = jose.Field('value')
|
||||
|
||||
|
||||
class Resource(jose.ImmutableMap):
|
||||
"""ACME Resource.
|
||||
|
||||
:ivar letsencrypt.acme.messages2.ResourceBody body: Resource body.
|
||||
:ivar str uri: Location of the resource.
|
||||
|
||||
"""
|
||||
__slots__ = ('body', 'uri')
|
||||
|
||||
|
||||
class ResourceBody(jose.JSONObjectWithFields):
|
||||
"""ACME Resource Body."""
|
||||
|
||||
|
||||
class RegistrationResource(Resource):
|
||||
"""Registration Resource.
|
||||
|
||||
:ivar letsencrypt.acme.messages2.Registration body:
|
||||
:ivar str new_authzr_uri: URI found in the 'next' ``Link`` header
|
||||
:ivar str terms_of_service: URL for the CA TOS.
|
||||
|
||||
"""
|
||||
__slots__ = ('body', 'uri', 'new_authzr_uri', 'terms_of_service')
|
||||
|
||||
|
||||
class Registration(ResourceBody):
|
||||
"""Registration Resource Body.
|
||||
|
||||
:ivar letsencrypt.acme.jose.jwk.JWK key: Public key.
|
||||
:ivar tuple contact:
|
||||
|
||||
"""
|
||||
|
||||
# on new-reg key server ignores 'key' and populates it based on
|
||||
# JWS.signature.combined.jwk
|
||||
key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json)
|
||||
contact = jose.Field('contact', omitempty=True, default=())
|
||||
recovery_token = jose.Field('recoveryToken', omitempty=True)
|
||||
agreement = jose.Field('agreement', omitempty=True)
|
||||
|
||||
|
||||
class ChallengeResource(Resource, jose.JSONObjectWithFields):
|
||||
"""Challenge Resource.
|
||||
|
||||
:ivar letsencrypt.acme.messages2.ChallengeBody body:
|
||||
:ivar str authzr_uri: URI found in the 'up' ``Link`` header.
|
||||
|
||||
"""
|
||||
__slots__ = ('body', 'authzr_uri')
|
||||
|
||||
@property
|
||||
def uri(self): # pylint: disable=missing-docstring,no-self-argument
|
||||
# bug? 'method already defined line None'
|
||||
# pylint: disable=function-redefined
|
||||
return self.body.uri
|
||||
|
||||
|
||||
class ChallengeBody(ResourceBody):
|
||||
"""Challenge Resource Body.
|
||||
|
||||
.. todo::
|
||||
Confusingly, this has a similar name to `.challenges.Challenge`,
|
||||
as well as `.achallenges.AnnotatedChallenge` or
|
||||
`.achallenges.Indexed`... Once `messages2` and `network2` is
|
||||
integrated with the rest of the client, this class functionality
|
||||
will be merged with `.challenges.Challenge`. Meanwhile,
|
||||
separation allows the ``master`` to be still interoperable with
|
||||
Node.js server (protocol v00). For the time being use names such
|
||||
as ``challb`` to distinguish instances of this class from
|
||||
``achall`` or ``ichall``.
|
||||
|
||||
:ivar letsencrypt.acme.messages2.Status status:
|
||||
:ivar datetime.datetime validated:
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ('chall',)
|
||||
uri = jose.Field('uri')
|
||||
status = jose.Field('status', decoder=Status.from_json)
|
||||
validated = fields.RFC3339Field('validated', omitempty=True)
|
||||
|
||||
def to_json(self):
|
||||
jobj = super(ChallengeBody, self).to_json()
|
||||
jobj.update(self.chall.to_json())
|
||||
return jobj
|
||||
|
||||
@classmethod
|
||||
def fields_from_json(cls, jobj):
|
||||
jobj_fields = super(ChallengeBody, cls).fields_from_json(jobj)
|
||||
jobj_fields['chall'] = challenges.Challenge.from_json(jobj)
|
||||
return jobj_fields
|
||||
|
||||
|
||||
class AuthorizationResource(Resource):
|
||||
"""Authorization Resource.
|
||||
|
||||
:ivar letsencrypt.acme.messages2.Authorization body:
|
||||
:ivar str new_cert_uri: URI found in the 'next' ``Link`` header
|
||||
|
||||
"""
|
||||
__slots__ = ('body', 'uri', 'new_cert_uri')
|
||||
|
||||
|
||||
class Authorization(ResourceBody):
|
||||
"""Authorization Resource Body.
|
||||
|
||||
:ivar letsencrypt.acme.messages2.Identifier identifier:
|
||||
:ivar list challenges: `list` of `Challenge`
|
||||
:ivar tuple combinations: Challenge combinations (`tuple` of `tuple`
|
||||
of `int`, as opposed to `list` of `list` from the spec).
|
||||
:ivar letsencrypt.acme.jose.jwk.JWK key: Public key.
|
||||
:ivar tuple contact:
|
||||
:ivar letsencrypt.acme.messages2.Status status:
|
||||
:ivar datetime.datetime expires:
|
||||
|
||||
"""
|
||||
|
||||
identifier = jose.Field('identifier', decoder=Identifier.from_json)
|
||||
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
|
||||
# be absent'... then acme-spec gives example with 'expires'
|
||||
# present... That's confusing!
|
||||
expires = fields.RFC3339Field('expires', omitempty=True)
|
||||
|
||||
@challenges.decoder
|
||||
def challenges(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
return tuple(ChallengeBody.from_json(chall) for chall in value)
|
||||
|
||||
@property
|
||||
def resolved_combinations(self):
|
||||
"""Combinations with challenges instead of indices."""
|
||||
return tuple(tuple(self.challenges[idx] for idx in combo)
|
||||
for combo in self.combinations)
|
||||
|
||||
|
||||
class CertificateRequest(jose.JSONObjectWithFields):
|
||||
"""ACME new-cert request.
|
||||
|
||||
:ivar letsencrypt.acme.jose.util.ComparableX509 csr:
|
||||
`M2Crypto.X509.Request` wrapped in `.ComparableX509`
|
||||
:ivar tuple authorizations: `tuple` of URIs (`str`)
|
||||
|
||||
"""
|
||||
csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr)
|
||||
authorizations = jose.Field('authorizations', decoder=tuple)
|
||||
|
||||
|
||||
class CertificateResource(Resource):
|
||||
"""Certificate Resource.
|
||||
|
||||
:ivar letsencrypt.acme.jose.util.ComparableX509 body:
|
||||
`M2Crypto.X509.X509` wrapped in `.ComparableX509`
|
||||
:ivar str cert_chain_uri: URI found in the 'up' ``Link`` header
|
||||
:ivar tuple authzrs: `tuple` of `AuthorizationResource`.
|
||||
|
||||
"""
|
||||
__slots__ = ('body', 'uri', 'cert_chain_uri', 'authzrs')
|
||||
|
||||
|
||||
class Revocation(jose.JSONObjectWithFields):
|
||||
"""Revocation message.
|
||||
|
||||
:ivar revoke: Either a `datetime.datetime` or `Revocation.NOW`.
|
||||
:ivar tuple authorizations: Same as `CertificateRequest.authorizations`
|
||||
|
||||
"""
|
||||
|
||||
NOW = 'now'
|
||||
"""A possible value for `revoke`, denoting that certificate should
|
||||
be revoked now."""
|
||||
|
||||
revoke = jose.Field('revoke')
|
||||
authorizations = CertificateRequest._fields['authorizations']
|
||||
|
||||
@revoke.decoder
|
||||
def revoke(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
if value == Revocation.NOW:
|
||||
return value
|
||||
else:
|
||||
return fields.RFC3339Field.default_decoder(value)
|
||||
|
||||
@revoke.encoder
|
||||
def revoke(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
if value == Revocation.NOW:
|
||||
return value
|
||||
else:
|
||||
return fields.RFC3339Field.default_encoder(value)
|
||||
172
letsencrypt/acme/messages2_test.py
Normal file
172
letsencrypt/acme/messages2_test.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""Tests for letsencrypt.acme.messages2."""
|
||||
import datetime
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
import pytz
|
||||
|
||||
from letsencrypt.acme import challenges
|
||||
from letsencrypt.acme import jose
|
||||
|
||||
|
||||
class ErrorTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.acme.messages2.Error."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.acme.messages2 import Error
|
||||
self.error = Error(detail='foo', typ='malformed')
|
||||
|
||||
def test_typ_prefix(self):
|
||||
self.assertEqual('malformed', self.error.typ)
|
||||
self.assertEqual(
|
||||
'urn:acme:error:malformed', self.error.to_json()['type'])
|
||||
self.assertEqual(
|
||||
'malformed', self.error.from_json(self.error.to_json()).typ)
|
||||
|
||||
def test_typ_decoder_missing_prefix(self):
|
||||
from letsencrypt.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'})
|
||||
|
||||
def test_typ_decoder_not_recognized(self):
|
||||
from letsencrypt.acme.messages2 import Error
|
||||
self.assertRaises(jose.DeserializationError, Error.from_json,
|
||||
{'detail': 'foo', 'type': 'urn:acme:error:baz'})
|
||||
|
||||
def test_description(self):
|
||||
self.assertEqual(
|
||||
'The request message was malformed', self.error.description)
|
||||
|
||||
|
||||
class ConstantTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.acme.messages2._Constant."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.acme.messages2 import _Constant
|
||||
class MockConstant(_Constant): # pylint: disable=missing-docstring
|
||||
POSSIBLE_NAMES = {}
|
||||
|
||||
self.MockConstant = MockConstant # pylint: disable=invalid-name
|
||||
self.const_a = MockConstant('a')
|
||||
self.const_b = MockConstant('b')
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual('a', self.const_a.to_json())
|
||||
self.assertEqual('b', self.const_b.to_json())
|
||||
|
||||
def test_from_json(self):
|
||||
self.assertEqual(self.const_a, self.MockConstant.from_json('a'))
|
||||
self.assertRaises(
|
||||
jose.DeserializationError, self.MockConstant.from_json, 'c')
|
||||
|
||||
def test_repr(self):
|
||||
self.assertEqual('MockConstant(a)', repr(self.const_a))
|
||||
self.assertEqual('MockConstant(b)', repr(self.const_b))
|
||||
|
||||
|
||||
class ChallengeResourceTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.acme.messages2.ChallengeResource."""
|
||||
|
||||
def test_uri(self):
|
||||
from letsencrypt.acme.messages2 import ChallengeResource
|
||||
self.assertEqual('http://challb', ChallengeResource(body=mock.MagicMock(
|
||||
uri='http://challb'), authzr_uri='http://authz').uri)
|
||||
|
||||
|
||||
class ChallengeBodyTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.acme.messages2.ChallengeBody."""
|
||||
|
||||
def setUp(self):
|
||||
self.chall = challenges.DNS(token='foo')
|
||||
|
||||
from letsencrypt.acme.messages2 import ChallengeBody
|
||||
from letsencrypt.acme.messages2 import STATUS_VALID
|
||||
self.status = STATUS_VALID
|
||||
self.challb = ChallengeBody(
|
||||
uri='http://challb', chall=self.chall, status=self.status)
|
||||
|
||||
self.jobj_to = {
|
||||
'uri': 'http://challb',
|
||||
'status': self.status,
|
||||
'type': 'dns',
|
||||
'token': 'foo',
|
||||
}
|
||||
self.jobj_from = self.jobj_to.copy()
|
||||
self.jobj_from['status'] = 'valid'
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.jobj_to, self.challb.to_json())
|
||||
|
||||
def test_fields_from_json(self):
|
||||
from letsencrypt.acme.messages2 import ChallengeBody
|
||||
self.assertEqual(self.challb, ChallengeBody.from_json(self.jobj_from))
|
||||
|
||||
|
||||
class AuthorizationTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.acme.messages2.Authorization."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.acme.messages2 import ChallengeBody
|
||||
from letsencrypt.acme.messages2 import STATUS_VALID
|
||||
self.challbs = (
|
||||
ChallengeBody(
|
||||
uri='http://challb1', status=STATUS_VALID,
|
||||
chall=challenges.SimpleHTTPS(token='IlirfxKKXAsHtmzK29Pj8A')),
|
||||
ChallengeBody(uri='http://challb2', status=STATUS_VALID,
|
||||
chall=challenges.DNS(token='DGyRejmCefe7v4NfDGDKfA')),
|
||||
ChallengeBody(uri='http://challb3', status=STATUS_VALID,
|
||||
chall=challenges.RecoveryToken()),
|
||||
)
|
||||
combinations = ((0, 2), (1, 2))
|
||||
|
||||
from letsencrypt.acme.messages2 import Authorization
|
||||
from letsencrypt.acme.messages2 import Identifier
|
||||
from letsencrypt.acme.messages2 import IDENTIFIER_FQDN
|
||||
identifier = Identifier(typ=IDENTIFIER_FQDN, value='example.com')
|
||||
self.authz = Authorization(
|
||||
identifier=identifier, combinations=combinations,
|
||||
challenges=self.challbs)
|
||||
|
||||
self.jobj_from = {
|
||||
'identifier': identifier.fully_serialize(),
|
||||
'challenges': [challb.fully_serialize() for challb in self.challbs],
|
||||
'combinations': combinations,
|
||||
}
|
||||
|
||||
def test_from_json(self):
|
||||
from letsencrypt.acme.messages2 import Authorization
|
||||
Authorization.from_json(self.jobj_from)
|
||||
|
||||
def test_resolved_combinations(self):
|
||||
self.assertEqual(self.authz.resolved_combinations, (
|
||||
(self.challbs[0], self.challbs[2]),
|
||||
(self.challbs[1], self.challbs[2]),
|
||||
))
|
||||
|
||||
|
||||
class RevocationTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.acme.messages2.RevocationTest."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.acme.messages2 import Revocation
|
||||
self.rev_now = Revocation(authorizations=(), revoke=Revocation.NOW)
|
||||
self.rev_date = Revocation(authorizations=(), revoke=datetime.datetime(
|
||||
2015, 3, 27, tzinfo=pytz.utc))
|
||||
self.jobj_now = {'authorizations': (), 'revoke': Revocation.NOW}
|
||||
self.jobj_date = {'authorizations': (),
|
||||
'revoke': '2015-03-27T00:00:00Z'}
|
||||
|
||||
def test_revoke_decoder(self):
|
||||
from letsencrypt.acme.messages2 import Revocation
|
||||
self.assertEqual(self.rev_now, Revocation.from_json(self.jobj_now))
|
||||
self.assertEqual(self.rev_date, Revocation.from_json(self.jobj_date))
|
||||
|
||||
def test_revoke_encoder(self):
|
||||
self.assertEqual(self.jobj_now, self.rev_now.to_json())
|
||||
self.assertEqual(self.jobj_date, self.rev_date.to_json())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -11,8 +11,9 @@ from letsencrypt.acme import jose
|
||||
from letsencrypt.acme import other
|
||||
|
||||
|
||||
KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string(
|
||||
'letsencrypt.client.tests', 'testdata/rsa256_key.pem'))
|
||||
KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
|
||||
pkg_resources.resource_string(
|
||||
'letsencrypt.client.tests', 'testdata/rsa256_key.pem')))
|
||||
CERT = jose.ComparableX509(M2Crypto.X509.load_cert(
|
||||
pkg_resources.resource_filename(
|
||||
'letsencrypt.client.tests', 'testdata/cert.pem')))
|
||||
|
||||
@@ -7,10 +7,12 @@ import Crypto.PublicKey.RSA
|
||||
from letsencrypt.acme import jose
|
||||
|
||||
|
||||
RSA256_KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string(
|
||||
'letsencrypt.client.tests', 'testdata/rsa256_key.pem'))
|
||||
RSA512_KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string(
|
||||
'letsencrypt.client.tests', 'testdata/rsa512_key.pem'))
|
||||
RSA256_KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
|
||||
pkg_resources.resource_string(
|
||||
'letsencrypt.client.tests', 'testdata/rsa256_key.pem')))
|
||||
RSA512_KEY = jose.HashableRSAKey(
|
||||
Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string(
|
||||
'letsencrypt.client.tests', 'testdata/rsa512_key.pem')))
|
||||
|
||||
|
||||
class SignatureTest(unittest.TestCase):
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""Let's Encrypt client.apache."""
|
||||
@@ -5,6 +5,7 @@ import sys
|
||||
import Crypto.PublicKey.RSA
|
||||
|
||||
from letsencrypt.acme import challenges
|
||||
from letsencrypt.acme import jose
|
||||
from letsencrypt.acme import messages
|
||||
|
||||
from letsencrypt.client import achallenges
|
||||
@@ -16,12 +17,12 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
"""ACME Authorization Handler for a client.
|
||||
|
||||
:ivar dv_auth: Authenticator capable of solving
|
||||
:const:`~letsencrypt.client.constants.DV_CHALLENGES`
|
||||
:class:`~letsencrypt.acme.challenges.DVChallenge` types
|
||||
:type dv_auth: :class:`letsencrypt.client.interfaces.IAuthenticator`
|
||||
|
||||
:ivar client_auth: Authenticator capable of solving
|
||||
:const:`~letsencrypt.client_auth.constants.CLIENT_CHALLENGES`
|
||||
:type client_auth: :class:`letsencrypt.client.interfaces.IAuthenticator`
|
||||
:ivar cont_auth: Authenticator capable of solving
|
||||
:class:`~letsencrypt.acme.challenges.ContinuityChallenge` types
|
||||
:type cont_auth: :class:`letsencrypt.client.interfaces.IAuthenticator`
|
||||
|
||||
:ivar network: Network object for sending and receiving authorization
|
||||
messages
|
||||
@@ -36,13 +37,13 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
:ivar dict paths: optimal path for authorization. eg. paths[domain]
|
||||
:ivar dict dv_c: Keys - domain, Values are DV challenges in the form of
|
||||
:class:`letsencrypt.client.achallenges.Indexed`
|
||||
:ivar dict client_c: Keys - domain, Values are Client challenges in the form
|
||||
of :class:`letsencrypt.client.achallenges.Indexed`
|
||||
:ivar dict cont_c: Keys - domain, Values are Continuity challenges in the
|
||||
form of :class:`letsencrypt.client.achallenges.Indexed`
|
||||
|
||||
"""
|
||||
def __init__(self, dv_auth, client_auth, network):
|
||||
def __init__(self, dv_auth, cont_auth, network):
|
||||
self.dv_auth = dv_auth
|
||||
self.client_auth = client_auth
|
||||
self.cont_auth = cont_auth
|
||||
self.network = network
|
||||
|
||||
self.domains = []
|
||||
@@ -52,7 +53,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
self.paths = dict()
|
||||
|
||||
self.dv_c = dict()
|
||||
self.client_c = dict()
|
||||
self.cont_c = dict()
|
||||
|
||||
def add_chall_msg(self, domain, msg, authkey):
|
||||
"""Add a challenge message to the AuthHandler.
|
||||
@@ -76,7 +77,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
self.authkey[domain] = authkey
|
||||
|
||||
def get_authorizations(self):
|
||||
"""Retreive all authorizations for challenges.
|
||||
"""Retrieve all authorizations for challenges.
|
||||
|
||||
:raises LetsEncryptAuthHandlerError: If unable to retrieve all
|
||||
authorizations
|
||||
@@ -119,8 +120,8 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
nonce=self.msgs[domain].nonce,
|
||||
responses=self.responses[domain],
|
||||
name=domain,
|
||||
key=Crypto.PublicKey.RSA.importKey(
|
||||
self.authkey[domain].pem)),
|
||||
key=jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
|
||||
self.authkey[domain].pem))),
|
||||
messages.Authorization)
|
||||
logging.info("Received Authorization for %s", domain)
|
||||
return auth
|
||||
@@ -147,24 +148,24 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
self._get_chall_pref(dom),
|
||||
self.msgs[dom].combinations)
|
||||
|
||||
self.dv_c[dom], self.client_c[dom] = self._challenge_factory(
|
||||
self.dv_c[dom], self.cont_c[dom] = self._challenge_factory(
|
||||
dom, self.paths[dom])
|
||||
|
||||
# Flatten challs for authenticator functions and remove index
|
||||
# Order is important here as we will not expose the outside
|
||||
# Authenticator to our own indices.
|
||||
flat_client = []
|
||||
flat_cont = []
|
||||
flat_dv = []
|
||||
|
||||
for dom in self.domains:
|
||||
flat_client.extend(ichall.achall for ichall in self.client_c[dom])
|
||||
flat_cont.extend(ichall.achall for ichall in self.cont_c[dom])
|
||||
flat_dv.extend(ichall.achall for ichall in self.dv_c[dom])
|
||||
|
||||
client_resp = []
|
||||
cont_resp = []
|
||||
dv_resp = []
|
||||
try:
|
||||
if flat_client:
|
||||
client_resp = self.client_auth.perform(flat_client)
|
||||
if flat_cont:
|
||||
cont_resp = self.cont_auth.perform(flat_cont)
|
||||
if flat_dv:
|
||||
dv_resp = self.dv_auth.perform(flat_dv)
|
||||
# This will catch both specific types of errors.
|
||||
@@ -181,8 +182,8 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
logging.info("Ready for verification...")
|
||||
|
||||
# Assemble Responses
|
||||
if client_resp:
|
||||
self._assign_responses(client_resp, self.client_c)
|
||||
if cont_resp:
|
||||
self._assign_responses(cont_resp, self.cont_c)
|
||||
if dv_resp:
|
||||
self._assign_responses(dv_resp, self.dv_c)
|
||||
|
||||
@@ -191,7 +192,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
:param list flat_list: flat_list of responses from an IAuthenticator
|
||||
:param dict ichall_dict: Master dict mapping all domains to a list of
|
||||
their associated 'client' and 'dv' Indexed challenges, or their
|
||||
their associated 'continuity' and 'dv' Indexed challenges, or their
|
||||
:class:`letsencrypt.client.achallenges.Indexed` list
|
||||
|
||||
"""
|
||||
@@ -203,7 +204,8 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
def _path_satisfied(self, dom):
|
||||
"""Returns whether a path has been completely satisfied."""
|
||||
return all(self.responses[dom][i] is not None for i in self.paths[dom])
|
||||
# Make sure that there are no 'None' or 'False' entries along path.
|
||||
return all(self.responses[dom][i] for i in self.paths[dom])
|
||||
|
||||
def _get_chall_pref(self, domain):
|
||||
"""Return list of challenge preferences.
|
||||
@@ -212,7 +214,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
"""
|
||||
chall_prefs = []
|
||||
chall_prefs.extend(self.client_auth.get_chall_pref(domain))
|
||||
chall_prefs.extend(self.cont_auth.get_chall_pref(domain))
|
||||
chall_prefs.extend(self.dv_auth.get_chall_pref(domain))
|
||||
return chall_prefs
|
||||
|
||||
@@ -227,11 +229,11 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
# Chose to make these lists instead of a generator to make it easier to
|
||||
# work with...
|
||||
dv_list = [ichall.achall for ichall in self.dv_c[domain]]
|
||||
client_list = [ichall.achall for ichall in self.client_c[domain]]
|
||||
cont_list = [ichall.achall for ichall in self.cont_c[domain]]
|
||||
if dv_list:
|
||||
self.dv_auth.cleanup(dv_list)
|
||||
if client_list:
|
||||
self.client_auth.cleanup(client_list)
|
||||
if cont_list:
|
||||
self.cont_auth.cleanup(cont_list)
|
||||
|
||||
def _cleanup_state(self, delete_list):
|
||||
"""Cleanup state after an authorization is received.
|
||||
@@ -246,7 +248,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
del self.authkey[domain]
|
||||
|
||||
del self.client_c[domain]
|
||||
del self.cont_c[domain]
|
||||
del self.dv_c[domain]
|
||||
|
||||
self.domains.remove(domain)
|
||||
@@ -258,9 +260,9 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
:param list path: List of indices from `challenges`.
|
||||
|
||||
:returns: dv_chall, list of
|
||||
:returns: dv_chall, list of DVChallenge type
|
||||
:class:`letsencrypt.client.achallenges.Indexed`
|
||||
client_chall, list of
|
||||
cont_chall, list of ContinuityChallenge type
|
||||
:class:`letsencrypt.client.achallenges.Indexed`
|
||||
:rtype: tuple
|
||||
|
||||
@@ -269,7 +271,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
"""
|
||||
dv_chall = []
|
||||
client_chall = []
|
||||
cont_chall = []
|
||||
|
||||
for index in path:
|
||||
chall = self.msgs[domain].challenges[index]
|
||||
@@ -303,35 +305,38 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
ichall = achallenges.Indexed(achall=achall, index=index)
|
||||
|
||||
if isinstance(chall, challenges.ClientChallenge):
|
||||
client_chall.append(ichall)
|
||||
if isinstance(chall, challenges.ContinuityChallenge):
|
||||
cont_chall.append(ichall)
|
||||
elif isinstance(chall, challenges.DVChallenge):
|
||||
dv_chall.append(ichall)
|
||||
|
||||
return dv_chall, client_chall
|
||||
return dv_chall, cont_chall
|
||||
|
||||
|
||||
def gen_challenge_path(challs, preferences, combinations):
|
||||
"""Generate a plan to get authority over the identity.
|
||||
|
||||
.. todo:: Make sure that the challenges are feasible...
|
||||
Example: Do you have the recovery key?
|
||||
.. todo:: This can be possibly be rewritten to use resolved_combinations.
|
||||
|
||||
:param list challs: A list of challenges
|
||||
:param tuple challs: A tuple of challenges
|
||||
(:class:`letsencrypt.acme.challenges.Challenge`) from
|
||||
:class:`letsencrypt.acme.messages.Challenge` server message to
|
||||
be fulfilled by the client in order to prove possession of the
|
||||
identifier.
|
||||
|
||||
:param list preferences: List of challenge preferences for domain
|
||||
(:class:`letsencrypt.acme.challenges.Challege` subclasses)
|
||||
(:class:`letsencrypt.acme.challenges.Challenge` subclasses)
|
||||
|
||||
:param list combinations: A collection of sets of challenges from
|
||||
:param tuple combinations: A collection of sets of challenges from
|
||||
:class:`letsencrypt.acme.messages.Challenge`, each of which would
|
||||
be sufficient to prove possession of the identifier.
|
||||
|
||||
:returns: List of indices from ``challenges``.
|
||||
:rtype: list
|
||||
:returns: tuple of indices from ``challenges``.
|
||||
:rtype: tuple
|
||||
|
||||
:raises letsencrypt.client.errors.LetsEncryptAuthHandlerError: If a
|
||||
path cannot be created that satisfies the CA given the preferences and
|
||||
combinations.
|
||||
|
||||
"""
|
||||
if combinations:
|
||||
@@ -348,29 +353,34 @@ def _find_smart_path(challs, preferences, combinations):
|
||||
|
||||
"""
|
||||
chall_cost = {}
|
||||
max_cost = 0
|
||||
max_cost = 1
|
||||
for i, chall_cls in enumerate(preferences):
|
||||
chall_cost[chall_cls] = i
|
||||
max_cost += i
|
||||
|
||||
# max_cost is now equal to sum(indices) + 1
|
||||
|
||||
best_combo = []
|
||||
# Set above completing all of the available challenges
|
||||
best_combo_cost = max_cost + 1
|
||||
best_combo_cost = max_cost
|
||||
|
||||
combo_total = 0
|
||||
for combo in combinations:
|
||||
for challenge_index in combo:
|
||||
combo_total += chall_cost.get(challs[
|
||||
challenge_index].__class__, max_cost)
|
||||
|
||||
if combo_total < best_combo_cost:
|
||||
best_combo = combo
|
||||
best_combo_cost = combo_total
|
||||
combo_total = 0
|
||||
|
||||
combo_total = 0
|
||||
|
||||
if not best_combo:
|
||||
logging.fatal("Client does not support any combination of "
|
||||
"challenges to satisfy ACME server")
|
||||
sys.exit(22)
|
||||
msg = ("Client does not support any combination of challenges that "
|
||||
"will satisfy the CA.")
|
||||
logging.fatal(msg)
|
||||
raise errors.LetsEncryptAuthHandlerError(msg)
|
||||
|
||||
return best_combo
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ import sys
|
||||
import Crypto.PublicKey.RSA
|
||||
import M2Crypto
|
||||
|
||||
from letsencrypt.acme import jose
|
||||
from letsencrypt.acme import messages
|
||||
from letsencrypt.acme.jose import util as jose_util
|
||||
|
||||
from letsencrypt.client import auth_handler
|
||||
from letsencrypt.client import client_authenticator
|
||||
from letsencrypt.client import continuity_auth
|
||||
from letsencrypt.client import crypto_util
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client import le_util
|
||||
@@ -18,7 +18,7 @@ from letsencrypt.client import network
|
||||
from letsencrypt.client import reverter
|
||||
from letsencrypt.client import revoker
|
||||
|
||||
from letsencrypt.client.apache import configurator
|
||||
from letsencrypt.client.plugins.apache import configurator
|
||||
from letsencrypt.client.display import ops as display_ops
|
||||
from letsencrypt.client.display import enhancements
|
||||
|
||||
@@ -33,7 +33,8 @@ class Client(object):
|
||||
:type authkey: :class:`letsencrypt.client.le_util.Key`
|
||||
|
||||
:ivar auth_handler: Object that supports the IAuthenticator interface.
|
||||
auth_handler contains both a dv_authenticator and a client_authenticator
|
||||
auth_handler contains both a dv_authenticator and a
|
||||
continuity_authenticator
|
||||
:type auth_handler: :class:`letsencrypt.client.auth_handler.AuthHandler`
|
||||
|
||||
:ivar installer: Object supporting the IInstaller interface.
|
||||
@@ -60,9 +61,9 @@ class Client(object):
|
||||
self.config = config
|
||||
|
||||
if dv_auth is not None:
|
||||
client_auth = client_authenticator.ClientAuthenticator(config)
|
||||
cont_auth = continuity_auth.ContinuityAuthenticator(config)
|
||||
self.auth_handler = auth_handler.AuthHandler(
|
||||
dv_auth, client_auth, self.network)
|
||||
dv_auth, cont_auth, self.network)
|
||||
else:
|
||||
self.auth_handler = None
|
||||
|
||||
@@ -130,9 +131,10 @@ class Client(object):
|
||||
logging.info("Preparing and sending CSR...")
|
||||
return self.network.send_and_receive_expected(
|
||||
messages.CertificateRequest.create(
|
||||
csr=jose_util.ComparableX509(
|
||||
csr=jose.ComparableX509(
|
||||
M2Crypto.X509.load_request_der_string(csr_der)),
|
||||
key=Crypto.PublicKey.RSA.importKey(self.authkey.pem)),
|
||||
key=jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
|
||||
self.authkey.pem))),
|
||||
messages.Certificate)
|
||||
|
||||
def save_certificate(self, certificate_msg, cert_path, chain_path):
|
||||
|
||||
@@ -31,7 +31,7 @@ List of expected options parameters:
|
||||
|
||||
|
||||
APACHE_MOD_SSL_CONF = pkg_resources.resource_filename(
|
||||
"letsencrypt.client.apache", "options-ssl.conf")
|
||||
"letsencrypt.client.plugins.apache", "options-ssl.conf")
|
||||
"""Path to the Apache mod_ssl config file found in the Let's Encrypt
|
||||
distribution."""
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Client Authenticator"""
|
||||
"""Continuity Authenticator"""
|
||||
import zope.interface
|
||||
|
||||
from letsencrypt.acme import challenges
|
||||
@@ -9,9 +9,9 @@ from letsencrypt.client import interfaces
|
||||
from letsencrypt.client import recovery_token
|
||||
|
||||
|
||||
class ClientAuthenticator(object):
|
||||
class ContinuityAuthenticator(object):
|
||||
"""IAuthenticator for
|
||||
:const:`~letsencrypt.client.constants.CLIENT_CHALLENGES`.
|
||||
:const:`~letsencrypt.acme.challenges.ContinuityChallenge` class challenges.
|
||||
|
||||
:ivar rec_token: Performs "recoveryToken" challenges
|
||||
:type rec_token: :class:`letsencrypt.client.recovery_token.RecoveryToken`
|
||||
@@ -41,7 +41,7 @@ class ClientAuthenticator(object):
|
||||
if isinstance(achall, achallenges.RecoveryToken):
|
||||
responses.append(self.rec_token.perform(achall))
|
||||
else:
|
||||
raise errors.LetsEncryptClientAuthError("Unexpected Challenge")
|
||||
raise errors.LetsEncryptContAuthError("Unexpected Challenge")
|
||||
return responses
|
||||
|
||||
def cleanup(self, achalls):
|
||||
@@ -50,4 +50,4 @@ class ClientAuthenticator(object):
|
||||
if isinstance(achall, achallenges.RecoveryToken):
|
||||
self.rec_token.cleanup(achall)
|
||||
else:
|
||||
raise errors.LetsEncryptClientAuthError("Unexpected Challenge")
|
||||
raise errors.LetsEncryptContAuthError("Unexpected Challenge")
|
||||
@@ -5,6 +5,14 @@ class LetsEncryptClientError(Exception):
|
||||
"""Generic Let's Encrypt client error."""
|
||||
|
||||
|
||||
class NetworkError(LetsEncryptClientError):
|
||||
"""Network error."""
|
||||
|
||||
|
||||
class UnexpectedUpdate(NetworkError):
|
||||
"""Unexpected update."""
|
||||
|
||||
|
||||
class LetsEncryptReverterError(LetsEncryptClientError):
|
||||
"""Let's Encrypt Reverter error."""
|
||||
|
||||
@@ -14,7 +22,7 @@ class LetsEncryptAuthHandlerError(LetsEncryptClientError):
|
||||
"""Let's Encrypt Auth Handler error."""
|
||||
|
||||
|
||||
class LetsEncryptClientAuthError(LetsEncryptAuthHandlerError):
|
||||
class LetsEncryptContAuthError(LetsEncryptAuthHandlerError):
|
||||
"""Let's Encrypt Client Authenticator error."""
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ from letsencrypt.acme import messages
|
||||
from letsencrypt.client import errors
|
||||
|
||||
|
||||
# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning
|
||||
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3()
|
||||
|
||||
logging.getLogger("requests").setLevel(logging.WARNING)
|
||||
|
||||
|
||||
|
||||
506
letsencrypt/client/network2.py
Normal file
506
letsencrypt/client/network2.py
Normal file
@@ -0,0 +1,506 @@
|
||||
"""Networking for ACME protocol v02."""
|
||||
import datetime
|
||||
import heapq
|
||||
import httplib
|
||||
import itertools
|
||||
import logging
|
||||
import time
|
||||
|
||||
import M2Crypto
|
||||
import requests
|
||||
import werkzeug
|
||||
|
||||
from letsencrypt.acme import jose
|
||||
from letsencrypt.acme import messages2
|
||||
|
||||
from letsencrypt.client import errors
|
||||
|
||||
|
||||
# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning
|
||||
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3()
|
||||
|
||||
|
||||
class Network(object):
|
||||
"""ACME networking.
|
||||
|
||||
.. todo::
|
||||
Clean up raised error types hierarchy, document, and handle (wrap)
|
||||
instances of `.DeserializationError` raised in `from_json()``.
|
||||
|
||||
:ivar str new_reg_uri: Location of new-reg
|
||||
:ivar key: `.JWK` (private)
|
||||
:ivar alg: `.JWASignature`
|
||||
|
||||
"""
|
||||
|
||||
DER_CONTENT_TYPE = 'application/pkix-cert'
|
||||
JSON_CONTENT_TYPE = 'application/json'
|
||||
JSON_ERROR_CONTENT_TYPE = 'application/problem+json'
|
||||
|
||||
def __init__(self, new_reg_uri, key, alg=jose.RS256):
|
||||
self.new_reg_uri = new_reg_uri
|
||||
self.key = key
|
||||
self.alg = alg
|
||||
|
||||
def _wrap_in_jws(self, obj):
|
||||
"""Wrap `JSONDeSerializable` object in JWS.
|
||||
|
||||
:rtype: `.JWS`
|
||||
|
||||
"""
|
||||
dumps = obj.json_dumps()
|
||||
logging.debug('Serialized JSON: %s', dumps)
|
||||
return jose.JWS.sign(
|
||||
payload=dumps, key=self.key, alg=self.alg).json_dumps()
|
||||
|
||||
@classmethod
|
||||
def _check_response(cls, response, content_type=None):
|
||||
"""Check response content and its type.
|
||||
|
||||
.. note::
|
||||
Checking is not strict: wrong server response ``Content-Type``
|
||||
HTTP header is ignored if response is an expected JSON object
|
||||
(c.f. Boulder #56).
|
||||
|
||||
:param str content_type: Expected Content-Type response header.
|
||||
If JSON is expected and not present in server response, this
|
||||
function will raise an error. Otherwise, wrong Content-Type
|
||||
is ignored, but logged.
|
||||
|
||||
:raises letsencrypt.messages2.Error: If server response body
|
||||
carries HTTP Problem (draft-ietf-appsawg-http-problem-00).
|
||||
:raises letsencrypt.errors.NetworkError: In case of other
|
||||
networking errors.
|
||||
|
||||
"""
|
||||
response_ct = response.headers.get('Content-Type')
|
||||
|
||||
try:
|
||||
# TODO: response.json() is called twice, once here, and
|
||||
# once in _get and _post clients
|
||||
jobj = response.json()
|
||||
except ValueError as error:
|
||||
jobj = None
|
||||
|
||||
if not response.ok:
|
||||
if jobj is not None:
|
||||
if response_ct != cls.JSON_ERROR_CONTENT_TYPE:
|
||||
logging.debug(
|
||||
'Ignoring wrong Content-Type (%r) for JSON Error',
|
||||
response_ct)
|
||||
|
||||
try:
|
||||
raise messages2.Error.from_json(jobj)
|
||||
except jose.DeserializationError as error:
|
||||
# Couldn't deserialize JSON object
|
||||
raise errors.NetworkError((response, error))
|
||||
else:
|
||||
# response is not JSON object
|
||||
raise errors.NetworkError(response)
|
||||
else:
|
||||
if jobj is not None and response_ct != cls.JSON_CONTENT_TYPE:
|
||||
logging.debug(
|
||||
'Ignoring wrong Content-Type (%r) for JSON decodable '
|
||||
'response', response_ct)
|
||||
|
||||
if content_type == cls.JSON_CONTENT_TYPE and jobj is None:
|
||||
raise errors.NetworkError(
|
||||
'Unexpected response Content-Type: {0}'.format(response_ct))
|
||||
|
||||
def _get(self, uri, content_type=JSON_CONTENT_TYPE, **kwargs):
|
||||
"""Send GET request.
|
||||
|
||||
:raises letsencrypt.client.errors.NetworkError:
|
||||
|
||||
:returns: HTTP Response
|
||||
:rtype: `requests.Response`
|
||||
|
||||
"""
|
||||
try:
|
||||
response = requests.get(uri, **kwargs)
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise errors.NetworkError(error)
|
||||
self._check_response(response, content_type=content_type)
|
||||
return response
|
||||
|
||||
def _post(self, uri, data, content_type=JSON_CONTENT_TYPE, **kwargs):
|
||||
"""Send POST data.
|
||||
|
||||
:param str content_type: Expected ``Content-Type``, fails if not set.
|
||||
|
||||
:raises letsencrypt.acme.messages2.NetworkError:
|
||||
|
||||
:returns: HTTP Response
|
||||
:rtype: `requests.Response`
|
||||
|
||||
"""
|
||||
logging.debug('Sending POST data: %s', data)
|
||||
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)
|
||||
|
||||
self._check_response(response, content_type=content_type)
|
||||
return response
|
||||
|
||||
@classmethod
|
||||
def _regr_from_response(cls, response, uri=None, new_authzr_uri=None,
|
||||
terms_of_service=None):
|
||||
terms_of_service = (
|
||||
response.links['terms-of-service']['url']
|
||||
if 'terms-of-service' in response.links else terms_of_service)
|
||||
|
||||
if new_authzr_uri is None:
|
||||
try:
|
||||
new_authzr_uri = response.links['next']['url']
|
||||
except KeyError:
|
||||
raise errors.NetworkError('"next" link missing')
|
||||
|
||||
return messages2.RegistrationResource(
|
||||
body=messages2.Registration.from_json(response.json()),
|
||||
uri=response.headers.get('Location', uri),
|
||||
new_authzr_uri=new_authzr_uri,
|
||||
terms_of_service=terms_of_service)
|
||||
|
||||
def register(self, contact=messages2.Registration._fields[
|
||||
'contact'].default):
|
||||
"""Register.
|
||||
|
||||
:param contact: Contact list, as accpeted by `.RegistrationResource`
|
||||
:type contact: `tuple`
|
||||
|
||||
:returns: Registration Resource.
|
||||
:rtype: `.RegistrationResource`
|
||||
|
||||
:raises letsencrypt.client.errors.UnexpectedUpdate:
|
||||
|
||||
"""
|
||||
new_reg = messages2.Registration(contact=contact)
|
||||
|
||||
response = self._post(self.new_reg_uri, self._wrap_in_jws(new_reg))
|
||||
assert response.status_code == httplib.CREATED # TODO: handle errors
|
||||
|
||||
regr = self._regr_from_response(response)
|
||||
if regr.body.key != self.key.public() or regr.body.contact != contact:
|
||||
raise errors.UnexpectedUpdate(regr)
|
||||
|
||||
return regr
|
||||
|
||||
def update_registration(self, regr):
|
||||
"""Update registration.
|
||||
|
||||
:pram regr: Registration Resource.
|
||||
:type regr: `.RegistrationResource`
|
||||
|
||||
:returns: Updated Registration Resource.
|
||||
:rtype: `.RegistrationResource`
|
||||
|
||||
"""
|
||||
response = self._post(regr.uri, self._wrap_in_jws(regr.body))
|
||||
|
||||
# TODO: Boulder returns httplib.ACCEPTED
|
||||
#assert response.status_code == httplib.OK
|
||||
|
||||
# TODO: Boulder does not set Location or Link on update
|
||||
# (c.f. acme-spec #94)
|
||||
|
||||
updated_regr = self._regr_from_response(
|
||||
response, uri=regr.uri, new_authzr_uri=regr.new_authzr_uri,
|
||||
terms_of_service=regr.terms_of_service)
|
||||
if updated_regr != regr:
|
||||
# TODO: Boulder reregisters with new recoveryToken and new URI
|
||||
raise errors.UnexpectedUpdate(regr)
|
||||
return updated_regr
|
||||
|
||||
def _authzr_from_response(self, response, identifier,
|
||||
uri=None, new_cert_uri=None):
|
||||
if new_cert_uri is None:
|
||||
try:
|
||||
new_cert_uri = response.links['next']['url']
|
||||
except KeyError:
|
||||
raise errors.NetworkError('"next" link missing')
|
||||
|
||||
authzr = messages2.AuthorizationResource(
|
||||
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):
|
||||
raise errors.UnexpectedUpdate(authzr)
|
||||
return authzr
|
||||
|
||||
def request_challenges(self, identifier, regr):
|
||||
"""Request challenges.
|
||||
|
||||
:param identifier: Identifier to be challenged.
|
||||
:type identifier: `.messages2.Identifier`
|
||||
|
||||
:param regr: Registration Resource.
|
||||
:type regr: `.RegistrationResource`
|
||||
|
||||
:returns: Authorization Resource.
|
||||
:rtype: `.AuthorizationResource`
|
||||
|
||||
"""
|
||||
new_authz = messages2.Authorization(identifier=identifier)
|
||||
response = self._post(regr.new_authzr_uri, self._wrap_in_jws(new_authz))
|
||||
assert response.status_code == httplib.CREATED # TODO: handle errors
|
||||
return self._authzr_from_response(response, identifier)
|
||||
|
||||
def request_domain_challenges(self, domain, regr):
|
||||
"""Request challenges for domain names.
|
||||
|
||||
This is simply a convenience function that wraps around
|
||||
`request_challenges`, but works with domain names instead of
|
||||
generic identifiers.
|
||||
|
||||
:param str domain: Domain name to be challenged.
|
||||
|
||||
"""
|
||||
return self.request_challenges(messages2.Identifier(
|
||||
typ=messages2.IDENTIFIER_FQDN, value=domain), regr)
|
||||
|
||||
def answer_challenge(self, challb, response):
|
||||
"""Answer challenge.
|
||||
|
||||
:param challb: Challenge Resource body.
|
||||
:type challb: `.ChallengeBody`
|
||||
|
||||
:param response: Corresponding Challenge response
|
||||
:type response: `.challenges.ChallengeResponse`
|
||||
|
||||
:returns: Challenge Resource with updated body.
|
||||
:rtype: `.ChallengeResource`
|
||||
|
||||
:raises errors.UnexpectedUpdate:
|
||||
|
||||
"""
|
||||
response = self._post(challb.uri, self._wrap_in_jws(response))
|
||||
try:
|
||||
authzr_uri = response.links['up']['url']
|
||||
except KeyError:
|
||||
raise errors.NetworkError('"up" Link header missing')
|
||||
challr = messages2.ChallengeResource(
|
||||
authzr_uri=authzr_uri,
|
||||
body=messages2.ChallengeBody.from_json(response.json()))
|
||||
# TODO: check that challr.uri == response.headers['Location']?
|
||||
if challr.uri != challb.uri:
|
||||
raise errors.UnexpectedUpdate(challr.uri)
|
||||
return challr
|
||||
|
||||
def answer_challenges(self, challbs, responses):
|
||||
"""Answer multiple challenges.
|
||||
|
||||
.. note:: This is a convenience function to make integration
|
||||
with old proto code easier and shall probably be removed
|
||||
once restification is over.
|
||||
|
||||
"""
|
||||
return [self.answer_challenge(challb, response)
|
||||
for challb, response in itertools.izip(challbs, responses)]
|
||||
|
||||
@classmethod
|
||||
def retry_after(cls, response, default):
|
||||
"""Compute next `poll` time based on response ``Retry-After`` header.
|
||||
|
||||
:param response: Response from `poll`.
|
||||
:type response: `requests.Response`
|
||||
|
||||
:param int default: Default value (in seconds), used when
|
||||
``Retry-After`` header is not present or invalid.
|
||||
|
||||
:returns: Time point when next `poll` should be performed.
|
||||
:rtype: `datetime.datetime`
|
||||
|
||||
"""
|
||||
retry_after = response.headers.get('Retry-After', str(default))
|
||||
try:
|
||||
seconds = int(retry_after)
|
||||
except ValueError:
|
||||
# pylint: disable=no-member
|
||||
decoded = werkzeug.parse_date(retry_after) # RFC1123
|
||||
if decoded is None:
|
||||
seconds = default
|
||||
else:
|
||||
return decoded
|
||||
|
||||
return datetime.datetime.now() + datetime.timedelta(seconds=seconds)
|
||||
|
||||
def poll(self, authzr):
|
||||
"""Poll Authorization Resource for status.
|
||||
|
||||
:param authzr: Authorization Resource
|
||||
:type authzr: `.AuthorizationResource`
|
||||
|
||||
:returns: Updated Authorization Resource and HTTP response.
|
||||
|
||||
:rtype: (`.AuthorizationResource`, `requests.Response`)
|
||||
|
||||
"""
|
||||
response = self._get(authzr.uri)
|
||||
updated_authzr = self._authzr_from_response(
|
||||
response, authzr.body.identifier, authzr.uri, authzr.new_cert_uri)
|
||||
# TODO: check and raise UnexpectedUpdate
|
||||
|
||||
return updated_authzr, response
|
||||
|
||||
def request_issuance(self, csr, authzrs):
|
||||
"""Request issuance.
|
||||
|
||||
:param csr: CSR
|
||||
:type csr: `M2Crypto.X509.Request` wrapped in `.ComparableX509`
|
||||
|
||||
:param authzrs: `list` of `.AuthorizationResource`
|
||||
|
||||
:returns: Issued certificate
|
||||
:rtype: `.messages2.CertificateResource`
|
||||
|
||||
"""
|
||||
assert authzrs, "Authorizations list is empty"
|
||||
|
||||
# TODO: assert len(authzrs) == number of SANs
|
||||
req = messages2.CertificateRequest(
|
||||
csr=csr, authorizations=tuple(authzr.uri for authzr in authzrs))
|
||||
|
||||
content_type = self.DER_CONTENT_TYPE # TODO: add 'cert_type 'argument
|
||||
response = self._post(
|
||||
authzrs[0].new_cert_uri, # TODO: acme-spec #90
|
||||
self._wrap_in_jws(req),
|
||||
content_type=content_type,
|
||||
headers={'Accept': content_type})
|
||||
|
||||
cert_chain_uri = response.links.get('up', {}).get('url')
|
||||
|
||||
try:
|
||||
uri = response.headers['Location']
|
||||
except KeyError:
|
||||
raise errors.NetworkError('"Location" Header missing')
|
||||
|
||||
return messages2.CertificateResource(
|
||||
uri=uri, authzrs=authzrs, cert_chain_uri=cert_chain_uri,
|
||||
body=jose.ComparableX509(
|
||||
M2Crypto.X509.load_cert_der_string(response.content)))
|
||||
|
||||
def poll_and_request_issuance(self, csr, authzrs, mintime=5):
|
||||
"""Poll and request issuance.
|
||||
|
||||
This function polls all provided Authorization Resource URIs
|
||||
until all challenges are valid, respecting ``Retry-After`` HTTP
|
||||
headers, and then calls `request_issuance`.
|
||||
|
||||
.. todo:: add `max_attempts` or `timeout`
|
||||
|
||||
:param csr: CSR.
|
||||
:type csr: `M2Crypto.X509.Request` wrapped in `.ComparableX509`
|
||||
|
||||
:param authzrs: `list` of `.AuthorizationResource`
|
||||
|
||||
:param int mintime: Minimum time before next attempt, used if
|
||||
``Retry-After`` is not present in the response.
|
||||
|
||||
:returns: ``(cert, updated_authzrs)`` `tuple` where ``cert`` is
|
||||
the issued certificate (`.messages2.CertificateResource.),
|
||||
and ``updated_authzrs`` is a `tuple` consisting of updated
|
||||
Authorization Resources (`.AuthorizationResource`) as
|
||||
present in the responses from server, and in the same order
|
||||
as the input ``authzrs``.
|
||||
:rtype: `tuple`
|
||||
|
||||
"""
|
||||
# priority queue with datetime (based od Retry-After) as key,
|
||||
# and original Authorization Resource as value
|
||||
waiting = [(datetime.datetime.now(), authzr) for authzr in authzrs]
|
||||
# mapping between original Authorization Resource and the most
|
||||
# recently updated one
|
||||
updated = dict((authzr, authzr) for authzr in authzrs)
|
||||
|
||||
while waiting:
|
||||
# find the smallest Retry-After, and sleep if necessary
|
||||
when, authzr = heapq.heappop(waiting)
|
||||
now = datetime.datetime.now()
|
||||
if when > now:
|
||||
seconds = (when - now).seconds
|
||||
logging.debug('Sleeping for %d seconds', seconds)
|
||||
time.sleep(seconds)
|
||||
|
||||
# Note that we poll with the latest updated Authorization
|
||||
# URI, which might have a different URI than initial one
|
||||
updated_authzr, response = self.poll(updated[authzr])
|
||||
updated[authzr] = updated_authzr
|
||||
|
||||
if updated_authzr.body.status != messages2.STATUS_VALID:
|
||||
# push back to the priority queue, with updated retry_after
|
||||
heapq.heappush(waiting, (self.retry_after(
|
||||
response, default=mintime), authzr))
|
||||
|
||||
updated_authzrs = tuple(updated[authzr] for authzr in authzrs)
|
||||
return self.request_issuance(csr, updated_authzrs), updated_authzrs
|
||||
|
||||
def _get_cert(self, uri):
|
||||
content_type = self.DER_CONTENT_TYPE # TODO: make it a param
|
||||
response = self._get(uri, headers={'Accept': content_type},
|
||||
content_type=content_type)
|
||||
return response, jose.ComparableX509(
|
||||
M2Crypto.X509.load_cert_der_string(response.content))
|
||||
|
||||
def check_cert(self, certr):
|
||||
"""Check for new cert.
|
||||
|
||||
:param certr: Certificate Resource
|
||||
:type certr: `.CertificateResource`
|
||||
|
||||
:returns: Updated Certificate Resource.
|
||||
:rtype: `.CertificateResource`
|
||||
|
||||
"""
|
||||
# TODO: acme-spec 5.1 table action should be renamed to
|
||||
# "refresh cert", and this method integrated with self.refresh
|
||||
response, cert = self._get_cert(certr.uri)
|
||||
if 'Location' not in response.headers:
|
||||
raise errors.NetworkError('Location header missing')
|
||||
if response.headers['Location'] != certr.uri:
|
||||
raise errors.UnexpectedUpdate(response.text)
|
||||
return certr.update(body=cert)
|
||||
|
||||
def refresh(self, certr):
|
||||
"""Refresh certificate.
|
||||
|
||||
:param certr: Certificate Resource
|
||||
:type certr: `.CertificateResource`
|
||||
|
||||
:returns: Updated Certificate Resource.
|
||||
:rtype: `.CertificateResource`
|
||||
|
||||
"""
|
||||
# TODO: If a client sends a refresh request and the server is
|
||||
# not willing to refresh the certificate, the server MUST
|
||||
# respond with status code 403 (Forbidden)
|
||||
return self.check_cert(certr)
|
||||
|
||||
def fetch_chain(self, certr):
|
||||
"""Fetch chain for certificate.
|
||||
|
||||
:param certr: Certificate Resource
|
||||
:type certr: `.CertificateResource`
|
||||
|
||||
:returns: Certificate chain, or `None` if no "up" Link was provided.
|
||||
:rtype: `M2Crypto.X509.X509` wrapped in `.ComparableX509`
|
||||
|
||||
"""
|
||||
if certr.cert_chain_uri is not None:
|
||||
return self._get_cert(certr.cert_chain_uri)
|
||||
|
||||
def revoke(self, certr, when=messages2.Revocation.NOW):
|
||||
"""Revoke certificate.
|
||||
|
||||
:param when: When should the revocation take place? Takes
|
||||
the same values as `.messages2.Revocation.revoke`.
|
||||
|
||||
"""
|
||||
rev = messages2.Revocation(revoke=when, authorizations=tuple(
|
||||
authzr.uri for authzr in certr.authzrs))
|
||||
response = self._post(certr.uri, self._wrap_in_jws(rev))
|
||||
if response.status_code != httplib.OK:
|
||||
raise errors.NetworkError(
|
||||
'Successful revocation must return HTTP OK status')
|
||||
1
letsencrypt/client/plugins/__init__.py
Normal file
1
letsencrypt/client/plugins/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Let's Encrypt client.plugins."""
|
||||
1
letsencrypt/client/plugins/apache/__init__.py
Normal file
1
letsencrypt/client/plugins/apache/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Let's Encrypt client.plugins.apache."""
|
||||
@@ -18,9 +18,9 @@ from letsencrypt.client import errors
|
||||
from letsencrypt.client import interfaces
|
||||
from letsencrypt.client import le_util
|
||||
|
||||
from letsencrypt.client.apache import dvsni
|
||||
from letsencrypt.client.apache import obj
|
||||
from letsencrypt.client.apache import parser
|
||||
from letsencrypt.client.plugins.apache import dvsni
|
||||
from letsencrypt.client.plugins.apache import obj
|
||||
from letsencrypt.client.plugins.apache import parser
|
||||
|
||||
|
||||
# TODO: Augeas sections ie. <VirtualHost>, <IfModule> beginning and closing
|
||||
@@ -68,11 +68,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
:type config: :class:`~letsencrypt.client.interfaces.IConfig`
|
||||
|
||||
:ivar parser: Handles low level parsing
|
||||
:type parser: :class:`letsencrypt.client.apache.parser`
|
||||
:type parser: :class:`~letsencrypt.client.plugins.apache.parser`
|
||||
|
||||
:ivar tup version: version of Apache
|
||||
:ivar list vhosts: All vhosts found in the configuration
|
||||
(:class:`list` of :class:`letsencrypt.client.apache.obj.VirtualHost`)
|
||||
(:class:`list` of
|
||||
:class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`)
|
||||
|
||||
:ivar dict assoc: Mapping between domains and vhosts
|
||||
|
||||
@@ -164,7 +165,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
parser.case_i("SSLCertificateChainFile"), None, vhost.path)
|
||||
|
||||
if len(path["cert_file"]) == 0 or len(path["cert_key"]) == 0:
|
||||
# Throw some "can't find all of the directives error"
|
||||
# Throw some can't find all of the directives error"
|
||||
logging.warn(
|
||||
"Cannot find a cert or key directive in %s", vhost.path)
|
||||
logging.warn("VirtualHost was not modified")
|
||||
@@ -203,7 +204,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
:param str target_name: domain name
|
||||
|
||||
:returns: ssl vhost associated with name
|
||||
:rtype: :class:`letsencrypt.client.apache.obj.VirtualHost`
|
||||
:rtype: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
|
||||
|
||||
"""
|
||||
# Allows for domain names to be associated with a virtual host
|
||||
@@ -223,7 +224,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
self.assoc[target_name] = vhost
|
||||
return vhost
|
||||
|
||||
# Check for non ssl vhosts with servernames/aliases == 'name'
|
||||
# Check for non ssl vhosts with servernames/aliases == "name"
|
||||
for vhost in self.vhosts:
|
||||
if not vhost.ssl and target_name in vhost.names:
|
||||
vhost = self.make_vhost_ssl(vhost)
|
||||
@@ -244,7 +245,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
:param str domain: domain name to associate
|
||||
|
||||
:param vhost: virtual host to associate with domain
|
||||
:type vhost: :class:`letsencrypt.client.apache.obj.VirtualHost`
|
||||
:type vhost: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
|
||||
|
||||
"""
|
||||
self.assoc[domain] = vhost
|
||||
@@ -281,15 +282,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
"""Helper function for get_virtual_hosts().
|
||||
|
||||
:param host: In progress vhost whose names will be added
|
||||
:type host: :class:`letsencrypt.client.apache.obj.VirtualHost`
|
||||
:type host: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
|
||||
|
||||
"""
|
||||
name_match = self.aug.match(("%s//*[self::directive=~regexp('%s')] | "
|
||||
"%s//*[self::directive=~regexp('%s')]" %
|
||||
(host.path,
|
||||
parser.case_i('ServerName'),
|
||||
parser.case_i("ServerName"),
|
||||
host.path,
|
||||
parser.case_i('ServerAlias'))))
|
||||
parser.case_i("ServerAlias"))))
|
||||
|
||||
for name in name_match:
|
||||
args = self.aug.match(name + "/*")
|
||||
@@ -302,7 +303,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
:param str path: Augeas path to virtual host
|
||||
|
||||
:returns: newly created vhost
|
||||
:rtype: :class:`letsencrypt.client.apache.obj.VirtualHost`
|
||||
:rtype: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
|
||||
|
||||
"""
|
||||
addrs = set()
|
||||
@@ -326,7 +327,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
"""Returns list of virtual hosts found in the Apache configuration.
|
||||
|
||||
:returns: List of
|
||||
:class:`letsencrypt.client.apache.obj.VirtualHost` objects
|
||||
:class:`~letsencrypt.client.plugins.apache.obj.VirtualHost` objects
|
||||
found in configuration
|
||||
:rtype: list
|
||||
|
||||
@@ -334,7 +335,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
# Search sites-available, httpd.conf for possible virtual hosts
|
||||
paths = self.aug.match(
|
||||
("/files%s/sites-available//*[label()=~regexp('%s')]" %
|
||||
(self.parser.root, parser.case_i('VirtualHost'))))
|
||||
(self.parser.root, parser.case_i("VirtualHost"))))
|
||||
vhs = []
|
||||
|
||||
for path in paths:
|
||||
@@ -404,7 +405,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
"""Checks to see if the server is ready for SNI challenges.
|
||||
|
||||
:param vhost: VirtualHost to check SNI compatibility
|
||||
:type vhost: :class:`letsencrypt.client.apache.obj.VirtualHost`
|
||||
:type vhost: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
|
||||
|
||||
:param str default_addr: TODO - investigate function further
|
||||
|
||||
@@ -436,10 +437,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
.. note:: This function saves the configuration
|
||||
|
||||
:param nonssl_vhost: Valid VH that doesn't have SSLEngine on
|
||||
:type nonssl_vhost: :class:`letsencrypt.client.apache.obj.VirtualHost`
|
||||
:type nonssl_vhost:
|
||||
:class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
|
||||
|
||||
:returns: SSL vhost
|
||||
:rtype: :class:`letsencrypt.client.apache.obj.VirtualHost`
|
||||
:rtype: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
|
||||
|
||||
"""
|
||||
avail_fp = nonssl_vhost.filep
|
||||
@@ -454,8 +456,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
self.reverter.register_file_creation(False, ssl_fp)
|
||||
|
||||
try:
|
||||
with open(avail_fp, 'r') as orig_file:
|
||||
with open(ssl_fp, 'w') as new_file:
|
||||
with open(avail_fp, "r") as orig_file:
|
||||
with open(ssl_fp, "w") as new_file:
|
||||
new_file.write("<IfModule mod_ssl.c>\n")
|
||||
for line in orig_file:
|
||||
new_file.write(line)
|
||||
@@ -471,7 +473,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
# change address to address:443
|
||||
addr_match = "/files%s//* [label()=~regexp('%s')]/arg"
|
||||
ssl_addr_p = self.aug.match(
|
||||
addr_match % (ssl_fp, parser.case_i('VirtualHost')))
|
||||
addr_match % (ssl_fp, parser.case_i("VirtualHost")))
|
||||
|
||||
for addr in ssl_addr_p:
|
||||
old_addr = obj.Addr.fromstring(
|
||||
@@ -482,7 +484,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
|
||||
# Add directives
|
||||
vh_p = self.aug.match("/files%s//* [label()=~regexp('%s')]" %
|
||||
(ssl_fp, parser.case_i('VirtualHost')))
|
||||
(ssl_fp, parser.case_i("VirtualHost")))
|
||||
if len(vh_p) != 1:
|
||||
logging.error("Error: should only be one vhost in %s", avail_fp)
|
||||
sys.exit(1)
|
||||
@@ -495,7 +497,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
|
||||
# Log actions and create save notes
|
||||
logging.info("Created an SSL vhost at %s", ssl_fp)
|
||||
self.save_notes += 'Created ssl vhost at %s\n' % ssl_fp
|
||||
self.save_notes += "Created ssl vhost at %s\n" % ssl_fp
|
||||
self.save()
|
||||
|
||||
# We know the length is one because of the assertion above
|
||||
@@ -548,8 +550,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
def _enable_redirect(self, ssl_vhost, unused_options):
|
||||
"""Redirect all equivalent HTTP traffic to ssl_vhost.
|
||||
|
||||
.. todo:: This enhancement should be rewritten and will unfortunately
|
||||
require lots of debugging by hand.
|
||||
.. todo:: This enhancement should be rewritten and will
|
||||
unfortunately require lots of debugging by hand.
|
||||
|
||||
Adds Redirect directive to the port 80 equivalent of ssl_vhost
|
||||
First the function attempts to find the vhost with equivalent
|
||||
ip addresses that serves on non-ssl ports
|
||||
@@ -558,13 +561,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
.. note:: This function saves the configuration
|
||||
|
||||
:param ssl_vhost: Destination of traffic, an ssl enabled vhost
|
||||
:type ssl_vhost: :class:`letsencrypt.client.apache.obj.VirtualHost`
|
||||
:type ssl_vhost:
|
||||
:class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
|
||||
|
||||
:param unused_options: Not currently used
|
||||
:type unused_options: Not Available
|
||||
|
||||
:returns: Success, general_vhost (HTTP vhost)
|
||||
:rtype: (bool, :class:`letsencrypt.client.apache.obj.VirtualHost`)
|
||||
:rtype: (bool,
|
||||
:class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`)
|
||||
|
||||
"""
|
||||
if not mod_loaded("rewrite_module", self.config.apache_ctl):
|
||||
@@ -595,7 +600,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
self.parser.add_dir(general_v.path, "RewriteEngine", "On")
|
||||
self.parser.add_dir(general_v.path, "RewriteRule",
|
||||
constants.APACHE_REWRITE_HTTPS_ARGS)
|
||||
self.save_notes += ('Redirecting host in %s to ssl vhost in %s\n' %
|
||||
self.save_notes += ("Redirecting host in %s to ssl vhost in %s\n" %
|
||||
(general_v.filep, ssl_vhost.filep))
|
||||
self.save()
|
||||
|
||||
@@ -616,7 +621,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
-1 is also returned in case of no redirection/rewrite directives
|
||||
|
||||
:param vhost: vhost to check
|
||||
:type vhost: :class:`letsencrypt.client.apache.obj.VirtualHost`
|
||||
:type vhost: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
|
||||
|
||||
:returns: Success, code value... see documentation
|
||||
:rtype: bool, int
|
||||
@@ -648,10 +653,13 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
"""Creates an http_vhost specifically to redirect for the ssl_vhost.
|
||||
|
||||
:param ssl_vhost: ssl vhost
|
||||
:type ssl_vhost: :class:`letsencrypt.client.apache.obj.VirtualHost`
|
||||
:type ssl_vhost:
|
||||
:class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
|
||||
|
||||
:returns: Success, vhost
|
||||
:rtype: (bool, :class:`letsencrypt.client.apache.obj.VirtualHost`)
|
||||
:returns: tuple of the form
|
||||
(`success`,
|
||||
:class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`)
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
# Consider changing this to a dictionary check
|
||||
@@ -699,7 +707,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
redirect_filename = "le-redirect-%s.conf" % ssl_vhost.names[0]
|
||||
|
||||
redirect_filepath = os.path.join(
|
||||
self.parser.root, 'sites-available', redirect_filename)
|
||||
self.parser.root, "sites-available", redirect_filename)
|
||||
|
||||
# Register the new file that will be created
|
||||
# Note: always register the creation before writing to ensure file will
|
||||
@@ -707,7 +715,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
self.reverter.register_file_creation(False, redirect_filepath)
|
||||
|
||||
# Write out file
|
||||
with open(redirect_filepath, 'w') as redirect_fd:
|
||||
with open(redirect_filepath, "w") as redirect_fd:
|
||||
redirect_fd.write(redirect_file)
|
||||
logging.info("Created redirect file: %s", redirect_filename)
|
||||
|
||||
@@ -717,8 +725,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
self.vhosts.append(new_vhost)
|
||||
|
||||
# Finally create documentation for the change
|
||||
self.save_notes += ('Created a port 80 vhost, %s, for redirection to '
|
||||
'ssl vhost %s\n' %
|
||||
self.save_notes += ("Created a port 80 vhost, %s, for redirection to "
|
||||
"ssl vhost %s\n" %
|
||||
(new_vhost.filep, ssl_vhost.filep))
|
||||
|
||||
def _conflicting_host(self, ssl_vhost):
|
||||
@@ -733,7 +741,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
if not conflict: returns space separated list of new host addrs
|
||||
|
||||
:param ssl_vhost: SSL Vhost to check for possible port 80 redirection
|
||||
:type ssl_vhost: :class:`letsencrypt.client.apache.obj.VirtualHost`
|
||||
:type ssl_vhost:
|
||||
:class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
|
||||
|
||||
:returns: TODO
|
||||
:rtype: TODO
|
||||
@@ -766,10 +775,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
Consider changing this into a dict check
|
||||
|
||||
:param ssl_vhost: ssl vhost to check
|
||||
:type ssl_vhost: :class:`letsencrypt.client.apache.obj.VirtualHost`
|
||||
:type ssl_vhost:
|
||||
:class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
|
||||
|
||||
:returns: HTTP vhost or None if unsuccessful
|
||||
:rtype: :class:`letsencrypt.client.apache.obj.VirtualHost` or None
|
||||
:rtype: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
|
||||
or None
|
||||
|
||||
"""
|
||||
# _default_:443 check
|
||||
@@ -859,7 +870,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
.. todo:: Make sure link is not broken...
|
||||
|
||||
:param vhost: vhost to enable
|
||||
:type vhost: :class:`letsencrypt.client.apache.obj.VirtualHost`
|
||||
:type vhost: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
|
||||
|
||||
:returns: Success
|
||||
:rtype: bool
|
||||
@@ -875,7 +886,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
os.symlink(vhost.filep, enabled_path)
|
||||
vhost.enabled = True
|
||||
logging.info("Enabling available site: %s", vhost.filep)
|
||||
self.save_notes += 'Enabled site %s\n' % vhost.filep
|
||||
self.save_notes += "Enabled site %s\n" % vhost.filep
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -897,7 +908,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
"""
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
['sudo', self.config.apache_ctl, 'configtest'], # TODO: sudo?
|
||||
["sudo", self.config.apache_ctl, "configtest"], # TODO: sudo?
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
stdout, stderr = proc.communicate()
|
||||
@@ -941,7 +952,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
"""
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
[self.config.apache_ctl, '-v'],
|
||||
[self.config.apache_ctl, "-v"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
text = proc.communicate()[0]
|
||||
@@ -956,7 +967,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
"Unable to find Apache version")
|
||||
|
||||
return tuple([int(i) for i in matches[0].split('.')])
|
||||
return tuple([int(i) for i in matches[0].split(".")])
|
||||
|
||||
def more_info(self):
|
||||
"""Human-readable string to help understand the module"""
|
||||
@@ -1031,8 +1042,8 @@ def enable_mod(mod_name, apache_init_script, apache_enmod):
|
||||
# Use check_output so the command will finish before reloading
|
||||
# TODO: a2enmod is debian specific...
|
||||
subprocess.check_call(["sudo", apache_enmod, mod_name], # TODO: sudo?
|
||||
stdout=open("/dev/null", 'w'),
|
||||
stderr=open("/dev/null", 'w'))
|
||||
stdout=open("/dev/null", "w"),
|
||||
stderr=open("/dev/null", "w"))
|
||||
apache_restart(apache_init_script)
|
||||
except (OSError, subprocess.CalledProcessError) as err:
|
||||
logging.error("Error enabling mod_%s", mod_name)
|
||||
@@ -1054,7 +1065,7 @@ def mod_loaded(module, apache_ctl):
|
||||
"""
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
[apache_ctl, '-M'],
|
||||
[apache_ctl, "-M"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
stdout, stderr = proc.communicate()
|
||||
@@ -1092,7 +1103,7 @@ def apache_restart(apache_init_script):
|
||||
|
||||
"""
|
||||
try:
|
||||
proc = subprocess.Popen([apache_init_script, 'restart'],
|
||||
proc = subprocess.Popen([apache_init_script, "restart"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
stdout, stderr = proc.communicate()
|
||||
@@ -2,20 +2,19 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from letsencrypt.client.apache import parser
|
||||
from letsencrypt.client.plugins.apache import parser
|
||||
|
||||
|
||||
class ApacheDvsni(object):
|
||||
"""Class performs DVSNI challenges within the Apache configurator.
|
||||
|
||||
:ivar configurator: ApacheConfigurator object
|
||||
:type configurator:
|
||||
:class:`letsencrypt.client.apache.configurator.ApacheConfigurator`
|
||||
:type configurator: :class:`~apache.configurator.ApacheConfigurator`
|
||||
|
||||
:ivar list achalls: Annotated :class:`~letsencrypt.client.achallenges.DVSNI`
|
||||
challenges.
|
||||
|
||||
:param list indicies: Meant to hold indices of challenges in a
|
||||
:param list indices: Meant to hold indices of challenges in a
|
||||
larger array. ApacheDvsni is capable of solving many challenges
|
||||
at once which causes an indexing issue within ApacheConfigurator
|
||||
who must return all responses in order. Imagine ApacheConfigurator
|
||||
@@ -26,6 +25,23 @@ class ApacheDvsni(object):
|
||||
:param str challenge_conf: location of the challenge config file
|
||||
|
||||
"""
|
||||
|
||||
VHOST_TEMPLATE = """\
|
||||
<VirtualHost {vhost}>
|
||||
ServerName {server_name}
|
||||
UseCanonicalName on
|
||||
SSLStrictSNIVHostCheck on
|
||||
|
||||
LimitRequestBody 1048576
|
||||
|
||||
Include {ssl_options_conf_path}
|
||||
SSLCertificateFile {cert_path}
|
||||
SSLCertificateKeyFile {key_path}
|
||||
|
||||
DocumentRoot {document_root}
|
||||
</VirtualHost>
|
||||
|
||||
"""
|
||||
def __init__(self, configurator):
|
||||
self.configurator = configurator
|
||||
self.achalls = []
|
||||
@@ -50,7 +66,7 @@ class ApacheDvsni(object):
|
||||
def perform(self):
|
||||
"""Peform a DVSNI challenge."""
|
||||
if not self.achalls:
|
||||
return None
|
||||
return []
|
||||
# Save any changes to the configuration as a precaution
|
||||
# About to make temporary changes to the config
|
||||
self.configurator.save()
|
||||
@@ -101,7 +117,7 @@ class ApacheDvsni(object):
|
||||
cert_pem, response = achall.gen_cert_and_response(s)
|
||||
|
||||
# Write out challenge cert
|
||||
with open(cert_path, 'w') as cert_chall_fd:
|
||||
with open(cert_path, "w") as cert_chall_fd:
|
||||
cert_chall_fd.write(cert_pem)
|
||||
|
||||
return response
|
||||
@@ -112,7 +128,7 @@ class ApacheDvsni(object):
|
||||
Result: Apache config includes virtual servers for issued challs
|
||||
|
||||
:param list ll_addrs: list of list of
|
||||
:class:`letsencrypt.client.apache.obj.Addr` to apply
|
||||
:class:`letsencrypt.client.plugins.apache.obj.Addr` to apply
|
||||
|
||||
"""
|
||||
# TODO: Use ip address of existing vhost instead of relying on FQDN
|
||||
@@ -125,7 +141,7 @@ class ApacheDvsni(object):
|
||||
self.configurator.reverter.register_file_creation(
|
||||
True, self.challenge_conf)
|
||||
|
||||
with open(self.challenge_conf, 'w') as new_conf:
|
||||
with open(self.challenge_conf, "w") as new_conf:
|
||||
new_conf.write(config_text)
|
||||
|
||||
def _conf_include_check(self, main_config):
|
||||
@@ -151,7 +167,7 @@ class ApacheDvsni(object):
|
||||
:type achall: :class:`letsencrypt.client.achallenges.DVSNI`
|
||||
|
||||
:param list ip_addrs: addresses of challenged domain
|
||||
:class:`list` of type :class:`letsencrypt.client.apache.obj.Addr`
|
||||
:class:`list` of type :class:`~apache.obj.Addr`
|
||||
|
||||
:returns: virtual host configuration text
|
||||
:rtype: str
|
||||
@@ -160,19 +176,16 @@ class ApacheDvsni(object):
|
||||
ips = " ".join(str(i) for i in ip_addrs)
|
||||
document_root = os.path.join(
|
||||
self.configurator.config.config_dir, "dvsni_page/")
|
||||
return ("<VirtualHost " + ips + ">\n"
|
||||
"ServerName " + achall.nonce_domain + "\n"
|
||||
"UseCanonicalName on\n"
|
||||
"SSLStrictSNIVHostCheck on\n"
|
||||
"\n"
|
||||
"LimitRequestBody 1048576\n"
|
||||
"\n"
|
||||
"Include " + self.configurator.parser.loc["ssl_options"] + "\n"
|
||||
"SSLCertificateFile " + self.get_cert_file(achall) + "\n"
|
||||
"SSLCertificateKeyFile " + achall.key.file + "\n"
|
||||
"\n"
|
||||
"DocumentRoot " + document_root + "\n"
|
||||
"</VirtualHost>\n\n")
|
||||
# TODO: Python docs is not clear how mutliline string literal
|
||||
# newlines are parsed on different platforms. At least on
|
||||
# Linux (Debian sid), when source file uses CRLF, Python still
|
||||
# parses it as "\n"... c.f.:
|
||||
# https://docs.python.org/2.7/reference/lexical_analysis.html
|
||||
return self.VHOST_TEMPLATE.format(
|
||||
vhost=ips, server_name=achall.nonce_domain,
|
||||
ssl_options_conf_path=self.configurator.parser.loc["ssl_options"],
|
||||
cert_path=self.get_cert_file(achall), key_path=achall.key.file,
|
||||
document_root=document_root).replace("\n", os.linesep)
|
||||
|
||||
def get_cert_file(self, achall):
|
||||
"""Returns standardized name for challenge certificate.
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Test for letsencrypt.client.apache.configurator."""
|
||||
"""Test for letsencrypt.client.plugins.apache.configurator."""
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
@@ -12,11 +12,11 @@ from letsencrypt.client import achallenges
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client import le_util
|
||||
|
||||
from letsencrypt.client.apache import configurator
|
||||
from letsencrypt.client.apache import obj
|
||||
from letsencrypt.client.apache import parser
|
||||
from letsencrypt.client.plugins.apache import configurator
|
||||
from letsencrypt.client.plugins.apache import obj
|
||||
from letsencrypt.client.plugins.apache import parser
|
||||
|
||||
from letsencrypt.client.tests.apache import util
|
||||
from letsencrypt.client.plugins.apache.tests import util
|
||||
|
||||
|
||||
class TwoVhost80Test(util.ApacheTest):
|
||||
@@ -25,7 +25,7 @@ class TwoVhost80Test(util.ApacheTest):
|
||||
def setUp(self):
|
||||
super(TwoVhost80Test, self).setUp()
|
||||
|
||||
with mock.patch("letsencrypt.client.apache.configurator."
|
||||
with mock.patch("letsencrypt.client.plugins.apache.configurator."
|
||||
"mod_loaded") as mock_load:
|
||||
mock_load.return_value = True
|
||||
self.config = util.get_apache_configurator(
|
||||
@@ -43,9 +43,15 @@ class TwoVhost80Test(util.ApacheTest):
|
||||
def test_get_all_names(self):
|
||||
names = self.config.get_all_names()
|
||||
self.assertEqual(names, set(
|
||||
['letsencrypt.demo', 'encryption-example.demo', 'ip-172-30-0-17']))
|
||||
["letsencrypt.demo", "encryption-example.demo", "ip-172-30-0-17"]))
|
||||
|
||||
def test_get_virtual_hosts(self):
|
||||
"""Make sure all vhosts are being properly found.
|
||||
|
||||
.. note:: If test fails, only finding 1 Vhost... it is likely that
|
||||
it is a problem with is_enabled.
|
||||
|
||||
"""
|
||||
vhs = self.config.get_virtual_hosts()
|
||||
self.assertEqual(len(vhs), 4)
|
||||
found = 0
|
||||
@@ -59,6 +65,14 @@ class TwoVhost80Test(util.ApacheTest):
|
||||
self.assertEqual(found, 4)
|
||||
|
||||
def test_is_site_enabled(self):
|
||||
"""Test if site is enabled.
|
||||
|
||||
.. note:: This test currently fails for hard links
|
||||
(which may happen if you move dirs incorrectly)
|
||||
.. warning:: This test does not work when running using the
|
||||
unittest.main() function. It incorrectly copies symlinks.
|
||||
|
||||
"""
|
||||
self.assertTrue(self.config.is_site_enabled(self.vh_truth[0].filep))
|
||||
self.assertFalse(self.config.is_site_enabled(self.vh_truth[1].filep))
|
||||
self.assertTrue(self.config.is_site_enabled(self.vh_truth[2].filep))
|
||||
@@ -134,9 +148,9 @@ class TwoVhost80Test(util.ApacheTest):
|
||||
|
||||
self.assertEqual(len(self.config.vhosts), 5)
|
||||
|
||||
@mock.patch("letsencrypt.client.apache.configurator."
|
||||
@mock.patch("letsencrypt.client.plugins.apache.configurator."
|
||||
"dvsni.ApacheDvsni.perform")
|
||||
@mock.patch("letsencrypt.client.apache.configurator."
|
||||
@mock.patch("letsencrypt.client.plugins.apache.configurator."
|
||||
"ApacheConfigurator.restart")
|
||||
def test_perform(self, mock_restart, mock_dvsni_perform):
|
||||
# Only tests functionality specific to configurator.perform
|
||||
@@ -166,7 +180,7 @@ class TwoVhost80Test(util.ApacheTest):
|
||||
|
||||
self.assertEqual(mock_restart.call_count, 1)
|
||||
|
||||
@mock.patch("letsencrypt.client.apache.configurator."
|
||||
@mock.patch("letsencrypt.client.plugins.apache.configurator."
|
||||
"subprocess.Popen")
|
||||
def test_get_version(self, mock_popen):
|
||||
mock_popen().communicate.return_value = (
|
||||
@@ -183,7 +197,7 @@ class TwoVhost80Test(util.ApacheTest):
|
||||
errors.LetsEncryptConfiguratorError, self.config.get_version)
|
||||
|
||||
mock_popen().communicate.return_value = (
|
||||
"Server Version: Apache/2.3\n Apache/2.4.7", "")
|
||||
"Server Version: Apache/2.3{0} Apache/2.4.7".format(os.linesep), "")
|
||||
self.assertRaises(
|
||||
errors.LetsEncryptConfiguratorError, self.config.get_version)
|
||||
|
||||
@@ -192,5 +206,5 @@ class TwoVhost80Test(util.ApacheTest):
|
||||
errors.LetsEncryptConfiguratorError, self.config.get_version)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Test for letsencrypt.client.apache.dvsni."""
|
||||
"""Test for letsencrypt.client.plugins.apache.dvsni."""
|
||||
import pkg_resources
|
||||
import unittest
|
||||
import shutil
|
||||
@@ -10,9 +10,9 @@ from letsencrypt.acme import challenges
|
||||
from letsencrypt.client import achallenges
|
||||
from letsencrypt.client import le_util
|
||||
|
||||
from letsencrypt.client.apache.obj import Addr
|
||||
from letsencrypt.client.plugins.apache.obj import Addr
|
||||
|
||||
from letsencrypt.client.tests.apache import util
|
||||
from letsencrypt.client.plugins.apache.tests import util
|
||||
|
||||
|
||||
class DvsniPerformTest(util.ApacheTest):
|
||||
@@ -21,20 +21,20 @@ class DvsniPerformTest(util.ApacheTest):
|
||||
def setUp(self):
|
||||
super(DvsniPerformTest, self).setUp()
|
||||
|
||||
with mock.patch("letsencrypt.client.apache.configurator."
|
||||
with mock.patch("letsencrypt.client.plugins.apache.configurator."
|
||||
"mod_loaded") as mock_load:
|
||||
mock_load.return_value = True
|
||||
config = util.get_apache_configurator(
|
||||
self.config_path, self.config_dir, self.work_dir,
|
||||
self.ssl_options)
|
||||
|
||||
from letsencrypt.client.apache import dvsni
|
||||
from letsencrypt.client.plugins.apache import dvsni
|
||||
self.sni = dvsni.ApacheDvsni(config)
|
||||
|
||||
rsa256_file = pkg_resources.resource_filename(
|
||||
"letsencrypt.client.tests", 'testdata/rsa256_key.pem')
|
||||
"letsencrypt.client.tests", "testdata/rsa256_key.pem")
|
||||
rsa256_pem = pkg_resources.resource_string(
|
||||
"letsencrypt.client.tests", 'testdata/rsa256_key.pem')
|
||||
"letsencrypt.client.tests", "testdata/rsa256_key.pem")
|
||||
|
||||
auth_key = le_util.Key(rsa256_file, rsa256_pem)
|
||||
self.achalls = [
|
||||
@@ -60,7 +60,7 @@ class DvsniPerformTest(util.ApacheTest):
|
||||
|
||||
def test_perform0(self):
|
||||
resp = self.sni.perform()
|
||||
self.assertTrue(resp is None)
|
||||
self.assertEqual(len(resp), 0)
|
||||
|
||||
def test_setup_challenge_cert(self):
|
||||
# This is a helper function that can be used for handling
|
||||
@@ -74,7 +74,7 @@ class DvsniPerformTest(util.ApacheTest):
|
||||
nonce_domain=self.achalls[0].nonce_domain)
|
||||
achall.gen_cert_and_response.return_value = ("pem", response)
|
||||
|
||||
with mock.patch("letsencrypt.client.apache.dvsni.open",
|
||||
with mock.patch("letsencrypt.client.plugins.apache.dvsni.open",
|
||||
m_open, create=True):
|
||||
# pylint: disable=protected-access
|
||||
self.assertEqual(response, self.sni._setup_challenge_cert(
|
||||
@@ -82,7 +82,7 @@ class DvsniPerformTest(util.ApacheTest):
|
||||
|
||||
self.assertTrue(m_open.called)
|
||||
self.assertEqual(
|
||||
m_open.call_args[0], (self.sni.get_cert_file(achall), 'w'))
|
||||
m_open.call_args[0], (self.sni.get_cert_file(achall), "w"))
|
||||
self.assertEqual(m_open().write.call_args[0][0], "pem")
|
||||
|
||||
def test_perform1(self):
|
||||
@@ -166,5 +166,5 @@ class DvsniPerformTest(util.ApacheTest):
|
||||
set([self.achalls[1].nonce_domain]))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,11 +1,11 @@
|
||||
"""Test the helper objects in apache.obj.py."""
|
||||
"""Test the helper objects in letsencrypt.client.plugins.apache.obj."""
|
||||
import unittest
|
||||
|
||||
|
||||
class AddrTest(unittest.TestCase):
|
||||
"""Test the Addr class."""
|
||||
def setUp(self):
|
||||
from letsencrypt.client.apache.obj import Addr
|
||||
from letsencrypt.client.plugins.apache.obj import Addr
|
||||
self.addr1 = Addr.fromstring("192.168.1.1")
|
||||
self.addr2 = Addr.fromstring("192.168.1.1:*")
|
||||
self.addr3 = Addr.fromstring("192.168.1.1:80")
|
||||
@@ -34,7 +34,7 @@ class AddrTest(unittest.TestCase):
|
||||
self.assertFalse(self.addr1 == 3333)
|
||||
|
||||
def test_set_inclusion(self):
|
||||
from letsencrypt.client.apache.obj import Addr
|
||||
from letsencrypt.client.plugins.apache.obj import Addr
|
||||
set_a = set([self.addr1, self.addr2])
|
||||
addr1b = Addr.fromstring("192.168.1.1")
|
||||
addr2b = Addr.fromstring("192.168.1.1:*")
|
||||
@@ -46,15 +46,15 @@ class AddrTest(unittest.TestCase):
|
||||
class VirtualHostTest(unittest.TestCase):
|
||||
"""Test the VirtualHost class."""
|
||||
def setUp(self):
|
||||
from letsencrypt.client.apache.obj import VirtualHost
|
||||
from letsencrypt.client.apache.obj import Addr
|
||||
from letsencrypt.client.plugins.apache.obj import VirtualHost
|
||||
from letsencrypt.client.plugins.apache.obj import Addr
|
||||
self.vhost1 = VirtualHost(
|
||||
"filep", "vh_path",
|
||||
set([Addr.fromstring("localhost")]), False, False)
|
||||
|
||||
def test_eq(self):
|
||||
from letsencrypt.client.apache.obj import Addr
|
||||
from letsencrypt.client.apache.obj import VirtualHost
|
||||
from letsencrypt.client.plugins.apache.obj import Addr
|
||||
from letsencrypt.client.plugins.apache.obj import VirtualHost
|
||||
vhost1b = VirtualHost(
|
||||
"filep", "vh_path",
|
||||
set([Addr.fromstring("localhost")]), False, False)
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests the ApacheParser class."""
|
||||
"""Tests for letsencrypt.client.plugins.apache.parser."""
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
@@ -11,7 +11,7 @@ import zope.component
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client.display import util as display_util
|
||||
|
||||
from letsencrypt.client.tests.apache import util
|
||||
from letsencrypt.client.plugins.apache.tests import util
|
||||
|
||||
|
||||
class ApacheParserTest(util.ApacheTest):
|
||||
@@ -22,7 +22,7 @@ class ApacheParserTest(util.ApacheTest):
|
||||
|
||||
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
|
||||
|
||||
from letsencrypt.client.apache.parser import ApacheParser
|
||||
from letsencrypt.client.plugins.apache.parser import ApacheParser
|
||||
self.aug = augeas.Augeas(flags=augeas.Augeas.NONE)
|
||||
self.parser = ApacheParser(self.aug, self.config_path, self.ssl_options)
|
||||
|
||||
@@ -32,19 +32,19 @@ class ApacheParserTest(util.ApacheTest):
|
||||
shutil.rmtree(self.work_dir)
|
||||
|
||||
def test_root_normalized(self):
|
||||
from letsencrypt.client.apache.parser import ApacheParser
|
||||
from letsencrypt.client.plugins.apache.parser import ApacheParser
|
||||
path = os.path.join(self.temp_dir, "debian_apache_2_4/////"
|
||||
"two_vhost_80/../two_vhost_80/apache2")
|
||||
parser = ApacheParser(self.aug, path, None)
|
||||
self.assertEqual(parser.root, self.config_path)
|
||||
|
||||
def test_root_absolute(self):
|
||||
from letsencrypt.client.apache.parser import ApacheParser
|
||||
from letsencrypt.client.plugins.apache.parser import ApacheParser
|
||||
parser = ApacheParser(self.aug, os.path.relpath(self.config_path), None)
|
||||
self.assertEqual(parser.root, self.config_path)
|
||||
|
||||
def test_root_no_trailing_slash(self):
|
||||
from letsencrypt.client.apache.parser import ApacheParser
|
||||
from letsencrypt.client.plugins.apache.parser import ApacheParser
|
||||
parser = ApacheParser(self.aug, self.config_path + os.path.sep, None)
|
||||
self.assertEqual(parser.root, self.config_path)
|
||||
|
||||
@@ -67,7 +67,7 @@ class ApacheParserTest(util.ApacheTest):
|
||||
self.assertTrue(matches)
|
||||
|
||||
def test_find_dir(self):
|
||||
from letsencrypt.client.apache.parser import case_i
|
||||
from letsencrypt.client.plugins.apache.parser import case_i
|
||||
test = self.parser.find_dir(case_i("Listen"), "443")
|
||||
# This will only look in enabled hosts
|
||||
test2 = self.parser.find_dir(case_i("documentroot"))
|
||||
@@ -92,7 +92,7 @@ class ApacheParserTest(util.ApacheTest):
|
||||
Path must be valid before attempting to add to augeas
|
||||
|
||||
"""
|
||||
from letsencrypt.client.apache.parser import get_aug_path
|
||||
from letsencrypt.client.plugins.apache.parser import get_aug_path
|
||||
self.parser.add_dir_to_ifmodssl(
|
||||
get_aug_path(self.parser.loc["default"]),
|
||||
"FakeDirective", "123")
|
||||
@@ -103,11 +103,11 @@ class ApacheParserTest(util.ApacheTest):
|
||||
self.assertTrue("IfModule" in matches[0])
|
||||
|
||||
def test_get_aug_path(self):
|
||||
from letsencrypt.client.apache.parser import get_aug_path
|
||||
from letsencrypt.client.plugins.apache.parser import get_aug_path
|
||||
self.assertEqual("/files/etc/apache", get_aug_path("/etc/apache"))
|
||||
|
||||
def test_set_locations(self):
|
||||
with mock.patch("letsencrypt.client.apache.parser."
|
||||
with mock.patch("letsencrypt.client.plugins.apache.parser."
|
||||
"os.path") as mock_path:
|
||||
|
||||
mock_path.isfile.return_value = False
|
||||
@@ -125,5 +125,5 @@ class ApacheParserTest(util.ApacheTest):
|
||||
self.assertEqual(results["default"], results["name"])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Common utilities for letsencrypt.client.apache."""
|
||||
"""Common utilities for letsencrypt.client.plugins.apache."""
|
||||
import os
|
||||
import pkg_resources
|
||||
import shutil
|
||||
@@ -8,8 +8,8 @@ import unittest
|
||||
import mock
|
||||
|
||||
from letsencrypt.client import constants
|
||||
from letsencrypt.client.apache import configurator
|
||||
from letsencrypt.client.apache import obj
|
||||
from letsencrypt.client.plugins.apache import configurator
|
||||
from letsencrypt.client.plugins.apache import obj
|
||||
|
||||
|
||||
class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods
|
||||
@@ -26,9 +26,9 @@ class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods
|
||||
self.temp_dir, "debian_apache_2_4/two_vhost_80/apache2")
|
||||
|
||||
self.rsa256_file = pkg_resources.resource_filename(
|
||||
"letsencrypt.client.tests", 'testdata/rsa256_key.pem')
|
||||
"letsencrypt.client.tests", "testdata/rsa256_key.pem")
|
||||
self.rsa256_pem = pkg_resources.resource_string(
|
||||
"letsencrypt.client.tests", 'testdata/rsa256_key.pem')
|
||||
"letsencrypt.client.tests", "testdata/rsa256_key.pem")
|
||||
|
||||
|
||||
def dir_setup(test_dir="debian_apache_2_4/two_vhost_80"):
|
||||
@@ -38,7 +38,7 @@ def dir_setup(test_dir="debian_apache_2_4/two_vhost_80"):
|
||||
work_dir = tempfile.mkdtemp("work")
|
||||
|
||||
test_configs = pkg_resources.resource_filename(
|
||||
"letsencrypt.client.tests", "testdata/%s" % test_dir)
|
||||
"letsencrypt.client.plugins.apache.tests", "testdata/%s" % test_dir)
|
||||
|
||||
shutil.copytree(
|
||||
test_configs, os.path.join(temp_dir, test_dir), symlinks=True)
|
||||
@@ -59,7 +59,7 @@ def get_apache_configurator(
|
||||
|
||||
backups = os.path.join(work_dir, "backups")
|
||||
|
||||
with mock.patch("letsencrypt.client.apache.configurator."
|
||||
with mock.patch("letsencrypt.client.plugins.apache.configurator."
|
||||
"subprocess.Popen") as mock_popen:
|
||||
# This just states that the ssl module is already loaded
|
||||
mock_popen().communicate.return_value = ("ssl_module", "")
|
||||
1
letsencrypt/client/plugins/standalone/__init__.py
Normal file
1
letsencrypt/client/plugins/standalone/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Let's Encrypt client.plugins.standalone."""
|
||||
4
letsencrypt/client/standalone_authenticator.py → letsencrypt/client/plugins/standalone/authenticator.py
Executable file → Normal file
4
letsencrypt/client/standalone_authenticator.py → letsencrypt/client/plugins/standalone/authenticator.py
Executable file → Normal file
@@ -33,7 +33,7 @@ class StandaloneAuthenticator(object):
|
||||
|
||||
description = "Standalone Authenticator"
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, unused_config):
|
||||
self.child_pid = None
|
||||
self.parent_pid = os.getpid()
|
||||
self.subproc_state = None
|
||||
@@ -410,5 +410,5 @@ class StandaloneAuthenticator(object):
|
||||
"on port 443 and perform DVSNI challenges. Once a certificate"
|
||||
"is attained, it will be saved in the "
|
||||
"(TODO) current working directory.{0}{0}"
|
||||
"Port 443 must be open in order to use the "
|
||||
"TCP port 443 must be available in order to use the "
|
||||
"Standalone Authenticator.".format(os.linesep))
|
||||
1
letsencrypt/client/plugins/standalone/tests/__init__.py
Normal file
1
letsencrypt/client/plugins/standalone/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Let's Encrypt Standalone Tests"""
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for letsencrypt.client.standalone_authenticator."""
|
||||
"""Tests for letsencrypt.client.plugins.standalone.authenticator."""
|
||||
import os
|
||||
import pkg_resources
|
||||
import psutil
|
||||
@@ -49,9 +49,9 @@ class CallableExhausted(Exception):
|
||||
class ChallPrefTest(unittest.TestCase):
|
||||
"""Tests for chall_pref() method."""
|
||||
def setUp(self):
|
||||
from letsencrypt.client.standalone_authenticator import \
|
||||
from letsencrypt.client.plugins.standalone.authenticator import \
|
||||
StandaloneAuthenticator
|
||||
self.authenticator = StandaloneAuthenticator()
|
||||
self.authenticator = StandaloneAuthenticator(None)
|
||||
|
||||
def test_chall_pref(self):
|
||||
self.assertEqual(self.authenticator.get_chall_pref("example.com"),
|
||||
@@ -61,11 +61,11 @@ class ChallPrefTest(unittest.TestCase):
|
||||
class SNICallbackTest(unittest.TestCase):
|
||||
"""Tests for sni_callback() method."""
|
||||
def setUp(self):
|
||||
from letsencrypt.client.standalone_authenticator import \
|
||||
from letsencrypt.client.plugins.standalone.authenticator import \
|
||||
StandaloneAuthenticator
|
||||
self.authenticator = StandaloneAuthenticator()
|
||||
self.authenticator = StandaloneAuthenticator(None)
|
||||
test_key = pkg_resources.resource_string(
|
||||
__name__, "testdata/rsa256_key.pem")
|
||||
"letsencrypt.client.tests", "testdata/rsa256_key.pem")
|
||||
key = le_util.Key("foo", test_key)
|
||||
self.cert = achallenges.DVSNI(
|
||||
chall=challenges.DVSNI(r="x"*32, nonce="abcdef"),
|
||||
@@ -104,9 +104,9 @@ class SNICallbackTest(unittest.TestCase):
|
||||
class ClientSignalHandlerTest(unittest.TestCase):
|
||||
"""Tests for client_signal_handler() method."""
|
||||
def setUp(self):
|
||||
from letsencrypt.client.standalone_authenticator import \
|
||||
from letsencrypt.client.plugins.standalone.authenticator import \
|
||||
StandaloneAuthenticator
|
||||
self.authenticator = StandaloneAuthenticator()
|
||||
self.authenticator = StandaloneAuthenticator(None)
|
||||
self.authenticator.tasks = {"foononce.acme.invalid": "stuff"}
|
||||
self.authenticator.child_pid = 12345
|
||||
|
||||
@@ -133,15 +133,15 @@ class ClientSignalHandlerTest(unittest.TestCase):
|
||||
class SubprocSignalHandlerTest(unittest.TestCase):
|
||||
"""Tests for subproc_signal_handler() method."""
|
||||
def setUp(self):
|
||||
from letsencrypt.client.standalone_authenticator import \
|
||||
from letsencrypt.client.plugins.standalone.authenticator import \
|
||||
StandaloneAuthenticator
|
||||
self.authenticator = StandaloneAuthenticator()
|
||||
self.authenticator = StandaloneAuthenticator(None)
|
||||
self.authenticator.tasks = {"foononce.acme.invalid": "stuff"}
|
||||
self.authenticator.child_pid = 12345
|
||||
self.authenticator.parent_pid = 23456
|
||||
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.os.kill")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.sys.exit")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.os.kill")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.sys.exit")
|
||||
def test_subproc_signal_handler(self, mock_exit, mock_kill):
|
||||
self.authenticator.ssl_conn = mock.MagicMock()
|
||||
self.authenticator.connection = mock.MagicMock()
|
||||
@@ -155,8 +155,8 @@ class SubprocSignalHandlerTest(unittest.TestCase):
|
||||
self.authenticator.parent_pid, signal.SIGUSR1)
|
||||
mock_exit.assert_called_once_with(0)
|
||||
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.os.kill")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.sys.exit")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.os.kill")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.sys.exit")
|
||||
def test_subproc_signal_handler_trouble(self, mock_exit, mock_kill):
|
||||
"""Test attempting to shut down a non-existent connection.
|
||||
|
||||
@@ -185,14 +185,15 @@ class SubprocSignalHandlerTest(unittest.TestCase):
|
||||
class AlreadyListeningTest(unittest.TestCase):
|
||||
"""Tests for already_listening() method."""
|
||||
def setUp(self):
|
||||
from letsencrypt.client.standalone_authenticator import \
|
||||
from letsencrypt.client.plugins.standalone.authenticator import \
|
||||
StandaloneAuthenticator
|
||||
self.authenticator = StandaloneAuthenticator()
|
||||
self.authenticator = StandaloneAuthenticator(None)
|
||||
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.psutil."
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.psutil."
|
||||
"net_connections")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.psutil.Process")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator."
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"psutil.Process")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"zope.component.getUtility")
|
||||
def test_race_condition(self, mock_get_utility, mock_process, mock_net):
|
||||
# This tests a race condition, or permission problem, or OS
|
||||
@@ -200,14 +201,14 @@ class AlreadyListeningTest(unittest.TestCase):
|
||||
# found to match the identified listening PID.
|
||||
from psutil._common import sconn
|
||||
conns = [
|
||||
sconn(fd=-1, family=2, type=1, laddr=('0.0.0.0', 30),
|
||||
raddr=(), status='LISTEN', pid=None),
|
||||
sconn(fd=3, family=2, type=1, laddr=('192.168.5.10', 32783),
|
||||
raddr=('20.40.60.80', 22), status='ESTABLISHED', pid=1234),
|
||||
sconn(fd=-1, family=10, type=1, laddr=('::1', 54321),
|
||||
raddr=('::1', 111), status='CLOSE_WAIT', pid=None),
|
||||
sconn(fd=3, family=2, type=1, laddr=('0.0.0.0', 17),
|
||||
raddr=(), status='LISTEN', pid=4416)]
|
||||
sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30),
|
||||
raddr=(), status="LISTEN", pid=None),
|
||||
sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783),
|
||||
raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234),
|
||||
sconn(fd=-1, family=10, type=1, laddr=("::1", 54321),
|
||||
raddr=("::1", 111), status="CLOSE_WAIT", pid=None),
|
||||
sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17),
|
||||
raddr=(), status="LISTEN", pid=4416)]
|
||||
mock_net.return_value = conns
|
||||
mock_process.side_effect = psutil.NoSuchProcess("No such PID")
|
||||
# We simulate being unable to find the process name of PID 4416,
|
||||
@@ -216,42 +217,44 @@ class AlreadyListeningTest(unittest.TestCase):
|
||||
self.assertEqual(mock_get_utility.generic_notification.call_count, 0)
|
||||
mock_process.assert_called_once_with(4416)
|
||||
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.psutil."
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.psutil."
|
||||
"net_connections")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.psutil.Process")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator."
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"psutil.Process")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"zope.component.getUtility")
|
||||
def test_not_listening(self, mock_get_utility, mock_process, mock_net):
|
||||
from psutil._common import sconn
|
||||
conns = [
|
||||
sconn(fd=-1, family=2, type=1, laddr=('0.0.0.0', 30),
|
||||
raddr=(), status='LISTEN', pid=None),
|
||||
sconn(fd=3, family=2, type=1, laddr=('192.168.5.10', 32783),
|
||||
raddr=('20.40.60.80', 22), status='ESTABLISHED', pid=1234),
|
||||
sconn(fd=-1, family=10, type=1, laddr=('::1', 54321),
|
||||
raddr=('::1', 111), status='CLOSE_WAIT', pid=None)]
|
||||
sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30),
|
||||
raddr=(), status="LISTEN", pid=None),
|
||||
sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783),
|
||||
raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234),
|
||||
sconn(fd=-1, family=10, type=1, laddr=("::1", 54321),
|
||||
raddr=("::1", 111), status="CLOSE_WAIT", pid=None)]
|
||||
mock_net.return_value = conns
|
||||
mock_process.name.return_value = "inetd"
|
||||
self.assertFalse(self.authenticator.already_listening(17))
|
||||
self.assertEqual(mock_get_utility.generic_notification.call_count, 0)
|
||||
self.assertEqual(mock_process.call_count, 0)
|
||||
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.psutil."
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.psutil."
|
||||
"net_connections")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.psutil.Process")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator."
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"psutil.Process")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"zope.component.getUtility")
|
||||
def test_listening_ipv4(self, mock_get_utility, mock_process, mock_net):
|
||||
from psutil._common import sconn
|
||||
conns = [
|
||||
sconn(fd=-1, family=2, type=1, laddr=('0.0.0.0', 30),
|
||||
raddr=(), status='LISTEN', pid=None),
|
||||
sconn(fd=3, family=2, type=1, laddr=('192.168.5.10', 32783),
|
||||
raddr=('20.40.60.80', 22), status='ESTABLISHED', pid=1234),
|
||||
sconn(fd=-1, family=10, type=1, laddr=('::1', 54321),
|
||||
raddr=('::1', 111), status='CLOSE_WAIT', pid=None),
|
||||
sconn(fd=3, family=2, type=1, laddr=('0.0.0.0', 17),
|
||||
raddr=(), status='LISTEN', pid=4416)]
|
||||
sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30),
|
||||
raddr=(), status="LISTEN", pid=None),
|
||||
sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783),
|
||||
raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234),
|
||||
sconn(fd=-1, family=10, type=1, laddr=("::1", 54321),
|
||||
raddr=("::1", 111), status="CLOSE_WAIT", pid=None),
|
||||
sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17),
|
||||
raddr=(), status="LISTEN", pid=4416)]
|
||||
mock_net.return_value = conns
|
||||
mock_process.name.return_value = "inetd"
|
||||
result = self.authenticator.already_listening(17)
|
||||
@@ -259,24 +262,25 @@ class AlreadyListeningTest(unittest.TestCase):
|
||||
self.assertEqual(mock_get_utility.call_count, 1)
|
||||
mock_process.assert_called_once_with(4416)
|
||||
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.psutil."
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.psutil."
|
||||
"net_connections")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.psutil.Process")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator."
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"psutil.Process")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"zope.component.getUtility")
|
||||
def test_listening_ipv6(self, mock_get_utility, mock_process, mock_net):
|
||||
from psutil._common import sconn
|
||||
conns = [
|
||||
sconn(fd=-1, family=2, type=1, laddr=('0.0.0.0', 30),
|
||||
raddr=(), status='LISTEN', pid=None),
|
||||
sconn(fd=3, family=2, type=1, laddr=('192.168.5.10', 32783),
|
||||
raddr=('20.40.60.80', 22), status='ESTABLISHED', pid=1234),
|
||||
sconn(fd=-1, family=10, type=1, laddr=('::1', 54321),
|
||||
raddr=('::1', 111), status='CLOSE_WAIT', pid=None),
|
||||
sconn(fd=3, family=10, type=1, laddr=('::', 12345), raddr=(),
|
||||
status='LISTEN', pid=4420),
|
||||
sconn(fd=3, family=2, type=1, laddr=('0.0.0.0', 17),
|
||||
raddr=(), status='LISTEN', pid=4416)]
|
||||
sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30),
|
||||
raddr=(), status="LISTEN", pid=None),
|
||||
sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783),
|
||||
raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234),
|
||||
sconn(fd=-1, family=10, type=1, laddr=("::1", 54321),
|
||||
raddr=("::1", 111), status="CLOSE_WAIT", pid=None),
|
||||
sconn(fd=3, family=10, type=1, laddr=("::", 12345), raddr=(),
|
||||
status="LISTEN", pid=4420),
|
||||
sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17),
|
||||
raddr=(), status="LISTEN", pid=4416)]
|
||||
mock_net.return_value = conns
|
||||
mock_process.name.return_value = "inetd"
|
||||
result = self.authenticator.already_listening(12345)
|
||||
@@ -288,12 +292,12 @@ class AlreadyListeningTest(unittest.TestCase):
|
||||
class PerformTest(unittest.TestCase):
|
||||
"""Tests for perform() method."""
|
||||
def setUp(self):
|
||||
from letsencrypt.client.standalone_authenticator import \
|
||||
from letsencrypt.client.plugins.standalone.authenticator import \
|
||||
StandaloneAuthenticator
|
||||
self.authenticator = StandaloneAuthenticator()
|
||||
self.authenticator = StandaloneAuthenticator(None)
|
||||
|
||||
test_key = pkg_resources.resource_string(
|
||||
__name__, "testdata/rsa256_key.pem")
|
||||
"letsencrypt.client.tests", "testdata/rsa256_key.pem")
|
||||
self.key = le_util.Key("something", test_key)
|
||||
|
||||
self.achall1 = achallenges.DVSNI(
|
||||
@@ -365,13 +369,13 @@ class PerformTest(unittest.TestCase):
|
||||
class StartListenerTest(unittest.TestCase):
|
||||
"""Tests for start_listener() method."""
|
||||
def setUp(self):
|
||||
from letsencrypt.client.standalone_authenticator import \
|
||||
from letsencrypt.client.plugins.standalone.authenticator import \
|
||||
StandaloneAuthenticator
|
||||
self.authenticator = StandaloneAuthenticator()
|
||||
self.authenticator = StandaloneAuthenticator(None)
|
||||
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator."
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"Crypto.Random.atfork")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.os.fork")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.os.fork")
|
||||
def test_start_listener_fork_parent(self, mock_fork, mock_atfork):
|
||||
self.authenticator.do_parent_process = mock.Mock()
|
||||
self.authenticator.do_parent_process.return_value = True
|
||||
@@ -384,9 +388,9 @@ class StartListenerTest(unittest.TestCase):
|
||||
self.authenticator.do_parent_process.assert_called_once_with(1717)
|
||||
mock_atfork.assert_called_once_with()
|
||||
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator."
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"Crypto.Random.atfork")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.os.fork")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.os.fork")
|
||||
def test_start_listener_fork_child(self, mock_fork, mock_atfork):
|
||||
self.authenticator.do_parent_process = mock.Mock()
|
||||
self.authenticator.do_child_process = mock.Mock()
|
||||
@@ -400,12 +404,13 @@ class StartListenerTest(unittest.TestCase):
|
||||
class DoParentProcessTest(unittest.TestCase):
|
||||
"""Tests for do_parent_process() method."""
|
||||
def setUp(self):
|
||||
from letsencrypt.client.standalone_authenticator import \
|
||||
from letsencrypt.client.plugins.standalone.authenticator import \
|
||||
StandaloneAuthenticator
|
||||
self.authenticator = StandaloneAuthenticator()
|
||||
self.authenticator = StandaloneAuthenticator(None)
|
||||
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.signal.signal")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator."
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"signal.signal")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"zope.component.getUtility")
|
||||
def test_do_parent_process_ok(self, mock_get_utility, mock_signal):
|
||||
self.authenticator.subproc_state = "ready"
|
||||
@@ -414,8 +419,9 @@ class DoParentProcessTest(unittest.TestCase):
|
||||
self.assertEqual(mock_get_utility.call_count, 1)
|
||||
self.assertEqual(mock_signal.call_count, 3)
|
||||
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.signal.signal")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator."
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"signal.signal")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"zope.component.getUtility")
|
||||
def test_do_parent_process_inuse(self, mock_get_utility, mock_signal):
|
||||
self.authenticator.subproc_state = "inuse"
|
||||
@@ -424,8 +430,9 @@ class DoParentProcessTest(unittest.TestCase):
|
||||
self.assertEqual(mock_get_utility.call_count, 1)
|
||||
self.assertEqual(mock_signal.call_count, 3)
|
||||
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.signal.signal")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator."
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"signal.signal")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"zope.component.getUtility")
|
||||
def test_do_parent_process_cantbind(self, mock_get_utility, mock_signal):
|
||||
self.authenticator.subproc_state = "cantbind"
|
||||
@@ -434,8 +441,9 @@ class DoParentProcessTest(unittest.TestCase):
|
||||
self.assertEqual(mock_get_utility.call_count, 1)
|
||||
self.assertEqual(mock_signal.call_count, 3)
|
||||
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.signal.signal")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator."
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"signal.signal")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"zope.component.getUtility")
|
||||
def test_do_parent_process_timeout(self, mock_get_utility, mock_signal):
|
||||
# Normally times out in 5 seconds and returns False. We can
|
||||
@@ -450,11 +458,11 @@ class DoParentProcessTest(unittest.TestCase):
|
||||
class DoChildProcessTest(unittest.TestCase):
|
||||
"""Tests for do_child_process() method."""
|
||||
def setUp(self):
|
||||
from letsencrypt.client.standalone_authenticator import \
|
||||
from letsencrypt.client.plugins.standalone.authenticator import \
|
||||
StandaloneAuthenticator
|
||||
self.authenticator = StandaloneAuthenticator()
|
||||
self.authenticator = StandaloneAuthenticator(None)
|
||||
test_key = pkg_resources.resource_string(
|
||||
__name__, "testdata/rsa256_key.pem")
|
||||
"letsencrypt.client.tests", "testdata/rsa256_key.pem")
|
||||
key = le_util.Key("foo", test_key)
|
||||
self.key = key
|
||||
self.cert = achallenges.DVSNI(
|
||||
@@ -466,9 +474,10 @@ class DoChildProcessTest(unittest.TestCase):
|
||||
self.authenticator.tasks = {"abcdef.acme.invalid": self.cert}
|
||||
self.authenticator.parent_pid = 12345
|
||||
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.socket.socket")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.os.kill")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.sys.exit")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"socket.socket")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.os.kill")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.sys.exit")
|
||||
def test_do_child_process_cantbind1(
|
||||
self, mock_exit, mock_kill, mock_socket):
|
||||
mock_exit.side_effect = IndentationError("subprocess would exit here")
|
||||
@@ -488,9 +497,10 @@ class DoChildProcessTest(unittest.TestCase):
|
||||
mock_exit.assert_called_once_with(1)
|
||||
mock_kill.assert_called_once_with(12345, signal.SIGUSR2)
|
||||
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.socket.socket")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.os.kill")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.sys.exit")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"socket.socket")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.os.kill")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.sys.exit")
|
||||
def test_do_child_process_cantbind2(self, mock_exit, mock_kill,
|
||||
mock_socket):
|
||||
mock_exit.side_effect = IndentationError("subprocess would exit here")
|
||||
@@ -504,7 +514,8 @@ class DoChildProcessTest(unittest.TestCase):
|
||||
mock_exit.assert_called_once_with(1)
|
||||
mock_kill.assert_called_once_with(12345, signal.SIGUSR1)
|
||||
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.socket.socket")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"socket.socket")
|
||||
def test_do_child_process_cantbind3(self, mock_socket):
|
||||
"""Test case where attempt to bind socket results in an unhandled
|
||||
socket error. (The expected behavior is arguably wrong because it
|
||||
@@ -517,10 +528,11 @@ class DoChildProcessTest(unittest.TestCase):
|
||||
self.assertRaises(
|
||||
socket.error, self.authenticator.do_child_process, 1717, self.key)
|
||||
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator."
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"OpenSSL.SSL.Connection")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.socket.socket")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.os.kill")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"socket.socket")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.os.kill")
|
||||
def test_do_child_process_success(
|
||||
self, mock_kill, mock_socket, mock_connection):
|
||||
sample_socket = mock.MagicMock()
|
||||
@@ -543,17 +555,18 @@ class DoChildProcessTest(unittest.TestCase):
|
||||
class CleanupTest(unittest.TestCase):
|
||||
"""Tests for cleanup() method."""
|
||||
def setUp(self):
|
||||
from letsencrypt.client.standalone_authenticator import \
|
||||
from letsencrypt.client.plugins.standalone.authenticator import \
|
||||
StandaloneAuthenticator
|
||||
self.authenticator = StandaloneAuthenticator()
|
||||
self.authenticator = StandaloneAuthenticator(None)
|
||||
self.achall = achallenges.DVSNI(
|
||||
chall=challenges.DVSNI(r="whee", nonce="foononce"),
|
||||
domain="foo.example.com", key="key")
|
||||
self.authenticator.tasks = {self.achall.nonce_domain: "stuff"}
|
||||
self.authenticator.child_pid = 12345
|
||||
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.os.kill")
|
||||
@mock.patch("letsencrypt.client.standalone_authenticator.time.sleep")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.os.kill")
|
||||
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
|
||||
"time.sleep")
|
||||
def test_cleanup(self, mock_sleep, mock_kill):
|
||||
mock_sleep.return_value = None
|
||||
mock_kill.return_value = None
|
||||
@@ -573,9 +586,9 @@ class CleanupTest(unittest.TestCase):
|
||||
class MoreInfoTest(unittest.TestCase):
|
||||
"""Tests for more_info() method. (trivially)"""
|
||||
def setUp(self):
|
||||
from letsencrypt.client.standalone_authenticator import (
|
||||
from letsencrypt.client.plugins.standalone.authenticator import (
|
||||
StandaloneAuthenticator)
|
||||
self.authenticator = StandaloneAuthenticator()
|
||||
self.authenticator = StandaloneAuthenticator(None)
|
||||
|
||||
def test_more_info(self):
|
||||
"""Make sure exceptions aren't raised."""
|
||||
@@ -585,9 +598,9 @@ class MoreInfoTest(unittest.TestCase):
|
||||
class InitTest(unittest.TestCase):
|
||||
"""Tests for more_info() method. (trivially)"""
|
||||
def setUp(self):
|
||||
from letsencrypt.client.standalone_authenticator import (
|
||||
from letsencrypt.client.plugins.standalone.authenticator import (
|
||||
StandaloneAuthenticator)
|
||||
self.authenticator = StandaloneAuthenticator()
|
||||
self.authenticator = StandaloneAuthenticator(None)
|
||||
|
||||
def test_prepare(self):
|
||||
"""Make sure exceptions aren't raised.
|
||||
@@ -83,7 +83,8 @@ class Reverter(object):
|
||||
def view_config_changes(self):
|
||||
"""Displays all saved checkpoints.
|
||||
|
||||
All checkpoints are printed to the console.
|
||||
All checkpoints are printed by
|
||||
:meth:`letsencrypt.client.interfaces.IDisplay.notification`.
|
||||
|
||||
.. todo:: Decide on a policy for error handling, OSError IOError...
|
||||
|
||||
@@ -130,17 +131,17 @@ class Reverter(object):
|
||||
os.linesep.join(output), display_util.HEIGHT)
|
||||
|
||||
def add_to_temp_checkpoint(self, save_files, save_notes):
|
||||
"""Add files to temporary checkpoint
|
||||
"""Add files to temporary checkpoint.
|
||||
|
||||
param set save_files: set of filepaths to save
|
||||
param str save_notes: notes about changes during the save
|
||||
:param set save_files: set of filepaths to save
|
||||
:param str save_notes: notes about changes during the save
|
||||
|
||||
"""
|
||||
self._add_to_checkpoint_dir(
|
||||
self.config.temp_checkpoint_dir, save_files, save_notes)
|
||||
|
||||
def add_to_checkpoint(self, save_files, save_notes):
|
||||
"""Add files to a permanent checkpoint
|
||||
"""Add files to a permanent checkpoint.
|
||||
|
||||
:param set save_files: set of filepaths to save
|
||||
:param str save_notes: notes about changes during the save
|
||||
@@ -324,15 +325,18 @@ class Reverter(object):
|
||||
new_fd.close()
|
||||
|
||||
def recovery_routine(self):
|
||||
"""Revert all previously modified files.
|
||||
"""Revert configuration to most recent finalized checkpoint.
|
||||
|
||||
First, any changes found in IConfig.temp_checkpoint_dir are removed,
|
||||
then IN_PROGRESS changes are removed The order is important.
|
||||
IN_PROGRESS is unable to add files that are already added by a TEMP
|
||||
change. Thus TEMP must be rolled back first because that will be the
|
||||
'latest' occurrence of the file.
|
||||
Remove all changes (temporary and permanent) that have not been
|
||||
finalized. This is useful to protect against crashes and other
|
||||
execution interruptions.
|
||||
|
||||
"""
|
||||
# First, any changes found in IConfig.temp_checkpoint_dir are removed,
|
||||
# then IN_PROGRESS changes are removed The order is important.
|
||||
# IN_PROGRESS is unable to add files that are already added by a TEMP
|
||||
# change. Thus TEMP must be rolled back first because that will be the
|
||||
# 'latest' occurrence of the file.
|
||||
self.revert_temporary_config()
|
||||
if os.path.isdir(self.config.in_progress_dir):
|
||||
try:
|
||||
@@ -385,11 +389,10 @@ class Reverter(object):
|
||||
return True
|
||||
|
||||
def finalize_checkpoint(self, title):
|
||||
"""Move IN_PROGRESS checkpoint to timestamped checkpoint.
|
||||
"""Finalize the checkpoint.
|
||||
|
||||
Adds title to self.config.in_progress_dir CHANGES_SINCE
|
||||
Move self.config.in_progress_dir to Backups directory and
|
||||
rename the directory as a timestamp
|
||||
Timestamps and permanently saves all changes made through the use
|
||||
of :func:`~add_to_checkpoint` and :func:`~register_file_creation`
|
||||
|
||||
:param str title: Title describing checkpoint
|
||||
|
||||
@@ -397,6 +400,9 @@ class Reverter(object):
|
||||
checkpoint is not able to be finalized.
|
||||
|
||||
"""
|
||||
# Adds title to self.config.in_progress_dir CHANGES_SINCE
|
||||
# Move self.config.in_progress_dir to Backups directory and
|
||||
# rename the directory as a timestamp
|
||||
# Check to make sure an "in progress" directory exists
|
||||
if not os.path.isdir(self.config.in_progress_dir):
|
||||
return
|
||||
|
||||
@@ -8,8 +8,10 @@ from letsencrypt.acme import challenges
|
||||
from letsencrypt.acme import jose
|
||||
|
||||
|
||||
KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string(
|
||||
"letsencrypt.client.tests", os.path.join("testdata", "rsa256_key.pem")))
|
||||
KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
|
||||
pkg_resources.resource_string(
|
||||
"letsencrypt.client.tests",
|
||||
os.path.join("testdata", "rsa256_key.pem"))))
|
||||
|
||||
# Challenges
|
||||
SIMPLE_HTTPS = challenges.SimpleHTTPS(
|
||||
@@ -27,40 +29,40 @@ POP = challenges.ProofOfPossession(
|
||||
alg="RS256", nonce="xD\xf9\xb9\xdbU\xed\xaa\x17\xf1y|\x81\x88\x99 ",
|
||||
hints=challenges.ProofOfPossession.Hints(
|
||||
jwk=jose.JWKRSA(key=KEY.publickey()),
|
||||
cert_fingerprints=[
|
||||
cert_fingerprints=(
|
||||
"93416768eb85e33adc4277f4c9acd63e7418fcfe",
|
||||
"16d95b7b63f1972b980b14c20291f3c0d1855d95",
|
||||
"48b46570d9fc6358108af43ad1649484def0debf"
|
||||
],
|
||||
certs=[], # TODO
|
||||
subject_key_identifiers=["d0083162dcc4c8a23ecb8aecbd86120e56fd24e5"],
|
||||
serial_numbers=[34234239832, 23993939911, 17],
|
||||
issuers=[
|
||||
),
|
||||
certs=(), # TODO
|
||||
subject_key_identifiers=("d0083162dcc4c8a23ecb8aecbd86120e56fd24e5"),
|
||||
serial_numbers=(34234239832, 23993939911, 17),
|
||||
issuers=(
|
||||
"C=US, O=SuperT LLC, CN=SuperTrustworthy Public CA",
|
||||
"O=LessTrustworthy CA Inc, CN=LessTrustworthy But StillSecure",
|
||||
],
|
||||
authorized_for=["www.example.com", "example.net"],
|
||||
),
|
||||
authorized_for=("www.example.com", "example.net"),
|
||||
)
|
||||
)
|
||||
|
||||
CHALLENGES = [SIMPLE_HTTPS, DVSNI, DNS, RECOVERY_CONTACT, RECOVERY_TOKEN, POP]
|
||||
DV_CHALLENGES = [chall for chall in CHALLENGES
|
||||
if isinstance(chall, challenges.DVChallenge)]
|
||||
CLIENT_CHALLENGES = [chall for chall in CHALLENGES
|
||||
if isinstance(chall, challenges.ClientChallenge)]
|
||||
CONT_CHALLENGES = [chall for chall in CHALLENGES
|
||||
if isinstance(chall, challenges.ContinuityChallenge)]
|
||||
|
||||
|
||||
def gen_combos(challs):
|
||||
"""Generate natural combinations for challs."""
|
||||
dv_chall = []
|
||||
renewal_chall = []
|
||||
cont_chall = []
|
||||
|
||||
for i, chall in enumerate(challs): # pylint: disable=redefined-outer-name
|
||||
if isinstance(chall, challenges.DVChallenge):
|
||||
dv_chall.append(i)
|
||||
else:
|
||||
renewal_chall.append(i)
|
||||
cont_chall.append(i)
|
||||
|
||||
# Gen combos for 1 of each type
|
||||
return [[i, j] for i in xrange(len(dv_chall))
|
||||
for j in xrange(len(renewal_chall))]
|
||||
# Gen combos for 1 of each type, lowest index first (makes testing easier)
|
||||
return tuple((i, j) if i < j else (j, i)
|
||||
for i in dv_chall for j in cont_chall)
|
||||
|
||||
@@ -30,17 +30,17 @@ class SatisfyChallengesTest(unittest.TestCase):
|
||||
from letsencrypt.client.auth_handler import AuthHandler
|
||||
|
||||
self.mock_dv_auth = mock.MagicMock(name="ApacheConfigurator")
|
||||
self.mock_client_auth = mock.MagicMock(name="ClientAuthenticator")
|
||||
self.mock_cont_auth = mock.MagicMock(name="ContinuityAuthenticator")
|
||||
|
||||
self.mock_dv_auth.get_chall_pref.return_value = [challenges.DVSNI]
|
||||
self.mock_client_auth.get_chall_pref.return_value = [
|
||||
self.mock_cont_auth.get_chall_pref.return_value = [
|
||||
challenges.RecoveryToken]
|
||||
|
||||
self.mock_client_auth.perform.side_effect = gen_auth_resp
|
||||
self.mock_cont_auth.perform.side_effect = gen_auth_resp
|
||||
self.mock_dv_auth.perform.side_effect = gen_auth_resp
|
||||
|
||||
self.handler = AuthHandler(
|
||||
self.mock_dv_auth, self.mock_client_auth, None)
|
||||
self.mock_dv_auth, self.mock_cont_auth, None)
|
||||
|
||||
logging.disable(logging.CRITICAL)
|
||||
|
||||
@@ -61,9 +61,9 @@ class SatisfyChallengesTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual("DVSNI0", self.handler.responses[dom][0])
|
||||
self.assertEqual(len(self.handler.dv_c), 1)
|
||||
self.assertEqual(len(self.handler.client_c), 1)
|
||||
self.assertEqual(len(self.handler.cont_c), 1)
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.client_c[dom]), 0)
|
||||
self.assertEqual(len(self.handler.cont_c[dom]), 0)
|
||||
|
||||
def test_name1_rectok1(self):
|
||||
dom = "0"
|
||||
@@ -78,16 +78,16 @@ class SatisfyChallengesTest(unittest.TestCase):
|
||||
self.assertEqual(len(self.handler.responses[dom]), 1)
|
||||
|
||||
# Test if statement for dv_auth perform
|
||||
self.assertEqual(self.mock_client_auth.perform.call_count, 1)
|
||||
self.assertEqual(self.mock_cont_auth.perform.call_count, 1)
|
||||
self.assertEqual(self.mock_dv_auth.perform.call_count, 0)
|
||||
|
||||
self.assertEqual("RecoveryToken0", self.handler.responses[dom][0])
|
||||
# Assert 1 domain
|
||||
self.assertEqual(len(self.handler.dv_c), 1)
|
||||
self.assertEqual(len(self.handler.client_c), 1)
|
||||
self.assertEqual(len(self.handler.cont_c), 1)
|
||||
# Assert 1 auth challenge, 0 dv
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 0)
|
||||
self.assertEqual(len(self.handler.client_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.cont_c[dom]), 1)
|
||||
|
||||
def test_name5_dvsni5(self):
|
||||
for i in xrange(5):
|
||||
@@ -102,11 +102,11 @@ class SatisfyChallengesTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(len(self.handler.responses), 5)
|
||||
self.assertEqual(len(self.handler.dv_c), 5)
|
||||
self.assertEqual(len(self.handler.client_c), 5)
|
||||
self.assertEqual(len(self.handler.cont_c), 5)
|
||||
# Each message contains 1 auth, 0 client
|
||||
|
||||
# Test proper call count for methods
|
||||
self.assertEqual(self.mock_client_auth.perform.call_count, 0)
|
||||
self.assertEqual(self.mock_cont_auth.perform.call_count, 0)
|
||||
self.assertEqual(self.mock_dv_auth.perform.call_count, 1)
|
||||
|
||||
for i in xrange(5):
|
||||
@@ -114,7 +114,7 @@ class SatisfyChallengesTest(unittest.TestCase):
|
||||
self.assertEqual(len(self.handler.responses[dom]), 1)
|
||||
self.assertEqual(self.handler.responses[dom][0], "DVSNI%d" % i)
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.client_c[dom]), 0)
|
||||
self.assertEqual(len(self.handler.cont_c[dom]), 0)
|
||||
self.assertTrue(isinstance(self.handler.dv_c[dom][0].achall,
|
||||
achallenges.DVSNI))
|
||||
|
||||
@@ -138,10 +138,10 @@ class SatisfyChallengesTest(unittest.TestCase):
|
||||
self.assertEqual(len(self.handler.responses[dom]),
|
||||
len(acme_util.DV_CHALLENGES))
|
||||
self.assertEqual(len(self.handler.dv_c), 1)
|
||||
self.assertEqual(len(self.handler.client_c), 1)
|
||||
self.assertEqual(len(self.handler.cont_c), 1)
|
||||
|
||||
# Test if statement for client_auth perform
|
||||
self.assertEqual(self.mock_client_auth.perform.call_count, 0)
|
||||
# Test if statement for cont_auth perform
|
||||
self.assertEqual(self.mock_cont_auth.perform.call_count, 0)
|
||||
self.assertEqual(self.mock_dv_auth.perform.call_count, 1)
|
||||
|
||||
self.assertEqual(
|
||||
@@ -149,7 +149,7 @@ class SatisfyChallengesTest(unittest.TestCase):
|
||||
self._get_exp_response(dom, path, acme_util.DV_CHALLENGES))
|
||||
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.client_c[dom]), 0)
|
||||
self.assertEqual(len(self.handler.cont_c[dom]), 0)
|
||||
self.assertTrue(isinstance(self.handler.dv_c[dom][0].achall,
|
||||
achallenges.SimpleHTTPS))
|
||||
|
||||
@@ -175,16 +175,16 @@ class SatisfyChallengesTest(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
len(self.handler.responses[dom]), len(acme_util.CHALLENGES))
|
||||
self.assertEqual(len(self.handler.dv_c), 1)
|
||||
self.assertEqual(len(self.handler.client_c), 1)
|
||||
self.assertEqual(len(self.handler.cont_c), 1)
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.client_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.cont_c[dom]), 1)
|
||||
|
||||
self.assertEqual(
|
||||
self.handler.responses[dom],
|
||||
self._get_exp_response(dom, path, acme_util.CHALLENGES))
|
||||
self.assertTrue(isinstance(self.handler.dv_c[dom][0].achall,
|
||||
achallenges.SimpleHTTPS))
|
||||
self.assertTrue(isinstance(self.handler.client_c[dom][0].achall,
|
||||
self.assertTrue(isinstance(self.handler.cont_c[dom][0].achall,
|
||||
achallenges.RecoveryToken))
|
||||
|
||||
@mock.patch("letsencrypt.client.auth_handler.gen_challenge_path")
|
||||
@@ -209,7 +209,7 @@ class SatisfyChallengesTest(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
len(self.handler.responses[str(i)]), len(acme_util.CHALLENGES))
|
||||
self.assertEqual(len(self.handler.dv_c), 5)
|
||||
self.assertEqual(len(self.handler.client_c), 5)
|
||||
self.assertEqual(len(self.handler.cont_c), 5)
|
||||
|
||||
for i in xrange(5):
|
||||
dom = str(i)
|
||||
@@ -217,11 +217,11 @@ class SatisfyChallengesTest(unittest.TestCase):
|
||||
self.handler.responses[dom],
|
||||
self._get_exp_response(dom, path, acme_util.CHALLENGES))
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.client_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.cont_c[dom]), 1)
|
||||
|
||||
self.assertTrue(isinstance(self.handler.dv_c[dom][0].achall,
|
||||
achallenges.DVSNI))
|
||||
self.assertTrue(isinstance(self.handler.client_c[dom][0].achall,
|
||||
self.assertTrue(isinstance(self.handler.cont_c[dom][0].achall,
|
||||
achallenges.RecoveryContact))
|
||||
|
||||
@mock.patch("letsencrypt.client.auth_handler.gen_challenge_path")
|
||||
@@ -255,7 +255,7 @@ class SatisfyChallengesTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(len(self.handler.responses), 5)
|
||||
self.assertEqual(len(self.handler.dv_c), 5)
|
||||
self.assertEqual(len(self.handler.client_c), 5)
|
||||
self.assertEqual(len(self.handler.cont_c), 5)
|
||||
|
||||
for i in xrange(5):
|
||||
dom = str(i)
|
||||
@@ -263,7 +263,7 @@ class SatisfyChallengesTest(unittest.TestCase):
|
||||
self.assertEqual(self.handler.responses[dom], resp)
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 1)
|
||||
self.assertEqual(
|
||||
len(self.handler.client_c[dom]), len(chosen_chall[i]) - 1)
|
||||
len(self.handler.cont_c[dom]), len(chosen_chall[i]) - 1)
|
||||
|
||||
self.assertTrue(isinstance(
|
||||
self.handler.dv_c["0"][0].achall, achallenges.DNS))
|
||||
@@ -276,10 +276,10 @@ class SatisfyChallengesTest(unittest.TestCase):
|
||||
self.assertTrue(isinstance(
|
||||
self.handler.dv_c["4"][0].achall, achallenges.DNS))
|
||||
|
||||
self.assertTrue(isinstance(self.handler.client_c["2"][0].achall,
|
||||
self.assertTrue(isinstance(self.handler.cont_c["2"][0].achall,
|
||||
achallenges.ProofOfPossession))
|
||||
self.assertTrue(isinstance(
|
||||
self.handler.client_c["4"][0].achall, achallenges.RecoveryToken))
|
||||
self.handler.cont_c["4"][0].achall, achallenges.RecoveryToken))
|
||||
|
||||
@mock.patch("letsencrypt.client.auth_handler.gen_challenge_path")
|
||||
def test_perform_exception_cleanup(self, mock_chall_path):
|
||||
@@ -309,11 +309,11 @@ class SatisfyChallengesTest(unittest.TestCase):
|
||||
|
||||
# Verify cleanup is actually run correctly
|
||||
self.assertEqual(self.mock_dv_auth.cleanup.call_count, 2)
|
||||
self.assertEqual(self.mock_client_auth.cleanup.call_count, 2)
|
||||
self.assertEqual(self.mock_cont_auth.cleanup.call_count, 2)
|
||||
|
||||
|
||||
dv_cleanup_args = self.mock_dv_auth.cleanup.call_args_list
|
||||
client_cleanup_args = self.mock_client_auth.cleanup.call_args_list
|
||||
cont_cleanup_args = self.mock_cont_auth.cleanup.call_args_list
|
||||
|
||||
# Check DV cleanup
|
||||
for i in xrange(2):
|
||||
@@ -325,10 +325,10 @@ class SatisfyChallengesTest(unittest.TestCase):
|
||||
|
||||
# Check Auth cleanup
|
||||
for i in xrange(2):
|
||||
client_chall_list = client_cleanup_args[i][0][0]
|
||||
self.assertEqual(len(client_chall_list), 1)
|
||||
cont_chall_list = cont_cleanup_args[i][0][0]
|
||||
self.assertEqual(len(cont_chall_list), 1)
|
||||
self.assertTrue(
|
||||
isinstance(client_chall_list[0], achallenges.ProofOfPossession))
|
||||
isinstance(cont_chall_list[0], achallenges.ProofOfPossession))
|
||||
|
||||
|
||||
def _get_exp_response(self, domain, path, challs):
|
||||
@@ -346,7 +346,7 @@ class GetAuthorizationsTest(unittest.TestCase):
|
||||
from letsencrypt.client.auth_handler import AuthHandler
|
||||
|
||||
self.mock_dv_auth = mock.MagicMock(name="ApacheConfigurator")
|
||||
self.mock_client_auth = mock.MagicMock(name="ClientAuthenticator")
|
||||
self.mock_cont_auth = mock.MagicMock(name="ContinuityAuthenticator")
|
||||
|
||||
self.mock_sat_chall = mock.MagicMock(name="_satisfy_challenges")
|
||||
self.mock_acme_auth = mock.MagicMock(name="acme_authorization")
|
||||
@@ -354,7 +354,7 @@ class GetAuthorizationsTest(unittest.TestCase):
|
||||
self.iteration = 0
|
||||
|
||||
self.handler = AuthHandler(
|
||||
self.mock_dv_auth, self.mock_client_auth, None)
|
||||
self.mock_dv_auth, self.mock_cont_auth, None)
|
||||
|
||||
self.handler._satisfy_challenges = self.mock_sat_chall
|
||||
self.handler.acme_authorization = self.mock_acme_auth
|
||||
@@ -388,7 +388,7 @@ class GetAuthorizationsTest(unittest.TestCase):
|
||||
# Assignment was > 80 char...
|
||||
dv_c, c_c = self.handler._challenge_factory(dom, [0])
|
||||
|
||||
self.handler.dv_c[dom], self.handler.client_c[dom] = dv_c, c_c
|
||||
self.handler.dv_c[dom], self.handler.cont_c[dom] = dv_c, c_c
|
||||
|
||||
def test_progress_failure(self):
|
||||
self.handler.add_chall_msg(
|
||||
@@ -414,7 +414,7 @@ class GetAuthorizationsTest(unittest.TestCase):
|
||||
self.handler.msgs[dom].challenges)
|
||||
dv_c, c_c = self.handler._challenge_factory(
|
||||
dom, self.handler.paths[dom])
|
||||
self.handler.dv_c[dom], self.handler.client_c[dom] = dv_c, c_c
|
||||
self.handler.dv_c[dom], self.handler.cont_c[dom] = dv_c, c_c
|
||||
|
||||
def test_incremental_progress(self):
|
||||
for dom, challs in [("0", acme_util.CHALLENGES),
|
||||
@@ -444,9 +444,9 @@ class GetAuthorizationsTest(unittest.TestCase):
|
||||
self.handler.paths["1"] = [2]
|
||||
# This is probably overkill... but set it anyway
|
||||
dv_c, c_c = self.handler._challenge_factory("0", [1, 3])
|
||||
self.handler.dv_c["0"], self.handler.client_c["0"] = dv_c, c_c
|
||||
self.handler.dv_c["0"], self.handler.cont_c["0"] = dv_c, c_c
|
||||
dv_c, c_c = self.handler._challenge_factory("1", [2])
|
||||
self.handler.dv_c["1"], self.handler.client_c["1"] = dv_c, c_c
|
||||
self.handler.dv_c["1"], self.handler.cont_c["1"] = dv_c, c_c
|
||||
|
||||
self.iteration += 1
|
||||
|
||||
@@ -481,7 +481,7 @@ class PathSatisfiedTest(unittest.TestCase):
|
||||
self.handler.responses[dom[0]] = [None, "sat", "sat2", None]
|
||||
|
||||
self.handler.paths[dom[1]] = [0]
|
||||
self.handler.responses[dom[1]] = ["sat", None, None, None]
|
||||
self.handler.responses[dom[1]] = ["sat", None, None, False]
|
||||
|
||||
self.handler.paths[dom[2]] = [0]
|
||||
self.handler.responses[dom[2]] = ["sat"]
|
||||
@@ -496,7 +496,7 @@ class PathSatisfiedTest(unittest.TestCase):
|
||||
self.assertTrue(self.handler._path_satisfied(dom[i]))
|
||||
|
||||
def test_not_satisfied(self):
|
||||
dom = ["0", "1", "2"]
|
||||
dom = ["0", "1", "2", "3"]
|
||||
self.handler.paths[dom[0]] = [1, 2]
|
||||
self.handler.responses[dom[0]] = ["sat1", None, "sat2", None]
|
||||
|
||||
@@ -506,10 +506,84 @@ class PathSatisfiedTest(unittest.TestCase):
|
||||
self.handler.paths[dom[2]] = [0]
|
||||
self.handler.responses[dom[2]] = [None]
|
||||
|
||||
self.handler.paths[dom[3]] = [0]
|
||||
self.handler.responses[dom[3]] = [False]
|
||||
|
||||
for i in xrange(3):
|
||||
self.assertFalse(self.handler._path_satisfied(dom[i]))
|
||||
|
||||
|
||||
class GenChallengePathTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.client.auth_handler.gen_challenge_path.
|
||||
|
||||
.. todo:: Add more tests for dumb_path... depending on what we want to do.
|
||||
|
||||
"""
|
||||
def setUp(self):
|
||||
logging.disable(logging.fatal)
|
||||
|
||||
def tearDown(self):
|
||||
logging.disable(logging.NOTSET)
|
||||
|
||||
@classmethod
|
||||
def _call(cls, challs, preferences, combinations):
|
||||
from letsencrypt.client.auth_handler import gen_challenge_path
|
||||
return gen_challenge_path(challs, preferences, combinations)
|
||||
|
||||
def test_common_case(self):
|
||||
"""Given DVSNI and SimpleHTTPS with appropriate combos."""
|
||||
challs = (acme_util.DVSNI, acme_util.SIMPLE_HTTPS)
|
||||
prefs = [challenges.DVSNI]
|
||||
combos = ((0,), (1,))
|
||||
|
||||
# Smart then trivial dumb path test
|
||||
self.assertEqual(self._call(challs, prefs, combos), (0,))
|
||||
self.assertTrue(self._call(challs, prefs, None))
|
||||
# Rearrange order...
|
||||
self.assertEqual(self._call(challs[::-1], prefs, combos), (1,))
|
||||
self.assertTrue(self._call(challs[::-1], prefs, None))
|
||||
|
||||
def test_common_case_with_continuity(self):
|
||||
challs = (acme_util.RECOVERY_TOKEN,
|
||||
acme_util.RECOVERY_CONTACT,
|
||||
acme_util.DVSNI,
|
||||
acme_util.SIMPLE_HTTPS)
|
||||
prefs = [challenges.RecoveryToken, challenges.DVSNI]
|
||||
combos = acme_util.gen_combos(challs)
|
||||
self.assertEqual(self._call(challs, prefs, combos), (0, 2))
|
||||
|
||||
# dumb_path() trivial test
|
||||
self.assertTrue(self._call(challs, prefs, None))
|
||||
|
||||
def test_full_cont_server(self):
|
||||
challs = (acme_util.RECOVERY_TOKEN,
|
||||
acme_util.RECOVERY_CONTACT,
|
||||
acme_util.POP,
|
||||
acme_util.DVSNI,
|
||||
acme_util.SIMPLE_HTTPS,
|
||||
acme_util.DNS)
|
||||
# Typical webserver client that can do everything except DNS
|
||||
# Attempted to make the order realistic
|
||||
prefs = [challenges.RecoveryToken,
|
||||
challenges.ProofOfPossession,
|
||||
challenges.SimpleHTTPS,
|
||||
challenges.DVSNI,
|
||||
challenges.RecoveryContact]
|
||||
combos = acme_util.gen_combos(challs)
|
||||
self.assertEqual(self._call(challs, prefs, combos), (0, 4))
|
||||
|
||||
# Dumb path trivial test
|
||||
self.assertTrue(self._call(challs, prefs, None))
|
||||
|
||||
def test_not_supported(self):
|
||||
challs = (acme_util.POP, acme_util.DVSNI)
|
||||
prefs = [challenges.DVSNI]
|
||||
combos = ((0, 1),)
|
||||
|
||||
self.assertRaises(errors.LetsEncryptAuthHandlerError,
|
||||
self._call, challs, prefs, combos)
|
||||
|
||||
|
||||
class MutuallyExclusiveTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.client.auth_handler.mutually_exclusive."""
|
||||
|
||||
|
||||
@@ -8,8 +8,9 @@ from letsencrypt.client import errors
|
||||
|
||||
class DetermineAuthenticatorTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from letsencrypt.client.apache.configurator import ApacheConfigurator
|
||||
from letsencrypt.client.standalone_authenticator import (
|
||||
from letsencrypt.client.plugins.apache.configurator import (
|
||||
ApacheConfigurator)
|
||||
from letsencrypt.client.plugins.standalone.authenticator import (
|
||||
StandaloneAuthenticator)
|
||||
|
||||
self.mock_stand = mock.MagicMock(
|
||||
@@ -65,7 +66,8 @@ class DetermineAuthenticatorTest(unittest.TestCase):
|
||||
class RollbackTest(unittest.TestCase):
|
||||
"""Test the rollback function."""
|
||||
def setUp(self):
|
||||
from letsencrypt.client.apache.configurator import ApacheConfigurator
|
||||
from letsencrypt.client.plugins.apache.configurator import (
|
||||
ApacheConfigurator)
|
||||
self.m_install = mock.MagicMock(spec=ApacheConfigurator)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Test the ClientAuthenticator dispatcher."""
|
||||
"""Test the ContinuityAuthenticator dispatcher."""
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
@@ -13,9 +13,9 @@ class PerformTest(unittest.TestCase):
|
||||
"""Test client perform function."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.client.client_authenticator import ClientAuthenticator
|
||||
from letsencrypt.client.continuity_auth import ContinuityAuthenticator
|
||||
|
||||
self.auth = ClientAuthenticator(
|
||||
self.auth = ContinuityAuthenticator(
|
||||
mock.MagicMock(server="demo_server.org"))
|
||||
self.auth.rec_token.perform = mock.MagicMock(
|
||||
name="rec_token_perform", side_effect=gen_client_resp)
|
||||
@@ -38,7 +38,7 @@ class PerformTest(unittest.TestCase):
|
||||
|
||||
def test_unexpected(self):
|
||||
self.assertRaises(
|
||||
errors.LetsEncryptClientAuthError, self.auth.perform, [
|
||||
errors.LetsEncryptContAuthError, self.auth.perform, [
|
||||
achallenges.DVSNI(chall=None, domain="0", key="invalid_key")])
|
||||
|
||||
def test_chall_pref(self):
|
||||
@@ -50,9 +50,9 @@ class CleanupTest(unittest.TestCase):
|
||||
"""Test the Authenticator cleanup function."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.client.client_authenticator import ClientAuthenticator
|
||||
from letsencrypt.client.continuity_auth import ContinuityAuthenticator
|
||||
|
||||
self.auth = ClientAuthenticator(
|
||||
self.auth = ContinuityAuthenticator(
|
||||
mock.MagicMock(server="demo_server.org"))
|
||||
self.mock_cleanup = mock.MagicMock(name="rec_token_cleanup")
|
||||
self.auth.rec_token.cleanup = self.mock_cleanup
|
||||
@@ -70,7 +70,7 @@ class CleanupTest(unittest.TestCase):
|
||||
token = achallenges.RecoveryToken(chall=None, domain="0")
|
||||
unexpected = achallenges.DVSNI(chall=None, domain="0", key="dummy_key")
|
||||
|
||||
self.assertRaises(errors.LetsEncryptClientAuthError,
|
||||
self.assertRaises(errors.LetsEncryptContAuthError,
|
||||
self.auth.cleanup, [token, unexpected])
|
||||
|
||||
|
||||
458
letsencrypt/client/tests/network2_test.py
Normal file
458
letsencrypt/client/tests/network2_test.py
Normal file
@@ -0,0 +1,458 @@
|
||||
"""Tests for letsencrypt.client.network2."""
|
||||
import datetime
|
||||
import httplib
|
||||
import os
|
||||
import pkg_resources
|
||||
import unittest
|
||||
|
||||
import M2Crypto
|
||||
import mock
|
||||
import requests
|
||||
|
||||
from letsencrypt.client import errors
|
||||
|
||||
from letsencrypt.acme import challenges
|
||||
from letsencrypt.acme import jose
|
||||
from letsencrypt.acme import messages2
|
||||
|
||||
|
||||
CERT = jose.ComparableX509(M2Crypto.X509.load_cert_string(
|
||||
pkg_resources.resource_string(
|
||||
__name__, os.path.join('testdata/cert.pem'))))
|
||||
CERT2 = jose.ComparableX509(M2Crypto.X509.load_cert_string(
|
||||
pkg_resources.resource_string(
|
||||
__name__, os.path.join('testdata/cert-san.pem'))))
|
||||
CSR = jose.ComparableX509(M2Crypto.X509.load_request_string(
|
||||
pkg_resources.resource_string(
|
||||
__name__, os.path.join('testdata/csr.pem'))))
|
||||
KEY = jose.JWKRSA.load(pkg_resources.resource_string(
|
||||
__name__, os.path.join('testdata/rsa512_key.pem')))
|
||||
KEY2 = jose.JWKRSA.load(pkg_resources.resource_string(
|
||||
__name__, os.path.join('testdata/rsa256_key.pem')))
|
||||
|
||||
|
||||
class NetworkTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.client.network2.Network."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes,too-many-public-methods
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.client.network2 import Network
|
||||
self.net = Network(
|
||||
new_reg_uri='https://www.letsencrypt-demo.org/acme/new-reg',
|
||||
key=KEY, alg=jose.RS256)
|
||||
self.response = mock.MagicMock(ok=True, status_code=httplib.OK)
|
||||
self.response.headers = {}
|
||||
self.response.links = {}
|
||||
|
||||
self.identifier = messages2.Identifier(
|
||||
typ=messages2.IDENTIFIER_FQDN, value='example.com')
|
||||
|
||||
# Registration
|
||||
self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212')
|
||||
reg = messages2.Registration(
|
||||
contact=self.contact, key=KEY.public(), recovery_token='t')
|
||||
self.regr = messages2.RegistrationResource(
|
||||
body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1',
|
||||
new_authzr_uri='https://www.letsencrypt-demo.org/acme/new-reg',
|
||||
terms_of_service='https://www.letsencrypt-demo.org/tos')
|
||||
|
||||
# Authorization
|
||||
authzr_uri = 'https://www.letsencrypt-demo.org/acme/authz/1'
|
||||
challb = messages2.ChallengeBody(
|
||||
uri=(authzr_uri + '/1'), status=messages2.STATUS_VALID,
|
||||
chall=challenges.DNS(token='foo'))
|
||||
self.challr = messages2.ChallengeResource(
|
||||
body=challb, authzr_uri=authzr_uri)
|
||||
self.authz = messages2.Authorization(
|
||||
identifier=messages2.Identifier(
|
||||
typ=messages2.IDENTIFIER_FQDN, value='example.com'),
|
||||
challenges=(challb,), combinations=None, key=KEY.public())
|
||||
self.authzr = messages2.AuthorizationResource(
|
||||
body=self.authz, uri=authzr_uri,
|
||||
new_cert_uri='https://www.letsencrypt-demo.org/acme/new-cert')
|
||||
|
||||
# Request issuance
|
||||
self.certr = messages2.CertificateResource(
|
||||
body=CERT, authzrs=(self.authzr,),
|
||||
uri='https://www.letsencrypt-demo.org/acme/cert/1',
|
||||
cert_chain_uri='https://www.letsencrypt-demo.org/ca')
|
||||
|
||||
def _mock_post_get(self):
|
||||
# pylint: disable=protected-access
|
||||
self.net._post = mock.MagicMock(return_value=self.response)
|
||||
self.net._get = mock.MagicMock(return_value=self.response)
|
||||
|
||||
def test_wrap_in_jws(self):
|
||||
class MockJSONDeSerializable(jose.JSONDeSerializable):
|
||||
# pylint: disable=missing-docstring
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
def to_json(self):
|
||||
return self.value
|
||||
@classmethod
|
||||
def from_json(cls, value):
|
||||
return cls(value)
|
||||
# pylint: disable=protected-access
|
||||
jws = self.net._wrap_in_jws(MockJSONDeSerializable('foo'))
|
||||
self.assertEqual(jose.JWS.json_loads(jws).payload, '"foo"')
|
||||
|
||||
def test_check_response_not_ok_jobj_no_error(self):
|
||||
self.response.ok = False
|
||||
self.response.json.return_value = {}
|
||||
# pylint: disable=protected-access
|
||||
self.assertRaises(
|
||||
errors.NetworkError, self.net._check_response, self.response)
|
||||
|
||||
def test_check_response_not_ok_jobj_error(self):
|
||||
self.response.ok = False
|
||||
self.response.json.return_value = messages2.Error(detail='foo')
|
||||
# pylint: disable=protected-access
|
||||
self.assertRaises(
|
||||
messages2.Error, self.net._check_response, self.response)
|
||||
|
||||
def test_check_response_not_ok_no_jobj(self):
|
||||
self.response.ok = False
|
||||
self.response.json.side_effect = ValueError
|
||||
# pylint: disable=protected-access
|
||||
self.assertRaises(
|
||||
errors.NetworkError, self.net._check_response, self.response)
|
||||
|
||||
def test_check_response_ok_no_jobj_ct_required(self):
|
||||
self.response.json.side_effect = ValueError
|
||||
for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']:
|
||||
self.response.headers['Content-Type'] = response_ct
|
||||
# pylint: disable=protected-access
|
||||
self.assertRaises(
|
||||
errors.NetworkError, self.net._check_response, self.response,
|
||||
content_type=self.net.JSON_CONTENT_TYPE)
|
||||
|
||||
def test_check_response_ok_no_jobj_no_ct(self):
|
||||
self.response.json.side_effect = ValueError
|
||||
for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']:
|
||||
self.response.headers['Content-Type'] = response_ct
|
||||
# pylint: disable=protected-access
|
||||
self.net._check_response(self.response)
|
||||
|
||||
def test_check_response_jobj(self):
|
||||
self.response.json.return_value = {}
|
||||
for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']:
|
||||
self.response.headers['Content-Type'] = response_ct
|
||||
# pylint: disable=protected-access
|
||||
self.net._check_response(self.response)
|
||||
|
||||
@mock.patch('letsencrypt.client.network2.requests')
|
||||
def test_get_requests_error_passthrough(self, requests_mock):
|
||||
requests_mock.exceptions = requests.exceptions
|
||||
requests_mock.get.side_effect = requests.exceptions.RequestException
|
||||
# pylint: disable=protected-access
|
||||
self.assertRaises(errors.NetworkError, self.net._get, 'uri')
|
||||
|
||||
@mock.patch('letsencrypt.client.network2.requests')
|
||||
def test_get(self, requests_mock):
|
||||
# pylint: disable=protected-access
|
||||
self.net._check_response = mock.MagicMock()
|
||||
self.net._get('uri', content_type='ct')
|
||||
self.net._check_response.assert_called_once_with(
|
||||
requests_mock.get('uri'), content_type='ct')
|
||||
|
||||
@mock.patch('letsencrypt.client.network2.requests')
|
||||
def test_post_requests_error_passthrough(self, requests_mock):
|
||||
requests_mock.exceptions = requests.exceptions
|
||||
requests_mock.post.side_effect = requests.exceptions.RequestException
|
||||
# pylint: disable=protected-access
|
||||
self.assertRaises(errors.NetworkError, self.net._post, 'uri', 'data')
|
||||
|
||||
@mock.patch('letsencrypt.client.network2.requests')
|
||||
def test_post(self, requests_mock):
|
||||
# pylint: disable=protected-access
|
||||
self.net._check_response = mock.MagicMock()
|
||||
self.net._post('uri', 'data', content_type='ct')
|
||||
self.net._check_response.assert_called_once_with(
|
||||
requests_mock.post('uri', 'data'), content_type='ct')
|
||||
|
||||
def test_register(self):
|
||||
self.response.status_code = httplib.CREATED
|
||||
self.response.json.return_value = self.regr.body.fully_serialize()
|
||||
self.response.headers['Location'] = self.regr.uri
|
||||
self.response.links.update({
|
||||
'next': {'url': self.regr.new_authzr_uri},
|
||||
'terms-of-service': {'url': self.regr.terms_of_service},
|
||||
})
|
||||
|
||||
self._mock_post_get()
|
||||
self.assertEqual(self.regr, self.net.register(self.contact))
|
||||
# TODO: test POST call arguments
|
||||
|
||||
# TODO: split here and separate test
|
||||
reg_wrong_key = self.regr.body.update(key=KEY2.public())
|
||||
self.response.json.return_value = reg_wrong_key.fully_serialize()
|
||||
self.assertRaises(
|
||||
errors.UnexpectedUpdate, self.net.register, self.contact)
|
||||
|
||||
def test_register_missing_next(self):
|
||||
self.response.status_code = httplib.CREATED
|
||||
self._mock_post_get()
|
||||
self.assertRaises(
|
||||
errors.NetworkError, self.net.register, self.regr.body)
|
||||
|
||||
def test_update_registration(self):
|
||||
self.response.headers['Location'] = self.regr.uri
|
||||
self.response.json.return_value = self.regr.body.fully_serialize()
|
||||
self._mock_post_get()
|
||||
self.assertEqual(self.regr, self.net.update_registration(self.regr))
|
||||
|
||||
# TODO: split here and separate test
|
||||
self.response.json.return_value = self.regr.body.update(
|
||||
contact=()).fully_serialize()
|
||||
self.assertRaises(
|
||||
errors.UnexpectedUpdate, self.net.update_registration, self.regr)
|
||||
|
||||
def test_request_challenges(self):
|
||||
self.response.status_code = httplib.CREATED
|
||||
self.response.headers['Location'] = self.authzr.uri
|
||||
self.response.json.return_value = self.authz.fully_serialize()
|
||||
self.response.links = {
|
||||
'next': {'url': self.authzr.new_cert_uri},
|
||||
}
|
||||
|
||||
self._mock_post_get()
|
||||
self.net.request_challenges(self.identifier, self.regr)
|
||||
# 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.fully_serialize()
|
||||
self.assertRaises(
|
||||
errors.UnexpectedUpdate, self.net.request_challenges,
|
||||
self.identifier, self.regr)
|
||||
|
||||
def test_request_challenges_missing_next(self):
|
||||
self.response.status_code = httplib.CREATED
|
||||
self._mock_post_get()
|
||||
self.assertRaises(
|
||||
errors.NetworkError, self.net.request_challenges,
|
||||
self.identifier, self.regr)
|
||||
|
||||
def test_request_domain_challenges(self):
|
||||
self.net.request_challenges = mock.MagicMock()
|
||||
self.assertEqual(
|
||||
self.net.request_challenges(self.identifier),
|
||||
self.net.request_domain_challenges('example.com', self.regr))
|
||||
|
||||
def test_answer_challenge(self):
|
||||
self.response.links['up'] = {'url': self.challr.authzr_uri}
|
||||
self.response.json.return_value = self.challr.body.fully_serialize()
|
||||
|
||||
chall_response = challenges.DNSResponse()
|
||||
|
||||
self._mock_post_get()
|
||||
self.net.answer_challenge(self.challr.body, chall_response)
|
||||
|
||||
# TODO: split here and separate test
|
||||
self.assertRaises(errors.UnexpectedUpdate, self.net.answer_challenge,
|
||||
self.challr.body.update(uri='foo'), chall_response)
|
||||
|
||||
def test_answer_challenge_missing_next(self):
|
||||
self._mock_post_get()
|
||||
self.assertRaises(errors.NetworkError, self.net.answer_challenge,
|
||||
self.challr.body, challenges.DNSResponse())
|
||||
|
||||
def test_answer_challenges(self):
|
||||
self.net.answer_challenge = mock.MagicMock()
|
||||
self.assertEqual(
|
||||
[self.net.answer_challenge(
|
||||
self.challr.body, challenges.DNSResponse())],
|
||||
self.net.answer_challenges(
|
||||
[self.challr.body], [challenges.DNSResponse()]))
|
||||
|
||||
def test_retry_after_date(self):
|
||||
self.response.headers['Retry-After'] = 'Fri, 31 Dec 1999 23:59:59 GMT'
|
||||
self.assertEqual(
|
||||
datetime.datetime(1999, 12, 31, 23, 59, 59),
|
||||
self.net.retry_after(response=self.response, default=10))
|
||||
|
||||
@mock.patch('letsencrypt.client.network2.datetime')
|
||||
def test_retry_after_invalid(self, dt_mock):
|
||||
dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)
|
||||
dt_mock.timedelta = datetime.timedelta
|
||||
|
||||
self.response.headers['Retry-After'] = 'foooo'
|
||||
self.assertEqual(
|
||||
datetime.datetime(2015, 3, 27, 0, 0, 10),
|
||||
self.net.retry_after(response=self.response, default=10))
|
||||
|
||||
@mock.patch('letsencrypt.client.network2.datetime')
|
||||
def test_retry_after_seconds(self, dt_mock):
|
||||
dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)
|
||||
dt_mock.timedelta = datetime.timedelta
|
||||
|
||||
self.response.headers['Retry-After'] = '50'
|
||||
self.assertEqual(
|
||||
datetime.datetime(2015, 3, 27, 0, 0, 50),
|
||||
self.net.retry_after(response=self.response, default=10))
|
||||
|
||||
@mock.patch('letsencrypt.client.network2.datetime')
|
||||
def test_retry_after_missing(self, dt_mock):
|
||||
dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)
|
||||
dt_mock.timedelta = datetime.timedelta
|
||||
|
||||
self.assertEqual(
|
||||
datetime.datetime(2015, 3, 27, 0, 0, 10),
|
||||
self.net.retry_after(response=self.response, default=10))
|
||||
|
||||
def test_poll(self):
|
||||
self.response.json.return_value = self.authzr.body.fully_serialize()
|
||||
self._mock_post_get()
|
||||
self.assertEqual((self.authzr, self.response),
|
||||
self.net.poll(self.authzr))
|
||||
|
||||
def test_request_issuance(self):
|
||||
self.response.content = CERT.as_der()
|
||||
self.response.headers['Location'] = self.certr.uri
|
||||
self.response.links['up'] = {'url': self.certr.cert_chain_uri}
|
||||
self._mock_post_get()
|
||||
self.assertEqual(
|
||||
self.certr, self.net.request_issuance(CSR, (self.authzr,)))
|
||||
# TODO: check POST args
|
||||
|
||||
def test_request_issuance_missing_up(self):
|
||||
self.response.content = CERT.as_der()
|
||||
self.response.headers['Location'] = self.certr.uri
|
||||
self._mock_post_get()
|
||||
self.assertEqual(
|
||||
self.certr.update(cert_chain_uri=None),
|
||||
self.net.request_issuance(CSR, (self.authzr,)))
|
||||
|
||||
def test_request_issuance_missing_location(self):
|
||||
self._mock_post_get()
|
||||
self.assertRaises(
|
||||
errors.NetworkError, self.net.request_issuance,
|
||||
CSR, (self.authzr,))
|
||||
|
||||
@mock.patch('letsencrypt.client.network2.datetime')
|
||||
@mock.patch('letsencrypt.client.network2.time')
|
||||
def test_poll_and_request_issuance(self, time_mock, dt_mock):
|
||||
# clock.dt | pylint: disable=no-member
|
||||
clock = mock.MagicMock(dt=datetime.datetime(2015, 3, 27))
|
||||
|
||||
def sleep(seconds):
|
||||
"""increment clock"""
|
||||
clock.dt += datetime.timedelta(seconds=seconds)
|
||||
time_mock.sleep.side_effect = sleep
|
||||
|
||||
def now():
|
||||
"""return current clock value"""
|
||||
return clock.dt
|
||||
dt_mock.datetime.now.side_effect = now
|
||||
dt_mock.timedelta = datetime.timedelta
|
||||
|
||||
def poll(authzr): # pylint: disable=missing-docstring
|
||||
# record poll start time based on the current clock value
|
||||
authzr.times.append(clock.dt)
|
||||
|
||||
# suppose it takes 2 seconds for server to produce the
|
||||
# result, increment clock
|
||||
clock.dt += datetime.timedelta(seconds=2)
|
||||
|
||||
if not authzr.retries: # no more retries
|
||||
done = mock.MagicMock(uri=authzr.uri, times=authzr.times)
|
||||
done.body.status = messages2.STATUS_VALID
|
||||
return done, []
|
||||
|
||||
# response (2nd result tuple element) is reduced to only
|
||||
# Retry-After header contents represented as integer
|
||||
# seconds; authzr.retries is a list of Retry-After
|
||||
# headers, head(retries) is peeled of as a current
|
||||
# Retry-After header, and tail(retries) is persisted for
|
||||
# later poll() calls
|
||||
return (mock.MagicMock(retries=authzr.retries[1:],
|
||||
uri=authzr.uri + '.', times=authzr.times),
|
||||
authzr.retries[0])
|
||||
self.net.poll = mock.MagicMock(side_effect=poll)
|
||||
|
||||
mintime = 7
|
||||
|
||||
def retry_after(response, default): # pylint: disable=missing-docstring
|
||||
# check that poll_and_request_issuance correctly passes mintime
|
||||
self.assertEqual(default, mintime)
|
||||
return clock.dt + datetime.timedelta(seconds=response)
|
||||
self.net.retry_after = mock.MagicMock(side_effect=retry_after)
|
||||
|
||||
def request_issuance(csr, authzrs): # pylint: disable=missing-docstring
|
||||
return csr, authzrs
|
||||
self.net.request_issuance = mock.MagicMock(side_effect=request_issuance)
|
||||
|
||||
csr = mock.MagicMock()
|
||||
authzrs = (
|
||||
mock.MagicMock(uri='a', times=[], retries=(8, 20, 30)),
|
||||
mock.MagicMock(uri='b', times=[], retries=(5,)),
|
||||
)
|
||||
|
||||
cert, updated_authzrs = self.net.poll_and_request_issuance(
|
||||
csr, authzrs, mintime=mintime)
|
||||
self.assertTrue(cert[0] is csr)
|
||||
self.assertTrue(cert[1] is updated_authzrs)
|
||||
self.assertEqual(updated_authzrs[0].uri, 'a...')
|
||||
self.assertEqual(updated_authzrs[1].uri, 'b.')
|
||||
self.assertEqual(updated_authzrs[0].times, [
|
||||
datetime.datetime(2015, 3, 27),
|
||||
# a is scheduled for 10, but b is polling [9..11), so it
|
||||
# will be picked up as soon as b is finished, without
|
||||
# additional sleeping
|
||||
datetime.datetime(2015, 3, 27, 0, 0, 11),
|
||||
datetime.datetime(2015, 3, 27, 0, 0, 33),
|
||||
datetime.datetime(2015, 3, 27, 0, 1, 5),
|
||||
])
|
||||
self.assertEqual(updated_authzrs[1].times, [
|
||||
datetime.datetime(2015, 3, 27, 0, 0, 2),
|
||||
datetime.datetime(2015, 3, 27, 0, 0, 9),
|
||||
])
|
||||
self.assertEqual(clock.dt, datetime.datetime(2015, 3, 27, 0, 1, 7))
|
||||
|
||||
def test_check_cert(self):
|
||||
self.response.headers['Location'] = self.certr.uri
|
||||
self.response.content = CERT2.as_der()
|
||||
self._mock_post_get()
|
||||
self.assertEqual(
|
||||
self.certr.update(body=CERT2), self.net.check_cert(self.certr))
|
||||
|
||||
# TODO: split here and separate test
|
||||
self.response.headers['Location'] = 'foo'
|
||||
self.assertRaises(
|
||||
errors.UnexpectedUpdate, self.net.check_cert, self.certr)
|
||||
|
||||
def test_check_cert_missing_location(self):
|
||||
self.response.content = CERT2.as_der()
|
||||
self._mock_post_get()
|
||||
self.assertRaises(errors.NetworkError, self.net.check_cert, self.certr)
|
||||
|
||||
def test_refresh(self):
|
||||
self.net.check_cert = mock.MagicMock()
|
||||
self.assertEqual(
|
||||
self.net.check_cert(self.certr), self.net.refresh(self.certr))
|
||||
|
||||
def test_fetch_chain(self):
|
||||
# pylint: disable=protected-access
|
||||
self.net._get_cert = mock.MagicMock()
|
||||
self.assertEqual(self.net._get_cert(self.certr.cert_chain_uri),
|
||||
self.net.fetch_chain(self.certr))
|
||||
|
||||
def test_fetch_chain_no_up_link(self):
|
||||
self.assertTrue(self.net.fetch_chain(self.certr.update(
|
||||
cert_chain_uri=None)) is None)
|
||||
|
||||
def test_revoke(self):
|
||||
self._mock_post_get()
|
||||
self.net.revoke(self.certr, when=messages2.Revocation.NOW)
|
||||
# pylint: disable=protected-access
|
||||
self.net._post.assert_called_once_with(self.certr.uri, mock.ANY)
|
||||
|
||||
def test_revoke_bad_status_raises_error(self):
|
||||
self.response.status_code = httplib.METHOD_NOT_ALLOWED
|
||||
self._mock_post_get()
|
||||
self.assertRaises(errors.NetworkError, self.net.revoke, self.certr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -10,7 +10,7 @@ import mock
|
||||
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client import le_util
|
||||
from letsencrypt.client.apache import configurator
|
||||
from letsencrypt.client.plugins.apache import configurator
|
||||
from letsencrypt.client.display import util as display_util
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user