mirror of
https://github.com/certbot/certbot.git
synced 2026-01-21 19:01:07 +03:00
Merge branch 'master' into plugin_tests
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -2,9 +2,9 @@
|
||||
*.egg-info/
|
||||
.eggs/
|
||||
build/
|
||||
dist/
|
||||
/venv/
|
||||
/venv3/
|
||||
dist*/
|
||||
/venv*/
|
||||
/kgs/
|
||||
/.tox/
|
||||
letsencrypt.log
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ load-plugins=linter_plugin
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use"--disable=all --enable=classes
|
||||
# --disable=W"
|
||||
disable=fixme,locally-disabled,abstract-class-not-used,bad-continuation,too-few-public-methods,no-self-use
|
||||
disable=fixme,locally-disabled,abstract-class-not-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name
|
||||
# abstract-class-not-used cannot be disabled locally (at least in pylint 1.4.1)
|
||||
|
||||
|
||||
|
||||
@@ -62,5 +62,5 @@ RUN virtualenv --no-site-packages -p python2 /opt/letsencrypt/venv && \
|
||||
# bash" and investigate, apply patches, etc.
|
||||
|
||||
ENV PATH /opt/letsencrypt/venv/bin:$PATH
|
||||
# TODO: is --text really necessary?
|
||||
ENTRYPOINT [ "letsencrypt", "--text" ]
|
||||
|
||||
ENTRYPOINT [ "letsencrypt" ]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Let's Encrypt:
|
||||
Copyright (c) Internet Security Research Group
|
||||
Let's Encrypt Python Client
|
||||
Copyright (c) Electronic Frontier Foundation and others
|
||||
Licensed Apache Version 2.0
|
||||
|
||||
Incorporating code from nginxparser
|
||||
|
||||
@@ -2,6 +2,8 @@ include requirements.txt
|
||||
include README.rst
|
||||
include CHANGES.rst
|
||||
include CONTRIBUTING.md
|
||||
include LICENSE.txt
|
||||
include linter_plugin.py
|
||||
include letsencrypt/EULA
|
||||
recursive-include docs *
|
||||
recursive-include letsencrypt/tests/testdata *
|
||||
|
||||
@@ -79,7 +79,7 @@ Current Features
|
||||
* web servers supported:
|
||||
|
||||
- apache/2.x (tested and working on Ubuntu Linux)
|
||||
- nginx/0.8.48+ (tested and mostly working on Ubuntu Linux)
|
||||
- nginx/0.8.48+ (under development)
|
||||
- standalone (runs its own webserver to prove you control the domain)
|
||||
|
||||
* the private key is generated locally on your system
|
||||
|
||||
190
acme/LICENSE.txt
Normal file
190
acme/LICENSE.txt
Normal file
@@ -0,0 +1,190 @@
|
||||
Copyright 2015 Electronic Frontier Foundation and others
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -1 +1,3 @@
|
||||
include LICENSE.txt
|
||||
include README.rst
|
||||
recursive-include acme/testdata *
|
||||
|
||||
1
acme/README.rst
Normal file
1
acme/README.rst
Normal file
@@ -0,0 +1 @@
|
||||
ACME protocol implementation for Python
|
||||
@@ -25,6 +25,14 @@ class Challenge(jose.TypedJSONObjectWithFields):
|
||||
"""ACME challenge."""
|
||||
TYPES = {}
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, jobj):
|
||||
try:
|
||||
return super(Challenge, cls).from_json(jobj)
|
||||
except jose.UnrecognizedTypeError as error:
|
||||
logger.debug(error)
|
||||
return UnrecognizedChallenge.from_json(jobj)
|
||||
|
||||
|
||||
class ContinuityChallenge(Challenge): # pylint: disable=abstract-method
|
||||
"""Client validation challenges."""
|
||||
@@ -42,6 +50,32 @@ class ChallengeResponse(jose.TypedJSONObjectWithFields):
|
||||
resource = fields.Resource(resource_type)
|
||||
|
||||
|
||||
class UnrecognizedChallenge(Challenge):
|
||||
"""Unrecognized challenge.
|
||||
|
||||
ACME specification defines a generic framework for challenges and
|
||||
defines some standard challenges that are implemented in this
|
||||
module. However, other implementations (including peers) might
|
||||
define additional challenge types, which should be ignored if
|
||||
unrecognized.
|
||||
|
||||
:ivar jobj: Original JSON decoded object.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, jobj):
|
||||
super(UnrecognizedChallenge, self).__init__()
|
||||
object.__setattr__(self, "jobj", jobj)
|
||||
|
||||
def to_partial_json(self):
|
||||
# pylint: disable=no-member
|
||||
return self.jobj
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, jobj):
|
||||
return cls(jobj)
|
||||
|
||||
|
||||
@Challenge.register
|
||||
class SimpleHTTP(DVChallenge):
|
||||
"""ACME "simpleHttp" challenge.
|
||||
@@ -542,7 +576,7 @@ class DNS(DVChallenge):
|
||||
def check_validation(self, validation, account_public_key):
|
||||
"""Check validation.
|
||||
|
||||
:param validation
|
||||
:param JWS validation:
|
||||
:type account_public_key:
|
||||
`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
|
||||
or
|
||||
|
||||
@@ -17,6 +17,32 @@ CERT = test_util.load_cert('cert.pem')
|
||||
KEY = test_util.load_rsa_private_key('rsa512_key.pem')
|
||||
|
||||
|
||||
class ChallengeTest(unittest.TestCase):
|
||||
|
||||
def test_from_json_unrecognized(self):
|
||||
from acme.challenges import Challenge
|
||||
from acme.challenges import UnrecognizedChallenge
|
||||
chall = UnrecognizedChallenge({"type": "foo"})
|
||||
# pylint: disable=no-member
|
||||
self.assertEqual(chall, Challenge.from_json(chall.jobj))
|
||||
|
||||
|
||||
class UnrecognizedChallengeTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
from acme.challenges import UnrecognizedChallenge
|
||||
self.jobj = {"type": "foo"}
|
||||
self.chall = UnrecognizedChallenge(self.jobj)
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jobj, self.chall.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.challenges import UnrecognizedChallenge
|
||||
self.assertEqual(
|
||||
self.chall, UnrecognizedChallenge.from_json(self.jobj))
|
||||
|
||||
|
||||
class SimpleHTTPTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
@@ -194,7 +194,7 @@ class JSONDeSerializable(object):
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
return self.json_dumps(sort_keys=True, indent=4, separators=(',', ': '))
|
||||
return self.json_dumps(sort_keys=True, indent=4)
|
||||
|
||||
@classmethod
|
||||
def json_dump_default(cls, python_object):
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Tests for acme.jose.interfaces."""
|
||||
import unittest
|
||||
|
||||
import six
|
||||
|
||||
|
||||
class JSONDeSerializableTest(unittest.TestCase):
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
@@ -90,8 +92,9 @@ class JSONDeSerializableTest(unittest.TestCase):
|
||||
self.assertEqual('["foo1", "foo2"]', self.seq.json_dumps())
|
||||
|
||||
def test_json_dumps_pretty(self):
|
||||
self.assertEqual(
|
||||
self.seq.json_dumps_pretty(), '[\n "foo1",\n "foo2"\n]')
|
||||
filler = ' ' if six.PY2 else ''
|
||||
self.assertEqual(self.seq.json_dumps_pretty(),
|
||||
'[\n "foo1",{0}\n "foo2"\n]'.format(filler))
|
||||
|
||||
def test_json_dump_default(self):
|
||||
from acme.jose.interfaces import JSONDeSerializable
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"""JSON Web Key."""
|
||||
import abc
|
||||
import binascii
|
||||
import json
|
||||
import logging
|
||||
|
||||
import cryptography.exceptions
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
@@ -27,6 +29,32 @@ class JWK(json_util.TypedJSONObjectWithFields):
|
||||
cryptography_key_types = ()
|
||||
"""Subclasses should override."""
|
||||
|
||||
required = NotImplemented
|
||||
"""Required members of public key's representation as defined by JWK/JWA."""
|
||||
|
||||
_thumbprint_json_dumps_params = {
|
||||
# "no whitespace or line breaks before or after any syntactic
|
||||
# elements"
|
||||
'indent': 0,
|
||||
'separators': (',', ':'),
|
||||
# "members ordered lexicographically by the Unicode [UNICODE]
|
||||
# code points of the member names"
|
||||
'sort_keys': True,
|
||||
}
|
||||
|
||||
def thumbprint(self, hash_function=hashes.SHA256):
|
||||
"""Compute JWK Thumbprint.
|
||||
|
||||
https://tools.ietf.org/html/rfc7638
|
||||
|
||||
"""
|
||||
digest = hashes.Hash(hash_function(), backend=default_backend())
|
||||
digest.update(json.dumps(
|
||||
dict((k, v) for k, v in six.iteritems(self.to_json())
|
||||
if k in self.required),
|
||||
**self._thumbprint_json_dumps_params).encode())
|
||||
return digest.finalize()
|
||||
|
||||
@abc.abstractmethod
|
||||
def public_key(self): # pragma: no cover
|
||||
"""Generate JWK with public key.
|
||||
@@ -60,7 +88,7 @@ class JWK(json_util.TypedJSONObjectWithFields):
|
||||
exceptions[loader] = error
|
||||
|
||||
# no luck
|
||||
raise errors.Error("Unable to deserialize key: {0}".format(exceptions))
|
||||
raise errors.Error('Unable to deserialize key: {0}'.format(exceptions))
|
||||
|
||||
@classmethod
|
||||
def load(cls, data, password=None, backend=None):
|
||||
@@ -81,17 +109,17 @@ class JWK(json_util.TypedJSONObjectWithFields):
|
||||
try:
|
||||
key = cls._load_cryptography_key(data, password, backend)
|
||||
except errors.Error as error:
|
||||
logger.debug("Loading symmetric key, assymentric failed: %s", error)
|
||||
logger.debug('Loading symmetric key, assymentric failed: %s', error)
|
||||
return JWKOct(key=data)
|
||||
|
||||
if cls.typ is not NotImplemented and not isinstance(
|
||||
key, cls.cryptography_key_types):
|
||||
raise errors.Error("Unable to deserialize {0} into {1}".format(
|
||||
raise errors.Error('Unable to deserialize {0} into {1}'.format(
|
||||
key.__class__, cls.__class__))
|
||||
for jwk_cls in six.itervalues(cls.TYPES):
|
||||
if isinstance(key, jwk_cls.cryptography_key_types):
|
||||
return jwk_cls(key=key)
|
||||
raise errors.Error("Unsupported algorithm: {0}".format(key.__class__))
|
||||
raise errors.Error('Unsupported algorithm: {0}'.format(key.__class__))
|
||||
|
||||
|
||||
@JWK.register
|
||||
@@ -105,6 +133,7 @@ class JWKES(JWK): # pragma: no cover
|
||||
typ = 'ES'
|
||||
cryptography_key_types = (
|
||||
ec.EllipticCurvePublicKey, ec.EllipticCurvePrivateKey)
|
||||
required = ('crv', JWK.type_field_name, 'x', 'y')
|
||||
|
||||
def fields_to_partial_json(self):
|
||||
raise NotImplementedError()
|
||||
@@ -122,6 +151,7 @@ class JWKOct(JWK):
|
||||
"""Symmetric JWK."""
|
||||
typ = 'oct'
|
||||
__slots__ = ('key',)
|
||||
required = ('k', JWK.type_field_name)
|
||||
|
||||
def fields_to_partial_json(self):
|
||||
# TODO: An "alg" member SHOULD also be present to identify the
|
||||
@@ -150,6 +180,7 @@ class JWKRSA(JWK):
|
||||
typ = 'RSA'
|
||||
cryptography_key_types = (rsa.RSAPublicKey, rsa.RSAPrivateKey)
|
||||
__slots__ = ('key',)
|
||||
required = ('e', JWK.type_field_name, 'n')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'key' in kwargs and not isinstance(
|
||||
@@ -204,7 +235,7 @@ class JWKRSA(JWK):
|
||||
jobj.get(x) for x in ('p', 'q', 'dp', 'dq', 'qi'))
|
||||
if tuple(param for param in all_params if param is None):
|
||||
raise errors.Error(
|
||||
"Some private parameters are missing: {0}".format(
|
||||
'Some private parameters are missing: {0}'.format(
|
||||
all_params))
|
||||
p, q, dp, dq, qi = tuple(
|
||||
cls._decode_param(x) for x in all_params)
|
||||
|
||||
@@ -25,9 +25,24 @@ class JWKTest(unittest.TestCase):
|
||||
self.assertRaises(errors.Error, JWKRSA.load, DSA_PEM)
|
||||
|
||||
|
||||
class JWKOctTest(unittest.TestCase):
|
||||
class JWKTestBaseMixin(object):
|
||||
"""Mixin test for JWK subclass tests."""
|
||||
|
||||
thumbprint = NotImplemented
|
||||
|
||||
def test_thumbprint_private(self):
|
||||
self.assertEqual(self.thumbprint, self.jwk.thumbprint())
|
||||
|
||||
def test_thumbprint_public(self):
|
||||
self.assertEqual(self.thumbprint, self.jwk.public_key().thumbprint())
|
||||
|
||||
|
||||
class JWKOctTest(unittest.TestCase, JWKTestBaseMixin):
|
||||
"""Tests for acme.jose.jwk.JWKOct."""
|
||||
|
||||
thumbprint = (b"=,\xdd;I\x1a+i\x02x\x8a\x12?06IM\xc2\x80"
|
||||
b"\xe4\xc3\x1a\xfc\x89\xf3)'\xce\xccm\xfd5")
|
||||
|
||||
def setUp(self):
|
||||
from acme.jose.jwk import JWKOct
|
||||
self.jwk = JWKOct(key=b'foo')
|
||||
@@ -52,10 +67,13 @@ class JWKOctTest(unittest.TestCase):
|
||||
self.assertTrue(self.jwk.public_key() is self.jwk)
|
||||
|
||||
|
||||
class JWKRSATest(unittest.TestCase):
|
||||
class JWKRSATest(unittest.TestCase, JWKTestBaseMixin):
|
||||
"""Tests for acme.jose.jwk.JWKRSA."""
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
||||
thumbprint = (b'\x08\xfa1\x87\x1d\x9b6H/*\x1eW\xc2\xe3\xf6P'
|
||||
b'\xefs\x0cKB\x87\xcf\x85yO\x045\x0e\x91\x80\x0b')
|
||||
|
||||
def setUp(self):
|
||||
from acme.jose.jwk import JWKRSA
|
||||
self.jwk256 = JWKRSA(key=RSA256_KEY.public_key())
|
||||
@@ -87,6 +105,7 @@ class JWKRSATest(unittest.TestCase):
|
||||
'dq': 'bHh2u7etM8LKKCF2pY2UdQ',
|
||||
'qi': 'oi45cEkbVoJjAbnQpFY87Q',
|
||||
})
|
||||
self.jwk = self.private
|
||||
|
||||
def test_init_auto_comparable(self):
|
||||
self.assertTrue(isinstance(
|
||||
|
||||
@@ -274,6 +274,7 @@ class AuthorizationTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from acme.messages import ChallengeBody
|
||||
from acme.messages import STATUS_VALID
|
||||
|
||||
self.challbs = (
|
||||
ChallengeBody(
|
||||
uri='http://challb1', status=STATUS_VALID,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# Symlinked in letsencrypt/tests/test_util.py, causes duplicate-code
|
||||
# warning that cannot be disabled locally.
|
||||
"""Test utilities.
|
||||
|
||||
.. warning:: This module is not part of the public API.
|
||||
|
||||
@@ -4,11 +4,12 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.0.0.dev20151008'
|
||||
|
||||
install_requires = [
|
||||
# load_pem_private/public_key (>=0.6)
|
||||
# rsa_recover_prime_factors (>=0.8)
|
||||
'cryptography>=0.8',
|
||||
'mock<1.1.0', # py26
|
||||
'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304)
|
||||
'pyasn1', # urllib3 InsecurePlatformWarning (#304)
|
||||
# Connection.set_tlsext_host_name (>=0.13), X509Req.get_extensions (>=0.15)
|
||||
@@ -16,14 +17,20 @@ install_requires = [
|
||||
'pyrfc3339',
|
||||
'pytz',
|
||||
'requests',
|
||||
'setuptools', # pkg_resources
|
||||
'six',
|
||||
'werkzeug',
|
||||
]
|
||||
|
||||
# env markers in extras_require cause problems with older pip: #517
|
||||
if sys.version_info < (2, 7):
|
||||
# only some distros recognize stdlib argparse as already satisfying
|
||||
install_requires.append('argparse')
|
||||
install_requires.extend([
|
||||
# only some distros recognize stdlib argparse as already satisfying
|
||||
'argparse',
|
||||
'mock<1.1.0',
|
||||
])
|
||||
else:
|
||||
install_requires.append('mock')
|
||||
|
||||
testing_extras = [
|
||||
'nose',
|
||||
@@ -33,7 +40,25 @@ testing_extras = [
|
||||
|
||||
setup(
|
||||
name='acme',
|
||||
version=version,
|
||||
description='ACME protocol implementation',
|
||||
url='https://github.com/letsencrypt/letsencrypt',
|
||||
author="Let's Encrypt Project",
|
||||
author_email='client-dev@letsencrypt.org',
|
||||
license='Apache License 2.0',
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
],
|
||||
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=install_requires,
|
||||
extras_require={
|
||||
'testing': testing_extras,
|
||||
|
||||
15
bootstrap/archlinux.sh
Executable file
15
bootstrap/archlinux.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
|
||||
# "python-virtualenv" is Python3, but "python2-virtualenv" provides
|
||||
# only "virtualenv2" binary, not "virtualenv" necessary in
|
||||
# ./bootstrap/dev/_common_venv.sh
|
||||
pacman -S \
|
||||
git \
|
||||
python2 \
|
||||
python-virtualenv \
|
||||
gcc \
|
||||
dialog \
|
||||
augeas \
|
||||
openssl \
|
||||
libffi \
|
||||
ca-certificates \
|
||||
1
bootstrap/dev/README
Normal file
1
bootstrap/dev/README
Normal file
@@ -0,0 +1 @@
|
||||
This directory contains developer setup.
|
||||
25
bootstrap/dev/_venv_common.sh
Executable file
25
bootstrap/dev/_venv_common.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh -xe
|
||||
|
||||
VENV_NAME=${VENV_NAME:-venv}
|
||||
|
||||
# .egg-info directories tend to cause bizzaire problems (e.g. `pip -e
|
||||
# .` might unexpectedly install letshelp-letsencrypt only, in case
|
||||
# `python letshelp-letsencrypt/setup.py build` has been called
|
||||
# earlier)
|
||||
rm -rf *.egg-info
|
||||
|
||||
# virtualenv setup is NOT idempotent: shutil.Error:
|
||||
# `/home/jakub/dev/letsencrypt/letsencrypt/venv/bin/python2` and
|
||||
# `venv/bin/python2` are the same file
|
||||
mv $VENV_NAME "$VENV_NAME.$(date +%s).bak" || true
|
||||
virtualenv --no-site-packages $VENV_NAME $VENV_ARGS
|
||||
. ./$VENV_NAME/bin/activate
|
||||
|
||||
# Separately install setuptools and pip to make sure following
|
||||
# invocations use latest
|
||||
pip install -U setuptools
|
||||
pip install -U pip
|
||||
pip install "$@"
|
||||
|
||||
echo "Please run the following command to activate developer environment:"
|
||||
echo "source $VENV_NAME/bin/activate"
|
||||
13
bootstrap/dev/venv.sh
Executable file
13
bootstrap/dev/venv.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh -xe
|
||||
# Developer virtualenv setup for Let's Encrypt client
|
||||
|
||||
export VENV_ARGS="--python python2"
|
||||
|
||||
./bootstrap/dev/_venv_common.sh \
|
||||
-r requirements.txt \
|
||||
-e acme[testing] \
|
||||
-e .[dev,docs,testing] \
|
||||
-e letsencrypt-apache \
|
||||
-e letsencrypt-nginx \
|
||||
-e letshelp-letsencrypt \
|
||||
-e letsencrypt-compatibility-test
|
||||
8
bootstrap/dev/venv3.sh
Executable file
8
bootstrap/dev/venv3.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh -xe
|
||||
# Developer Python3 virtualenv setup for Let's Encrypt
|
||||
|
||||
export VENV_NAME="${VENV_NAME:-venv3}"
|
||||
export VENV_ARGS="--python python3"
|
||||
|
||||
./bootstrap/dev/_venv_common.sh \
|
||||
-e acme[testing] \
|
||||
@@ -21,9 +21,3 @@
|
||||
|
||||
.. automodule:: letsencrypt.display.enhancements
|
||||
:members:
|
||||
|
||||
:mod:`letsencrypt.display.revocation`
|
||||
=====================================
|
||||
|
||||
.. automodule:: letsencrypt.display.revocation
|
||||
:members:
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
:mod:`letsencrypt.recovery_token`
|
||||
--------------------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.recovery_token
|
||||
:members:
|
||||
@@ -1,5 +0,0 @@
|
||||
:mod:`letsencrypt.revoker`
|
||||
--------------------------
|
||||
|
||||
.. automodule:: letsencrypt.revoker
|
||||
:members:
|
||||
@@ -30,7 +30,7 @@ here = os.path.abspath(os.path.dirname(__file__))
|
||||
# read version number (and other metadata) from package init
|
||||
init_fn = os.path.join(here, '..', 'letsencrypt', '__init__.py')
|
||||
with codecs.open(init_fn, encoding='utf8') as fd:
|
||||
meta = dict(re.findall(r"""__([a-z]+)__ = "([^"]+)""", fd.read()))
|
||||
meta = dict(re.findall(r"""__([a-z]+)__ = '([^']+)""", fd.read()))
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
|
||||
@@ -7,38 +7,37 @@ Contributing
|
||||
Hacking
|
||||
=======
|
||||
|
||||
Start by :doc:`installing dependencies and setting up Let's Encrypt
|
||||
<using>`.
|
||||
All changes in your pull request **must** have 100% unit test coverage, pass
|
||||
our `integration`_ tests, **and** be compliant with the
|
||||
:ref:`coding style <coding-style>`.
|
||||
|
||||
When you're done activate the virtualenv:
|
||||
|
||||
Bootstrap
|
||||
---------
|
||||
|
||||
Start by :ref:`installing Let's Encrypt prerequisites
|
||||
<prerequisites>`. Then run:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
source ./venv/bin/activate
|
||||
./bootstrap/dev/venv.sh
|
||||
|
||||
This step should prepend you prompt with ``(venv)`` and save you from
|
||||
typing ``./venv/bin/...``. It is also required to run some of the
|
||||
`testing`_ tools. Virtualenv can be disabled at any time by typing
|
||||
``deactivate``. More information can be found in `virtualenv
|
||||
Activate the virtualenv:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
source ./$VENV_NAME/bin/activate
|
||||
|
||||
This step should prepend you prompt with ``($VENV_NAME)`` and save you
|
||||
from typing ``./$VENV_NAME/bin/...``. It is also required to run some
|
||||
of the `testing`_ tools. Virtualenv can be disabled at any time by
|
||||
typing ``deactivate``. More information can be found in `virtualenv
|
||||
documentation`_.
|
||||
|
||||
Install the development packages:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
pip install -r requirements.txt -e acme -e .[dev,docs,testing] -e letsencrypt-apache -e letsencrypt-nginx -e letshelp-letsencrypt
|
||||
|
||||
.. note:: `-e` (short for `--editable`) turns on *editable mode* in
|
||||
which any source code changes in the current working
|
||||
directory are "live" and no further `pip install ...`
|
||||
invocations are necessary while developing.
|
||||
|
||||
This is roughly equivalent to `python setup.py develop`. For
|
||||
more info see `man pip`.
|
||||
|
||||
The code base, including your pull requests, **must** have 100% unit
|
||||
test coverage, pass our `integration`_ tests **and** be compliant with
|
||||
the :ref:`coding style <coding-style>`.
|
||||
Note that packages are installed in so called *editable mode*, in
|
||||
which any source code changes in the current working directory are
|
||||
"live" and no further ``./bootstrap/dev/venv.sh`` or ``pip install
|
||||
...`` invocations are necessary while developing.
|
||||
|
||||
.. _`virtualenv documentation`: https://virtualenv.pypa.io
|
||||
|
||||
@@ -67,8 +66,10 @@ The following tools are there to help you:
|
||||
|
||||
Integration
|
||||
~~~~~~~~~~~
|
||||
Mac OS X users: Run `./tests/mac-bootstrap.sh` instead of `boulder-start.sh` to
|
||||
install dependencies, configure the environment, and start boulder.
|
||||
|
||||
First, install `Go`_ 1.5, libtool-ltdl, mariadb-server and
|
||||
Otherwise, install `Go`_ 1.5, libtool-ltdl, mariadb-server and
|
||||
rabbitmq-server and then start Boulder_, an ACME CA server::
|
||||
|
||||
./tests/boulder-start.sh
|
||||
|
||||
53
docs/pkgs/letsencrypt_compatibility_test.rst
Normal file
53
docs/pkgs/letsencrypt_compatibility_test.rst
Normal file
@@ -0,0 +1,53 @@
|
||||
:mod:`letsencrypt_compatibility_test`
|
||||
-------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt_compatibility_test
|
||||
:members:
|
||||
|
||||
:mod:`letsencrypt_compatibility_test.errors`
|
||||
============================================
|
||||
|
||||
.. automodule:: letsencrypt_compatibility_test.errors
|
||||
:members:
|
||||
|
||||
:mod:`letsencrypt_compatibility_test.interfaces`
|
||||
================================================
|
||||
|
||||
.. automodule:: letsencrypt_compatibility_test.interfaces
|
||||
:members:
|
||||
|
||||
:mod:`letsencrypt_compatibility_test.test_driver`
|
||||
=================================================
|
||||
|
||||
.. automodule:: letsencrypt_compatibility_test.test_driver
|
||||
:members:
|
||||
|
||||
:mod:`letsencrypt_compatibility_test.util`
|
||||
==========================================
|
||||
|
||||
.. automodule:: letsencrypt_compatibility_test.util
|
||||
:members:
|
||||
|
||||
:mod:`letsencrypt_compatibility_test.configurators`
|
||||
===================================================
|
||||
|
||||
.. automodule:: letsencrypt_compatibility_test.configurators
|
||||
:members:
|
||||
|
||||
:mod:`letsencrypt_compatibility_test.configurators.apache`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. automodule:: letsencrypt_compatibility_test.configurators.apache
|
||||
:members:
|
||||
|
||||
:mod:`letsencrypt_compatibility_test.configurators.apache.apache24`
|
||||
-------------------------------------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt_compatibility_test.configurators.apache.apache24
|
||||
:members:
|
||||
|
||||
:mod:`letsencrypt_compatibility_test.configurators.apache.common`
|
||||
-------------------------------------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt_compatibility_test.configurators.apache.common
|
||||
:members:
|
||||
11
docs/pkgs/letshelp_letsencrypt.rst
Normal file
11
docs/pkgs/letshelp_letsencrypt.rst
Normal file
@@ -0,0 +1,11 @@
|
||||
:mod:`letshelp_letsencrypt`
|
||||
---------------------------
|
||||
|
||||
.. automodule:: letshelp_letsencrypt
|
||||
:members:
|
||||
|
||||
:mod:`letshelp_letsencrypt.apache`
|
||||
==================================
|
||||
|
||||
.. automodule:: letshelp_letsencrypt.apache
|
||||
:members:
|
||||
@@ -42,6 +42,8 @@ above method instead.
|
||||
https://github.com/letsencrypt/letsencrypt/archive/master.zip
|
||||
|
||||
|
||||
.. _prerequisites:
|
||||
|
||||
Prerequisites
|
||||
=============
|
||||
|
||||
@@ -121,11 +123,13 @@ Installation
|
||||
============
|
||||
|
||||
.. "pip install acme" doesn't search for "acme" in cwd, just like "pip
|
||||
install -e acme" does
|
||||
install -e acme" does; `-U setuptools pip` necessary for #722
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
virtualenv --no-site-packages -p python2 venv
|
||||
./venv/bin/pip install -U setuptools
|
||||
./venv/bin/pip install -U pip
|
||||
./venv/bin/pip install -r requirements.txt acme/ . letsencrypt-apache/ letsencrypt-nginx/
|
||||
|
||||
.. warning:: Please do **not** use ``python setup.py install``. Please
|
||||
|
||||
@@ -9,6 +9,7 @@ domains = example.com
|
||||
|
||||
text = True
|
||||
agree-eula = True
|
||||
agree-tos = True
|
||||
debug = True
|
||||
# Unfortunately, it's not possible to specify "verbose" multiple times
|
||||
# (correspondingly to -vvvvvv)
|
||||
|
||||
190
letsencrypt-apache/LICENSE.txt
Normal file
190
letsencrypt-apache/LICENSE.txt
Normal file
@@ -0,0 +1,190 @@
|
||||
Copyright 2015 Electronic Frontier Foundation and others
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -1,2 +1,4 @@
|
||||
include LICENSE.txt
|
||||
include README.rst
|
||||
recursive-include letsencrypt_apache/tests/testdata *
|
||||
include letsencrypt_apache/options-ssl-apache.conf
|
||||
|
||||
1
letsencrypt-apache/README.rst
Normal file
1
letsencrypt-apache/README.rst
Normal file
@@ -0,0 +1 @@
|
||||
Apache plugin for Let's Encrypt client
|
||||
@@ -137,6 +137,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
:raises .errors.PluginError: If there is any other error
|
||||
|
||||
"""
|
||||
# Verify Apache is installed
|
||||
for exe in (self.conf("ctl"), self.conf("enmod"),
|
||||
self.conf("dismod"), self.conf("init-script")):
|
||||
if not le_util.exe_exists(exe):
|
||||
raise errors.NoInstallationError
|
||||
|
||||
# Make sure configuration is valid
|
||||
self.config_test()
|
||||
|
||||
@@ -1162,7 +1168,7 @@ def _get_mod_deps(mod_name):
|
||||
changes.
|
||||
.. warning:: If all deps are not included, it may cause incorrect parsing
|
||||
behavior, due to enable_mod's shortcut for updating the parser's
|
||||
currently defined modules (:method:`.ApacheConfigurator._add_parser_mod`)
|
||||
currently defined modules (`.ApacheConfigurator._add_parser_mod`)
|
||||
This would only present a major problem in extremely atypical
|
||||
configs that use ifmod for the missing deps.
|
||||
|
||||
|
||||
@@ -241,6 +241,10 @@ class ApacheParser(object):
|
||||
Directives should be in the form of a case insensitive regex currently
|
||||
|
||||
.. todo:: arg should probably be a list
|
||||
.. todo:: arg search currently only supports direct matching. It does
|
||||
not handle the case of variables or quoted arguments. This should
|
||||
be adapted to use a generic search for the directive and then do a
|
||||
case-insensitive self.get_arg filter
|
||||
|
||||
Note: Augeas is inherently case sensitive while Apache is case
|
||||
insensitive. Augeas 1.0 allows case insensitive regexes like
|
||||
@@ -315,6 +319,14 @@ class ApacheParser(object):
|
||||
|
||||
"""
|
||||
value = self.aug.get(match)
|
||||
|
||||
# No need to strip quotes for variables, as apache2ctl already does this
|
||||
# but we do need to strip quotes for all normal arguments.
|
||||
|
||||
# Note: normal argument may be a quoted variable
|
||||
# e.g. strip now, not later
|
||||
value = value.strip("'\"")
|
||||
|
||||
variables = ApacheParser.arg_var_interpreter.findall(value)
|
||||
|
||||
for var in variables:
|
||||
@@ -390,6 +402,9 @@ class ApacheParser(object):
|
||||
# logger.error("Error: Invalid regexp characters in %s", arg)
|
||||
# return []
|
||||
|
||||
# Remove beginning and ending quotes
|
||||
arg = arg.strip("'\"")
|
||||
|
||||
# Standardize the include argument based on server root
|
||||
if not arg.startswith("/"):
|
||||
# Normpath will condense ../
|
||||
|
||||
@@ -32,6 +32,7 @@ class ComplexParserTest(util.ParserTest):
|
||||
"COMPLEX": "",
|
||||
"tls_port": "1234",
|
||||
"fnmatch_filename": "test_fnmatch.conf",
|
||||
"tls_port_str": "1234"
|
||||
}
|
||||
)
|
||||
|
||||
@@ -49,6 +50,12 @@ class ComplexParserTest(util.ParserTest):
|
||||
self.assertEqual(len(matches), 1)
|
||||
self.assertEqual(self.parser.get_arg(matches[0]), "1234")
|
||||
|
||||
def test_basic_variable_parsing_quotes(self):
|
||||
matches = self.parser.find_dir("TestVariablePortStr")
|
||||
|
||||
self.assertEqual(len(matches), 1)
|
||||
self.assertEqual(self.parser.get_arg(matches[0]), "1234")
|
||||
|
||||
def test_invalid_variable_parsing(self):
|
||||
del self.parser.variables["tls_port"]
|
||||
|
||||
@@ -89,6 +96,7 @@ class ComplexParserTest(util.ParserTest):
|
||||
else:
|
||||
self.assertFalse(self.parser.find_dir("FNMATCH_DIRECTIVE"))
|
||||
|
||||
# NOTE: Only run one test per function otherwise you will have inf recursion
|
||||
def test_include(self):
|
||||
self.verify_fnmatch("test_fnmatch.?onf")
|
||||
|
||||
@@ -101,6 +109,12 @@ class ComplexParserTest(util.ParserTest):
|
||||
def test_include_fullpath_trailing_slash(self):
|
||||
self.verify_fnmatch(self.config_path + "//")
|
||||
|
||||
def test_include_single_quotes(self):
|
||||
self.verify_fnmatch("'" + self.config_path + "'")
|
||||
|
||||
def test_include_double_quotes(self):
|
||||
self.verify_fnmatch('"' + self.config_path + '"')
|
||||
|
||||
def test_include_variable(self):
|
||||
self.verify_fnmatch("../complex_parsing/${fnmatch_filename}")
|
||||
|
||||
|
||||
@@ -37,8 +37,16 @@ class TwoVhost80Test(util.ApacheTest):
|
||||
shutil.rmtree(self.config_dir)
|
||||
shutil.rmtree(self.work_dir)
|
||||
|
||||
@mock.patch("letsencrypt_apache.configurator.le_util.exe_exists")
|
||||
def test_prepare_no_install(self, mock_exe_exists):
|
||||
mock_exe_exists.return_value = False
|
||||
self.assertRaises(
|
||||
errors.NoInstallationError, self.config.prepare)
|
||||
|
||||
@mock.patch("letsencrypt_apache.parser.ApacheParser")
|
||||
def test_prepare_version(self, _):
|
||||
@mock.patch("letsencrypt_apache.configurator.le_util.exe_exists")
|
||||
def test_prepare_version(self, mock_exe_exists, _):
|
||||
mock_exe_exists.return_value = True
|
||||
self.config.version = None
|
||||
self.config.config_test = mock.Mock()
|
||||
self.config.get_version = mock.Mock(return_value=(1, 1))
|
||||
|
||||
@@ -46,6 +46,8 @@ IncludeOptional sites-enabled/*.conf
|
||||
Define COMPLEX
|
||||
|
||||
Define tls_port 1234
|
||||
Define tls_port_str "1234"
|
||||
|
||||
Define fnmatch_filename test_fnmatch.conf
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
TestVariablePort ${tls_port}
|
||||
TestVariablePortStr "${tls_port_str}"
|
||||
|
||||
LoadModule status_module modules/mod_status.so
|
||||
|
||||
|
||||
@@ -66,31 +66,34 @@ def get_apache_configurator(
|
||||
|
||||
"""
|
||||
backups = os.path.join(work_dir, "backups")
|
||||
mock_le_config = mock.MagicMock(
|
||||
apache_server_root=config_path,
|
||||
apache_le_vhost_ext=constants.CLI_DEFAULTS["le_vhost_ext"],
|
||||
backup_dir=backups,
|
||||
config_dir=config_dir,
|
||||
temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"),
|
||||
in_progress_dir=os.path.join(backups, "IN_PROGRESS"),
|
||||
work_dir=work_dir)
|
||||
|
||||
with mock.patch("letsencrypt_apache.configurator."
|
||||
"subprocess.Popen") as mock_popen:
|
||||
with mock.patch("letsencrypt_apache.parser.ApacheParser."
|
||||
"update_runtime_variables"):
|
||||
# This indicates config_test passes
|
||||
mock_popen().communicate.return_value = ("Fine output", "No problems")
|
||||
mock_popen().returncode = 0
|
||||
# This indicates config_test passes
|
||||
mock_popen().communicate.return_value = ("Fine output", "No problems")
|
||||
mock_popen().returncode = 0
|
||||
with mock.patch("letsencrypt_apache.configurator.le_util."
|
||||
"exe_exists") as mock_exe_exists:
|
||||
mock_exe_exists.return_value = True
|
||||
with mock.patch("letsencrypt_apache.parser.ApacheParser."
|
||||
"update_runtime_variables"):
|
||||
config = configurator.ApacheConfigurator(
|
||||
config=mock_le_config,
|
||||
name="apache",
|
||||
version=version)
|
||||
# This allows testing scripts to set it a bit more quickly
|
||||
if conf is not None:
|
||||
config.conf = conf # pragma: no cover
|
||||
|
||||
config = configurator.ApacheConfigurator(
|
||||
config=mock.MagicMock(
|
||||
apache_server_root=config_path,
|
||||
apache_le_vhost_ext=constants.CLI_DEFAULTS["le_vhost_ext"],
|
||||
backup_dir=backups,
|
||||
config_dir=config_dir,
|
||||
temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"),
|
||||
in_progress_dir=os.path.join(backups, "IN_PROGRESS"),
|
||||
work_dir=work_dir),
|
||||
name="apache",
|
||||
version=version)
|
||||
# This allows testing scripts to set it a bit more quickly
|
||||
if conf is not None:
|
||||
config.conf = conf # pragma: no cover
|
||||
|
||||
config.prepare()
|
||||
config.prepare()
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@@ -1,24 +1,56 @@
|
||||
import sys
|
||||
|
||||
from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.0.0.dev20151008'
|
||||
|
||||
install_requires = [
|
||||
'acme',
|
||||
'letsencrypt',
|
||||
'mock<1.1.0', # py26
|
||||
'acme=={0}'.format(version),
|
||||
'letsencrypt=={0}'.format(version),
|
||||
'python-augeas',
|
||||
'setuptools', # pkg_resources
|
||||
'zope.component',
|
||||
'zope.interface',
|
||||
]
|
||||
|
||||
if sys.version_info < (2, 7):
|
||||
install_requires.append('mock<1.1.0')
|
||||
else:
|
||||
install_requires.append('mock')
|
||||
|
||||
setup(
|
||||
name='letsencrypt-apache',
|
||||
version=version,
|
||||
description="Apache plugin for Let's Encrypt client",
|
||||
url='https://github.com/letsencrypt/letsencrypt',
|
||||
author="Let's Encrypt Project",
|
||||
author_email='client-dev@letsencrypt.org',
|
||||
license='Apache License 2.0',
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Environment :: Plugins',
|
||||
'Intended Audience :: System Administrators',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
'Topic :: System :: Networking',
|
||||
'Topic :: System :: Systems Administration',
|
||||
'Topic :: Utilities',
|
||||
],
|
||||
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=install_requires,
|
||||
entry_points={
|
||||
'letsencrypt.plugins': [
|
||||
'apache = letsencrypt_apache.configurator:ApacheConfigurator',
|
||||
],
|
||||
},
|
||||
include_package_data=True,
|
||||
)
|
||||
|
||||
190
letsencrypt-compatibility-test/LICENSE.txt
Normal file
190
letsencrypt-compatibility-test/LICENSE.txt
Normal file
@@ -0,0 +1,190 @@
|
||||
Copyright 2015 Electronic Frontier Foundation and others
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -1 +1,6 @@
|
||||
include LICENSE.txt
|
||||
include README.rst
|
||||
include letsencrypt_compatibility_test/configurators/apache/a2enmod.sh
|
||||
include letsencrypt_compatibility_test/configurators/apache/a2dismod.sh
|
||||
include letsencrypt_compatibility_test/configurators/apache/Dockerfile
|
||||
recursive-include letsencrypt_compatibility_test/testdata *
|
||||
|
||||
1
letsencrypt-compatibility-test/README.rst
Normal file
1
letsencrypt-compatibility-test/README.rst
Normal file
@@ -0,0 +1 @@
|
||||
Compatibility tests for Let's Encrypt client
|
||||
@@ -1,19 +1,45 @@
|
||||
import sys
|
||||
|
||||
from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.1.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
'letsencrypt',
|
||||
'letsencrypt-apache',
|
||||
'letsencrypt-nginx',
|
||||
'letsencrypt=={0}'.format(version),
|
||||
'letsencrypt-apache=={0}'.format(version),
|
||||
'letsencrypt-nginx=={0}'.format(version),
|
||||
'docker-py',
|
||||
'mock<1.1.0', # py26
|
||||
'zope.interface',
|
||||
]
|
||||
|
||||
if sys.version_info < (2, 7):
|
||||
install_requires.append('mock<1.1.0')
|
||||
else:
|
||||
install_requires.append('mock')
|
||||
|
||||
setup(
|
||||
name='letsencrypt-compatibility-test',
|
||||
version=version,
|
||||
description="Compatibility tests for Let's Encrypt client",
|
||||
url='https://github.com/letsencrypt/letsencrypt',
|
||||
author="Let's Encrypt Project",
|
||||
author_email='client-dev@letsencrypt.org',
|
||||
license='Apache License 2.0',
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
],
|
||||
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=install_requires,
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
|
||||
190
letsencrypt-nginx/LICENSE.txt
Normal file
190
letsencrypt-nginx/LICENSE.txt
Normal file
@@ -0,0 +1,190 @@
|
||||
Copyright 2015 Electronic Frontier Foundation and others
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -1,2 +1,4 @@
|
||||
include LICENSE.txt
|
||||
include README.rst
|
||||
recursive-include letsencrypt_nginx/tests/testdata *
|
||||
include letsencrypt_nginx/options-ssl-nginx.conf
|
||||
|
||||
1
letsencrypt-nginx/README.rst
Normal file
1
letsencrypt-nginx/README.rst
Normal file
@@ -0,0 +1 @@
|
||||
Nginx plugin for Let's Encrypt client
|
||||
@@ -56,7 +56,7 @@ class NginxConfigurator(common.Plugin):
|
||||
zope.interface.implements(interfaces.IAuthenticator, interfaces.IInstaller)
|
||||
zope.interface.classProvides(interfaces.IPluginFactory)
|
||||
|
||||
description = "Nginx Web Server"
|
||||
description = "Nginx Web Server - currently doesn't work"
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add):
|
||||
|
||||
@@ -1,24 +1,56 @@
|
||||
import sys
|
||||
|
||||
from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.0.0.dev20151008'
|
||||
|
||||
install_requires = [
|
||||
'acme',
|
||||
'letsencrypt',
|
||||
'mock<1.1.0', # py26
|
||||
'acme=={0}'.format(version),
|
||||
'letsencrypt=={0}'.format(version),
|
||||
'PyOpenSSL',
|
||||
'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary?
|
||||
'setuptools', # pkg_resources
|
||||
'zope.interface',
|
||||
]
|
||||
|
||||
if sys.version_info < (2, 7):
|
||||
install_requires.append('mock<1.1.0')
|
||||
else:
|
||||
install_requires.append('mock')
|
||||
|
||||
setup(
|
||||
name='letsencrypt-nginx',
|
||||
version=version,
|
||||
description="Nginx plugin for Let's Encrypt client",
|
||||
url='https://github.com/letsencrypt/letsencrypt',
|
||||
author="Let's Encrypt Project",
|
||||
author_email='client-dev@letsencrypt.org',
|
||||
license='Apache License 2.0',
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Environment :: Plugins',
|
||||
'Intended Audience :: System Administrators',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
'Topic :: System :: Networking',
|
||||
'Topic :: System :: Systems Administration',
|
||||
'Topic :: Utilities',
|
||||
],
|
||||
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=install_requires,
|
||||
entry_points={
|
||||
'letsencrypt.plugins': [
|
||||
'nginx = letsencrypt_nginx.configurator:NginxConfigurator',
|
||||
],
|
||||
},
|
||||
include_package_data=True,
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Let's Encrypt client."""
|
||||
|
||||
# version number like 1.2.3a0, must have at least 2 parts, like 1.2
|
||||
__version__ = "0.1"
|
||||
__version__ = '0.0.0.dev20151008'
|
||||
|
||||
@@ -54,7 +54,7 @@ class Account(object): # pylint: disable=too-few-public-methods
|
||||
tz=pytz.UTC).replace(microsecond=0),
|
||||
creation_host=socket.getfqdn()) if meta is None else meta
|
||||
|
||||
self.id = hashlib.md5( # pylint: disable=invalid-name
|
||||
self.id = hashlib.md5(
|
||||
self.key.key.public_key().public_bytes(
|
||||
encoding=serialization.Encoding.DER,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo)
|
||||
@@ -92,13 +92,13 @@ def report_new_account(acc, config):
|
||||
"contain certificates and private keys obtained by Let's Encrypt "
|
||||
"so making regular backups of this folder is ideal.".format(
|
||||
config.config_dir),
|
||||
reporter.MEDIUM_PRIORITY, True)
|
||||
reporter.MEDIUM_PRIORITY)
|
||||
|
||||
if acc.regr.body.emails:
|
||||
recovery_msg = ("If you lose your account credentials, you can "
|
||||
"recover through e-mails sent to {0}.".format(
|
||||
", ".join(acc.regr.body.emails)))
|
||||
reporter.add_message(recovery_msg, reporter.HIGH_PRIORITY, True)
|
||||
reporter.add_message(recovery_msg, reporter.HIGH_PRIORITY)
|
||||
|
||||
|
||||
class AccountMemoryStorage(interfaces.AccountStorage):
|
||||
|
||||
@@ -11,6 +11,7 @@ from acme import messages
|
||||
from letsencrypt import achallenges
|
||||
from letsencrypt import constants
|
||||
from letsencrypt import errors
|
||||
from letsencrypt import error_handler
|
||||
from letsencrypt import interfaces
|
||||
|
||||
|
||||
@@ -106,17 +107,16 @@ class AuthHandler(object):
|
||||
"""Get Responses for challenges from authenticators."""
|
||||
cont_resp = []
|
||||
dv_resp = []
|
||||
try:
|
||||
if self.cont_c:
|
||||
cont_resp = self.cont_auth.perform(self.cont_c)
|
||||
if self.dv_c:
|
||||
dv_resp = self.dv_auth.perform(self.dv_c)
|
||||
# This will catch both specific types of errors.
|
||||
except errors.AuthorizationError:
|
||||
logger.critical("Failure in setting up challenges.")
|
||||
logger.info("Attempting to clean up outstanding challenges...")
|
||||
self._cleanup_challenges()
|
||||
raise
|
||||
with error_handler.ErrorHandler(self._cleanup_challenges):
|
||||
try:
|
||||
if self.cont_c:
|
||||
cont_resp = self.cont_auth.perform(self.cont_c)
|
||||
if self.dv_c:
|
||||
dv_resp = self.dv_auth.perform(self.dv_c)
|
||||
except errors.AuthorizationError:
|
||||
logger.critical("Failure in setting up challenges.")
|
||||
logger.info("Attempting to clean up outstanding challenges...")
|
||||
raise
|
||||
|
||||
assert len(cont_resp) == len(self.cont_c)
|
||||
assert len(dv_resp) == len(self.dv_c)
|
||||
@@ -531,7 +531,7 @@ def _report_failed_challs(failed_achalls):
|
||||
reporter = zope.component.getUtility(interfaces.IReporter)
|
||||
for achalls in problems.itervalues():
|
||||
reporter.add_message(
|
||||
_generate_failed_chall_msg(achalls), reporter.MEDIUM_PRIORITY, True)
|
||||
_generate_failed_chall_msg(achalls), reporter.MEDIUM_PRIORITY)
|
||||
|
||||
|
||||
def _generate_failed_chall_msg(failed_achalls):
|
||||
|
||||
@@ -24,6 +24,7 @@ from acme import jose
|
||||
import letsencrypt
|
||||
|
||||
from letsencrypt import account
|
||||
from letsencrypt import colored_logging
|
||||
from letsencrypt import configuration
|
||||
from letsencrypt import constants
|
||||
from letsencrypt import client
|
||||
@@ -79,8 +80,8 @@ More detailed help:
|
||||
-h, --help [topic] print this message, or detailed help on a topic;
|
||||
the available topics are:
|
||||
|
||||
all, apache, automation, nginx, paths, security, testing, or any of the
|
||||
subcommands
|
||||
all, apache, automation, manual, nginx, paths, security, testing, or any of
|
||||
the subcommands
|
||||
"""
|
||||
|
||||
|
||||
@@ -172,6 +173,9 @@ def _find_duplicative_certs(domains, config, renew_config):
|
||||
identical_names_cert, subset_names_cert = None, None
|
||||
|
||||
configs_dir = renew_config.renewal_configs_dir
|
||||
# Verify the directory is there
|
||||
le_util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid())
|
||||
|
||||
cli_config = configuration.RenewerConfiguration(config)
|
||||
for renewal_file in os.listdir(configs_dir):
|
||||
try:
|
||||
@@ -196,8 +200,116 @@ def _find_duplicative_certs(domains, config, renew_config):
|
||||
return identical_names_cert, subset_names_cert
|
||||
|
||||
|
||||
def _treat_as_renewal(config, domains):
|
||||
"""Determine whether or not the call should be treated as a renewal.
|
||||
|
||||
:returns: RenewableCert or None if renewal shouldn't occur.
|
||||
:rtype: :class:`.storage.RenewableCert`
|
||||
|
||||
:raises .Error: If the user would like to rerun the client again.
|
||||
|
||||
"""
|
||||
renewal = False
|
||||
|
||||
# Considering the possibility that the requested certificate is
|
||||
# related to an existing certificate. (config.duplicate, which
|
||||
# is set with --duplicate, skips all of this logic and forces any
|
||||
# kind of certificate to be obtained with renewal = False.)
|
||||
if not config.duplicate:
|
||||
ident_names_cert, subset_names_cert = _find_duplicative_certs(
|
||||
domains, config, configuration.RenewerConfiguration(config))
|
||||
# I am not sure whether that correctly reads the systemwide
|
||||
# configuration file.
|
||||
question = None
|
||||
if ident_names_cert is not None:
|
||||
question = (
|
||||
"You have an existing certificate that contains exactly the "
|
||||
"same domains you requested (ref: {0}){br}{br}Do you want to "
|
||||
"renew and replace this certificate with a newly-issued one?"
|
||||
).format(ident_names_cert.configfile.filename, br=os.linesep)
|
||||
elif subset_names_cert is not None:
|
||||
question = (
|
||||
"You have an existing certificate that contains a portion of "
|
||||
"the domains you requested (ref: {0}){br}{br}It contains these "
|
||||
"names: {1}{br}{br}You requested these names for the new "
|
||||
"certificate: {2}.{br}{br}Do you want to replace this existing "
|
||||
"certificate with the new certificate?"
|
||||
).format(subset_names_cert.configfile.filename,
|
||||
", ".join(subset_names_cert.names()),
|
||||
", ".join(domains),
|
||||
br=os.linesep)
|
||||
if question is None:
|
||||
# We aren't in a duplicative-names situation at all, so we don't
|
||||
# have to tell or ask the user anything about this.
|
||||
pass
|
||||
elif config.renew_by_default or zope.component.getUtility(
|
||||
interfaces.IDisplay).yesno(question, "Replace", "Cancel"):
|
||||
renewal = True
|
||||
else:
|
||||
reporter_util = zope.component.getUtility(interfaces.IReporter)
|
||||
reporter_util.add_message(
|
||||
"To obtain a new certificate that {0} an existing certificate "
|
||||
"in its domain-name coverage, you must use the --duplicate "
|
||||
"option.{br}{br}For example:{br}{br}{1} --duplicate {2}".format(
|
||||
"duplicates" if ident_names_cert is not None else
|
||||
"overlaps with",
|
||||
sys.argv[0], " ".join(sys.argv[1:]),
|
||||
br=os.linesep
|
||||
),
|
||||
reporter_util.HIGH_PRIORITY)
|
||||
raise errors.Error(
|
||||
"User did not use proper CLI and would like "
|
||||
"to reinvoke the client.")
|
||||
|
||||
if renewal:
|
||||
return ident_names_cert if ident_names_cert is not None else subset_names_cert
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _report_new_cert(cert_path):
|
||||
"""Reports the creation of a new certificate to the user."""
|
||||
reporter_util = zope.component.getUtility(interfaces.IReporter)
|
||||
reporter_util.add_message("Congratulations! Your certificate has been "
|
||||
"saved at {0}.".format(cert_path),
|
||||
reporter_util.MEDIUM_PRIORITY)
|
||||
|
||||
|
||||
def _auth_from_domains(le_client, config, domains, plugins):
|
||||
"""Authenticate and enroll certificate."""
|
||||
# Note: This can raise errors... caught above us though.
|
||||
lineage = _treat_as_renewal(config, domains)
|
||||
|
||||
if lineage is not None:
|
||||
# TODO: schoen wishes to reuse key - discussion
|
||||
# https://github.com/letsencrypt/letsencrypt/pull/777/files#r40498574
|
||||
new_certr, new_chain, new_key, _ = le_client.obtain_certificate(domains)
|
||||
# TODO: Check whether it worked! <- or make sure errors are thrown (jdk)
|
||||
lineage.save_successor(
|
||||
lineage.latest_common_version(), OpenSSL.crypto.dump_certificate(
|
||||
OpenSSL.crypto.FILETYPE_PEM, new_certr.body),
|
||||
new_key.pem, crypto_util.dump_pyopenssl_chain(new_chain))
|
||||
|
||||
lineage.update_all_links_to(lineage.latest_common_version())
|
||||
# TODO: Check return value of save_successor
|
||||
# TODO: Also update lineage renewal config with any relevant
|
||||
# configuration values from this attempt? <- Absolutely (jdkasten)
|
||||
else:
|
||||
# TREAT AS NEW REQUEST
|
||||
lineage = le_client.obtain_and_enroll_certificate(domains, plugins)
|
||||
if not lineage:
|
||||
raise errors.Error("Certificate could not be obtained")
|
||||
|
||||
_report_new_cert(lineage.cert)
|
||||
|
||||
return lineage
|
||||
|
||||
|
||||
# TODO: Make run as close to auth + install as possible
|
||||
# Possible difficulties: args.csr was hacked into auth
|
||||
def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-locals
|
||||
"""Obtain a certificate and install."""
|
||||
# Begin authenticator and installer setup
|
||||
if args.configurator is not None and (args.installer is not None or
|
||||
args.authenticator is not None):
|
||||
return ("Either --configurator or --authenticator/--installer"
|
||||
@@ -216,92 +328,28 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo
|
||||
|
||||
if installer is None or authenticator is None:
|
||||
return "Configurator could not be determined"
|
||||
# End authenticator and installer setup
|
||||
|
||||
domains = _find_domains(args, installer)
|
||||
|
||||
treat_as_renewal = False
|
||||
|
||||
# Considering the possibility that the requested certificate is
|
||||
# related to an existing certificate. (config.duplicate, which
|
||||
# is set with --duplicate, skips all of this logic and forces any
|
||||
# kind of certificate to be obtained with treat_as_renewal = False.)
|
||||
if not config.duplicate:
|
||||
identical_names_cert, subset_names_cert = _find_duplicative_certs(
|
||||
domains, config, configuration.RenewerConfiguration(config))
|
||||
# I am not sure whether that correctly reads the systemwide
|
||||
# configuration file.
|
||||
question = None
|
||||
if identical_names_cert is not None:
|
||||
question = (
|
||||
"You have an existing certificate that contains exactly the "
|
||||
"same domains you requested (ref: {0})\n\nDo you want to "
|
||||
"renew and replace this certificate with a newly-issued one?"
|
||||
).format(identical_names_cert.configfile.filename)
|
||||
elif subset_names_cert is not None:
|
||||
question = (
|
||||
"You have an existing certificate that contains a portion of "
|
||||
"the domains you requested (ref: {0})\n\nIt contains these "
|
||||
"names: {1}\n\nYou requested these names for the new "
|
||||
"certificate: {2}.\n\nDo you want to replace this existing "
|
||||
"certificate with the new certificate?"
|
||||
).format(subset_names_cert.configfile.filename,
|
||||
", ".join(subset_names_cert.names()),
|
||||
", ".join(domains))
|
||||
if question is None:
|
||||
# We aren't in a duplicative-names situation at all, so we don't
|
||||
# have to tell or ask the user anything about this.
|
||||
pass
|
||||
elif zope.component.getUtility(interfaces.IDisplay).yesno(
|
||||
question, "Replace", "Cancel"):
|
||||
treat_as_renewal = True
|
||||
else:
|
||||
reporter_util = zope.component.getUtility(interfaces.IReporter)
|
||||
reporter_util.add_message(
|
||||
"To obtain a new certificate that {0} an existing certificate "
|
||||
"in its domain-name coverage, you must use the --duplicate "
|
||||
"option.\n\nFor example:\n\n{1} --duplicate {2}".format(
|
||||
"duplicates" if identical_names_cert is not None else
|
||||
"overlaps with", sys.argv[0], " ".join(sys.argv[1:])),
|
||||
reporter_util.HIGH_PRIORITY)
|
||||
return 1
|
||||
|
||||
# Attempting to obtain the certificate
|
||||
# TODO: Handle errors from _init_le_client?
|
||||
le_client = _init_le_client(args, config, authenticator, installer)
|
||||
if treat_as_renewal:
|
||||
lineage = identical_names_cert if identical_names_cert is not None else subset_names_cert
|
||||
# TODO: Use existing privkey instead of generating a new one
|
||||
new_certr, new_chain, new_key, _ = le_client.obtain_certificate(domains)
|
||||
# TODO: Check whether it worked!
|
||||
lineage.save_successor(
|
||||
lineage.latest_common_version(), OpenSSL.crypto.dump_certificate(
|
||||
OpenSSL.crypto.FILETYPE_PEM, new_certr.body),
|
||||
new_key.pem, crypto_util.dump_pyopenssl_chain(new_chain))
|
||||
|
||||
lineage.update_all_links_to(lineage.latest_common_version())
|
||||
# TODO: Check return value of save_successor
|
||||
# TODO: Also update lineage renewal config with any relevant
|
||||
# configuration values from this attempt?
|
||||
le_client.deploy_certificate(
|
||||
domains, lineage.privkey, lineage.cert, lineage.chain)
|
||||
display_ops.success_renewal(domains)
|
||||
else:
|
||||
# TREAT AS NEW REQUEST
|
||||
lineage = le_client.obtain_and_enroll_certificate(
|
||||
domains, authenticator, installer, plugins)
|
||||
if not lineage:
|
||||
return "Certificate could not be obtained"
|
||||
# TODO: This treats the key as changed even when it wasn't
|
||||
# TODO: We also need to pass the fullchain (for Nginx)
|
||||
le_client.deploy_certificate(
|
||||
domains, lineage.privkey, lineage.cert, lineage.chain)
|
||||
le_client.enhance_config(domains, args.redirect)
|
||||
lineage = _auth_from_domains(le_client, config, domains, plugins)
|
||||
|
||||
# TODO: We also need to pass the fullchain (for Nginx)
|
||||
le_client.deploy_certificate(
|
||||
domains, lineage.privkey, lineage.cert, lineage.chain)
|
||||
le_client.enhance_config(domains, args.redirect)
|
||||
|
||||
if len(lineage.available_versions("cert")) == 1:
|
||||
display_ops.success_installation(domains)
|
||||
else:
|
||||
display_ops.success_renewal(domains)
|
||||
|
||||
|
||||
def auth(args, config, plugins):
|
||||
"""Authenticate & obtain cert, but do not install it."""
|
||||
# XXX: Update for renewer / RenewableCert
|
||||
|
||||
if args.domains is not None and args.csr is not None:
|
||||
# TODO: --csr could have a priority, when --domains is
|
||||
@@ -321,16 +369,16 @@ def auth(args, config, plugins):
|
||||
# TODO: Handle errors from _init_le_client?
|
||||
le_client = _init_le_client(args, config, authenticator, installer)
|
||||
|
||||
# This is a special case; cert and chain are simply saved
|
||||
if args.csr is not None:
|
||||
certr, chain = le_client.obtain_certificate_from_csr(le_util.CSR(
|
||||
file=args.csr[0], data=args.csr[1], form="der"))
|
||||
le_client.save_certificate(
|
||||
certr, chain, args.cert_path, args.chain_path)
|
||||
_report_new_cert(args.cert_path)
|
||||
else:
|
||||
domains = _find_domains(args, installer)
|
||||
if not le_client.obtain_and_enroll_certificate(
|
||||
domains, authenticator, installer, plugins):
|
||||
return "Certificate could not be obtained"
|
||||
_auth_from_domains(le_client, config, domains, plugins)
|
||||
|
||||
|
||||
def install(args, config, plugins):
|
||||
@@ -383,7 +431,7 @@ def plugins_cmd(args, config, plugins): # TODO: Use IDisplay rather than print
|
||||
logger.debug("Expected interfaces: %s", args.ifaces)
|
||||
|
||||
ifaces = [] if args.ifaces is None else args.ifaces
|
||||
filtered = plugins.ifaces(ifaces)
|
||||
filtered = plugins.visible().ifaces(ifaces)
|
||||
logger.debug("Filtered plugins: %r", filtered)
|
||||
|
||||
if not args.init and not args.prepare:
|
||||
@@ -452,9 +500,6 @@ class SilentParser(object): # pylint: disable=too-few-public-methods
|
||||
self.parser.add_argument(*args, **kwargs)
|
||||
|
||||
|
||||
HELP_TOPICS = ["all", "security", "paths", "automation", "testing", "plugins"]
|
||||
|
||||
|
||||
class HelpfulArgumentParser(object):
|
||||
"""Argparse Wrapper.
|
||||
|
||||
@@ -476,12 +521,13 @@ class HelpfulArgumentParser(object):
|
||||
self.parser._add_config_file_help = False # pylint: disable=protected-access
|
||||
self.silent_parser = SilentParser(self.parser)
|
||||
|
||||
self.verb = None
|
||||
self.args = self.preprocess_args(args)
|
||||
help1 = self.prescan_for_flag("-h", self.help_topics)
|
||||
help2 = self.prescan_for_flag("--help", self.help_topics)
|
||||
assert max(True, "a") == "a", "Gravity changed direction"
|
||||
help_arg = max(help1, help2)
|
||||
if help_arg:
|
||||
if help_arg is True:
|
||||
# just --help with no topic; avoid argparse altogether
|
||||
print USAGE
|
||||
sys.exit(0)
|
||||
@@ -492,13 +538,22 @@ class HelpfulArgumentParser(object):
|
||||
def preprocess_args(self, args):
|
||||
"""Work around some limitations in argparse.
|
||||
|
||||
Currently, add the default verb "run" as a default.
|
||||
Currently: add the default verb "run" as a default, and ensure that the
|
||||
subcommand / verb comes last.
|
||||
"""
|
||||
if "-h" in args or "--help" in args:
|
||||
# all verbs double as help arguments; don't get them confused
|
||||
self.verb = "help"
|
||||
return args
|
||||
|
||||
for token in args:
|
||||
for i, token in enumerate(args):
|
||||
if token in VERBS:
|
||||
return args
|
||||
return ["run"] + args
|
||||
reordered = args[:i] + args[i+1:] + [args[i]]
|
||||
self.verb = token
|
||||
return reordered
|
||||
|
||||
self.verb = "run"
|
||||
return args + ["run"]
|
||||
|
||||
def prescan_for_flag(self, flag, possible_arguments):
|
||||
"""Checks cli input for flags.
|
||||
@@ -618,8 +673,9 @@ def create_parser(plugins, args):
|
||||
version="%(prog)s {0}".format(letsencrypt.__version__),
|
||||
help="show program's version number and exit")
|
||||
helpful.add(
|
||||
"automation", "--no-confirm", dest="no_confirm", action="store_true",
|
||||
help="Turn off confirmation screens, currently used for --revoke")
|
||||
"automation", "--renew-by-default", action="store_true",
|
||||
help="Select renewal by default when domains are a superset of a "
|
||||
"a previously attained cert")
|
||||
helpful.add(
|
||||
"automation", "--agree-eula", dest="eula", action="store_true",
|
||||
help="Agree to the Let's Encrypt Developer Preview EULA")
|
||||
@@ -646,8 +702,6 @@ def create_parser(plugins, args):
|
||||
help=config_help("dvsni_port"))
|
||||
helpful.add("testing", "--simple-http-port", type=int,
|
||||
help=config_help("simple_http_port"))
|
||||
helpful.add("testing", "--no-simple-http-tls", action="store_true",
|
||||
help=config_help("no_simple_http_tls"))
|
||||
|
||||
helpful.add_group(
|
||||
"security", description="Security parameters & server settings")
|
||||
@@ -673,79 +727,84 @@ def create_parser(plugins, args):
|
||||
|
||||
return helpful.parser, helpful.args
|
||||
|
||||
|
||||
# For now unfortunately this constant just needs to match the code below;
|
||||
# there isn't an elegant way to autogenerate it in time.
|
||||
VERBS = ["run", "auth", "install", "revoke", "rollback", "config_changes",
|
||||
"plugins"]
|
||||
VERBS = ["run", "auth", "install", "revoke", "rollback", "config_changes", "plugins"]
|
||||
HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + VERBS
|
||||
|
||||
|
||||
def _create_subparsers(helpful):
|
||||
subparsers = helpful.parser.add_subparsers(metavar="SUBCOMMAND")
|
||||
|
||||
def add_subparser(name, func): # pylint: disable=missing-docstring
|
||||
subparser = subparsers.add_parser(
|
||||
name, help=func.__doc__.splitlines()[0], description=func.__doc__)
|
||||
def add_subparser(name): # pylint: disable=missing-docstring
|
||||
if name == "plugins":
|
||||
func = plugins_cmd
|
||||
else:
|
||||
func = eval(name) # pylint: disable=eval-used
|
||||
h = func.__doc__.splitlines()[0]
|
||||
subparser = subparsers.add_parser(name, help=h, description=func.__doc__)
|
||||
subparser.set_defaults(func=func)
|
||||
return subparser
|
||||
|
||||
# the order of add_subparser() calls is important: it defines the
|
||||
# order in which subparser names will be displayed in --help
|
||||
add_subparser("run", run)
|
||||
parser_auth = add_subparser("auth", auth)
|
||||
parser_install = add_subparser("install", install)
|
||||
parser_revoke = add_subparser("revoke", revoke)
|
||||
parser_rollback = add_subparser("rollback", rollback)
|
||||
add_subparser("config_changes", config_changes)
|
||||
parser_plugins = add_subparser("plugins", plugins_cmd)
|
||||
# these add_subparser objects return objects to which arguments could be
|
||||
# attached, but they have annoying arg ordering constrains so we use
|
||||
# groups instead: https://github.com/letsencrypt/letsencrypt/issues/820
|
||||
for v in VERBS:
|
||||
add_subparser(v)
|
||||
|
||||
parser_auth.add_argument(
|
||||
"--csr", type=read_file, help="Path to a Certificate Signing "
|
||||
"Request (CSR) in DER format.")
|
||||
parser_auth.add_argument(
|
||||
"--cert-path", default=flag_default("auth_cert_path"),
|
||||
help="When using --csr this is where certificate is saved.")
|
||||
parser_auth.add_argument(
|
||||
"--chain-path", default=flag_default("auth_chain_path"),
|
||||
help="When using --csr this is where certificate chain is saved.")
|
||||
helpful.add_group("auth", description="Options for modifying how a cert is obtained")
|
||||
helpful.add_group("install", description="Options for modifying how a cert is deployed")
|
||||
helpful.add_group("revoke", description="Options for revocation of certs")
|
||||
helpful.add_group("rollback", description="Options for reverting config changes")
|
||||
helpful.add_group("plugins", description="Plugin options")
|
||||
|
||||
parser_install.add_argument(
|
||||
"--cert-path", required=True, help="Path to a certificate that "
|
||||
"is going to be installed.")
|
||||
parser_install.add_argument(
|
||||
"--key-path", required=True, help="Accompanying private key")
|
||||
parser_install.add_argument(
|
||||
"--chain-path", help="Accompanying path to a certificate chain.")
|
||||
parser_revoke.add_argument(
|
||||
"--cert-path", type=read_file, help="Revoke a specific certificate.",
|
||||
required=True)
|
||||
parser_revoke.add_argument(
|
||||
"--key-path", type=read_file,
|
||||
help="Revoke certificate using its accompanying key. Useful if "
|
||||
"Account Key is lost.")
|
||||
helpful.add("auth",
|
||||
"--csr", type=read_file,
|
||||
help="Path to a Certificate Signing Request (CSR) in DER format.")
|
||||
helpful.add("rollback",
|
||||
"--checkpoints", type=int, metavar="N",
|
||||
default=flag_default("rollback_checkpoints"),
|
||||
help="Revert configuration N number of checkpoints.")
|
||||
|
||||
parser_rollback.add_argument(
|
||||
"--checkpoints", type=int, metavar="N",
|
||||
default=flag_default("rollback_checkpoints"),
|
||||
help="Revert configuration N number of checkpoints.")
|
||||
|
||||
parser_plugins.add_argument(
|
||||
"--init", action="store_true", help="Initialize plugins.")
|
||||
parser_plugins.add_argument(
|
||||
"--prepare", action="store_true",
|
||||
help="Initialize and prepare plugins.")
|
||||
parser_plugins.add_argument(
|
||||
"--authenticators", action="append_const", dest="ifaces",
|
||||
const=interfaces.IAuthenticator,
|
||||
help="Limit to authenticator plugins only.")
|
||||
parser_plugins.add_argument(
|
||||
"--installers", action="append_const", dest="ifaces",
|
||||
const=interfaces.IInstaller, help="Limit to installer plugins only.")
|
||||
helpful.add("plugins",
|
||||
"--init", action="store_true", help="Initialize plugins.")
|
||||
helpful.add("plugins",
|
||||
"--prepare", action="store_true", help="Initialize and prepare plugins.")
|
||||
helpful.add("plugins",
|
||||
"--authenticators", action="append_const", dest="ifaces",
|
||||
const=interfaces.IAuthenticator, help="Limit to authenticator plugins only.")
|
||||
helpful.add("plugins",
|
||||
"--installers", action="append_const", dest="ifaces",
|
||||
const=interfaces.IInstaller, help="Limit to installer plugins only.")
|
||||
|
||||
|
||||
def _paths_parser(helpful):
|
||||
add = helpful.add
|
||||
verb = helpful.verb
|
||||
helpful.add_group(
|
||||
"paths", description="Arguments changing execution paths & servers")
|
||||
|
||||
cph = "Path to where cert is saved (with auth), installed (with install --csr) or revoked."
|
||||
if verb == "auth":
|
||||
add("paths", "--cert-path", default=flag_default("auth_cert_path"), help=cph)
|
||||
elif verb == "revoke":
|
||||
add("paths", "--cert-path", type=read_file, required=True, help=cph)
|
||||
else:
|
||||
add("paths", "--cert-path", help=cph, required=(verb == "install"))
|
||||
|
||||
# revoke --key-path reads a file, install --key-path takes a string
|
||||
add("paths", "--key-path", type=((verb == "revoke" and read_file) or str),
|
||||
required=(verb == "install"),
|
||||
help="Path to private key for cert creation or revocation (if account key is missing)")
|
||||
|
||||
default_cp = None
|
||||
if verb == "auth":
|
||||
default_cp = flag_default("auth_chain_path")
|
||||
add("paths", "--chain-path", default=default_cp,
|
||||
help="Accompanying path to a certificate chain.")
|
||||
add("paths", "--config-dir", default=flag_default("config_dir"),
|
||||
help=config_help("config_dir"))
|
||||
add("paths", "--work-dir", default=flag_default("work_dir"),
|
||||
@@ -784,7 +843,7 @@ def _setup_logging(args):
|
||||
level = -args.verbose_count * 10
|
||||
fmt = "%(asctime)s:%(levelname)s:%(name)s:%(message)s"
|
||||
if args.text_mode:
|
||||
handler = logging.StreamHandler()
|
||||
handler = colored_logging.StreamHandler()
|
||||
handler.setFormatter(logging.Formatter(fmt))
|
||||
else:
|
||||
handler = log.DialogHandler()
|
||||
@@ -829,7 +888,8 @@ def _handle_exception(exc_type, exc_value, trace, args):
|
||||
|
||||
"""
|
||||
logger.debug(
|
||||
"Exiting abnormally:\n%s",
|
||||
"Exiting abnormally:%s%s",
|
||||
os.linesep,
|
||||
"".join(traceback.format_exception(exc_type, exc_value, trace)))
|
||||
|
||||
if issubclass(exc_type, Exception) and (args is None or not args.debug):
|
||||
|
||||
@@ -18,10 +18,10 @@ from letsencrypt import constants
|
||||
from letsencrypt import continuity_auth
|
||||
from letsencrypt import crypto_util
|
||||
from letsencrypt import errors
|
||||
from letsencrypt import error_handler
|
||||
from letsencrypt import interfaces
|
||||
from letsencrypt import le_util
|
||||
from letsencrypt import reverter
|
||||
from letsencrypt import revoker
|
||||
from letsencrypt import storage
|
||||
|
||||
from letsencrypt.display import ops as display_ops
|
||||
@@ -111,6 +111,8 @@ class Client(object):
|
||||
:ivar .AuthHandler auth_handler: Authorizations handler that will
|
||||
dispatch DV and Continuity challenges to appropriate
|
||||
authenticators (providing `.IAuthenticator` interface).
|
||||
:ivar .IAuthenticator dv_auth: Prepared (`.IAuthenticator.prepare`)
|
||||
authenticator that can solve the `.constants.DV_CHALLENGES`.
|
||||
:ivar .IInstaller installer: Installer.
|
||||
:ivar acme.client.Client acme: Optional ACME client API handle.
|
||||
You might already have one from `register`.
|
||||
@@ -118,14 +120,10 @@ class Client(object):
|
||||
"""
|
||||
|
||||
def __init__(self, config, account_, dv_auth, installer, acme=None):
|
||||
"""Initialize a client.
|
||||
|
||||
:param .IAuthenticator dv_auth: Prepared (`.IAuthenticator.prepare`)
|
||||
authenticator that can solve the `.constants.DV_CHALLENGES`.
|
||||
|
||||
"""
|
||||
"""Initialize a client."""
|
||||
self.config = config
|
||||
self.account = account_
|
||||
self.dv_auth = dv_auth
|
||||
self.installer = installer
|
||||
|
||||
# Initialize ACME if account is provided
|
||||
@@ -211,12 +209,11 @@ class Client(object):
|
||||
# Create CSR from names
|
||||
key = crypto_util.init_save_key(
|
||||
self.config.rsa_key_size, self.config.key_dir)
|
||||
csr = crypto_util.init_save_csr(key, domains, self.config.cert_dir)
|
||||
csr = crypto_util.init_save_csr(key, domains, self.config.csr_dir)
|
||||
|
||||
return self._obtain_certificate(domains, csr) + (key, csr)
|
||||
|
||||
def obtain_and_enroll_certificate(
|
||||
self, domains, authenticator, installer, plugins):
|
||||
def obtain_and_enroll_certificate(self, domains, plugins):
|
||||
"""Obtain and enroll certificate.
|
||||
|
||||
Get a new certificate for the specified domains using the specified
|
||||
@@ -224,12 +221,6 @@ class Client(object):
|
||||
containing it.
|
||||
|
||||
:param list domains: Domains to request.
|
||||
:param authenticator: The authenticator to use.
|
||||
:type authenticator: :class:`letsencrypt.interfaces.IAuthenticator`
|
||||
|
||||
:param installer: The installer to use.
|
||||
:type installer: :class:`letsencrypt.interfaces.IInstaller`
|
||||
|
||||
:param plugins: A PluginsFactory object.
|
||||
|
||||
:returns: A new :class:`letsencrypt.storage.RenewableCert` instance
|
||||
@@ -241,9 +232,10 @@ class Client(object):
|
||||
|
||||
# TODO: remove this dirty hack
|
||||
self.config.namespace.authenticator = plugins.find_init(
|
||||
authenticator).name
|
||||
if installer is not None:
|
||||
self.config.namespace.installer = plugins.find_init(installer).name
|
||||
self.dv_auth).name
|
||||
if self.installer is not None:
|
||||
self.config.namespace.installer = plugins.find_init(
|
||||
self.installer).name
|
||||
|
||||
# XXX: We clearly need a more general and correct way of getting
|
||||
# options into the configobj for the RenewableCert instance.
|
||||
@@ -276,25 +268,21 @@ class Client(object):
|
||||
:param .RenewableCert cert: Newly issued certificate
|
||||
|
||||
"""
|
||||
if ("autorenew" not in cert.configuration or
|
||||
cert.configuration.as_bool("autorenew")):
|
||||
if ("autodeploy" not in cert.configuration or
|
||||
cert.configuration.as_bool("autodeploy")):
|
||||
if cert.autorenewal_is_enabled():
|
||||
if cert.autodeployment_is_enabled():
|
||||
msg = "Automatic renewal and deployment has "
|
||||
else:
|
||||
msg = "Automatic renewal but not automatic deployment has "
|
||||
elif cert.autodeployment_is_enabled():
|
||||
msg = "Automatic deployment but not automatic renewal has "
|
||||
else:
|
||||
if ("autodeploy" not in cert.configuration or
|
||||
cert.configuration.as_bool("autodeploy")):
|
||||
msg = "Automatic deployment but not automatic renewal has "
|
||||
else:
|
||||
msg = "Automatic renewal and deployment has not "
|
||||
msg = "Automatic renewal and deployment has not "
|
||||
|
||||
msg += ("been enabled for your certificate. These settings can be "
|
||||
"configured in the directories under {0}.").format(
|
||||
cert.cli_config.renewal_configs_dir)
|
||||
reporter = zope.component.getUtility(interfaces.IReporter)
|
||||
reporter.add_message(msg, reporter.LOW_PRIORITY, True)
|
||||
reporter.add_message(msg, reporter.LOW_PRIORITY)
|
||||
|
||||
def save_certificate(self, certr, chain_cert, cert_path, chain_path):
|
||||
# pylint: disable=no-self-use
|
||||
@@ -364,16 +352,17 @@ class Client(object):
|
||||
|
||||
chain_path = None if chain_path is None else os.path.abspath(chain_path)
|
||||
|
||||
for dom in domains:
|
||||
# TODO: Provide a fullchain reference for installers like
|
||||
# nginx that want it
|
||||
self.installer.deploy_cert(
|
||||
dom, os.path.abspath(cert_path),
|
||||
os.path.abspath(privkey_path), chain_path)
|
||||
with error_handler.ErrorHandler(self.installer.recovery_routine):
|
||||
for dom in domains:
|
||||
# TODO: Provide a fullchain reference for installers like
|
||||
# nginx that want it
|
||||
self.installer.deploy_cert(
|
||||
dom, os.path.abspath(cert_path),
|
||||
os.path.abspath(privkey_path), chain_path)
|
||||
|
||||
self.installer.save("Deployed Let's Encrypt Certificate")
|
||||
# sites may have been enabled / final cleanup
|
||||
self.installer.restart()
|
||||
self.installer.save("Deployed Let's Encrypt Certificate")
|
||||
# sites may have been enabled / final cleanup
|
||||
self.installer.restart()
|
||||
|
||||
def enhance_config(self, domains, redirect=None):
|
||||
"""Enhance the configuration.
|
||||
@@ -399,6 +388,8 @@ class Client(object):
|
||||
if redirect is None:
|
||||
redirect = enhancements.ask("redirect")
|
||||
|
||||
# When support for more enhancements are added, the call to the
|
||||
# plugin's `enhance` function should be wrapped by an ErrorHandler
|
||||
if redirect:
|
||||
self.redirect_to_ssl(domains)
|
||||
|
||||
@@ -409,14 +400,16 @@ class Client(object):
|
||||
:type vhost: :class:`letsencrypt.interfaces.IInstaller`
|
||||
|
||||
"""
|
||||
for dom in domains:
|
||||
try:
|
||||
self.installer.enhance(dom, "redirect")
|
||||
except errors.PluginError:
|
||||
logger.warn("Unable to perform redirect for %s", dom)
|
||||
with error_handler.ErrorHandler(self.installer.recovery_routine):
|
||||
for dom in domains:
|
||||
try:
|
||||
self.installer.enhance(dom, "redirect")
|
||||
except errors.PluginError:
|
||||
logger.warn("Unable to perform redirect for %s", dom)
|
||||
raise
|
||||
|
||||
self.installer.save("Add Redirects")
|
||||
self.installer.restart()
|
||||
self.installer.save("Add Redirects")
|
||||
self.installer.restart()
|
||||
|
||||
|
||||
def validate_key_csr(privkey, csr=None):
|
||||
@@ -485,27 +478,6 @@ def rollback(default_installer, checkpoints, config, plugins):
|
||||
installer.restart()
|
||||
|
||||
|
||||
def revoke(default_installer, config, plugins, no_confirm, cert, authkey):
|
||||
"""Revoke certificates.
|
||||
|
||||
:param config: Configuration.
|
||||
:type config: :class:`letsencrypt.interfaces.IConfig`
|
||||
|
||||
"""
|
||||
installer = display_ops.pick_installer(
|
||||
config, default_installer, plugins, question="Which installer "
|
||||
"should be used for certificate revocation?")
|
||||
|
||||
revoc = revoker.Revoker(installer, config, no_confirm)
|
||||
# Cert is most selective, so it is chosen first.
|
||||
if cert is not None:
|
||||
revoc.revoke_from_cert(cert[0])
|
||||
elif authkey is not None:
|
||||
revoc.revoke_from_key(le_util.Key(authkey[0], authkey[1]))
|
||||
else:
|
||||
revoc.revoke_from_menu()
|
||||
|
||||
|
||||
def view_config_changes(config):
|
||||
"""View checkpoints and associated configuration changes.
|
||||
|
||||
|
||||
40
letsencrypt/colored_logging.py
Normal file
40
letsencrypt/colored_logging.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""A formatter and StreamHandler for colorizing logging output."""
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from letsencrypt import le_util
|
||||
|
||||
|
||||
class StreamHandler(logging.StreamHandler):
|
||||
"""Sends colored logging output to a stream.
|
||||
|
||||
If the specified stream is not a tty, the class works like the
|
||||
standard logging.StreamHandler. Default red_level is logging.WARNING.
|
||||
|
||||
:ivar bool colored: True if output should be colored
|
||||
:ivar bool red_level: The level at which to output
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, stream=None):
|
||||
super(StreamHandler, self).__init__(stream)
|
||||
self.colored = (sys.stderr.isatty() if stream is None else
|
||||
stream.isatty())
|
||||
self.red_level = logging.WARNING
|
||||
|
||||
def format(self, record):
|
||||
"""Formats the string representation of record.
|
||||
|
||||
:param logging.LogRecord record: Record to be formatted
|
||||
|
||||
:returns: Formatted, string representation of record
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
output = super(StreamHandler, self).format(record)
|
||||
if self.colored and record.levelno >= self.red_level:
|
||||
return ''.join((le_util.ANSI_SGR_RED,
|
||||
output,
|
||||
le_util.ANSI_SGR_RESET))
|
||||
else:
|
||||
return output
|
||||
@@ -18,8 +18,7 @@ class NamespaceConfig(object):
|
||||
paths defined in :py:mod:`letsencrypt.constants`:
|
||||
|
||||
- `accounts_dir`
|
||||
- `cert_dir`
|
||||
- `cert_key_backup`
|
||||
- `csr_dir`
|
||||
- `in_progress_dir`
|
||||
- `key_dir`
|
||||
- `renewer_config_file`
|
||||
@@ -54,13 +53,8 @@ class NamespaceConfig(object):
|
||||
return os.path.join(self.namespace.work_dir, constants.BACKUP_DIR)
|
||||
|
||||
@property
|
||||
def cert_dir(self): # pylint: disable=missing-docstring
|
||||
return os.path.join(self.namespace.config_dir, constants.CERT_DIR)
|
||||
|
||||
@property
|
||||
def cert_key_backup(self): # pylint: disable=missing-docstring
|
||||
return os.path.join(self.namespace.work_dir,
|
||||
constants.CERT_KEY_BACKUP_DIR, self.server_path)
|
||||
def csr_dir(self): # pylint: disable=missing-docstring
|
||||
return os.path.join(self.namespace.config_dir, constants.CSR_DIR)
|
||||
|
||||
@property
|
||||
def in_progress_dir(self): # pylint: disable=missing-docstring
|
||||
|
||||
@@ -68,12 +68,8 @@ ACCOUNTS_DIR = "accounts"
|
||||
BACKUP_DIR = "backups"
|
||||
"""Directory (relative to `IConfig.work_dir`) where backups are kept."""
|
||||
|
||||
CERT_DIR = "certs"
|
||||
"""See `.IConfig.cert_dir`."""
|
||||
|
||||
CERT_KEY_BACKUP_DIR = "keys-certs"
|
||||
"""Directory where all certificates and keys are stored (relative to
|
||||
`IConfig.work_dir`). Used for easy revocation."""
|
||||
CSR_DIR = "csr"
|
||||
"""See `.IConfig.csr_dir`."""
|
||||
|
||||
IN_PROGRESS_DIR = "IN_PROGRESS"
|
||||
"""Directory used before a permanent checkpoint is finalized (relative to
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
is capable of handling the signatures.
|
||||
|
||||
"""
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
|
||||
@@ -201,29 +200,26 @@ def valid_privkey(privkey):
|
||||
return False
|
||||
|
||||
|
||||
def _pyopenssl_load(data, method, types=(
|
||||
OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)):
|
||||
openssl_errors = []
|
||||
for filetype in types:
|
||||
try:
|
||||
return method(filetype, data), filetype
|
||||
except OpenSSL.crypto.Error as error: # TODO: anything else?
|
||||
openssl_errors.append(error)
|
||||
raise errors.Error("Unable to load: {0}".format(",".join(
|
||||
str(error) for error in openssl_errors)))
|
||||
|
||||
|
||||
def pyopenssl_load_certificate(data):
|
||||
"""Load PEM/DER certificate.
|
||||
|
||||
:raises errors.Error:
|
||||
|
||||
"""
|
||||
return _pyopenssl_load(data, OpenSSL.crypto.load_certificate)
|
||||
|
||||
openssl_errors = []
|
||||
|
||||
for file_type in (OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1):
|
||||
try:
|
||||
return OpenSSL.crypto.load_certificate(file_type, data), file_type
|
||||
except OpenSSL.crypto.Error as error: # TODO: other errors?
|
||||
openssl_errors.append(error)
|
||||
raise errors.Error("Unable to load: {0}".format(",".join(
|
||||
str(error) for error in openssl_errors)))
|
||||
|
||||
|
||||
def _get_sans_from_cert_or_req(
|
||||
cert_or_req_str, load_func, typ=OpenSSL.crypto.FILETYPE_PEM):
|
||||
def _get_sans_from_cert_or_req(cert_or_req_str, load_func,
|
||||
typ=OpenSSL.crypto.FILETYPE_PEM):
|
||||
try:
|
||||
cert_or_req = load_func(typ, cert_or_req_str)
|
||||
except OpenSSL.crypto.Error as error:
|
||||
@@ -261,24 +257,6 @@ def get_sans_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM):
|
||||
csr, OpenSSL.crypto.load_certificate_request, typ)
|
||||
|
||||
|
||||
def asn1_generalizedtime_to_dt(timestamp):
|
||||
"""Convert ASN.1 GENERALIZEDTIME to datetime.
|
||||
|
||||
Useful for deserialization of `OpenSSL.crypto.X509.get_notAfter` and
|
||||
`OpenSSL.crypto.X509.get_notAfter` outputs.
|
||||
|
||||
.. todo:: This function support only one format: `%Y%m%d%H%M%SZ`.
|
||||
Implement remaining two.
|
||||
|
||||
"""
|
||||
return datetime.datetime.strptime(timestamp, '%Y%m%d%H%M%SZ')
|
||||
|
||||
|
||||
def pyopenssl_x509_name_as_text(x509name):
|
||||
"""Convert `OpenSSL.crypto.X509Name` to text."""
|
||||
return "/".join("{0}={1}" for key, value in x509name.get_components())
|
||||
|
||||
|
||||
def dump_pyopenssl_chain(chain, filetype=OpenSSL.crypto.FILETYPE_PEM):
|
||||
"""Dump certificate chain into a bundle.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from letsencrypt.display import util as display_util
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Define a helper function to avoid verbose code
|
||||
util = zope.component.getUtility # pylint: disable=invalid-name
|
||||
util = zope.component.getUtility
|
||||
|
||||
|
||||
def ask(enhancement):
|
||||
|
||||
@@ -12,7 +12,7 @@ from letsencrypt.display import util as display_util
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Define a helper function to avoid verbose code
|
||||
util = zope.component.getUtility # pylint: disable=invalid-name
|
||||
util = zope.component.getUtility
|
||||
|
||||
|
||||
def choose_plugin(prepared, question):
|
||||
@@ -65,7 +65,7 @@ def pick_plugin(config, default, plugins, question, ifaces):
|
||||
# throw more UX-friendly error if default not in plugins
|
||||
filtered = plugins.filter(lambda p_ep: p_ep.name == default)
|
||||
else:
|
||||
filtered = plugins.ifaces(ifaces)
|
||||
filtered = plugins.visible().ifaces(ifaces)
|
||||
|
||||
filtered.init(config)
|
||||
verified = filtered.verify(ifaces)
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
"""Revocation UI class."""
|
||||
import os
|
||||
|
||||
import zope.component
|
||||
|
||||
from letsencrypt import interfaces
|
||||
from letsencrypt.display import util as display_util
|
||||
|
||||
# Define a helper function to avoid verbose code
|
||||
util = zope.component.getUtility # pylint: disable=invalid-name
|
||||
|
||||
|
||||
def display_certs(certs):
|
||||
"""Display the certificates in a menu for revocation.
|
||||
|
||||
:param list certs: each is a :class:`letsencrypt.revoker.Cert`
|
||||
|
||||
:returns: tuple of the form (code, selection) where
|
||||
code is a display exit code
|
||||
selection is the user's int selection
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
list_choices = [
|
||||
"%s | %s | %s" % (
|
||||
str(cert.get_cn().ljust(display_util.WIDTH - 39)),
|
||||
cert.get_not_before().strftime("%m-%d-%y"),
|
||||
"Installed" if cert.installed and cert.installed != ["Unknown"]
|
||||
else "") for cert in certs
|
||||
]
|
||||
|
||||
code, tag = util(interfaces.IDisplay).menu(
|
||||
"Which certificates would you like to revoke?",
|
||||
list_choices, help_label="More Info", ok_label="Revoke",
|
||||
cancel_label="Exit")
|
||||
|
||||
return code, tag
|
||||
|
||||
|
||||
def confirm_revocation(cert):
|
||||
"""Confirm revocation screen.
|
||||
|
||||
:param cert: certificate object
|
||||
:type cert: :class:
|
||||
|
||||
:returns: True if user would like to revoke, False otherwise
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
return util(interfaces.IDisplay).yesno(
|
||||
"Are you sure you would like to revoke the following "
|
||||
"certificate:{0}{cert}This action cannot be reversed!".format(
|
||||
os.linesep, cert=cert.pretty_print()))
|
||||
|
||||
|
||||
def more_info_cert(cert):
|
||||
"""Displays more info about the cert.
|
||||
|
||||
:param dict cert: cert dict used throughout revoker.py
|
||||
|
||||
"""
|
||||
util(interfaces.IDisplay).notification(
|
||||
"Certificate Information:{0}{1}".format(
|
||||
os.linesep, cert.pretty_print()),
|
||||
height=display_util.HEIGHT)
|
||||
|
||||
|
||||
def success_revocation(cert):
|
||||
"""Display a success message.
|
||||
|
||||
:param cert: cert that was revoked
|
||||
:type cert: :class:`letsencrypt.revoker.Cert`
|
||||
|
||||
"""
|
||||
util(interfaces.IDisplay).notification(
|
||||
"You have successfully revoked the certificate for "
|
||||
"%s" % cert.get_cn())
|
||||
99
letsencrypt/error_handler.py
Normal file
99
letsencrypt/error_handler.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""Registers functions to be called if an exception or signal occurs."""
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import traceback
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# _SIGNALS stores the signals that will be handled by the ErrorHandler. These
|
||||
# signals were chosen as their default handler terminates the process and could
|
||||
# potentially occur from inside Python. Signals such as SIGILL were not
|
||||
# included as they could be a sign of something devious and we should terminate
|
||||
# immediately.
|
||||
_SIGNALS = ([signal.SIGTERM] if os.name == "nt" else
|
||||
[signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT,
|
||||
signal.SIGXCPU, signal.SIGXFSZ])
|
||||
|
||||
|
||||
class ErrorHandler(object):
|
||||
"""Registers functions to be called if an exception or signal occurs.
|
||||
|
||||
This class allows you to register functions that will be called when
|
||||
an exception (excluding SystemExit) or signal is encountered. The
|
||||
class works best as a context manager. For example:
|
||||
|
||||
with ErrorHandler(cleanup_func):
|
||||
do_something()
|
||||
|
||||
If an exception is raised out of do_something, cleanup_func will be
|
||||
called. The exception is not caught by the ErrorHandler. Similarly,
|
||||
if a signal is encountered, cleanup_func is called followed by the
|
||||
previously registered signal handler.
|
||||
|
||||
Every registered function is attempted to be run to completion
|
||||
exactly once. If a registered function raises an exception, it is
|
||||
logged and the next function is called. If a (different) handled
|
||||
signal occurs while calling a registered function, it is attempted
|
||||
to be called again by the next signal handler.
|
||||
|
||||
"""
|
||||
def __init__(self, func=None):
|
||||
self.funcs = []
|
||||
self.prev_handlers = {}
|
||||
if func is not None:
|
||||
self.register(func)
|
||||
|
||||
def __enter__(self):
|
||||
self.set_signal_handlers()
|
||||
|
||||
def __exit__(self, exec_type, exec_value, trace):
|
||||
# SystemExit is ignored to properly handle forks that don't exec
|
||||
if exec_type not in (None, SystemExit):
|
||||
logger.debug("Encountered exception:\n%s", "".join(
|
||||
traceback.format_exception(exec_type, exec_value, trace)))
|
||||
self.call_registered()
|
||||
self.reset_signal_handlers()
|
||||
|
||||
def register(self, func):
|
||||
"""Registers func to be called if an error occurs."""
|
||||
self.funcs.append(func)
|
||||
|
||||
def call_registered(self):
|
||||
"""Calls all registered functions"""
|
||||
logger.debug("Calling registered functions")
|
||||
while self.funcs:
|
||||
try:
|
||||
self.funcs[-1]()
|
||||
except Exception as error: # pylint: disable=broad-except
|
||||
logger.error("Encountered exception during recovery")
|
||||
logger.exception(error)
|
||||
self.funcs.pop()
|
||||
|
||||
def set_signal_handlers(self):
|
||||
"""Sets signal handlers for signals in _SIGNALS."""
|
||||
for signum in _SIGNALS:
|
||||
prev_handler = signal.getsignal(signum)
|
||||
# If prev_handler is None, the handler was set outside of Python
|
||||
if prev_handler is not None:
|
||||
self.prev_handlers[signum] = prev_handler
|
||||
signal.signal(signum, self._signal_handler)
|
||||
|
||||
def reset_signal_handlers(self):
|
||||
"""Resets signal handlers for signals in _SIGNALS."""
|
||||
for signum in self.prev_handlers:
|
||||
signal.signal(signum, self.prev_handlers[signum])
|
||||
self.prev_handlers.clear()
|
||||
|
||||
def _signal_handler(self, signum, unused_frame):
|
||||
"""Calls registered functions and the previous signal handler.
|
||||
|
||||
:param int signum: number of current signal
|
||||
|
||||
"""
|
||||
logger.debug("Singal %s encountered", signum)
|
||||
self.call_registered()
|
||||
signal.signal(signum, self.prev_handlers[signum])
|
||||
os.kill(os.getpid(), signum)
|
||||
@@ -205,12 +205,9 @@ class IConfig(zope.interface.Interface):
|
||||
accounts_dir = zope.interface.Attribute(
|
||||
"Directory where all account information is stored.")
|
||||
backup_dir = zope.interface.Attribute("Configuration backups directory.")
|
||||
cert_dir = zope.interface.Attribute(
|
||||
csr_dir = zope.interface.Attribute(
|
||||
"Directory where newly generated Certificate Signing Requests "
|
||||
"(CSRs) and certificates not enrolled in the renewer are saved.")
|
||||
cert_key_backup = zope.interface.Attribute(
|
||||
"Directory where all certificates and keys are stored. "
|
||||
"Used for easy revocation.")
|
||||
"(CSRs) are saved.")
|
||||
in_progress_dir = zope.interface.Attribute(
|
||||
"Directory used before a permanent checkpoint is finalized.")
|
||||
key_dir = zope.interface.Attribute("Keys storage.")
|
||||
@@ -226,8 +223,6 @@ class IConfig(zope.interface.Interface):
|
||||
"Port number to perform DVSNI challenge. "
|
||||
"Boulder in testing mode defaults to 5001.")
|
||||
|
||||
no_simple_http_tls = zope.interface.Attribute(
|
||||
"Do not use TLS when solving SimpleHTTP challenges.")
|
||||
simple_http_port = zope.interface.Attribute(
|
||||
"Port used in the SimpleHttp challenge.")
|
||||
|
||||
@@ -321,6 +316,17 @@ class IInstaller(IPlugin):
|
||||
|
||||
"""
|
||||
|
||||
def recovery_routine():
|
||||
"""Revert configuration to most recent finalized checkpoint.
|
||||
|
||||
Remove all changes (temporary and permanent) that have not been
|
||||
finalized. This is useful to protect against crashes and other
|
||||
execution interruptions.
|
||||
|
||||
:raises .errors.PluginError: If unable to recover the configuration
|
||||
|
||||
"""
|
||||
|
||||
def view_config_changes():
|
||||
"""Display all of the LE config changes.
|
||||
|
||||
@@ -470,7 +476,7 @@ class IReporter(zope.interface.Interface):
|
||||
LOW_PRIORITY = zope.interface.Attribute(
|
||||
"Used to denote low priority messages")
|
||||
|
||||
def add_message(self, msg, priority, on_crash=False):
|
||||
def add_message(self, msg, priority, on_crash=True):
|
||||
"""Adds msg to the list of messages to be printed.
|
||||
|
||||
:param str msg: Message to be displayed to the user.
|
||||
|
||||
@@ -18,6 +18,15 @@ Key = collections.namedtuple("Key", "file pem")
|
||||
CSR = collections.namedtuple("CSR", "file data form")
|
||||
|
||||
|
||||
# ANSI SGR escape codes
|
||||
# Formats text as bold or with increased intensity
|
||||
ANSI_SGR_BOLD = '\033[1m'
|
||||
# Colors text red
|
||||
ANSI_SGR_RED = "\033[31m"
|
||||
# Resets output format
|
||||
ANSI_SGR_RESET = "\033[0m"
|
||||
|
||||
|
||||
def run_script(params):
|
||||
"""Run the script with the given params.
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class DialogHandler(logging.Handler): # pylint: disable=too-few-public-methods
|
||||
logging.Handler.__init__(self, level)
|
||||
self.height = height
|
||||
self.width = width
|
||||
# "dialog" collides with module name... pylint: disable=invalid-name
|
||||
# "dialog" collides with module name...
|
||||
self.d = dialog.Dialog() if d is None else d
|
||||
self.lines = []
|
||||
|
||||
|
||||
@@ -23,10 +23,10 @@ def dest_namespace(name):
|
||||
"""ArgumentParser dest namespace (prefix of all destinations)."""
|
||||
return name.replace("-", "_") + "_"
|
||||
|
||||
private_ips_regex = re.compile( # pylint: disable=invalid-name
|
||||
private_ips_regex = re.compile(
|
||||
r"(^127\.0\.0\.1)|(^10\.)|(^172\.1[6-9]\.)|"
|
||||
r"(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.)")
|
||||
hostname_regex = re.compile( # pylint: disable=invalid-name
|
||||
hostname_regex = re.compile(
|
||||
r"^(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*[a-z]+$", re.IGNORECASE)
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ class Dvsni(object):
|
||||
achall.chall.encode("token") + '.pem')
|
||||
|
||||
def _setup_challenge_cert(self, achall, s=None):
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
"""Generate and write out challenge certificate."""
|
||||
cert_path = self.get_cert_path(achall)
|
||||
key_path = self.get_key_path(achall)
|
||||
|
||||
@@ -50,6 +50,11 @@ class PluginEntryPoint(object):
|
||||
"""Description with name. Handy for UI."""
|
||||
return "{0} ({1})".format(self.description, self.name)
|
||||
|
||||
@property
|
||||
def hidden(self):
|
||||
"""Should this plugin be hidden from UI?"""
|
||||
return getattr(self.plugin_cls, "hidden", False)
|
||||
|
||||
def ifaces(self, *ifaces_groups):
|
||||
"""Does plugin implements specified interface groups?"""
|
||||
return not ifaces_groups or any(
|
||||
@@ -183,6 +188,10 @@ class PluginsRegistry(collections.Mapping):
|
||||
return type(self)(dict((name, plugin_ep) for name, plugin_ep
|
||||
in self._plugins.iteritems() if pred(plugin_ep)))
|
||||
|
||||
def visible(self):
|
||||
"""Filter plugins based on visibility."""
|
||||
return self.filter(lambda plugin_ep: not plugin_ep.hidden)
|
||||
|
||||
def ifaces(self, *ifaces_groups):
|
||||
"""Filter plugins based on interfaces."""
|
||||
# pylint: disable=star-args
|
||||
|
||||
@@ -23,7 +23,7 @@ from letsencrypt.plugins import common
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ManualAuthenticator(common.Plugin):
|
||||
class Authenticator(common.Plugin):
|
||||
"""Manual Authenticator.
|
||||
|
||||
.. todo:: Support for `~.challenges.DVSNI`.
|
||||
@@ -53,7 +53,7 @@ command on the target server (as root):
|
||||
# served and makes it more obvious that Python command will serve
|
||||
# anything recursively under the cwd
|
||||
|
||||
HTTP_TEMPLATE = """\
|
||||
CMD_TEMPLATE = """\
|
||||
mkdir -p {root}/public_html/{response.URI_ROOT_PATH}
|
||||
cd {root}/public_html
|
||||
echo -n {validation} > {response.URI_ROOT_PATH}/{encoded_token}
|
||||
@@ -63,33 +63,10 @@ $(command -v python2 || command -v python2.7 || command -v python2.6) -c \\
|
||||
SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map = {{'': '{ct}'}}; \\
|
||||
s = BaseHTTPServer.HTTPServer(('', {port}), SimpleHTTPServer.SimpleHTTPRequestHandler); \\
|
||||
s.serve_forever()" """
|
||||
"""Non-TLS command template."""
|
||||
|
||||
# https://www.piware.de/2011/01/creating-an-https-server-in-python/
|
||||
HTTPS_TEMPLATE = """\
|
||||
mkdir -p {root}/public_html/{response.URI_ROOT_PATH}
|
||||
cd {root}/public_html
|
||||
echo -n {validation} > {response.URI_ROOT_PATH}/{encoded_token}
|
||||
# run only once per server:
|
||||
openssl req -new -newkey rsa:4096 -subj "/" -days 1 -nodes -x509 -keyout ../key.pem -out ../cert.pem
|
||||
$(command -v python2 || command -v python2.7 || command -v python2.6) -c \\
|
||||
"import BaseHTTPServer, SimpleHTTPServer, ssl; \\
|
||||
SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map = {{'': '{ct}'}}; \\
|
||||
s = BaseHTTPServer.HTTPServer(('', {port}), SimpleHTTPServer.SimpleHTTPRequestHandler); \\
|
||||
s.socket = ssl.wrap_socket(s.socket, keyfile='../key.pem', certfile='../cert.pem'); \\
|
||||
s.serve_forever()" """
|
||||
"""TLS command template.
|
||||
|
||||
According to the ACME specification, "the ACME server MUST ignore
|
||||
the certificate provided by the HTTPS server", so the first command
|
||||
generates temporary self-signed certificate.
|
||||
|
||||
"""
|
||||
"""Command template."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ManualAuthenticator, self).__init__(*args, **kwargs)
|
||||
self.template = (self.HTTP_TEMPLATE if self.config.no_simple_http_tls
|
||||
else self.HTTPS_TEMPLATE)
|
||||
super(Authenticator, self).__init__(*args, **kwargs)
|
||||
self._root = (tempfile.mkdtemp() if self.conf("test-mode")
|
||||
else "/tmp/letsencrypt")
|
||||
self._httpd = None
|
||||
@@ -97,8 +74,7 @@ s.serve_forever()" """
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add):
|
||||
add("test-mode", action="store_true",
|
||||
help="Test mode. Executes the manual command in subprocess. "
|
||||
"Requires openssl to be installed unless --no-simple-http-tls.")
|
||||
help="Test mode. Executes the manual command in subprocess.")
|
||||
|
||||
def prepare(self): # pylint: disable=missing-docstring,no-self-use
|
||||
pass # pragma: no cover
|
||||
@@ -142,11 +118,11 @@ binary for temporary key/certificate generation.""".replace("\n", "")
|
||||
# users, but will not work if multiple domains point at the
|
||||
# same server: default command doesn't support virtual hosts
|
||||
response, validation = achall.gen_response_and_validation(
|
||||
tls=(not self.config.no_simple_http_tls))
|
||||
tls=False) # SimpleHTTP TLS is dead: ietf-wg-acme/acme#7
|
||||
|
||||
port = (response.port if self.config.simple_http_port is None
|
||||
else int(self.config.simple_http_port))
|
||||
command = self.template.format(
|
||||
command = self.CMD_TEMPLATE.format(
|
||||
root=self._root, achall=achall, response=response,
|
||||
validation=pipes.quote(validation.json_dumps()),
|
||||
encoded_token=achall.chall.encode("token"),
|
||||
@@ -182,6 +158,8 @@ binary for temporary key/certificate generation.""".replace("\n", "")
|
||||
achall.account_key.public_key(), self.config.simple_http_port):
|
||||
return response
|
||||
else:
|
||||
logger.error(
|
||||
"Self-verify of challenge failed, authorization abandoned.")
|
||||
if self.conf("test-mode") and self._httpd.poll() is not None:
|
||||
# simply verify cause command failure...
|
||||
return False
|
||||
|
||||
@@ -17,22 +17,20 @@ from letsencrypt.tests import test_util
|
||||
KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
|
||||
|
||||
|
||||
class ManualAuthenticatorTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.plugins.manual.ManualAuthenticator."""
|
||||
class AuthenticatorTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.plugins.manual.Authenticator."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.plugins.manual import ManualAuthenticator
|
||||
from letsencrypt.plugins.manual import Authenticator
|
||||
self.config = mock.MagicMock(
|
||||
no_simple_http_tls=True, simple_http_port=4430,
|
||||
manual_test_mode=False)
|
||||
self.auth = ManualAuthenticator(config=self.config, name="manual")
|
||||
simple_http_port=8080, manual_test_mode=False)
|
||||
self.auth = Authenticator(config=self.config, name="manual")
|
||||
self.achalls = [achallenges.SimpleHTTP(
|
||||
challb=acme_util.SIMPLE_HTTP_P, domain="foo.com", account_key=KEY)]
|
||||
|
||||
config_test_mode = mock.MagicMock(
|
||||
no_simple_http_tls=True, simple_http_port=4430,
|
||||
manual_test_mode=True)
|
||||
self.auth_test_mode = ManualAuthenticator(
|
||||
simple_http_port=8080, manual_test_mode=True)
|
||||
self.auth_test_mode = Authenticator(
|
||||
config=config_test_mode, name="manual")
|
||||
|
||||
def test_more_info(self):
|
||||
@@ -55,7 +53,7 @@ class ManualAuthenticatorTest(unittest.TestCase):
|
||||
self.assertEqual([resp], self.auth.perform(self.achalls))
|
||||
self.assertEqual(1, mock_raw_input.call_count)
|
||||
mock_verify.assert_called_with(
|
||||
self.achalls[0].challb.chall, "foo.com", KEY.public_key(), 4430)
|
||||
self.achalls[0].challb.chall, "foo.com", KEY.public_key(), 8080)
|
||||
|
||||
message = mock_stdout.write.mock_calls[0][1][0]
|
||||
self.assertTrue(self.achalls[0].chall.encode("token") in message)
|
||||
@@ -68,7 +66,7 @@ class ManualAuthenticatorTest(unittest.TestCase):
|
||||
mock_popen.side_effect = OSError
|
||||
self.assertEqual([False], self.auth_test_mode.perform(self.achalls))
|
||||
|
||||
@mock.patch("letsencrypt.plugins.manual.socket.socket", autospec=True)
|
||||
@mock.patch("letsencrypt.plugins.manual.socket.socket")
|
||||
@mock.patch("letsencrypt.plugins.manual.time.sleep", autospec=True)
|
||||
@mock.patch("letsencrypt.plugins.manual.subprocess.Popen", autospec=True)
|
||||
def test_perform_test_command_run_failure(
|
||||
@@ -78,7 +76,7 @@ class ManualAuthenticatorTest(unittest.TestCase):
|
||||
self.assertRaises(
|
||||
errors.Error, self.auth_test_mode.perform, self.achalls)
|
||||
|
||||
@mock.patch("letsencrypt.plugins.manual.socket.socket", autospec=True)
|
||||
@mock.patch("letsencrypt.plugins.manual.socket.socket")
|
||||
@mock.patch("letsencrypt.plugins.manual.time.sleep", autospec=True)
|
||||
@mock.patch("acme.challenges.SimpleHTTPResponse.simple_verify",
|
||||
autospec=True)
|
||||
|
||||
@@ -17,6 +17,7 @@ class Installer(common.Plugin):
|
||||
zope.interface.classProvides(interfaces.IPluginFactory)
|
||||
|
||||
description = "Null Installer"
|
||||
hidden = True
|
||||
|
||||
# pylint: disable=missing-docstring,no-self-use
|
||||
|
||||
@@ -47,6 +48,9 @@ class Installer(common.Plugin):
|
||||
def rollback_checkpoints(self, rollback=1):
|
||||
pass # pragma: no cover
|
||||
|
||||
def recovery_routine(self):
|
||||
pass # pragma: no cover
|
||||
|
||||
def view_config_changes(self):
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import textwrap
|
||||
import zope.interface
|
||||
|
||||
from letsencrypt import interfaces
|
||||
from letsencrypt import le_util
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -30,14 +31,12 @@ class Reporter(object):
|
||||
LOW_PRIORITY = 2
|
||||
"""Low priority constant. See `add_message`."""
|
||||
|
||||
_RESET = '\033[0m'
|
||||
_BOLD = '\033[1m'
|
||||
_msg_type = collections.namedtuple('ReporterMsg', 'priority text on_crash')
|
||||
|
||||
def __init__(self):
|
||||
self.messages = Queue.PriorityQueue()
|
||||
|
||||
def add_message(self, msg, priority, on_crash=False):
|
||||
def add_message(self, msg, priority, on_crash=True):
|
||||
"""Adds msg to the list of messages to be printed.
|
||||
|
||||
:param str msg: Message to be displayed to the user.
|
||||
@@ -76,7 +75,7 @@ class Reporter(object):
|
||||
no_exception = sys.exc_info()[0] is None
|
||||
bold_on = sys.stdout.isatty()
|
||||
if bold_on:
|
||||
print self._BOLD
|
||||
print le_util.ANSI_SGR_BOLD
|
||||
print 'IMPORTANT NOTES:'
|
||||
first_wrapper = textwrap.TextWrapper(
|
||||
initial_indent=' - ', subsequent_indent=(' ' * 3))
|
||||
@@ -87,7 +86,7 @@ class Reporter(object):
|
||||
msg = self.messages.get()
|
||||
if no_exception or msg.on_crash:
|
||||
if bold_on and msg.priority > self.HIGH_PRIORITY:
|
||||
sys.stdout.write(self._RESET)
|
||||
sys.stdout.write(le_util.ANSI_SGR_RESET)
|
||||
bold_on = False
|
||||
lines = msg.text.splitlines()
|
||||
print first_wrapper.fill(lines[0])
|
||||
@@ -95,4 +94,4 @@ class Reporter(object):
|
||||
print "\n".join(
|
||||
next_wrapper.fill(line) for line in lines[1:])
|
||||
if bold_on:
|
||||
sys.stdout.write(self._RESET)
|
||||
sys.stdout.write(le_util.ANSI_SGR_RESET)
|
||||
|
||||
@@ -1,560 +0,0 @@
|
||||
"""Revoker module to enable LE revocations.
|
||||
|
||||
The backend of this module would fit a database quite nicely, but in order to
|
||||
minimize dependencies and maintain transparency, the class currently implements
|
||||
its own storage system. The number of certs that will likely be stored on any
|
||||
given client might not warrant requiring a database.
|
||||
|
||||
"""
|
||||
import collections
|
||||
import csv
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
import OpenSSL
|
||||
|
||||
from acme import client as acme_client
|
||||
from acme import crypto_util as acme_crypto_util
|
||||
from acme.jose import util as jose_util
|
||||
|
||||
from letsencrypt import crypto_util
|
||||
from letsencrypt import errors
|
||||
from letsencrypt import le_util
|
||||
|
||||
from letsencrypt.display import util as display_util
|
||||
from letsencrypt.display import revocation
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Revoker(object):
|
||||
"""A revocation class for LE.
|
||||
|
||||
.. todo:: Add a method to specify your own certificate for revocation - CLI
|
||||
|
||||
:ivar .acme.client.Client acme: ACME client
|
||||
|
||||
:ivar installer: Installer object
|
||||
:type installer: :class:`~letsencrypt.interfaces.IInstaller`
|
||||
|
||||
:ivar config: Configuration.
|
||||
:type config: :class:`~letsencrypt.interfaces.IConfig`
|
||||
|
||||
:ivar bool no_confirm: Whether or not to ask for confirmation for revocation
|
||||
|
||||
"""
|
||||
def __init__(self, installer, config, no_confirm=False):
|
||||
# XXX
|
||||
self.acme = acme_client.Client(directory=None, key=None, alg=None)
|
||||
|
||||
self.installer = installer
|
||||
self.config = config
|
||||
self.no_confirm = no_confirm
|
||||
|
||||
le_util.make_or_verify_dir(config.cert_key_backup, 0o700, os.geteuid(),
|
||||
self.config.strict_permissions)
|
||||
|
||||
# TODO: Find a better solution for this...
|
||||
self.list_path = os.path.join(config.cert_key_backup, "LIST")
|
||||
# Make sure that the file is available for use for rest of class
|
||||
open(self.list_path, "a").close()
|
||||
|
||||
def revoke_from_key(self, authkey):
|
||||
"""Revoke all certificates under an authorized key.
|
||||
|
||||
:param authkey: Authorized key used in previous transactions
|
||||
:type authkey: :class:`letsencrypt.le_util.Key`
|
||||
|
||||
"""
|
||||
certs = []
|
||||
try:
|
||||
clean_pem = OpenSSL.crypto.dump_privatekey(
|
||||
OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.load_privatekey(
|
||||
OpenSSL.crypto.FILETYPE_PEM, authkey.pem))
|
||||
except OpenSSL.crypto.Error as error:
|
||||
logger.debug(error, exc_info=True)
|
||||
raise errors.RevokerError(
|
||||
"Invalid key file specified to revoke_from_key")
|
||||
|
||||
with open(self.list_path, "rb") as csvfile:
|
||||
csvreader = csv.reader(csvfile)
|
||||
for row in csvreader:
|
||||
# idx, cert, key
|
||||
# Add all keys that match to marked list
|
||||
# Note: The key can be different than the pub key found in the
|
||||
# certificate.
|
||||
_, b_k = self._row_to_backup(row)
|
||||
try:
|
||||
test_pem = OpenSSL.crypto.dump_privatekey(
|
||||
OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.load_privatekey(
|
||||
OpenSSL.crypto.FILETYPE_PEM, open(b_k).read()))
|
||||
except OpenSSL.crypto.Error as error:
|
||||
logger.debug(error, exc_info=True)
|
||||
# This should never happen given the assumptions of the
|
||||
# module. If it does, it is probably best to delete the
|
||||
# the offending key/cert. For now... just raise an exception
|
||||
raise errors.RevokerError("%s - backup file is corrupted.")
|
||||
|
||||
if clean_pem == test_pem:
|
||||
certs.append(
|
||||
Cert.fromrow(row, self.config.cert_key_backup))
|
||||
if certs:
|
||||
self._safe_revoke(certs)
|
||||
else:
|
||||
logger.info("No certificates using the authorized key were found.")
|
||||
|
||||
def revoke_from_cert(self, cert_path):
|
||||
"""Revoke a certificate by specifying a file path.
|
||||
|
||||
.. todo:: Add the ability to revoke the certificate even if the cert
|
||||
is not stored locally. A path to the auth key will need to be
|
||||
attained from the user.
|
||||
|
||||
:param str cert_path: path to ACME certificate in pem form
|
||||
|
||||
"""
|
||||
# Locate the correct certificate (do not rely on filename)
|
||||
cert_to_revoke = Cert(cert_path)
|
||||
|
||||
with open(self.list_path, "rb") as csvfile:
|
||||
csvreader = csv.reader(csvfile)
|
||||
for row in csvreader:
|
||||
cert = Cert.fromrow(row, self.config.cert_key_backup)
|
||||
|
||||
if cert.get_der() == cert_to_revoke.get_der():
|
||||
self._safe_revoke([cert])
|
||||
return
|
||||
|
||||
logger.info("Associated ACME certificate was not found.")
|
||||
|
||||
def revoke_from_menu(self):
|
||||
"""List trusted Let's Encrypt certificates."""
|
||||
|
||||
csha1_vhlist = self._get_installed_locations()
|
||||
certs = self._populate_saved_certs(csha1_vhlist)
|
||||
|
||||
while True:
|
||||
if certs:
|
||||
code, selection = revocation.display_certs(certs)
|
||||
|
||||
if code == display_util.OK:
|
||||
revoked_certs = self._safe_revoke([certs[selection]])
|
||||
# Since we are currently only revoking one cert at a time...
|
||||
if revoked_certs:
|
||||
del certs[selection]
|
||||
elif code == display_util.HELP:
|
||||
revocation.more_info_cert(certs[selection])
|
||||
else:
|
||||
return
|
||||
else:
|
||||
logger.info(
|
||||
"There are not any trusted Let's Encrypt "
|
||||
"certificates for this server.")
|
||||
return
|
||||
|
||||
def _populate_saved_certs(self, csha1_vhlist):
|
||||
# pylint: disable=no-self-use
|
||||
"""Populate a list of all the saved certs.
|
||||
|
||||
It is important to read from the file rather than the directory.
|
||||
We assume that the LIST file is the master record and depending on
|
||||
program crashes, this may differ from what is actually in the directory.
|
||||
Namely, additional certs/keys may exist. There should never be any
|
||||
certs/keys in the LIST that don't exist in the directory however.
|
||||
|
||||
:param dict csha1_vhlist: map from cert sha1 fingerprints to a list
|
||||
of it's installed location paths.
|
||||
|
||||
"""
|
||||
certs = []
|
||||
with open(self.list_path, "rb") as csvfile:
|
||||
csvreader = csv.reader(csvfile)
|
||||
# idx, orig_cert, orig_key
|
||||
for row in csvreader:
|
||||
cert = Cert.fromrow(row, self.config.cert_key_backup)
|
||||
|
||||
# If we were able to find the cert installed... update status
|
||||
cert.installed = csha1_vhlist.get(cert.get_fingerprint(), [])
|
||||
|
||||
certs.append(cert)
|
||||
|
||||
return certs
|
||||
|
||||
def _get_installed_locations(self):
|
||||
"""Get installed locations of certificates.
|
||||
|
||||
:returns: map from cert sha1 fingerprint to :class:`list` of vhosts
|
||||
where the certificate is installed.
|
||||
|
||||
"""
|
||||
csha1_vhlist = {}
|
||||
|
||||
if self.installer is None:
|
||||
return csha1_vhlist
|
||||
|
||||
for (cert_path, _, path) in self.installer.get_all_certs_keys():
|
||||
try:
|
||||
with open(cert_path) as cert_file:
|
||||
cert_data = cert_file.read()
|
||||
except IOError:
|
||||
continue
|
||||
try:
|
||||
cert_obj, _ = crypto_util.pyopenssl_load_certificate(cert_data)
|
||||
except errors.Error:
|
||||
continue
|
||||
cert_sha1 = cert_obj.digest("sha1")
|
||||
if cert_sha1 in csha1_vhlist:
|
||||
csha1_vhlist[cert_sha1].append(path)
|
||||
else:
|
||||
csha1_vhlist[cert_sha1] = [path]
|
||||
|
||||
return csha1_vhlist
|
||||
|
||||
def _safe_revoke(self, certs):
|
||||
"""Confirm and revoke certificates.
|
||||
|
||||
:param certs: certs intended to be revoked
|
||||
:type certs: :class:`list` of :class:`letsencrypt.revoker.Cert`
|
||||
|
||||
:returns: certs successfully revoked
|
||||
:rtype: :class:`list` of :class:`letsencrypt.revoker.Cert`
|
||||
|
||||
"""
|
||||
success_list = []
|
||||
try:
|
||||
for cert in certs:
|
||||
if self.no_confirm or revocation.confirm_revocation(cert):
|
||||
try:
|
||||
self._acme_revoke(cert)
|
||||
except errors.Error:
|
||||
# TODO: Improve error handling when networking is set...
|
||||
logger.error(
|
||||
"Unable to revoke cert:%s%s", os.linesep, str(cert))
|
||||
success_list.append(cert)
|
||||
revocation.success_revocation(cert)
|
||||
finally:
|
||||
if success_list:
|
||||
self._remove_certs_keys(success_list)
|
||||
|
||||
return success_list
|
||||
|
||||
def _acme_revoke(self, cert):
|
||||
"""Revoke the certificate with the ACME server.
|
||||
|
||||
:param cert: certificate to revoke
|
||||
:type cert: :class:`letsencrypt.revoker.Cert`
|
||||
|
||||
:returns: TODO
|
||||
|
||||
"""
|
||||
# XXX | pylint: disable=unused-variable
|
||||
|
||||
# pylint: disable=protected-access
|
||||
certificate = jose_util.ComparableX509(cert._cert)
|
||||
try:
|
||||
with open(cert.backup_key_path, "rU") as backup_key_file:
|
||||
key = OpenSSL.crypto.load_privatekey(
|
||||
OpenSSL.crypto.FILETYPE_PEM, backup_key_file.read())
|
||||
# If the key file doesn't exist... or is corrupted
|
||||
except OpenSSL.crypto.Error as error:
|
||||
logger.debug(error, exc_info=True)
|
||||
raise errors.RevokerError(
|
||||
"Corrupted backup key file: %s" % cert.backup_key_path)
|
||||
|
||||
return self.acme.revoke(cert=None) # XXX
|
||||
|
||||
def _remove_certs_keys(self, cert_list): # pylint: disable=no-self-use
|
||||
"""Remove certificate and key.
|
||||
|
||||
:param list cert_list: Must contain certs, each is of type
|
||||
:class:`letsencrypt.revoker.Cert`
|
||||
|
||||
"""
|
||||
# This must occur first, LIST is the official key
|
||||
self._remove_certs_from_list(cert_list)
|
||||
|
||||
# Remove files
|
||||
for cert in cert_list:
|
||||
os.remove(cert.backup_path)
|
||||
os.remove(cert.backup_key_path)
|
||||
|
||||
def _remove_certs_from_list(self, cert_list): # pylint: disable=no-self-use
|
||||
"""Remove a certificate from the LIST file.
|
||||
|
||||
:param list cert_list: Must contain valid certs, each is of type
|
||||
:class:`letsencrypt.revoker.Cert`
|
||||
|
||||
"""
|
||||
newfile_handle, list_path2 = tempfile.mkstemp(".tmp", "LIST")
|
||||
idx = 0
|
||||
|
||||
with open(self.list_path, "rb") as orgfile:
|
||||
csvreader = csv.reader(orgfile)
|
||||
with os.fdopen(newfile_handle, "wb") as newfile:
|
||||
csvwriter = csv.writer(newfile)
|
||||
|
||||
for row in csvreader:
|
||||
if idx >= len(cert_list) or row != cert_list[idx].get_row():
|
||||
csvwriter.writerow(row)
|
||||
else:
|
||||
idx += 1
|
||||
|
||||
# This should never happen...
|
||||
if idx != len(cert_list):
|
||||
raise errors.RevokerError(
|
||||
"Did not find all cert_list items to remove from LIST")
|
||||
|
||||
shutil.copy2(list_path2, self.list_path)
|
||||
os.remove(list_path2)
|
||||
|
||||
def _row_to_backup(self, row):
|
||||
"""Convenience function
|
||||
|
||||
:param list row: csv file row 'idx', 'cert_path', 'key_path'
|
||||
|
||||
:returns: tuple of the form ('backup_cert_path', 'backup_key_path')
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
return (self._get_backup(self.config.cert_key_backup, row[0], row[1]),
|
||||
self._get_backup(self.config.cert_key_backup, row[0], row[2]))
|
||||
|
||||
@classmethod
|
||||
def store_cert_key(cls, cert_path, key_path, config):
|
||||
"""Store certificate key. (Used to allow quick revocation)
|
||||
|
||||
:param str cert_path: Path to a certificate file.
|
||||
:param str key_path: Path to authorized key for certificate
|
||||
|
||||
:ivar config: Configuration.
|
||||
:type config: :class:`~letsencrypt.interfaces.IConfig`
|
||||
|
||||
"""
|
||||
list_path = os.path.join(config.cert_key_backup, "LIST")
|
||||
le_util.make_or_verify_dir(config.cert_key_backup, 0o700, os.geteuid(),
|
||||
config.strict_permissions)
|
||||
|
||||
cls._catalog_files(
|
||||
config.cert_key_backup, cert_path, key_path, list_path)
|
||||
|
||||
@classmethod
|
||||
def _catalog_files(cls, backup_dir, cert_path, key_path, list_path):
|
||||
idx = 0
|
||||
if os.path.isfile(list_path):
|
||||
with open(list_path, "r+b") as csvfile:
|
||||
csvreader = csv.reader(csvfile)
|
||||
|
||||
# Find the highest index in the file
|
||||
for row in csvreader:
|
||||
idx = int(row[0]) + 1
|
||||
csvwriter = csv.writer(csvfile)
|
||||
# You must move the files before appending the row
|
||||
cls._copy_files(backup_dir, idx, cert_path, key_path)
|
||||
csvwriter.writerow([str(idx), cert_path, key_path])
|
||||
|
||||
else:
|
||||
with open(list_path, "wb") as csvfile:
|
||||
csvwriter = csv.writer(csvfile)
|
||||
# You must move the files before appending the row
|
||||
cls._copy_files(backup_dir, idx, cert_path, key_path)
|
||||
csvwriter.writerow([str(idx), cert_path, key_path])
|
||||
|
||||
@classmethod
|
||||
def _copy_files(cls, backup_dir, idx, cert_path, key_path):
|
||||
"""Copies the files into the backup dir appropriately."""
|
||||
shutil.copy2(cert_path, cls._get_backup(backup_dir, idx, cert_path))
|
||||
shutil.copy2(key_path, cls._get_backup(backup_dir, idx, key_path))
|
||||
|
||||
@classmethod
|
||||
def _get_backup(cls, backup_dir, idx, orig_path):
|
||||
"""Returns the path to the backup."""
|
||||
return os.path.join(
|
||||
backup_dir, "{name}_{idx}".format(
|
||||
name=os.path.basename(orig_path), idx=str(idx)))
|
||||
|
||||
|
||||
class Cert(object):
|
||||
"""Cert object used for Revocation convenience.
|
||||
|
||||
:ivar _cert: Certificate
|
||||
:type _cert: :class:`OpenSSL.crypto.X509`
|
||||
|
||||
:ivar int idx: convenience index used for listing
|
||||
:ivar orig: (`str` path - original certificate, `str` status)
|
||||
:type orig: :class:`PathStatus`
|
||||
:ivar orig_key: (`str` path - original auth key, `str` status)
|
||||
:type orig_key: :class:`PathStatus`
|
||||
:ivar str backup_path: backup filepath of the certificate
|
||||
:ivar str backup_key_path: backup filepath of the authorized key
|
||||
|
||||
:ivar list installed: `list` of `str` describing all locations the cert
|
||||
is installed
|
||||
|
||||
"""
|
||||
PathStatus = collections.namedtuple("PathStatus", "path status")
|
||||
"""Convenience container to hold path and status info"""
|
||||
|
||||
DELETED_MSG = "This file has been moved or deleted"
|
||||
CHANGED_MSG = "This file has changed"
|
||||
|
||||
def __init__(self, cert_path):
|
||||
"""Cert initialization
|
||||
|
||||
:param str cert_filepath: Name of file containing certificate in
|
||||
PEM format.
|
||||
|
||||
"""
|
||||
try:
|
||||
with open(cert_path) as cert_file:
|
||||
cert_data = cert_file.read()
|
||||
except IOError:
|
||||
raise errors.RevokerError(
|
||||
"Error loading certificate: %s" % cert_path)
|
||||
|
||||
try:
|
||||
self._cert = OpenSSL.crypto.load_certificate(
|
||||
OpenSSL.crypto.FILETYPE_PEM, cert_data)
|
||||
except OpenSSL.crypto.Error:
|
||||
raise errors.RevokerError(
|
||||
"Error loading certificate: %s" % cert_path)
|
||||
|
||||
self.idx = -1
|
||||
|
||||
self.orig = None
|
||||
self.orig_key = None
|
||||
self.backup_path = ""
|
||||
self.backup_key_path = ""
|
||||
|
||||
self.installed = ["Unknown"]
|
||||
|
||||
@classmethod
|
||||
def fromrow(cls, row, backup_dir):
|
||||
# pylint: disable=protected-access
|
||||
"""Initialize Cert from a csv row."""
|
||||
idx = int(row[0])
|
||||
backup = Revoker._get_backup(backup_dir, idx, row[1])
|
||||
backup_key = Revoker._get_backup(backup_dir, idx, row[2])
|
||||
|
||||
obj = cls(backup)
|
||||
obj.add_meta(idx, row[1], row[2], backup, backup_key)
|
||||
return obj
|
||||
|
||||
def get_row(self):
|
||||
"""Returns a list in CSV format. If meta data is available."""
|
||||
if self.orig is not None and self.orig_key is not None:
|
||||
return [str(self.idx), self.orig.path, self.orig_key.path]
|
||||
return None
|
||||
|
||||
def add_meta(self, idx, orig, orig_key, backup, backup_key):
|
||||
"""Add meta data to cert
|
||||
|
||||
:param int idx: convenience index for revoker
|
||||
:param tuple orig: (`str` original certificate filepath, `str` status)
|
||||
:param tuple orig_key: (`str` original auth key path, `str` status)
|
||||
:param str backup: backup certificate filepath
|
||||
:param str backup_key: backup key filepath
|
||||
|
||||
"""
|
||||
status = ""
|
||||
key_status = ""
|
||||
|
||||
# Verify original cert path
|
||||
if not os.path.isfile(orig):
|
||||
status = Cert.DELETED_MSG
|
||||
else:
|
||||
with open(orig) as orig_file:
|
||||
orig_data = orig_file.read()
|
||||
o_cert = OpenSSL.crypto.load_certificate(
|
||||
OpenSSL.crypto.FILETYPE_PEM, orig_data)
|
||||
if self.get_fingerprint() != o_cert.digest("sha1"):
|
||||
status = Cert.CHANGED_MSG
|
||||
|
||||
# Verify original key path
|
||||
if not os.path.isfile(orig_key):
|
||||
key_status = Cert.DELETED_MSG
|
||||
else:
|
||||
with open(orig_key, "r") as fd:
|
||||
key_pem = fd.read()
|
||||
with open(backup_key, "r") as fd:
|
||||
backup_key_pem = fd.read()
|
||||
if key_pem != backup_key_pem:
|
||||
key_status = Cert.CHANGED_MSG
|
||||
|
||||
self.idx = idx
|
||||
self.orig = Cert.PathStatus(orig, status)
|
||||
self.orig_key = Cert.PathStatus(orig_key, key_status)
|
||||
self.backup_path = backup
|
||||
self.backup_key_path = backup_key
|
||||
|
||||
def get_cn(self):
|
||||
"""Get common name."""
|
||||
return self._cert.get_subject().CN
|
||||
|
||||
def get_fingerprint(self):
|
||||
"""Get SHA1 fingerprint."""
|
||||
return self._cert.digest("sha1")
|
||||
|
||||
def get_not_before(self):
|
||||
"""Get not_valid_before field."""
|
||||
return crypto_util.asn1_generalizedtime_to_dt(
|
||||
self._cert.get_notBefore())
|
||||
|
||||
def get_not_after(self):
|
||||
"""Get not_valid_after field."""
|
||||
return crypto_util.asn1_generalizedtime_to_dt(
|
||||
self._cert.get_notAfter())
|
||||
|
||||
def get_der(self):
|
||||
"""Get certificate in der format."""
|
||||
return OpenSSL.crypto.dump_certificate(
|
||||
OpenSSL.crypto.FILETYPE_ASN1, self._cert)
|
||||
|
||||
def get_pub_key(self):
|
||||
"""Get public key size.
|
||||
|
||||
.. todo:: Support for ECC
|
||||
|
||||
"""
|
||||
return "RSA {0}".format(self._cert.get_pubkey().bits)
|
||||
|
||||
def get_san(self):
|
||||
"""Get subject alternative name if available."""
|
||||
# pylint: disable=protected-access
|
||||
return ", ".join(acme_crypto_util._pyopenssl_cert_or_req_san(self._cert))
|
||||
|
||||
def __str__(self):
|
||||
text = [
|
||||
"Subject: %s" % crypto_util.pyopenssl_x509_name_as_text(
|
||||
self._cert.get_subject()),
|
||||
"SAN: %s" % self.get_san(),
|
||||
"Issuer: %s" % crypto_util.pyopenssl_x509_name_as_text(
|
||||
self._cert.get_issuer()),
|
||||
"Public Key: %s" % self.get_pub_key(),
|
||||
"Not Before: %s" % str(self.get_not_before()),
|
||||
"Not After: %s" % str(self.get_not_after()),
|
||||
"Serial Number: %s" % self._cert.get_serial_number(),
|
||||
"SHA1: %s%s" % (self.get_fingerprint(), os.linesep),
|
||||
"Installed: %s" % ", ".join(self.installed),
|
||||
]
|
||||
|
||||
if self.orig is not None:
|
||||
if self.orig.status == "":
|
||||
text.append("Path: %s" % self.orig.path)
|
||||
else:
|
||||
text.append("Orig Path: %s (%s)" % self.orig)
|
||||
if self.orig_key is not None:
|
||||
if self.orig_key.status == "":
|
||||
text.append("Auth Key Path: %s" % self.orig_key.path)
|
||||
else:
|
||||
text.append("Orig Auth Key Path: %s (%s)" % self.orig_key)
|
||||
|
||||
text.append("")
|
||||
return os.linesep.join(text)
|
||||
|
||||
def pretty_print(self):
|
||||
"""Nicely frames a cert str"""
|
||||
frame = "-" * (display_util.WIDTH - 4) + os.linesep
|
||||
return "{frame}{cert}{frame}".format(frame=frame, cert=str(self))
|
||||
@@ -129,7 +129,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
||||
self.chain = self.configuration["chain"]
|
||||
self.fullchain = self.configuration["fullchain"]
|
||||
|
||||
def consistent(self):
|
||||
def _consistent(self):
|
||||
"""Are the files associated with this lineage self-consistent?
|
||||
|
||||
:returns: Whether the files stored in connection with this
|
||||
@@ -187,7 +187,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
||||
# for x in ALL_FOUR))) == 1
|
||||
return True
|
||||
|
||||
def fix(self):
|
||||
def _fix(self):
|
||||
"""Attempt to fix defects or inconsistencies in this lineage.
|
||||
|
||||
.. todo:: Currently unimplemented.
|
||||
@@ -347,7 +347,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
||||
smallest_current = min(self.current_version(x) for x in ALL_FOUR)
|
||||
return smallest_current < self.latest_common_version()
|
||||
|
||||
def update_link_to(self, kind, version):
|
||||
def _update_link_to(self, kind, version):
|
||||
"""Make the specified item point at the specified version.
|
||||
|
||||
(Note that this method doesn't verify that the specified version
|
||||
@@ -379,7 +379,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
||||
:param int version: the desired version"""
|
||||
|
||||
for kind in ALL_FOUR:
|
||||
self.update_link_to(kind, version)
|
||||
self._update_link_to(kind, version)
|
||||
|
||||
def _notafterbefore(self, method, version):
|
||||
"""Internal helper function for finding notbefore/notafter."""
|
||||
@@ -439,6 +439,18 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
||||
with open(target) as f:
|
||||
return crypto_util.get_sans_from_cert(f.read())
|
||||
|
||||
def autodeployment_is_enabled(self):
|
||||
"""Is automatic deployment enabled for this cert?
|
||||
|
||||
If autodeploy is not specified, defaults to True.
|
||||
|
||||
:returns: True if automatic deployment is enabled
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
return ("autodeploy" not in self.configuration or
|
||||
self.configuration.as_bool("autodeploy"))
|
||||
|
||||
def should_autodeploy(self):
|
||||
"""Should this lineage now automatically deploy a newer version?
|
||||
|
||||
@@ -453,8 +465,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
if ("autodeploy" not in self.configuration or
|
||||
self.configuration.as_bool("autodeploy")):
|
||||
if self.autodeployment_is_enabled():
|
||||
if self.has_pending_deployment():
|
||||
interval = self.configuration.get("deploy_before_expiry",
|
||||
"5 days")
|
||||
@@ -488,6 +499,18 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
||||
# certificate is not revoked).
|
||||
return False
|
||||
|
||||
def autorenewal_is_enabled(self):
|
||||
"""Is automatic renewal enabled for this cert?
|
||||
|
||||
If autorenew is not specified, defaults to True.
|
||||
|
||||
:returns: True if automatic renewal is enabled
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
return ("autorenew" not in self.configuration or
|
||||
self.configuration.as_bool("autorenew"))
|
||||
|
||||
def should_autorenew(self):
|
||||
"""Should we now try to autorenew the most recent cert version?
|
||||
|
||||
@@ -504,8 +527,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
if ("autorenew" not in self.configuration or
|
||||
self.configuration.as_bool("autorenew")):
|
||||
if self.autorenewal_is_enabled():
|
||||
# Consider whether to attempt to autorenew this cert now
|
||||
|
||||
# Renewals on the basis of revocation
|
||||
@@ -520,7 +542,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
||||
remaining = expiry - now
|
||||
if remaining < autorenew_interval:
|
||||
return True
|
||||
return False
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def new_lineage(cls, lineagename, cert, privkey, chain,
|
||||
|
||||
@@ -355,7 +355,7 @@ class GenChallengePathTest(unittest.TestCase):
|
||||
class MutuallyExclusiveTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.auth_handler.mutually_exclusive."""
|
||||
|
||||
# pylint: disable=invalid-name,missing-docstring,too-few-public-methods
|
||||
# pylint: disable=missing-docstring,too-few-public-methods
|
||||
class A(object):
|
||||
pass
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import itertools
|
||||
import os
|
||||
import shutil
|
||||
import StringIO
|
||||
import traceback
|
||||
import tempfile
|
||||
import unittest
|
||||
@@ -16,6 +17,9 @@ from letsencrypt.tests import renewer_test
|
||||
from letsencrypt.tests import test_util
|
||||
|
||||
|
||||
CSR = test_util.vector_path('csr.der')
|
||||
|
||||
|
||||
class CLITest(unittest.TestCase):
|
||||
"""Tests for different commands."""
|
||||
|
||||
@@ -39,12 +43,52 @@ class CLITest(unittest.TestCase):
|
||||
ret = cli.main(args)
|
||||
return ret, stdout, stderr, client
|
||||
|
||||
def _call_stdout(self, args):
|
||||
"""
|
||||
Variant of _call that preserves stdout so that it can be mocked by the
|
||||
caller.
|
||||
"""
|
||||
from letsencrypt import cli
|
||||
args = ['--text', '--config-dir', self.config_dir,
|
||||
'--work-dir', self.work_dir, '--logs-dir', self.logs_dir,
|
||||
'--agree-eula'] + args
|
||||
with mock.patch('letsencrypt.cli.sys.stderr') as stderr:
|
||||
with mock.patch('letsencrypt.cli.client') as client:
|
||||
ret = cli.main(args)
|
||||
return ret, None, stderr, client
|
||||
|
||||
def test_no_flags(self):
|
||||
self.assertRaises(SystemExit, self._call, [])
|
||||
with mock.patch('letsencrypt.cli.run') as mock_run:
|
||||
self._call([])
|
||||
self.assertEqual(1, mock_run.call_count)
|
||||
|
||||
def test_help(self):
|
||||
self.assertRaises(SystemExit, self._call, ['--help'])
|
||||
self.assertRaises(SystemExit, self._call, ['--help all'])
|
||||
self.assertRaises(SystemExit, self._call, ['--help', 'all'])
|
||||
output = StringIO.StringIO()
|
||||
with mock.patch('letsencrypt.cli.sys.stdout', new=output):
|
||||
self.assertRaises(SystemExit, self._call_stdout, ['--help', 'all'])
|
||||
out = output.getvalue()
|
||||
self.assertTrue("--configurator" in out)
|
||||
self.assertTrue("how a cert is deployed" in out)
|
||||
self.assertTrue("--manual-test-mode" in out)
|
||||
output.truncate(0)
|
||||
self.assertRaises(SystemExit, self._call_stdout, ['-h', 'nginx'])
|
||||
out = output.getvalue()
|
||||
self.assertTrue("--nginx-ctl" in out)
|
||||
self.assertTrue("--manual-test-mode" not in out)
|
||||
self.assertTrue("--checkpoints" not in out)
|
||||
output.truncate(0)
|
||||
self.assertRaises(SystemExit, self._call_stdout, ['--help', 'plugins'])
|
||||
out = output.getvalue()
|
||||
self.assertTrue("--manual-test-mode" not in out)
|
||||
self.assertTrue("--prepare" in out)
|
||||
self.assertTrue("Plugin options" in out)
|
||||
output.truncate(0)
|
||||
self.assertRaises(SystemExit, self._call_stdout, ['-h'])
|
||||
out = output.getvalue()
|
||||
from letsencrypt import cli
|
||||
self.assertTrue(cli.USAGE in out)
|
||||
|
||||
def test_rollback(self):
|
||||
_, _, _, client = self._call(['rollback'])
|
||||
@@ -65,40 +109,114 @@ class CLITest(unittest.TestCase):
|
||||
for r in xrange(len(flags)))):
|
||||
self._call(['plugins'] + list(args))
|
||||
|
||||
@mock.patch("letsencrypt.cli.sys")
|
||||
def test_auth_bad_args(self):
|
||||
ret, _, _, _ = self._call(['-d', 'foo.bar', 'auth', '--csr', CSR])
|
||||
self.assertEqual(ret, '--domains and --csr are mutually exclusive')
|
||||
|
||||
ret, _, _, _ = self._call(['-a', 'bad_auth', 'auth'])
|
||||
self.assertEqual(ret, 'Authenticator could not be determined')
|
||||
|
||||
@mock.patch('letsencrypt.cli.zope.component.getUtility')
|
||||
def test_auth_new_request_success(self, mock_get_utility):
|
||||
cert_path = '/etc/letsencrypt/live/foo.bar'
|
||||
mock_lineage = mock.MagicMock(cert=cert_path)
|
||||
mock_client = mock.MagicMock()
|
||||
mock_client.obtain_and_enroll_certificate.return_value = mock_lineage
|
||||
self._auth_new_request_common(mock_client)
|
||||
self.assertEqual(
|
||||
mock_client.obtain_and_enroll_certificate.call_count, 1)
|
||||
self.assertTrue(
|
||||
cert_path in mock_get_utility().add_message.call_args[0][0])
|
||||
|
||||
def test_auth_new_request_failure(self):
|
||||
mock_client = mock.MagicMock()
|
||||
mock_client.obtain_and_enroll_certificate.return_value = False
|
||||
self.assertRaises(errors.Error,
|
||||
self._auth_new_request_common, mock_client)
|
||||
|
||||
def _auth_new_request_common(self, mock_client):
|
||||
with mock.patch('letsencrypt.cli._treat_as_renewal') as mock_renewal:
|
||||
mock_renewal.return_value = None
|
||||
with mock.patch('letsencrypt.cli._init_le_client') as mock_init:
|
||||
mock_init.return_value = mock_client
|
||||
self._call(['-d', 'foo.bar', '-a', 'standalone', 'auth'])
|
||||
|
||||
@mock.patch('letsencrypt.cli.zope.component.getUtility')
|
||||
@mock.patch('letsencrypt.cli._treat_as_renewal')
|
||||
@mock.patch('letsencrypt.cli._init_le_client')
|
||||
def test_auth_renewal(self, mock_init, mock_renewal, mock_get_utility):
|
||||
cert_path = '/etc/letsencrypt/live/foo.bar'
|
||||
mock_lineage = mock.MagicMock(cert=cert_path)
|
||||
mock_cert = mock.MagicMock(body='body')
|
||||
mock_key = mock.MagicMock(pem='pem_key')
|
||||
mock_renewal.return_value = mock_lineage
|
||||
mock_client = mock.MagicMock()
|
||||
mock_client.obtain_certificate.return_value = (mock_cert, 'chain',
|
||||
mock_key, 'csr')
|
||||
mock_init.return_value = mock_client
|
||||
with mock.patch('letsencrypt.cli.OpenSSL'):
|
||||
with mock.patch('letsencrypt.cli.crypto_util'):
|
||||
self._call(['-d', 'foo.bar', '-a', 'standalone', 'auth'])
|
||||
mock_client.obtain_certificate.assert_called_once_with(['foo.bar'])
|
||||
self.assertEqual(mock_lineage.save_successor.call_count, 1)
|
||||
mock_lineage.update_all_links_to.assert_called_once_with(
|
||||
mock_lineage.latest_common_version())
|
||||
self.assertTrue(
|
||||
cert_path in mock_get_utility().add_message.call_args[0][0])
|
||||
|
||||
@mock.patch('letsencrypt.cli.display_ops.pick_installer')
|
||||
@mock.patch('letsencrypt.cli.zope.component.getUtility')
|
||||
@mock.patch('letsencrypt.cli._init_le_client')
|
||||
def test_auth_csr(self, mock_init, mock_get_utility, mock_pick_installer):
|
||||
cert_path = '/etc/letsencrypt/live/foo.bar'
|
||||
mock_client = mock.MagicMock()
|
||||
mock_client.obtain_certificate_from_csr.return_value = ('certr',
|
||||
'chain')
|
||||
mock_init.return_value = mock_client
|
||||
installer = 'installer'
|
||||
self._call(
|
||||
['-a', 'standalone', '-i', installer, 'auth', '--csr', CSR,
|
||||
'--cert-path', cert_path, '--chain-path', '/'])
|
||||
self.assertEqual(mock_pick_installer.call_args[0][1], installer)
|
||||
mock_client.save_certificate.assert_called_once_with(
|
||||
'certr', 'chain', cert_path, '/')
|
||||
self.assertTrue(
|
||||
cert_path in mock_get_utility().add_message.call_args[0][0])
|
||||
|
||||
@mock.patch('letsencrypt.cli.sys')
|
||||
def test_handle_exception(self, mock_sys):
|
||||
# pylint: disable=protected-access
|
||||
from letsencrypt import cli
|
||||
|
||||
mock_open = mock.mock_open()
|
||||
with mock.patch("letsencrypt.cli.open", mock_open, create=True):
|
||||
exception = Exception("detail")
|
||||
with mock.patch('letsencrypt.cli.open', mock_open, create=True):
|
||||
exception = Exception('detail')
|
||||
cli._handle_exception(
|
||||
Exception, exc_value=exception, trace=None, args=None)
|
||||
mock_open().write.assert_called_once_with("".join(
|
||||
mock_open().write.assert_called_once_with(''.join(
|
||||
traceback.format_exception_only(Exception, exception)))
|
||||
error_msg = mock_sys.exit.call_args_list[0][0][0]
|
||||
self.assertTrue("unexpected error" in error_msg)
|
||||
self.assertTrue('unexpected error' in error_msg)
|
||||
|
||||
with mock.patch("letsencrypt.cli.open", mock_open, create=True):
|
||||
with mock.patch('letsencrypt.cli.open', mock_open, create=True):
|
||||
mock_open.side_effect = [KeyboardInterrupt]
|
||||
error = errors.Error("detail")
|
||||
error = errors.Error('detail')
|
||||
cli._handle_exception(
|
||||
errors.Error, exc_value=error, trace=None, args=None)
|
||||
# assert_any_call used because sys.exit doesn't exit in cli.py
|
||||
mock_sys.exit.assert_any_call("".join(
|
||||
mock_sys.exit.assert_any_call(''.join(
|
||||
traceback.format_exception_only(errors.Error, error)))
|
||||
|
||||
args = mock.MagicMock(debug=False)
|
||||
cli._handle_exception(
|
||||
Exception, exc_value=Exception("detail"), trace=None, args=args)
|
||||
Exception, exc_value=Exception('detail'), trace=None, args=args)
|
||||
error_msg = mock_sys.exit.call_args_list[-1][0][0]
|
||||
self.assertTrue("unexpected error" in error_msg)
|
||||
self.assertTrue('unexpected error' in error_msg)
|
||||
|
||||
interrupt = KeyboardInterrupt("detail")
|
||||
interrupt = KeyboardInterrupt('detail')
|
||||
cli._handle_exception(
|
||||
KeyboardInterrupt, exc_value=interrupt, trace=None, args=None)
|
||||
mock_sys.exit.assert_called_with("".join(
|
||||
mock_sys.exit.assert_called_with(''.join(
|
||||
traceback.format_exception_only(KeyboardInterrupt, interrupt)))
|
||||
|
||||
|
||||
@@ -108,13 +226,13 @@ class DetermineAccountTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.args = mock.MagicMock(account=None, email=None)
|
||||
self.config = configuration.NamespaceConfig(self.args)
|
||||
self.accs = [mock.MagicMock(id="x"), mock.MagicMock(id="y")]
|
||||
self.accs = [mock.MagicMock(id='x'), mock.MagicMock(id='y')]
|
||||
self.account_storage = account.AccountMemoryStorage()
|
||||
|
||||
def _call(self):
|
||||
# pylint: disable=protected-access
|
||||
from letsencrypt.cli import _determine_account
|
||||
with mock.patch("letsencrypt.cli.account.AccountFileStorage") as mock_storage:
|
||||
with mock.patch('letsencrypt.cli.account.AccountFileStorage') as mock_storage:
|
||||
mock_storage.return_value = self.account_storage
|
||||
return _determine_account(self.args, self.config)
|
||||
|
||||
@@ -131,7 +249,7 @@ class DetermineAccountTest(unittest.TestCase):
|
||||
self.assertEqual(self.accs[0].id, self.args.account)
|
||||
self.assertTrue(self.args.email is None)
|
||||
|
||||
@mock.patch("letsencrypt.client.display_ops.choose_account")
|
||||
@mock.patch('letsencrypt.client.display_ops.choose_account')
|
||||
def test_multiple_accounts(self, mock_choose_accounts):
|
||||
for acc in self.accs:
|
||||
self.account_storage.save(acc)
|
||||
@@ -142,11 +260,11 @@ class DetermineAccountTest(unittest.TestCase):
|
||||
self.assertEqual(self.accs[1].id, self.args.account)
|
||||
self.assertTrue(self.args.email is None)
|
||||
|
||||
@mock.patch("letsencrypt.client.display_ops.get_email")
|
||||
@mock.patch('letsencrypt.client.display_ops.get_email')
|
||||
def test_no_accounts_no_email(self, mock_get_email):
|
||||
mock_get_email.return_value = "foo@bar.baz"
|
||||
mock_get_email.return_value = 'foo@bar.baz'
|
||||
|
||||
with mock.patch("letsencrypt.cli.client") as client:
|
||||
with mock.patch('letsencrypt.cli.client') as client:
|
||||
client.register.return_value = (
|
||||
self.accs[0], mock.sentinel.acme)
|
||||
self.assertEqual((self.accs[0], mock.sentinel.acme), self._call())
|
||||
@@ -154,15 +272,15 @@ class DetermineAccountTest(unittest.TestCase):
|
||||
self.config, self.account_storage, tos_cb=mock.ANY)
|
||||
|
||||
self.assertEqual(self.accs[0].id, self.args.account)
|
||||
self.assertEqual("foo@bar.baz", self.args.email)
|
||||
self.assertEqual('foo@bar.baz', self.args.email)
|
||||
|
||||
def test_no_accounts_email(self):
|
||||
self.args.email = "other email"
|
||||
with mock.patch("letsencrypt.cli.client") as client:
|
||||
self.args.email = 'other email'
|
||||
with mock.patch('letsencrypt.cli.client') as client:
|
||||
client.register.return_value = (self.accs[1], mock.sentinel.acme)
|
||||
self._call()
|
||||
self.assertEqual(self.accs[1].id, self.args.account)
|
||||
self.assertEqual("other email", self.args.email)
|
||||
self.assertEqual('other email', self.args.email)
|
||||
|
||||
|
||||
class DuplicativeCertsTest(renewer_test.BaseRenewableCertTest):
|
||||
@@ -176,32 +294,33 @@ class DuplicativeCertsTest(renewer_test.BaseRenewableCertTest):
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.tempdir)
|
||||
|
||||
def test_find_duplicative_names(self):
|
||||
@mock.patch('letsencrypt.le_util.make_or_verify_dir')
|
||||
def test_find_duplicative_names(self, unused_makedir):
|
||||
from letsencrypt.cli import _find_duplicative_certs
|
||||
test_cert = test_util.load_vector("cert-san.pem")
|
||||
with open(self.test_rc.cert, "w") as f:
|
||||
test_cert = test_util.load_vector('cert-san.pem')
|
||||
with open(self.test_rc.cert, 'w') as f:
|
||||
f.write(test_cert)
|
||||
|
||||
# No overlap at all
|
||||
result = _find_duplicative_certs(["wow.net", "hooray.org"],
|
||||
result = _find_duplicative_certs(['wow.net', 'hooray.org'],
|
||||
self.config, self.cli_config)
|
||||
self.assertEqual(result, (None, None))
|
||||
|
||||
# Totally identical
|
||||
result = _find_duplicative_certs(["example.com", "www.example.com"],
|
||||
result = _find_duplicative_certs(['example.com', 'www.example.com'],
|
||||
self.config, self.cli_config)
|
||||
self.assertTrue(result[0].configfile.filename.endswith("example.org.conf"))
|
||||
self.assertTrue(result[0].configfile.filename.endswith('example.org.conf'))
|
||||
self.assertEqual(result[1], None)
|
||||
|
||||
# Superset
|
||||
result = _find_duplicative_certs(["example.com", "www.example.com",
|
||||
"something.new"], self.config,
|
||||
result = _find_duplicative_certs(['example.com', 'www.example.com',
|
||||
'something.new'], self.config,
|
||||
self.cli_config)
|
||||
self.assertEqual(result[0], None)
|
||||
self.assertTrue(result[1].configfile.filename.endswith("example.org.conf"))
|
||||
self.assertTrue(result[1].configfile.filename.endswith('example.org.conf'))
|
||||
|
||||
# Partial overlap doesn't count
|
||||
result = _find_duplicative_certs(["example.com", "something.new"],
|
||||
result = _find_duplicative_certs(['example.com', 'something.new'],
|
||||
self.config, self.cli_config)
|
||||
self.assertEqual(result, (None, None))
|
||||
|
||||
|
||||
@@ -4,14 +4,12 @@ import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import configobj
|
||||
import OpenSSL
|
||||
import mock
|
||||
|
||||
from acme import jose
|
||||
|
||||
from letsencrypt import account
|
||||
from letsencrypt import configuration
|
||||
from letsencrypt import errors
|
||||
from letsencrypt import le_util
|
||||
|
||||
@@ -113,36 +111,35 @@ class ClientTest(unittest.TestCase):
|
||||
mock_crypto_util.init_save_key.assert_called_once_with(
|
||||
self.config.rsa_key_size, self.config.key_dir)
|
||||
mock_crypto_util.init_save_csr.assert_called_once_with(
|
||||
mock.sentinel.key, domains, self.config.cert_dir)
|
||||
mock.sentinel.key, domains, self.config.csr_dir)
|
||||
self._check_obtain_certificate()
|
||||
|
||||
@mock.patch("letsencrypt.client.zope.component.getUtility")
|
||||
def test_report_renewal_status(self, mock_zope):
|
||||
# pylint: disable=protected-access
|
||||
cert = mock.MagicMock()
|
||||
cert.configuration = configobj.ConfigObj()
|
||||
cert.cli_config = configuration.RenewerConfiguration(self.config)
|
||||
cert.cli_config.renewal_configs_dir = "/foo/bar/baz"
|
||||
|
||||
cert.configuration["autorenew"] = "True"
|
||||
cert.configuration["autodeploy"] = "True"
|
||||
cert.autorenewal_is_enabled.return_value = True
|
||||
cert.autodeployment_is_enabled.return_value = True
|
||||
self.client._report_renewal_status(cert)
|
||||
msg = mock_zope().add_message.call_args[0][0]
|
||||
self.assertTrue("renewal and deployment has been" in msg)
|
||||
self.assertTrue(cert.cli_config.renewal_configs_dir in msg)
|
||||
|
||||
cert.configuration["autorenew"] = "False"
|
||||
cert.autorenewal_is_enabled.return_value = False
|
||||
self.client._report_renewal_status(cert)
|
||||
msg = mock_zope().add_message.call_args[0][0]
|
||||
self.assertTrue("deployment but not automatic renewal" in msg)
|
||||
self.assertTrue(cert.cli_config.renewal_configs_dir in msg)
|
||||
|
||||
cert.configuration["autodeploy"] = "False"
|
||||
cert.autodeployment_is_enabled.return_value = False
|
||||
self.client._report_renewal_status(cert)
|
||||
msg = mock_zope().add_message.call_args[0][0]
|
||||
self.assertTrue("renewal and deployment has not" in msg)
|
||||
self.assertTrue(cert.cli_config.renewal_configs_dir in msg)
|
||||
|
||||
cert.configuration["autorenew"] = "True"
|
||||
cert.autorenewal_is_enabled.return_value = True
|
||||
self.client._report_renewal_status(cert)
|
||||
msg = mock_zope().add_message.call_args[0][0]
|
||||
self.assertTrue("renewal but not automatic deployment" in msg)
|
||||
@@ -178,6 +175,39 @@ class ClientTest(unittest.TestCase):
|
||||
|
||||
shutil.rmtree(tmp_path)
|
||||
|
||||
def test_deploy_certificate(self):
|
||||
self.assertRaises(errors.Error, self.client.deploy_certificate,
|
||||
["foo.bar"], "key", "cert", "chain")
|
||||
|
||||
installer = mock.MagicMock()
|
||||
self.client.installer = installer
|
||||
|
||||
self.client.deploy_certificate(["foo.bar"], "key", "cert", "chain")
|
||||
installer.deploy_cert.assert_called_once_with(
|
||||
"foo.bar", os.path.abspath("cert"),
|
||||
os.path.abspath("key"), os.path.abspath("chain"))
|
||||
self.assertEqual(installer.save.call_count, 1)
|
||||
installer.restart.assert_called_once_with()
|
||||
|
||||
@mock.patch("letsencrypt.client.enhancements")
|
||||
def test_enhance_config(self, mock_enhancements):
|
||||
self.assertRaises(errors.Error,
|
||||
self.client.enhance_config, ["foo.bar"])
|
||||
|
||||
mock_enhancements.ask.return_value = True
|
||||
installer = mock.MagicMock()
|
||||
self.client.installer = installer
|
||||
|
||||
self.client.enhance_config(["foo.bar"])
|
||||
installer.enhance.assert_called_once_with("foo.bar", "redirect")
|
||||
self.assertEqual(installer.save.call_count, 1)
|
||||
installer.restart.assert_called_once_with()
|
||||
|
||||
installer.enhance.side_effect = errors.PluginError
|
||||
self.assertRaises(errors.PluginError,
|
||||
self.client.enhance_config, ["foo.bar"], True)
|
||||
installer.recovery_routine.assert_called_once_with()
|
||||
|
||||
|
||||
class RollbackTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.client.rollback."""
|
||||
|
||||
40
letsencrypt/tests/colored_logging_test.py
Normal file
40
letsencrypt/tests/colored_logging_test.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""Tests for letsencrypt.colored_logging."""
|
||||
import logging
|
||||
import StringIO
|
||||
import unittest
|
||||
|
||||
from letsencrypt import le_util
|
||||
|
||||
|
||||
class StreamHandlerTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.colored_logging."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt import colored_logging
|
||||
|
||||
self.stream = StringIO.StringIO()
|
||||
self.stream.isatty = lambda: True
|
||||
self.handler = colored_logging.StreamHandler(self.stream)
|
||||
|
||||
self.logger = logging.getLogger()
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
self.logger.addHandler(self.handler)
|
||||
|
||||
def test_format(self):
|
||||
msg = 'I did a thing'
|
||||
self.logger.debug(msg)
|
||||
self.assertEqual(self.stream.getvalue(), '{0}\n'.format(msg))
|
||||
|
||||
def test_format_and_red_level(self):
|
||||
msg = 'I did another thing'
|
||||
self.handler.red_level = logging.DEBUG
|
||||
self.logger.debug(msg)
|
||||
|
||||
self.assertEqual(self.stream.getvalue(),
|
||||
'{0}{1}{2}\n'.format(le_util.ANSI_SGR_RED,
|
||||
msg,
|
||||
le_util.ANSI_SGR_RESET))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
@@ -32,8 +32,8 @@ class NamespaceConfigTest(unittest.TestCase):
|
||||
def test_dynamic_dirs(self, constants):
|
||||
constants.ACCOUNTS_DIR = 'acc'
|
||||
constants.BACKUP_DIR = 'backups'
|
||||
constants.CERT_KEY_BACKUP_DIR = 'c/'
|
||||
constants.CERT_DIR = 'certs'
|
||||
constants.CSR_DIR = 'csr'
|
||||
|
||||
constants.IN_PROGRESS_DIR = '../p'
|
||||
constants.KEY_DIR = 'keys'
|
||||
constants.TEMP_CHECKPOINT_DIR = 't'
|
||||
@@ -41,9 +41,7 @@ class NamespaceConfigTest(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
self.config.accounts_dir, '/tmp/config/acc/acme-server.org:443/new')
|
||||
self.assertEqual(self.config.backup_dir, '/tmp/foo/backups')
|
||||
self.assertEqual(self.config.cert_dir, '/tmp/config/certs')
|
||||
self.assertEqual(
|
||||
self.config.cert_key_backup, '/tmp/foo/c/acme-server.org:443/new')
|
||||
self.assertEqual(self.config.csr_dir, '/tmp/config/csr')
|
||||
self.assertEqual(self.config.in_progress_dir, '/tmp/foo/../p')
|
||||
self.assertEqual(self.config.key_dir, '/tmp/config/keys')
|
||||
self.assertEqual(self.config.temp_checkpoint_dir, '/tmp/foo/t')
|
||||
@@ -59,9 +57,9 @@ class RenewerConfigurationTest(unittest.TestCase):
|
||||
|
||||
@mock.patch('letsencrypt.configuration.constants')
|
||||
def test_dynamic_dirs(self, constants):
|
||||
constants.ARCHIVE_DIR = "a"
|
||||
constants.ARCHIVE_DIR = 'a'
|
||||
constants.LIVE_DIR = 'l'
|
||||
constants.RENEWAL_CONFIGS_DIR = "renewal_configs"
|
||||
constants.RENEWAL_CONFIGS_DIR = 'renewal_configs'
|
||||
constants.RENEWER_CONFIG_FILENAME = 'r.conf'
|
||||
|
||||
self.assertEqual(self.config.archive_dir, '/tmp/config/a')
|
||||
|
||||
@@ -6,7 +6,10 @@ import unittest
|
||||
|
||||
import OpenSSL
|
||||
import mock
|
||||
import zope.component
|
||||
|
||||
from letsencrypt import errors
|
||||
from letsencrypt import interfaces
|
||||
from letsencrypt.tests import test_util
|
||||
|
||||
|
||||
@@ -20,6 +23,8 @@ class InitSaveKeyTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.crypto_util.init_save_key."""
|
||||
def setUp(self):
|
||||
logging.disable(logging.CRITICAL)
|
||||
zope.component.provideUtility(
|
||||
mock.Mock(strict_permissions=True), interfaces.IConfig)
|
||||
self.key_dir = tempfile.mkdtemp('key_dir')
|
||||
|
||||
def tearDown(self):
|
||||
@@ -48,6 +53,8 @@ class InitSaveCSRTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.crypto_util.init_save_csr."""
|
||||
|
||||
def setUp(self):
|
||||
zope.component.provideUtility(
|
||||
mock.Mock(strict_permissions=True), interfaces.IConfig)
|
||||
self.csr_dir = tempfile.mkdtemp('csr_dir')
|
||||
|
||||
def tearDown(self):
|
||||
@@ -207,5 +214,23 @@ class GetSANsFromCSRTest(unittest.TestCase):
|
||||
[], self._call(test_util.load_vector('csr-nosans.pem')))
|
||||
|
||||
|
||||
class CertLoaderTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.crypto_util.pyopenssl_load_certificate"""
|
||||
|
||||
def test_load_valid_cert(self):
|
||||
from letsencrypt.crypto_util import pyopenssl_load_certificate
|
||||
|
||||
cert, file_type = pyopenssl_load_certificate(CERT)
|
||||
self.assertEqual(cert.digest('sha1'),
|
||||
OpenSSL.crypto.load_certificate(file_type, CERT).digest('sha1'))
|
||||
|
||||
def test_load_invalid_cert(self):
|
||||
from letsencrypt.crypto_util import pyopenssl_load_certificate
|
||||
bad_cert_data = CERT.replace("BEGIN CERTIFICATE", "ASDFASDFASDF!!!")
|
||||
|
||||
with self.assertRaises(errors.Error):
|
||||
pyopenssl_load_certificate(bad_cert_data)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
|
||||
@@ -84,7 +84,7 @@ class PickPluginTest(unittest.TestCase):
|
||||
|
||||
def test_no_default(self):
|
||||
self._call()
|
||||
self.assertEqual(1, self.reg.ifaces.call_count)
|
||||
self.assertEqual(1, self.reg.visible().ifaces.call_count)
|
||||
|
||||
def test_no_candidate(self):
|
||||
self.assertTrue(self._call() is None)
|
||||
@@ -94,7 +94,8 @@ class PickPluginTest(unittest.TestCase):
|
||||
plugin_ep.init.return_value = "foo"
|
||||
plugin_ep.misconfigured = False
|
||||
|
||||
self.reg.ifaces().verify().available.return_value = {"bar": plugin_ep}
|
||||
self.reg.visible().ifaces().verify().available.return_value = {
|
||||
"bar": plugin_ep}
|
||||
self.assertEqual("foo", self._call())
|
||||
|
||||
def test_single_misconfigured(self):
|
||||
@@ -102,13 +103,14 @@ class PickPluginTest(unittest.TestCase):
|
||||
plugin_ep.init.return_value = "foo"
|
||||
plugin_ep.misconfigured = True
|
||||
|
||||
self.reg.ifaces().verify().available.return_value = {"bar": plugin_ep}
|
||||
self.reg.visible().ifaces().verify().available.return_value = {
|
||||
"bar": plugin_ep}
|
||||
self.assertTrue(self._call() is None)
|
||||
|
||||
def test_multiple(self):
|
||||
plugin_ep = mock.MagicMock()
|
||||
plugin_ep.init.return_value = "foo"
|
||||
self.reg.ifaces().verify().available.return_value = {
|
||||
self.reg.visible().ifaces().verify().available.return_value = {
|
||||
"bar": plugin_ep,
|
||||
"baz": plugin_ep,
|
||||
}
|
||||
@@ -119,7 +121,7 @@ class PickPluginTest(unittest.TestCase):
|
||||
[plugin_ep, plugin_ep], self.question)
|
||||
|
||||
def test_choose_plugin_none(self):
|
||||
self.reg.ifaces().verify().available.return_value = {
|
||||
self.reg.visible().ifaces().verify().available.return_value = {
|
||||
"bar": None,
|
||||
"baz": None,
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
"""Test :mod:`letsencrypt.display.revocation`."""
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
import zope.component
|
||||
|
||||
from letsencrypt.display import util as display_util
|
||||
|
||||
from letsencrypt.tests import test_util
|
||||
|
||||
|
||||
class DisplayCertsTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from letsencrypt.revoker import Cert
|
||||
self.cert0 = Cert(test_util.vector_path("cert.pem"))
|
||||
self.cert1 = Cert(test_util.vector_path("cert-san.pem"))
|
||||
|
||||
self.certs = [self.cert0, self.cert1]
|
||||
|
||||
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
|
||||
|
||||
@classmethod
|
||||
def _call(cls, certs):
|
||||
from letsencrypt.display.revocation import display_certs
|
||||
return display_certs(certs)
|
||||
|
||||
@mock.patch("letsencrypt.display.revocation.util")
|
||||
def test_revocation(self, mock_util):
|
||||
mock_util().menu.return_value = (display_util.OK, 0)
|
||||
|
||||
code, choice = self._call(self.certs)
|
||||
|
||||
self.assertEqual(display_util.OK, code)
|
||||
self.assertEqual(self.certs[choice], self.cert0)
|
||||
|
||||
@mock.patch("letsencrypt.display.revocation.util")
|
||||
def test_cancel(self, mock_util):
|
||||
mock_util().menu.return_value = (display_util.CANCEL, -1)
|
||||
|
||||
code, _ = self._call(self.certs)
|
||||
self.assertEqual(display_util.CANCEL, code)
|
||||
|
||||
|
||||
class MoreInfoCertTest(unittest.TestCase):
|
||||
# pylint: disable=too-few-public-methods
|
||||
@classmethod
|
||||
def _call(cls, cert):
|
||||
from letsencrypt.display.revocation import more_info_cert
|
||||
more_info_cert(cert)
|
||||
|
||||
@mock.patch("letsencrypt.display.revocation.util")
|
||||
def test_more_info(self, mock_util):
|
||||
self._call(mock.MagicMock())
|
||||
|
||||
self.assertEqual(mock_util().notification.call_count, 1)
|
||||
|
||||
|
||||
class SuccessRevocationTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from letsencrypt.revoker import Cert
|
||||
self.cert = Cert(test_util.vector_path("cert.pem"))
|
||||
|
||||
@classmethod
|
||||
def _call(cls, cert):
|
||||
from letsencrypt.display.revocation import success_revocation
|
||||
success_revocation(cert)
|
||||
|
||||
# Pretty trivial test... something is displayed...
|
||||
@mock.patch("letsencrypt.display.revocation.util")
|
||||
def test_success_revocation(self, mock_util):
|
||||
self._call(self.cert)
|
||||
|
||||
self.assertEqual(mock_util().notification.call_count, 1)
|
||||
|
||||
|
||||
class ConfirmRevocationTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from letsencrypt.revoker import Cert
|
||||
self.cert = Cert(test_util.vector_path("cert.pem"))
|
||||
|
||||
@classmethod
|
||||
def _call(cls, cert):
|
||||
from letsencrypt.display.revocation import confirm_revocation
|
||||
return confirm_revocation(cert)
|
||||
|
||||
@mock.patch("letsencrypt.display.revocation.util")
|
||||
def test_confirm_revocation(self, mock_util):
|
||||
mock_util().yesno.return_value = True
|
||||
self.assertTrue(self._call(self.cert))
|
||||
|
||||
mock_util().yesno.return_value = False
|
||||
self.assertFalse(self._call(self.cert))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
64
letsencrypt/tests/error_handler_test.py
Normal file
64
letsencrypt/tests/error_handler_test.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""Tests for letsencrypt.error_handler."""
|
||||
import signal
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
|
||||
class ErrorHandlerTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.error_handler."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt import error_handler
|
||||
|
||||
self.init_func = mock.MagicMock()
|
||||
self.handler = error_handler.ErrorHandler(self.init_func)
|
||||
# pylint: disable=protected-access
|
||||
self.signals = error_handler._SIGNALS
|
||||
|
||||
def test_context_manager(self):
|
||||
try:
|
||||
with self.handler:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
pass
|
||||
self.init_func.assert_called_once_with()
|
||||
|
||||
@mock.patch('letsencrypt.error_handler.os')
|
||||
@mock.patch('letsencrypt.error_handler.signal')
|
||||
def test_signal_handler(self, mock_signal, mock_os):
|
||||
# pylint: disable=protected-access
|
||||
mock_signal.getsignal.return_value = signal.SIG_DFL
|
||||
self.handler.set_signal_handlers()
|
||||
signal_handler = self.handler._signal_handler
|
||||
for signum in self.signals:
|
||||
mock_signal.signal.assert_any_call(signum, signal_handler)
|
||||
|
||||
signum = self.signals[0]
|
||||
signal_handler(signum, None)
|
||||
self.init_func.assert_called_once_with()
|
||||
mock_os.kill.assert_called_once_with(mock_os.getpid(), signum)
|
||||
|
||||
self.handler.reset_signal_handlers()
|
||||
for signum in self.signals:
|
||||
mock_signal.signal.assert_any_call(signum, signal.SIG_DFL)
|
||||
|
||||
def test_bad_recovery(self):
|
||||
bad_func = mock.MagicMock(side_effect=[ValueError])
|
||||
self.handler.register(bad_func)
|
||||
self.handler.call_registered()
|
||||
self.init_func.assert_called_once_with()
|
||||
bad_func.assert_called_once_with()
|
||||
|
||||
def test_sysexit_ignored(self):
|
||||
try:
|
||||
with self.handler:
|
||||
sys.exit(0)
|
||||
except SystemExit:
|
||||
pass
|
||||
self.assertFalse(self.init_func.called)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
@@ -8,7 +8,7 @@ import mock
|
||||
class DialogHandlerTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.d = mock.MagicMock() # pylint: disable=invalid-name
|
||||
self.d = mock.MagicMock()
|
||||
|
||||
from letsencrypt.log import DialogHandler
|
||||
self.handler = DialogHandler(height=2, width=6, d=self.d)
|
||||
|
||||
@@ -33,7 +33,12 @@ def fill_with_sample_data(rc_object):
|
||||
|
||||
|
||||
class BaseRenewableCertTest(unittest.TestCase):
|
||||
"""Base class for setting up Renewable Cert tests.
|
||||
|
||||
.. note:: It may be required to write out self.config for
|
||||
your test. Check :class:`.cli_test.DuplicateCertTest` for an example.
|
||||
|
||||
"""
|
||||
def setUp(self):
|
||||
from letsencrypt import storage
|
||||
self.tempdir = tempfile.mkdtemp()
|
||||
@@ -118,46 +123,47 @@ class RenewableCertTests(BaseRenewableCertTest):
|
||||
self.assertRaises(
|
||||
errors.CertStorageError, storage.RenewableCert, config, defaults)
|
||||
|
||||
def test_consistent(self): # pylint: disable=too-many-statements
|
||||
def test_consistent(self):
|
||||
# pylint: disable=too-many-statements,protected-access
|
||||
oldcert = self.test_rc.cert
|
||||
self.test_rc.cert = "relative/path"
|
||||
# Absolute path for item requirement
|
||||
self.assertFalse(self.test_rc.consistent())
|
||||
self.assertFalse(self.test_rc._consistent())
|
||||
self.test_rc.cert = oldcert
|
||||
# Items must exist requirement
|
||||
self.assertFalse(self.test_rc.consistent())
|
||||
self.assertFalse(self.test_rc._consistent())
|
||||
# Items must be symlinks requirements
|
||||
fill_with_sample_data(self.test_rc)
|
||||
self.assertFalse(self.test_rc.consistent())
|
||||
self.assertFalse(self.test_rc._consistent())
|
||||
unlink_all(self.test_rc)
|
||||
# Items must point to desired place if they are relative
|
||||
for kind in ALL_FOUR:
|
||||
os.symlink(os.path.join("..", kind + "17.pem"),
|
||||
getattr(self.test_rc, kind))
|
||||
self.assertFalse(self.test_rc.consistent())
|
||||
self.assertFalse(self.test_rc._consistent())
|
||||
unlink_all(self.test_rc)
|
||||
# Items must point to desired place if they are absolute
|
||||
for kind in ALL_FOUR:
|
||||
os.symlink(os.path.join(self.tempdir, kind + "17.pem"),
|
||||
getattr(self.test_rc, kind))
|
||||
self.assertFalse(self.test_rc.consistent())
|
||||
self.assertFalse(self.test_rc._consistent())
|
||||
unlink_all(self.test_rc)
|
||||
# Items must point to things that exist
|
||||
for kind in ALL_FOUR:
|
||||
os.symlink(os.path.join("..", "..", "archive", "example.org",
|
||||
kind + "17.pem"),
|
||||
getattr(self.test_rc, kind))
|
||||
self.assertFalse(self.test_rc.consistent())
|
||||
self.assertFalse(self.test_rc._consistent())
|
||||
# This version should work
|
||||
fill_with_sample_data(self.test_rc)
|
||||
self.assertTrue(self.test_rc.consistent())
|
||||
self.assertTrue(self.test_rc._consistent())
|
||||
# Items must point to things that follow the naming convention
|
||||
os.unlink(self.test_rc.fullchain)
|
||||
os.symlink(os.path.join("..", "..", "archive", "example.org",
|
||||
"fullchain_17.pem"), self.test_rc.fullchain)
|
||||
with open(self.test_rc.fullchain, "w") as f:
|
||||
f.write("wrongly-named fullchain")
|
||||
self.assertFalse(self.test_rc.consistent())
|
||||
self.assertFalse(self.test_rc._consistent())
|
||||
|
||||
def test_current_target(self):
|
||||
# Relative path logic
|
||||
@@ -254,14 +260,15 @@ class RenewableCertTests(BaseRenewableCertTest):
|
||||
with open(where, "w") as f:
|
||||
f.write(kind)
|
||||
self.assertEqual(ver, self.test_rc.current_version(kind))
|
||||
self.test_rc.update_link_to("cert", 3)
|
||||
self.test_rc.update_link_to("privkey", 2)
|
||||
# pylint: disable=protected-access
|
||||
self.test_rc._update_link_to("cert", 3)
|
||||
self.test_rc._update_link_to("privkey", 2)
|
||||
self.assertEqual(3, self.test_rc.current_version("cert"))
|
||||
self.assertEqual(2, self.test_rc.current_version("privkey"))
|
||||
self.assertEqual(5, self.test_rc.current_version("chain"))
|
||||
self.assertEqual(5, self.test_rc.current_version("fullchain"))
|
||||
# Currently we are allowed to update to a version that doesn't exist
|
||||
self.test_rc.update_link_to("chain", 3000)
|
||||
self.test_rc._update_link_to("chain", 3000)
|
||||
# However, current_version doesn't allow querying the resulting
|
||||
# version (because it's a broken link).
|
||||
self.assertEqual(os.path.basename(os.readlink(self.test_rc.chain)),
|
||||
@@ -400,6 +407,14 @@ class RenewableCertTests(BaseRenewableCertTest):
|
||||
self.assertEqual(self.test_rc.should_autodeploy(), result)
|
||||
self.assertEqual(self.test_rc.should_autorenew(), result)
|
||||
|
||||
def test_autodeployment_is_enabled(self):
|
||||
self.assertTrue(self.test_rc.autodeployment_is_enabled())
|
||||
self.test_rc.configuration["autodeploy"] = "1"
|
||||
self.assertTrue(self.test_rc.autodeployment_is_enabled())
|
||||
|
||||
self.test_rc.configuration["autodeploy"] = "0"
|
||||
self.assertFalse(self.test_rc.autodeployment_is_enabled())
|
||||
|
||||
def test_should_autodeploy(self):
|
||||
"""Test should_autodeploy() on the basis of reasons other than
|
||||
expiry time window."""
|
||||
@@ -420,6 +435,14 @@ class RenewableCertTests(BaseRenewableCertTest):
|
||||
f.write(kind)
|
||||
self.assertFalse(self.test_rc.should_autodeploy())
|
||||
|
||||
def test_autorenewal_is_enabled(self):
|
||||
self.assertTrue(self.test_rc.autorenewal_is_enabled())
|
||||
self.test_rc.configuration["autorenew"] = "1"
|
||||
self.assertTrue(self.test_rc.autorenewal_is_enabled())
|
||||
|
||||
self.test_rc.configuration["autorenew"] = "0"
|
||||
self.assertFalse(self.test_rc.autorenewal_is_enabled())
|
||||
|
||||
@mock.patch("letsencrypt.storage.RenewableCert.ocsp_revoked")
|
||||
def test_should_autorenew(self, mock_ocsp):
|
||||
"""Test should_autorenew on the basis of reasons other than
|
||||
@@ -502,7 +525,8 @@ class RenewableCertTests(BaseRenewableCertTest):
|
||||
self.defaults, self.cli_config)
|
||||
# This consistency check tests most relevant properties about the
|
||||
# newly created cert lineage.
|
||||
self.assertTrue(result.consistent())
|
||||
# pylint: disable=protected-access
|
||||
self.assertTrue(result._consistent())
|
||||
self.assertTrue(os.path.exists(os.path.join(
|
||||
self.cli_config.renewal_configs_dir, "the-lineage.com.conf")))
|
||||
with open(result.fullchain) as f:
|
||||
@@ -573,9 +597,10 @@ class RenewableCertTests(BaseRenewableCertTest):
|
||||
self.assertRaises(
|
||||
errors.CertStorageError,
|
||||
self.test_rc.newest_available_version, "elephant")
|
||||
# pylint: disable=protected-access
|
||||
self.assertRaises(
|
||||
errors.CertStorageError,
|
||||
self.test_rc.update_link_to, "elephant", 17)
|
||||
self.test_rc._update_link_to, "elephant", 17)
|
||||
|
||||
def test_ocsp_revoked(self):
|
||||
# XXX: This is currently hardcoded to False due to a lack of an
|
||||
|
||||
@@ -82,9 +82,11 @@ class ReporterTest(unittest.TestCase):
|
||||
self.assertTrue("Low" not in output)
|
||||
|
||||
def _add_messages(self):
|
||||
self.reporter.add_message("High", self.reporter.HIGH_PRIORITY, True)
|
||||
self.reporter.add_message("Med", self.reporter.MEDIUM_PRIORITY)
|
||||
self.reporter.add_message("Low", self.reporter.LOW_PRIORITY)
|
||||
self.reporter.add_message("High", self.reporter.HIGH_PRIORITY)
|
||||
self.reporter.add_message(
|
||||
"Med", self.reporter.MEDIUM_PRIORITY, on_crash=False)
|
||||
self.reporter.add_message(
|
||||
"Low", self.reporter.LOW_PRIORITY, on_crash=False)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -85,7 +85,7 @@ class ReverterCheckpointLocalTest(unittest.TestCase):
|
||||
self.assertEqual(read_in(self.config1), "directive-dir1")
|
||||
|
||||
def test_multiple_registration_fail_and_revert(self):
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
config3 = os.path.join(self.dir1, "config3.txt")
|
||||
update_file(config3, "Config3")
|
||||
config4 = os.path.join(self.dir2, "config4.txt")
|
||||
@@ -173,7 +173,7 @@ class ReverterCheckpointLocalTest(unittest.TestCase):
|
||||
self.assertRaises(errors.ReverterError, self.reverter.recovery_routine)
|
||||
|
||||
def test_recover_checkpoint_revert_temp_failures(self):
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
mock_recover = mock.MagicMock(
|
||||
side_effect=errors.ReverterError("e"))
|
||||
|
||||
@@ -291,7 +291,7 @@ class TestFullCheckpointsReverter(unittest.TestCase):
|
||||
errors.ReverterError, self.reverter.rollback_checkpoints, "one")
|
||||
|
||||
def test_rollback_finalize_checkpoint_valid_inputs(self):
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
config3 = self._setup_three_checkpoints()
|
||||
|
||||
# Check resulting backup directory
|
||||
@@ -334,7 +334,7 @@ class TestFullCheckpointsReverter(unittest.TestCase):
|
||||
|
||||
@mock.patch("letsencrypt.reverter.os.rename")
|
||||
def test_finalize_checkpoint_no_rename_directory(self, mock_rename):
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
self.reverter.add_to_checkpoint(self.sets[0], "perm save")
|
||||
mock_rename.side_effect = OSError
|
||||
|
||||
|
||||
@@ -1,409 +0,0 @@
|
||||
"""Test letsencrypt.revoker."""
|
||||
import csv
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
import OpenSSL
|
||||
|
||||
from letsencrypt import errors
|
||||
from letsencrypt import le_util
|
||||
from letsencrypt.display import util as display_util
|
||||
|
||||
from letsencrypt.tests import test_util
|
||||
|
||||
|
||||
KEY = OpenSSL.crypto.load_privatekey(
|
||||
OpenSSL.crypto.FILETYPE_PEM, test_util.load_vector("rsa512_key.pem"))
|
||||
|
||||
|
||||
class RevokerBase(unittest.TestCase): # pylint: disable=too-few-public-methods
|
||||
"""Base Class for Revoker Tests."""
|
||||
def setUp(self):
|
||||
self.paths, self.certs, self.key_path = create_revoker_certs()
|
||||
|
||||
self.backup_dir = tempfile.mkdtemp("cert_backup")
|
||||
self.mock_config = mock.MagicMock(cert_key_backup=self.backup_dir)
|
||||
|
||||
self.list_path = os.path.join(self.backup_dir, "LIST")
|
||||
|
||||
def _store_certs(self):
|
||||
# pylint: disable=protected-access
|
||||
from letsencrypt.revoker import Revoker
|
||||
Revoker.store_cert_key(self.paths[0], self.key_path, self.mock_config)
|
||||
Revoker.store_cert_key(self.paths[1], self.key_path, self.mock_config)
|
||||
|
||||
# Set metadata
|
||||
for i in xrange(2):
|
||||
self.certs[i].add_meta(
|
||||
i, self.paths[i], self.key_path,
|
||||
Revoker._get_backup(self.backup_dir, i, self.paths[i]),
|
||||
Revoker._get_backup(self.backup_dir, i, self.key_path))
|
||||
|
||||
def _get_rows(self):
|
||||
with open(self.list_path, "rb") as csvfile:
|
||||
return [row for row in csv.reader(csvfile)]
|
||||
|
||||
def _write_rows(self, rows):
|
||||
with open(self.list_path, "wb") as csvfile:
|
||||
csvwriter = csv.writer(csvfile)
|
||||
for row in rows:
|
||||
csvwriter.writerow(row)
|
||||
|
||||
|
||||
class RevokerTest(RevokerBase):
|
||||
def setUp(self):
|
||||
from letsencrypt.revoker import Revoker
|
||||
super(RevokerTest, self).setUp()
|
||||
|
||||
with open(self.key_path) as key_file:
|
||||
self.key = le_util.Key(self.key_path, key_file.read())
|
||||
|
||||
self._store_certs()
|
||||
|
||||
self.revoker = Revoker(
|
||||
installer=mock.MagicMock(), config=self.mock_config)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.backup_dir)
|
||||
|
||||
@mock.patch("acme.client.Client.revoke")
|
||||
@mock.patch("letsencrypt.revoker.revocation")
|
||||
def test_revoke_by_key_all(self, mock_display, mock_acme):
|
||||
mock_display().confirm_revocation.return_value = True
|
||||
|
||||
self.revoker.revoke_from_key(self.key)
|
||||
self.assertEqual(self._get_rows(), [])
|
||||
|
||||
# Check to make sure backups were eliminated
|
||||
for i in xrange(2):
|
||||
self.assertFalse(self._backups_exist(self.certs[i].get_row()))
|
||||
|
||||
self.assertEqual(mock_acme.call_count, 2)
|
||||
|
||||
@mock.patch("letsencrypt.revoker.OpenSSL.crypto.load_privatekey")
|
||||
def test_revoke_by_invalid_keys(self, mock_load_privatekey):
|
||||
mock_load_privatekey.side_effect = OpenSSL.crypto.Error
|
||||
self.assertRaises(
|
||||
errors.RevokerError, self.revoker.revoke_from_key, self.key)
|
||||
|
||||
mock_load_privatekey.side_effect = [KEY, OpenSSL.crypto.Error]
|
||||
self.assertRaises(
|
||||
errors.RevokerError, self.revoker.revoke_from_key, self.key)
|
||||
|
||||
@mock.patch("acme.client.Client.revoke")
|
||||
@mock.patch("letsencrypt.revoker.revocation")
|
||||
def test_revoke_by_wrong_key(self, mock_display, mock_acme):
|
||||
mock_display().confirm_revocation.return_value = True
|
||||
|
||||
key_path = test_util.vector_path("rsa256_key.pem")
|
||||
|
||||
wrong_key = le_util.Key(key_path, open(key_path).read())
|
||||
self.revoker.revoke_from_key(wrong_key)
|
||||
|
||||
# Nothing was removed
|
||||
self.assertEqual(len(self._get_rows()), 2)
|
||||
# No revocation went through
|
||||
self.assertEqual(mock_acme.call_count, 0)
|
||||
|
||||
@mock.patch("acme.client.Client.revoke")
|
||||
@mock.patch("letsencrypt.revoker.revocation")
|
||||
def test_revoke_by_cert(self, mock_display, mock_acme):
|
||||
mock_display().confirm_revocation.return_value = True
|
||||
|
||||
self.revoker.revoke_from_cert(self.paths[1])
|
||||
|
||||
row0 = self.certs[0].get_row()
|
||||
row1 = self.certs[1].get_row()
|
||||
|
||||
self.assertEqual(self._get_rows(), [row0])
|
||||
|
||||
self.assertTrue(self._backups_exist(row0))
|
||||
self.assertFalse(self._backups_exist(row1))
|
||||
|
||||
self.assertEqual(mock_acme.call_count, 1)
|
||||
|
||||
@mock.patch("acme.client.Client.revoke")
|
||||
@mock.patch("letsencrypt.revoker.revocation")
|
||||
def test_revoke_by_cert_not_found(self, mock_display, mock_acme):
|
||||
mock_display().confirm_revocation.return_value = True
|
||||
|
||||
self.revoker.revoke_from_cert(self.paths[0])
|
||||
self.revoker.revoke_from_cert(self.paths[0])
|
||||
|
||||
row0 = self.certs[0].get_row()
|
||||
row1 = self.certs[1].get_row()
|
||||
|
||||
# Same check as last time... just reversed.
|
||||
self.assertEqual(self._get_rows(), [row1])
|
||||
|
||||
self.assertTrue(self._backups_exist(row1))
|
||||
self.assertFalse(self._backups_exist(row0))
|
||||
|
||||
self.assertEqual(mock_acme.call_count, 1)
|
||||
|
||||
@mock.patch("acme.client.Client.revoke")
|
||||
@mock.patch("letsencrypt.revoker.revocation")
|
||||
def test_revoke_by_menu(self, mock_display, mock_acme):
|
||||
mock_display().confirm_revocation.return_value = True
|
||||
mock_display.display_certs.side_effect = [
|
||||
(display_util.HELP, 0),
|
||||
(display_util.OK, 0),
|
||||
(display_util.CANCEL, -1),
|
||||
]
|
||||
|
||||
self.revoker.revoke_from_menu()
|
||||
|
||||
row0 = self.certs[0].get_row()
|
||||
row1 = self.certs[1].get_row()
|
||||
|
||||
self.assertEqual(self._get_rows(), [row1])
|
||||
|
||||
self.assertFalse(self._backups_exist(row0))
|
||||
self.assertTrue(self._backups_exist(row1))
|
||||
|
||||
self.assertEqual(mock_acme.call_count, 1)
|
||||
self.assertEqual(mock_display.more_info_cert.call_count, 1)
|
||||
|
||||
@mock.patch("letsencrypt.revoker.logger")
|
||||
@mock.patch("acme.client.Client.revoke")
|
||||
@mock.patch("letsencrypt.revoker.revocation")
|
||||
def test_revoke_by_menu_delete_all(self, mock_display, mock_acme, mock_log):
|
||||
mock_display().confirm_revocation.return_value = True
|
||||
mock_display.display_certs.return_value = (display_util.OK, 0)
|
||||
|
||||
self.revoker.revoke_from_menu()
|
||||
|
||||
self.assertEqual(self._get_rows(), [])
|
||||
|
||||
# Everything should be deleted...
|
||||
for i in xrange(2):
|
||||
self.assertFalse(self._backups_exist(self.certs[i].get_row()))
|
||||
|
||||
self.assertEqual(mock_acme.call_count, 2)
|
||||
# Info is called when there aren't any certs left...
|
||||
self.assertTrue(mock_log.info.called)
|
||||
|
||||
@mock.patch("letsencrypt.revoker.revocation")
|
||||
@mock.patch("letsencrypt.revoker.Revoker._acme_revoke")
|
||||
@mock.patch("letsencrypt.revoker.logger")
|
||||
def test_safe_revoke_acme_fail(self, mock_log, mock_revoke, mock_display):
|
||||
# pylint: disable=protected-access
|
||||
mock_revoke.side_effect = errors.Error
|
||||
mock_display().confirm_revocation.return_value = True
|
||||
|
||||
self.revoker._safe_revoke(self.certs)
|
||||
self.assertTrue(mock_log.error.called)
|
||||
|
||||
@mock.patch("letsencrypt.revoker.OpenSSL.crypto.load_privatekey")
|
||||
def test_acme_revoke_failure(self, mock_load_privatekey):
|
||||
# pylint: disable=protected-access
|
||||
mock_load_privatekey.side_effect = OpenSSL.crypto.Error
|
||||
self.assertRaises(
|
||||
errors.Error, self.revoker._acme_revoke, self.certs[0])
|
||||
|
||||
def test_remove_certs_from_list_bad_certs(self):
|
||||
# pylint: disable=protected-access
|
||||
from letsencrypt.revoker import Cert
|
||||
|
||||
new_cert = Cert(self.paths[0])
|
||||
|
||||
# This isn't stored in the db
|
||||
new_cert.idx = 10
|
||||
new_cert.backup_path = self.paths[0]
|
||||
new_cert.backup_key_path = self.key_path
|
||||
new_cert.orig = Cert.PathStatus("false path", "not here")
|
||||
new_cert.orig_key = Cert.PathStatus("false path", "not here")
|
||||
|
||||
self.assertRaises(errors.RevokerError,
|
||||
self.revoker._remove_certs_from_list, [new_cert])
|
||||
|
||||
def _backups_exist(self, row):
|
||||
# pylint: disable=protected-access
|
||||
cert_path, key_path = self.revoker._row_to_backup(row)
|
||||
return os.path.isfile(cert_path) and os.path.isfile(key_path)
|
||||
|
||||
|
||||
class RevokerInstallerTest(RevokerBase):
|
||||
def setUp(self):
|
||||
super(RevokerInstallerTest, self).setUp()
|
||||
|
||||
self.installs = [
|
||||
["installation/path0a", "installation/path0b"],
|
||||
["installation/path1"],
|
||||
]
|
||||
|
||||
self.certs_keys = [
|
||||
(self.paths[0], self.key_path, self.installs[0][0]),
|
||||
(self.paths[0], self.key_path, self.installs[0][1]),
|
||||
(self.paths[1], self.key_path, self.installs[1][0]),
|
||||
]
|
||||
|
||||
self._store_certs()
|
||||
|
||||
def _get_revoker(self, installer):
|
||||
from letsencrypt.revoker import Revoker
|
||||
return Revoker(installer, self.mock_config)
|
||||
|
||||
def test_no_installer_get_installed_locations(self):
|
||||
# pylint: disable=protected-access
|
||||
revoker = self._get_revoker(None)
|
||||
self.assertEqual(revoker._get_installed_locations(), {})
|
||||
|
||||
def test_get_installed_locations(self):
|
||||
# pylint: disable=protected-access
|
||||
mock_installer = mock.MagicMock()
|
||||
mock_installer.get_all_certs_keys.return_value = self.certs_keys
|
||||
|
||||
revoker = self._get_revoker(mock_installer)
|
||||
sha_vh = revoker._get_installed_locations()
|
||||
|
||||
self.assertEqual(len(sha_vh), 2)
|
||||
for i, cert in enumerate(self.certs):
|
||||
self.assertTrue(cert.get_fingerprint() in sha_vh)
|
||||
self.assertEqual(
|
||||
sha_vh[cert.get_fingerprint()], self.installs[i])
|
||||
|
||||
@mock.patch("letsencrypt.revoker.OpenSSL.crypto.load_certificate")
|
||||
def test_get_installed_load_failure(self, mock_load_certificate):
|
||||
mock_installer = mock.MagicMock()
|
||||
mock_installer.get_all_certs_keys.return_value = self.certs_keys
|
||||
|
||||
mock_load_certificate.side_effect = OpenSSL.crypto.Error
|
||||
|
||||
revoker = self._get_revoker(mock_installer)
|
||||
|
||||
# pylint: disable=protected-access
|
||||
self.assertEqual(revoker._get_installed_locations(), {})
|
||||
|
||||
def test_get_installed_load_failure_open(self):
|
||||
tmp = tempfile.mkdtemp()
|
||||
mock_installer = mock.MagicMock()
|
||||
mock_installer.get_all_certs_keys.return_value = [(
|
||||
os.path.join(tmp, 'missing'), None, None)]
|
||||
revoker = self._get_revoker(mock_installer)
|
||||
# pylint: disable=protected-access
|
||||
self.assertEqual(revoker._get_installed_locations(), {})
|
||||
os.rmdir(tmp)
|
||||
|
||||
|
||||
class RevokerClassMethodsTest(RevokerBase):
|
||||
def setUp(self):
|
||||
super(RevokerClassMethodsTest, self).setUp()
|
||||
self.mock_config = mock.MagicMock(cert_key_backup=self.backup_dir)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.backup_dir)
|
||||
|
||||
def _call(self, cert_path, key_path):
|
||||
from letsencrypt.revoker import Revoker
|
||||
Revoker.store_cert_key(cert_path, key_path, self.mock_config)
|
||||
|
||||
def test_store_two(self):
|
||||
from letsencrypt.revoker import Revoker
|
||||
self._call(self.paths[0], self.key_path)
|
||||
self._call(self.paths[1], self.key_path)
|
||||
|
||||
self.assertTrue(os.path.isfile(self.list_path))
|
||||
rows = self._get_rows()
|
||||
|
||||
for i, row in enumerate(rows):
|
||||
# pylint: disable=protected-access
|
||||
self.assertTrue(os.path.isfile(
|
||||
Revoker._get_backup(self.backup_dir, i, self.paths[i])))
|
||||
self.assertTrue(os.path.isfile(
|
||||
Revoker._get_backup(self.backup_dir, i, self.key_path)))
|
||||
self.assertEqual([str(i), self.paths[i], self.key_path], row)
|
||||
|
||||
self.assertEqual(len(rows), 2)
|
||||
|
||||
def test_store_one_mixed(self):
|
||||
from letsencrypt.revoker import Revoker
|
||||
self._write_rows(
|
||||
[["5", "blank", "blank"], ["18", "dc", "dc"], ["21", "b", "b"]])
|
||||
self._call(self.paths[0], self.key_path)
|
||||
|
||||
self.assertEqual(
|
||||
self._get_rows()[3], ["22", self.paths[0], self.key_path])
|
||||
|
||||
# pylint: disable=protected-access
|
||||
self.assertTrue(os.path.isfile(
|
||||
Revoker._get_backup(self.backup_dir, 22, self.paths[0])))
|
||||
self.assertTrue(os.path.isfile(
|
||||
Revoker._get_backup(self.backup_dir, 22, self.key_path)))
|
||||
|
||||
|
||||
class CertTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.paths, self.certs, self.key_path = create_revoker_certs()
|
||||
|
||||
def test_failed_load(self):
|
||||
from letsencrypt.revoker import Cert
|
||||
self.assertRaises(errors.RevokerError, Cert, self.key_path)
|
||||
|
||||
def test_failed_load_open(self):
|
||||
tmp = tempfile.mkdtemp()
|
||||
from letsencrypt.revoker import Cert
|
||||
self.assertRaises(
|
||||
errors.RevokerError, Cert, os.path.join(tmp, 'missing'))
|
||||
os.rmdir(tmp)
|
||||
|
||||
def test_no_row(self):
|
||||
self.assertEqual(self.certs[0].get_row(), None)
|
||||
|
||||
def test_meta_moved_files(self):
|
||||
from letsencrypt.revoker import Cert
|
||||
fake_path = "/not/a/real/path/r72d3t6"
|
||||
self.certs[0].add_meta(
|
||||
0, fake_path, fake_path, self.paths[0], self.key_path)
|
||||
|
||||
self.assertEqual(self.certs[0].orig.status, Cert.DELETED_MSG)
|
||||
self.assertEqual(self.certs[0].orig_key.status, Cert.DELETED_MSG)
|
||||
|
||||
def test_meta_changed_files(self):
|
||||
from letsencrypt.revoker import Cert
|
||||
self.certs[0].add_meta(
|
||||
0, self.paths[1], self.paths[1], self.paths[0], self.key_path)
|
||||
|
||||
self.assertEqual(self.certs[0].orig.status, Cert.CHANGED_MSG)
|
||||
self.assertEqual(self.certs[0].orig_key.status, Cert.CHANGED_MSG)
|
||||
|
||||
def test_meta_no_status(self):
|
||||
self.certs[0].add_meta(
|
||||
0, self.paths[0], self.key_path, self.paths[0], self.key_path)
|
||||
|
||||
self.assertEqual(self.certs[0].orig.status, "")
|
||||
self.assertEqual(self.certs[0].orig_key.status, "")
|
||||
|
||||
def test_print_meta(self):
|
||||
"""Just make sure there aren't any major errors."""
|
||||
self.certs[0].add_meta(
|
||||
0, self.paths[0], self.key_path, self.paths[0], self.key_path)
|
||||
# Changed path and deleted file
|
||||
self.certs[1].add_meta(
|
||||
1, self.paths[0], "/not/a/path", self.paths[1], self.key_path)
|
||||
self.assertTrue(self.certs[0].pretty_print())
|
||||
self.assertTrue(self.certs[1].pretty_print())
|
||||
|
||||
def test_print_no_meta(self):
|
||||
self.assertTrue(self.certs[0].pretty_print())
|
||||
self.assertTrue(self.certs[1].pretty_print())
|
||||
|
||||
|
||||
def create_revoker_certs():
|
||||
"""Create a few revoker.Cert objects."""
|
||||
cert0_path = test_util.vector_path("cert.pem")
|
||||
cert1_path = test_util.vector_path("cert-san.pem")
|
||||
key_path = test_util.vector_path("rsa512_key.pem")
|
||||
|
||||
from letsencrypt.revoker import Cert
|
||||
cert0 = Cert(cert0_path)
|
||||
cert1 = Cert(cert1_path)
|
||||
|
||||
return [cert0_path, cert1_path], [cert0, cert1], key_path
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
@@ -1 +0,0 @@
|
||||
../../acme/acme/test_util.py
|
||||
67
letsencrypt/tests/test_util.py
Normal file
67
letsencrypt/tests/test_util.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""Test utilities.
|
||||
|
||||
.. warning:: This module is not part of the public API.
|
||||
|
||||
"""
|
||||
import os
|
||||
import pkg_resources
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
import OpenSSL
|
||||
|
||||
from acme import jose
|
||||
|
||||
|
||||
def vector_path(*names):
|
||||
"""Path to a test vector."""
|
||||
return pkg_resources.resource_filename(
|
||||
__name__, os.path.join('testdata', *names))
|
||||
|
||||
|
||||
def load_vector(*names):
|
||||
"""Load contents of a test vector."""
|
||||
# luckily, resource_string opens file in binary mode
|
||||
return pkg_resources.resource_string(
|
||||
__name__, os.path.join('testdata', *names))
|
||||
|
||||
|
||||
def _guess_loader(filename, loader_pem, loader_der):
|
||||
_, ext = os.path.splitext(filename)
|
||||
if ext.lower() == '.pem':
|
||||
return loader_pem
|
||||
elif ext.lower() == '.der':
|
||||
return loader_der
|
||||
else: # pragma: no cover
|
||||
raise ValueError("Loader could not be recognized based on extension")
|
||||
|
||||
|
||||
def load_cert(*names):
|
||||
"""Load certificate."""
|
||||
loader = _guess_loader(
|
||||
names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
|
||||
return jose.ComparableX509(OpenSSL.crypto.load_certificate(
|
||||
loader, load_vector(*names)))
|
||||
|
||||
|
||||
def load_csr(*names):
|
||||
"""Load certificate request."""
|
||||
loader = _guess_loader(
|
||||
names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
|
||||
return jose.ComparableX509(OpenSSL.crypto.load_certificate_request(
|
||||
loader, load_vector(*names)))
|
||||
|
||||
|
||||
def load_rsa_private_key(*names):
|
||||
"""Load RSA private key."""
|
||||
loader = _guess_loader(names[-1], serialization.load_pem_private_key,
|
||||
serialization.load_der_private_key)
|
||||
return jose.ComparableRSAKey(loader(
|
||||
load_vector(*names), password=None, backend=default_backend()))
|
||||
|
||||
|
||||
def load_pyopenssl_private_key(*names):
|
||||
"""Load pyOpenSSL private key."""
|
||||
loader = _guess_loader(
|
||||
names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
|
||||
return OpenSSL.crypto.load_privatekey(loader, load_vector(*names))
|
||||
190
letshelp-letsencrypt/LICENSE.txt
Normal file
190
letshelp-letsencrypt/LICENSE.txt
Normal file
@@ -0,0 +1,190 @@
|
||||
Copyright 2015 Electronic Frontier Foundation and others
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -1 +1,3 @@
|
||||
recursive-include letshelp-letsencrypt/testdata *
|
||||
include LICENSE.txt
|
||||
include README.rst
|
||||
recursive-include letshelp_letsencrypt/testdata *
|
||||
|
||||
1
letshelp-letsencrypt/README.rst
Normal file
1
letshelp-letsencrypt/README.rst
Normal file
@@ -0,0 +1 @@
|
||||
Let's help Let's Encrypt client
|
||||
@@ -4,20 +4,46 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
install_requires = []
|
||||
version = '0.0.0.dev20151008'
|
||||
|
||||
install_requires = [
|
||||
'setuptools', # pkg_resources
|
||||
]
|
||||
if sys.version_info < (2, 7):
|
||||
install_requires.append("mock<1.1.0")
|
||||
install_requires.append('mock<1.1.0')
|
||||
else:
|
||||
install_requires.append("mock")
|
||||
install_requires.append('mock')
|
||||
|
||||
setup(
|
||||
name="letshelp-letsencrypt",
|
||||
name='letshelp-letsencrypt',
|
||||
version=version,
|
||||
description="Let's help Let's Encrypt client",
|
||||
url='https://github.com/letsencrypt/letsencrypt',
|
||||
author="Let's Encrypt Project",
|
||||
author_email='client-dev@letsencrypt.org',
|
||||
license='Apache License 2.0',
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Intended Audience :: System Administrators',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
'Topic :: System :: Networking',
|
||||
'Topic :: System :: Systems Administration',
|
||||
'Topic :: Utilities',
|
||||
],
|
||||
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=install_requires,
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
"letshelp-letsencrypt-apache = letshelp_letsencrypt.apache:main",
|
||||
'letshelp-letsencrypt-apache = letshelp_letsencrypt.apache:main',
|
||||
],
|
||||
},
|
||||
include_package_data=True,
|
||||
)
|
||||
|
||||
@@ -11,3 +11,5 @@
|
||||
-e .[docs]
|
||||
-e letsencrypt-apache
|
||||
-e letsencrypt-nginx
|
||||
-e letsencrypt-compatibility-test
|
||||
-e letshelp-letsencrypt
|
||||
|
||||
34
setup.py
34
setup.py
@@ -24,17 +24,17 @@ here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
# read version number (and other metadata) from package init
|
||||
init_fn = os.path.join(here, 'letsencrypt', '__init__.py')
|
||||
meta = dict(re.findall(r"""__([a-z]+)__ = "([^"]+)""", read_file(init_fn)))
|
||||
meta = dict(re.findall(r"""__([a-z]+)__ = '([^']+)""", read_file(init_fn)))
|
||||
|
||||
readme = read_file(os.path.join(here, 'README.rst'))
|
||||
changes = read_file(os.path.join(here, 'CHANGES.rst'))
|
||||
version = meta['version']
|
||||
|
||||
install_requires = [
|
||||
'acme',
|
||||
'acme=={0}'.format(version),
|
||||
'ConfigArgParse',
|
||||
'configobj',
|
||||
'cryptography>=0.7', # load_pem_x509_certificate
|
||||
'mock<1.1.0', # py26
|
||||
'parsedatetime',
|
||||
'psutil>=2.1.0', # net_connections introduced in 2.1.0
|
||||
'PyOpenSSL',
|
||||
@@ -42,19 +42,27 @@ install_requires = [
|
||||
'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280
|
||||
'pytz',
|
||||
'requests',
|
||||
'setuptools', # pkg_resources
|
||||
'zope.component',
|
||||
'zope.interface',
|
||||
]
|
||||
|
||||
# env markers in extras_require cause problems with older pip: #517
|
||||
if sys.version_info < (2, 7):
|
||||
# only some distros recognize stdlib argparse as already satisfying
|
||||
install_requires.append('argparse')
|
||||
install_requires.extend([
|
||||
# only some distros recognize stdlib argparse as already satisfying
|
||||
'argparse',
|
||||
'mock<1.1.0',
|
||||
])
|
||||
else:
|
||||
install_requires.append('mock')
|
||||
|
||||
dev_extras = [
|
||||
# Pin astroid==1.3.5, pylint==1.4.2 as a workaround for #289
|
||||
'astroid==1.3.5',
|
||||
'pylint==1.4.2', # upstream #248
|
||||
'twine',
|
||||
'wheel',
|
||||
]
|
||||
|
||||
docs_extras = [
|
||||
@@ -74,13 +82,15 @@ testing_extras = [
|
||||
|
||||
setup(
|
||||
name='letsencrypt',
|
||||
version=meta['version'],
|
||||
description="Let's Encrypt",
|
||||
version=version,
|
||||
description="Let's Encrypt client",
|
||||
long_description=readme, # later: + '\n\n' + changes
|
||||
url='https://github.com/letsencrypt/letsencrypt',
|
||||
author="Let's Encrypt Project",
|
||||
author_email='client-dev@letsencrypt.org',
|
||||
license='Apache License 2.0',
|
||||
url='https://letsencrypt.org',
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Environment :: Console',
|
||||
'Environment :: Console :: Curses',
|
||||
'Intended Audience :: System Administrators',
|
||||
@@ -98,6 +108,8 @@ setup(
|
||||
],
|
||||
|
||||
packages=find_packages(exclude=['docs', 'examples', 'tests', 'venv']),
|
||||
include_package_data=True,
|
||||
|
||||
install_requires=install_requires,
|
||||
extras_require={
|
||||
'dev': dev_extras,
|
||||
@@ -116,14 +128,10 @@ setup(
|
||||
'letsencrypt-renewer = letsencrypt.renewer:main',
|
||||
],
|
||||
'letsencrypt.plugins': [
|
||||
'manual = letsencrypt.plugins.manual:ManualAuthenticator',
|
||||
# TODO: null should probably not be presented to the user
|
||||
'manual = letsencrypt.plugins.manual:Authenticator',
|
||||
'null = letsencrypt.plugins.null:Installer',
|
||||
'standalone = letsencrypt.plugins.standalone.authenticator'
|
||||
':StandaloneAuthenticator',
|
||||
],
|
||||
},
|
||||
|
||||
zip_safe=False, # letsencrypt/tests/test_util.py is a symlink!
|
||||
include_package_data=True,
|
||||
)
|
||||
|
||||
@@ -24,7 +24,6 @@ common() {
|
||||
common --domains le1.wtf auth
|
||||
common --domains le2.wtf run
|
||||
common -a manual -d le.wtf auth
|
||||
common -a manual -d le.wtf --no-simple-http-tls auth
|
||||
|
||||
export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \
|
||||
OPENSSL_CNF=examples/openssl.cnf
|
||||
|
||||
@@ -1,6 +1,29 @@
|
||||
#!/bin/sh -xe
|
||||
#!/bin/bash
|
||||
# Download and run Boulder instance for integration testing
|
||||
|
||||
|
||||
# ugh, go version output is like:
|
||||
# go version go1.4.2 linux/amd64
|
||||
GOVER=`go version | cut -d" " -f3 | cut -do -f2`
|
||||
|
||||
# version comparison
|
||||
function verlte {
|
||||
#OS X doesn't support version sorting; emulate with sed
|
||||
if [ `uname` == 'Darwin' ]; then
|
||||
[ "$1" = "`echo -e \"$1\n$2\" | sed 's/\b\([0-9]\)\b/0\1/g' \
|
||||
| sort | sed 's/\b0\([0-9]\)/\1/g' | head -n1`" ]
|
||||
else
|
||||
[ "$1" = "`echo -e "$1\n$2" | sort -V | head -n1`" ]
|
||||
fi
|
||||
}
|
||||
|
||||
if ! verlte 1.5 "$GOVER" ; then
|
||||
echo "We require go version 1.5 or later; you have... $GOVER"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -xe
|
||||
|
||||
export GOPATH="${GOPATH:-/tmp/go}"
|
||||
export PATH="$GOPATH/bin:$PATH"
|
||||
|
||||
@@ -9,7 +32,11 @@ export PATH="$GOPATH/bin:$PATH"
|
||||
go get -d github.com/letsencrypt/boulder/...
|
||||
cd $GOPATH/src/github.com/letsencrypt/boulder
|
||||
# goose is needed for ./test/create_db.sh
|
||||
go get bitbucket.org/liamstask/goose/cmd/goose
|
||||
if ! go get bitbucket.org/liamstask/goose/cmd/goose ; then
|
||||
echo Problems installing goose... perhaps rm -rf \$GOPATH \("$GOPATH"\)
|
||||
echo and try again...
|
||||
exit 1
|
||||
fi
|
||||
./test/create_db.sh
|
||||
./start.py &
|
||||
# Hopefully start.py bootstraps before integration test is started...
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user