mirror of
https://github.com/certbot/certbot.git
synced 2026-06-07 15:52:08 -04:00
Merge remote-tracking branch 'github/letsencrypt/master' into lint
This commit is contained in:
commit
33c2aed021
39 changed files with 420 additions and 113 deletions
27
.travis.yml
27
.travis.yml
|
|
@ -1,8 +1,5 @@
|
|||
language: python
|
||||
|
||||
go:
|
||||
- 1.5
|
||||
|
||||
services:
|
||||
- rabbitmq
|
||||
- mysql
|
||||
|
|
@ -10,8 +7,6 @@ services:
|
|||
# http://docs.travis-ci.com/user/ci-environment/#CI-environment-OS
|
||||
# gimme has to be kept in sync with Boulder's Go version setting in .travis.yml
|
||||
before_install:
|
||||
- travis_retry sudo ./bootstrap/ubuntu.sh
|
||||
- travis_retry sudo apt-get install --no-install-recommends nginx-light openssl
|
||||
- '[ "xxx$BOULDER_INTEGRATION" = "xxx" ] || eval "$(gimme 1.5)"'
|
||||
|
||||
# using separate envs with different TOXENVs creates 4x1 Travis build
|
||||
|
|
@ -24,14 +19,34 @@ env:
|
|||
matrix:
|
||||
- TOXENV=py26 BOULDER_INTEGRATION=1
|
||||
- TOXENV=py27 BOULDER_INTEGRATION=1
|
||||
- TOXENV=py33
|
||||
- TOXENV=py34
|
||||
- TOXENV=lint
|
||||
- TOXENV=cover
|
||||
|
||||
# make sure simplehttp simple verification works (custom /etc/hosts)
|
||||
sudo: false # containers
|
||||
addons:
|
||||
# make sure simplehttp simple verification works (custom /etc/hosts)
|
||||
hosts:
|
||||
- le.wtf
|
||||
mariadb: "10.0"
|
||||
apt:
|
||||
packages: # keep in sync with bootstrap/ubuntu.sh and Boulder
|
||||
- lsb-release
|
||||
- python
|
||||
- python-dev
|
||||
- python-virtualenv
|
||||
- gcc
|
||||
- dialog
|
||||
- libaugeas0
|
||||
- libssl-dev
|
||||
- libffi-dev
|
||||
- ca-certificates
|
||||
# For letsencrypt-nginx integration testing
|
||||
- nginx-light
|
||||
- openssl
|
||||
# For Boulder integration testing
|
||||
- rsyslog
|
||||
|
||||
install: "travis_retry pip install tox coveralls"
|
||||
before_script: '[ "xxx$BOULDER_INTEGRATION" = "xxx" ] || ./tests/boulder-start.sh'
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ class SimpleHTTPResponseTest(unittest.TestCase):
|
|||
account_public_key=account_key.public_key()))
|
||||
|
||||
@mock.patch("acme.challenges.requests.get")
|
||||
def test_simple_verify_good_token(self, mock_get):
|
||||
def test_simple_verify_good_validation(self, mock_get):
|
||||
account_key = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
|
||||
for resp in self.resp_http, self.resp_https:
|
||||
mock_get.reset_mock()
|
||||
|
|
@ -156,9 +156,9 @@ class SimpleHTTPResponseTest(unittest.TestCase):
|
|||
"local", self.chall), verify=False)
|
||||
|
||||
@mock.patch("acme.challenges.requests.get")
|
||||
def test_simple_verify_bad_token(self, mock_get):
|
||||
def test_simple_verify_bad_validation(self, mock_get):
|
||||
mock_get.return_value = mock.MagicMock(
|
||||
text=self.chall.token + "!", headers=self.good_headers)
|
||||
text="!", headers=self.good_headers)
|
||||
self.assertFalse(self.resp_http.simple_verify(
|
||||
self.chall, "local", None))
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ import heapq
|
|||
import logging
|
||||
import time
|
||||
|
||||
import six
|
||||
from six.moves import http_client # pylint: disable=import-error
|
||||
|
||||
import OpenSSL
|
||||
import requests
|
||||
import six
|
||||
import sys
|
||||
import werkzeug
|
||||
|
||||
from acme import errors
|
||||
|
|
@ -19,8 +20,9 @@ from acme import messages
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Python does not validate certificates by default before version 2.7.9
|
||||
# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning
|
||||
if six.PY2:
|
||||
if sys.version_info < (2, 7, 9): # pragma: no cover
|
||||
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3()
|
||||
|
||||
|
||||
|
|
@ -31,7 +33,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
|
|||
Clean up raised error types hierarchy, document, and handle (wrap)
|
||||
instances of `.DeserializationError` raised in `from_json()`.
|
||||
|
||||
:ivar str new_reg_uri: Location of new-reg
|
||||
:ivar messages.Directory directory:
|
||||
:ivar key: `.JWK` (private)
|
||||
:ivar alg: `.JWASignature`
|
||||
:ivar bool verify_ssl: Verify SSL certificates?
|
||||
|
|
@ -42,12 +44,23 @@ class Client(object): # pylint: disable=too-many-instance-attributes
|
|||
"""
|
||||
DER_CONTENT_TYPE = 'application/pkix-cert'
|
||||
|
||||
def __init__(self, new_reg_uri, key, alg=jose.RS256,
|
||||
verify_ssl=True, net=None):
|
||||
self.new_reg_uri = new_reg_uri
|
||||
def __init__(self, directory, key, alg=jose.RS256, verify_ssl=True,
|
||||
net=None):
|
||||
"""Initialize.
|
||||
|
||||
:param directory: Directory Resource (`.messages.Directory`) or
|
||||
URI from which the resource will be downloaded.
|
||||
|
||||
"""
|
||||
self.key = key
|
||||
self.net = ClientNetwork(key, alg, verify_ssl) if net is None else net
|
||||
|
||||
if isinstance(directory, six.string_types):
|
||||
self.directory = messages.Directory.from_json(
|
||||
self.net.get(directory).json())
|
||||
else:
|
||||
self.directory = directory
|
||||
|
||||
@classmethod
|
||||
def _regr_from_response(cls, response, uri=None, new_authzr_uri=None,
|
||||
terms_of_service=None):
|
||||
|
|
@ -81,7 +94,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
|
|||
new_reg = messages.NewRegistration() if new_reg is None else new_reg
|
||||
assert isinstance(new_reg, messages.NewRegistration)
|
||||
|
||||
response = self.net.post(self.new_reg_uri, new_reg)
|
||||
response = self.net.post(self.directory[new_reg], new_reg)
|
||||
# TODO: handle errors
|
||||
assert response.status_code == http_client.CREATED
|
||||
|
||||
|
|
@ -440,8 +453,9 @@ class Client(object): # pylint: disable=too-many-instance-attributes
|
|||
:raises .ClientError: If revocation is unsuccessful.
|
||||
|
||||
"""
|
||||
response = self.net.post(messages.Revocation.url(self.new_reg_uri),
|
||||
messages.Revocation(certificate=cert))
|
||||
response = self.net.post(self.directory[messages.Revocation],
|
||||
messages.Revocation(certificate=cert),
|
||||
content_type=None)
|
||||
if response.status_code != http_client.OK:
|
||||
raise errors.ClientError(
|
||||
'Successful revocation must return HTTP OK status')
|
||||
|
|
@ -559,7 +573,7 @@ class ClientNetwork(object):
|
|||
"""Send HEAD request without checking the response.
|
||||
|
||||
Note, that `_check_response` is not called, as it is expected
|
||||
that status code other than successfuly 2xx will be returned, or
|
||||
that status code other than successfully 2xx will be returned, or
|
||||
messages2.Error will be raised by the server.
|
||||
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -33,10 +33,14 @@ class ClientTest(unittest.TestCase):
|
|||
self.net.post.return_value = self.response
|
||||
self.net.get.return_value = self.response
|
||||
|
||||
self.directory = messages.Directory({
|
||||
messages.NewRegistration: 'https://www.letsencrypt-demo.org/acme/new-reg',
|
||||
messages.Revocation: 'https://www.letsencrypt-demo.org/acme/revoke-cert',
|
||||
})
|
||||
|
||||
from acme.client import Client
|
||||
self.client = Client(
|
||||
new_reg_uri='https://www.letsencrypt-demo.org/acme/new-reg',
|
||||
key=KEY, alg=jose.RS256, net=self.net)
|
||||
directory=self.directory, key=KEY, alg=jose.RS256, net=self.net)
|
||||
|
||||
self.identifier = messages.Identifier(
|
||||
typ=messages.IDENTIFIER_FQDN, value='example.com')
|
||||
|
|
@ -72,6 +76,13 @@ class ClientTest(unittest.TestCase):
|
|||
uri='https://www.letsencrypt-demo.org/acme/cert/1',
|
||||
cert_chain_uri='https://www.letsencrypt-demo.org/ca')
|
||||
|
||||
def test_init_downloads_directory(self):
|
||||
uri = 'http://www.letsencrypt-demo.org/directory'
|
||||
from acme.client import Client
|
||||
self.client = Client(
|
||||
directory=uri, key=KEY, alg=jose.RS256, net=self.net)
|
||||
self.net.get.assert_called_once_with(uri)
|
||||
|
||||
def test_register(self):
|
||||
# "Instance of 'Field' has no to_json/update member" bug:
|
||||
# pylint: disable=no-member
|
||||
|
|
@ -348,8 +359,8 @@ class ClientTest(unittest.TestCase):
|
|||
|
||||
def test_revoke(self):
|
||||
self.client.revoke(self.certr.body)
|
||||
self.net.post.assert_called_once_with(messages.Revocation.url(
|
||||
self.client.new_reg_uri), mock.ANY)
|
||||
self.net.post.assert_called_once_with(
|
||||
self.directory[messages.Revocation], mock.ANY, content_type=None)
|
||||
|
||||
def test_revoke_bad_status_raises_error(self):
|
||||
self.response.status_code = http_client.METHOD_NOT_ALLOWED
|
||||
|
|
|
|||
|
|
@ -55,10 +55,11 @@ class ServeProbeSNITest(unittest.TestCase):
|
|||
def test_probe_not_recognized_name(self):
|
||||
self.assertRaises(errors.Error, self._probe, b'bar')
|
||||
|
||||
def test_probe_connection_error(self):
|
||||
self._probe(b'foo')
|
||||
time.sleep(1) # TODO: avoid race conditions in other way
|
||||
self.assertRaises(errors.Error, self._probe, b'bar')
|
||||
# TODO: py33/py34 tox hangs forever on do_hendshake in second probe
|
||||
#def probe_connection_error(self):
|
||||
# self._probe(b'foo')
|
||||
# #time.sleep(1) # TODO: avoid race conditions in other way
|
||||
# self.assertRaises(errors.Error, self._probe, b'bar')
|
||||
|
||||
|
||||
class PyOpenSSLCertOrReqSANTest(unittest.TestCase):
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class JSONDeSerializable(object):
|
|||
be encoded into a JSON document. **Full serialization** produces
|
||||
a Python object composed of only basic types as required by the
|
||||
:ref:`conversion table <conversion-table>`. **Partial
|
||||
serialization** (acomplished by :meth:`to_partial_json`)
|
||||
serialization** (accomplished by :meth:`to_partial_json`)
|
||||
produces a Python object that might also be built from other
|
||||
:class:`JSONDeSerializable` objects.
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class Header(json_util.JSONObjectWithFields):
|
|||
.. warning:: This class does not support any extensions through
|
||||
the "crit" (Critical) Header Parameter (4.1.11) and as a
|
||||
conforming implementation, :meth:`from_json` treats its
|
||||
occurence as an error. Please subclass if you seek for
|
||||
occurrence as an error. Please subclass if you seek for
|
||||
a different behaviour.
|
||||
|
||||
:ivar x5tS256: "x5t#S256"
|
||||
|
|
|
|||
|
|
@ -107,8 +107,8 @@ class ComparableRSAKey(ComparableKey): # pylint: disable=too-few-public-methods
|
|||
"""Wrapper for `cryptography` RSA keys.
|
||||
|
||||
Wraps around:
|
||||
- `cryptography.hazmat.primitives.assymetric.RSAPrivateKey`
|
||||
- `cryptography.hazmat.primitives.assymetric.RSAPublicKey`
|
||||
- `cryptography.hazmat.primitives.asymmetric.RSAPrivateKey`
|
||||
- `cryptography.hazmat.primitives.asymmetric.RSAPublicKey`
|
||||
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
"""ACME protocol messages."""
|
||||
import collections
|
||||
|
||||
from six.moves.urllib import parse as urllib_parse # pylint: disable=import-error
|
||||
|
||||
from acme import challenges
|
||||
from acme import fields
|
||||
from acme import jose
|
||||
from acme import util
|
||||
|
||||
|
||||
class Error(jose.JSONObjectWithFields, Exception):
|
||||
|
|
@ -128,6 +127,56 @@ class Identifier(jose.JSONObjectWithFields):
|
|||
value = jose.Field('value')
|
||||
|
||||
|
||||
class Directory(jose.JSONDeSerializable):
|
||||
"""Directory."""
|
||||
|
||||
_REGISTERED_TYPES = {}
|
||||
|
||||
@classmethod
|
||||
def _canon_key(cls, key):
|
||||
return getattr(key, 'resource_type', key)
|
||||
|
||||
@classmethod
|
||||
def register(cls, resource_body_cls):
|
||||
"""Register resource."""
|
||||
assert resource_body_cls.resource_type not in cls._REGISTERED_TYPES
|
||||
cls._REGISTERED_TYPES[resource_body_cls.resource_type] = resource_body_cls
|
||||
return resource_body_cls
|
||||
|
||||
def __init__(self, jobj):
|
||||
canon_jobj = util.map_keys(jobj, self._canon_key)
|
||||
if not set(canon_jobj).issubset(self._REGISTERED_TYPES):
|
||||
# TODO: acme-spec is not clear about this: 'It is a JSON
|
||||
# dictionary, whose keys are the "resource" values listed
|
||||
# in {{https-requests}}'z
|
||||
raise ValueError('Wrong directory fields')
|
||||
# TODO: check that everything is an absolute URL; acme-spec is
|
||||
# not clear on that
|
||||
self._jobj = canon_jobj
|
||||
|
||||
def __getattr__(self, name):
|
||||
try:
|
||||
return self[name.replace('_', '-')]
|
||||
except KeyError as error:
|
||||
raise AttributeError(str(error))
|
||||
|
||||
def __getitem__(self, name):
|
||||
try:
|
||||
return self._jobj[self._canon_key(name)]
|
||||
except KeyError:
|
||||
raise KeyError('Directory field not found')
|
||||
|
||||
def to_partial_json(self):
|
||||
return self._jobj
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, jobj):
|
||||
try:
|
||||
return cls(jobj)
|
||||
except ValueError as error:
|
||||
raise jose.DeserializationError(str(error))
|
||||
|
||||
|
||||
class Resource(jose.JSONObjectWithFields):
|
||||
"""ACME Resource.
|
||||
|
||||
|
|
@ -217,6 +266,7 @@ class Registration(ResourceBody):
|
|||
return self._filter_contact(self.email_prefix)
|
||||
|
||||
|
||||
@Directory.register
|
||||
class NewRegistration(Registration):
|
||||
"""New registration."""
|
||||
resource_type = 'new-reg'
|
||||
|
|
@ -332,6 +382,7 @@ class Authorization(ResourceBody):
|
|||
for combo in self.combinations)
|
||||
|
||||
|
||||
@Directory.register
|
||||
class NewAuthorization(Authorization):
|
||||
"""New authorization."""
|
||||
resource_type = 'new-authz'
|
||||
|
|
@ -349,6 +400,7 @@ class AuthorizationResource(ResourceWithURI):
|
|||
new_cert_uri = jose.Field('new_cert_uri')
|
||||
|
||||
|
||||
@Directory.register
|
||||
class CertificateRequest(jose.JSONObjectWithFields):
|
||||
"""ACME new-cert request.
|
||||
|
||||
|
|
@ -374,6 +426,7 @@ class CertificateResource(ResourceWithURI):
|
|||
authzrs = jose.Field('authzrs')
|
||||
|
||||
|
||||
@Directory.register
|
||||
class Revocation(jose.JSONObjectWithFields):
|
||||
"""Revocation message.
|
||||
|
||||
|
|
@ -385,16 +438,3 @@ class Revocation(jose.JSONObjectWithFields):
|
|||
resource = fields.Resource(resource_type)
|
||||
certificate = jose.Field(
|
||||
'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert)
|
||||
|
||||
# TODO: acme-spec#138, this allows only one ACME server instance per domain
|
||||
PATH = '/acme/revoke-cert'
|
||||
"""Path to revocation URL, see `url`"""
|
||||
|
||||
@classmethod
|
||||
def url(cls, base):
|
||||
"""Get revocation URL.
|
||||
|
||||
:param str base: New Registration Resource or server (root) URL.
|
||||
|
||||
"""
|
||||
return urllib_parse.urljoin(base, cls.PATH)
|
||||
|
|
|
|||
|
|
@ -93,6 +93,45 @@ class ConstantTest(unittest.TestCase):
|
|||
self.assertFalse(self.const_a != const_a_prime)
|
||||
|
||||
|
||||
class DirectoryTest(unittest.TestCase):
|
||||
"""Tests for acme.messages.Directory."""
|
||||
|
||||
def setUp(self):
|
||||
from acme.messages import Directory
|
||||
self.dir = Directory({
|
||||
'new-reg': 'reg',
|
||||
mock.MagicMock(resource_type='new-cert'): 'cert',
|
||||
})
|
||||
|
||||
def test_init_wrong_key_value_error(self):
|
||||
from acme.messages import Directory
|
||||
self.assertRaises(ValueError, Directory, {'foo': 'bar'})
|
||||
|
||||
def test_getitem(self):
|
||||
self.assertEqual('reg', self.dir['new-reg'])
|
||||
from acme.messages import NewRegistration
|
||||
self.assertEqual('reg', self.dir[NewRegistration])
|
||||
self.assertEqual('reg', self.dir[NewRegistration()])
|
||||
|
||||
def test_getitem_fails_with_key_error(self):
|
||||
self.assertRaises(KeyError, self.dir.__getitem__, 'foo')
|
||||
|
||||
def test_getattr(self):
|
||||
self.assertEqual('reg', self.dir.new_reg)
|
||||
|
||||
def test_getattr_fails_with_attribute_error(self):
|
||||
self.assertRaises(AttributeError, self.dir.__getattr__, 'foo')
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(
|
||||
self.dir.to_partial_json(), {'new-reg': 'reg', 'new-cert': 'cert'})
|
||||
|
||||
def test_from_json_deserialization_error_on_wrong_key(self):
|
||||
from acme.messages import Directory
|
||||
self.assertRaises(
|
||||
jose.DeserializationError, Directory.from_json, {'foo': 'bar'})
|
||||
|
||||
|
||||
class RegistrationTest(unittest.TestCase):
|
||||
"""Tests for acme.messages.Registration."""
|
||||
|
||||
|
|
@ -320,13 +359,6 @@ class CertificateResourceTest(unittest.TestCase):
|
|||
class RevocationTest(unittest.TestCase):
|
||||
"""Tests for acme.messages.RevocationTest."""
|
||||
|
||||
def test_url(self):
|
||||
from acme.messages import Revocation
|
||||
url = 'https://letsencrypt-demo.org/acme/revoke-cert'
|
||||
self.assertEqual(url, Revocation.url('https://letsencrypt-demo.org'))
|
||||
self.assertEqual(
|
||||
url, Revocation.url('https://letsencrypt-demo.org/acme/new-reg'))
|
||||
|
||||
def setUp(self):
|
||||
from acme.messages import Revocation
|
||||
self.rev = Revocation(certificate=CERT)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class Signature(jose.JSONObjectWithFields):
|
|||
:param bytes msg: Message to be signed.
|
||||
|
||||
:param key: Key used for signing.
|
||||
:type key: `cryptography.hazmat.primitives.assymetric.rsa.RSAPrivateKey`
|
||||
:type key: `cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey`
|
||||
(optionally wrapped in `.ComparableRSAKey`).
|
||||
|
||||
:param bytes nonce: Nonce to be used. If None, nonce of
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Symlinked in letsencrypt/tests/test_util.py, casues duplicate-code
|
||||
# Symlinked in letsencrypt/tests/test_util.py, causes duplicate-code
|
||||
# warning that cannot be disabled locally.
|
||||
"""Test utilities.
|
||||
|
||||
|
|
|
|||
7
acme/acme/util.py
Normal file
7
acme/acme/util.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"""ACME utilities."""
|
||||
import six
|
||||
|
||||
|
||||
def map_keys(dikt, func):
|
||||
"""Map dictionary keys."""
|
||||
return dict((func(key), value) for key, value in six.iteritems(dikt))
|
||||
16
acme/acme/util_test.py
Normal file
16
acme/acme/util_test.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"""Tests for acme.util."""
|
||||
import unittest
|
||||
|
||||
|
||||
class MapKeysTest(unittest.TestCase):
|
||||
"""Tests for acme.util.map_keys."""
|
||||
|
||||
def test_it(self):
|
||||
from acme.util import map_keys
|
||||
self.assertEqual({'a': 'b', 'c': 'd'},
|
||||
map_keys({'a': 'b', 'c': 'd'}, lambda key: key))
|
||||
self.assertEqual({2: 2, 4: 4}, map_keys({1: 2, 3: 4}, lambda x: x + 1))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
|
|
@ -5,16 +5,15 @@ from setuptools import find_packages
|
|||
|
||||
|
||||
install_requires = [
|
||||
'argparse',
|
||||
# load_pem_private/public_key (>=0.6)
|
||||
# rsa_recover_prime_factors (>=0.8)
|
||||
'cryptography>=0.8',
|
||||
'mock<1.1.0', # py26
|
||||
'pyrfc3339',
|
||||
'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304)
|
||||
'pyasn1', # urllib3 InsecurePlatformWarning (#304)
|
||||
# Connection.set_tlsext_host_name (>=0.13), X509Req.get_extensions (>=0.15)
|
||||
'PyOpenSSL>=0.15',
|
||||
'pyrfc3339',
|
||||
'pytz',
|
||||
'requests',
|
||||
'six',
|
||||
|
|
|
|||
|
|
@ -4,8 +4,19 @@
|
|||
# - Fedora 22 (x64)
|
||||
# - Centos 7 (x64: on AWS EC2 t2.micro, DigitalOcean droplet)
|
||||
|
||||
if type yum 2>/dev/null
|
||||
then
|
||||
tool=yum
|
||||
elif type dnf 2>/dev/null
|
||||
then
|
||||
tool=dnf
|
||||
else
|
||||
echo "Neither yum nor dnf found. Aborting bootstrap!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# "git-core" seems to be an alias for "git" in CentOS 7 (yum search fails)
|
||||
yum install -y \
|
||||
$tool install -y \
|
||||
git-core \
|
||||
python \
|
||||
python-devel \
|
||||
|
|
|
|||
8
bootstrap/freebsd.sh
Executable file
8
bootstrap/freebsd.sh
Executable file
|
|
@ -0,0 +1,8 @@
|
|||
#!/bin/sh -xe
|
||||
|
||||
pkg install -Ay \
|
||||
git \
|
||||
python \
|
||||
py27-virtualenv \
|
||||
augeas \
|
||||
libffi \
|
||||
|
|
@ -52,7 +52,8 @@ The following tools are there to help you:
|
|||
before submitting a new pull request.
|
||||
|
||||
- ``tox -e cover`` checks the test coverage only. Calling the
|
||||
``./tox.cover.sh`` script directly might be a bit quicker, though.
|
||||
``./tox.cover.sh`` script directly (or even ``./tox.cover.sh $pkg1
|
||||
$pkg2 ...`` for any subpackages) might be a bit quicker, though.
|
||||
|
||||
- ``tox -e lint`` checks the style of the whole project, while
|
||||
``pylint --rcfile=.pylintrc path`` will check a single file or
|
||||
|
|
|
|||
|
|
@ -102,6 +102,21 @@ Centos 7
|
|||
sudo ./bootstrap/centos.sh
|
||||
|
||||
|
||||
FreeBSD
|
||||
-------
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
sudo ./bootstrap/freebsd.sh
|
||||
|
||||
Bootstrap script for FreeBSD uses ``pkg`` for package installation,
|
||||
i.e. it does not use ports.
|
||||
|
||||
FreeBSD by default uses ``tcsh``. In order to activate virtulenv (see
|
||||
below), you will need a compatbile shell, e.g. ``pkg install bash &&
|
||||
bash``.
|
||||
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
|
|
@ -129,7 +144,7 @@ To get a new certificate run:
|
|||
|
||||
.. code-block:: shell
|
||||
|
||||
./venv/bin/letsencrypt auth
|
||||
sudo ./venv/bin/letsencrypt auth
|
||||
|
||||
The ``letsencrypt`` commandline tool has a builtin help:
|
||||
|
||||
|
|
|
|||
|
|
@ -490,7 +490,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
|
||||
"""
|
||||
if "ssl_module" not in self.parser.modules:
|
||||
logger.info("Loading mod_ssl into Apache Server")
|
||||
self.enable_mod("ssl", temp=temp)
|
||||
|
||||
# Check for Listen <port>
|
||||
|
|
@ -1000,15 +999,34 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
"Unsupported directory layout. You may try to enable mod %s "
|
||||
"and try again." % mod_name)
|
||||
|
||||
deps = _get_mod_deps(mod_name)
|
||||
|
||||
# Enable all dependencies
|
||||
for dep in deps:
|
||||
if (dep + "_module") not in self.parser.modules:
|
||||
self._enable_mod_debian(dep, temp)
|
||||
self._add_parser_mod(dep)
|
||||
|
||||
note = "Enabled dependency of %s module - %s" % (mod_name, dep)
|
||||
if not temp:
|
||||
self.save_notes += note + os.linesep
|
||||
logger.debug(note)
|
||||
|
||||
# Enable actual module
|
||||
self._enable_mod_debian(mod_name, temp)
|
||||
self.save_notes += "Enabled %s module in Apache" % mod_name
|
||||
logger.debug("Enabled Apache %s module", mod_name)
|
||||
self._add_parser_mod(mod_name)
|
||||
|
||||
if not temp:
|
||||
self.save_notes += "Enabled %s module in Apache\n" % mod_name
|
||||
logger.info("Enabled Apache %s module", mod_name)
|
||||
|
||||
# Modules can enable additional config files. Variables may be defined
|
||||
# within these new configuration sections.
|
||||
# Restart is not necessary as DUMP_RUN_CFG uses latest config.
|
||||
self.parser.update_runtime_variables(self.conf("ctl"))
|
||||
|
||||
def _add_parser_mod(self, mod_name):
|
||||
"""Shortcut for updating parser modules."""
|
||||
self.parser.modules.add(mod_name + "_module")
|
||||
self.parser.modules.add("mod_" + mod_name + ".c")
|
||||
|
||||
|
|
@ -1136,6 +1154,25 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
self.parser.init_modules()
|
||||
|
||||
|
||||
def _get_mod_deps(mod_name):
|
||||
"""Get known module dependencies.
|
||||
|
||||
.. note:: This does not need to be accurate in order for the client to
|
||||
run. This simply keeps things clean if the user decides to revert
|
||||
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`)
|
||||
This would only present a major problem in extremely atypical
|
||||
configs that use ifmod for the missing deps.
|
||||
|
||||
"""
|
||||
deps = {
|
||||
"ssl": ["setenvif", "mime", "socache_shmcb"]
|
||||
}
|
||||
return deps.get(mod_name, [])
|
||||
|
||||
|
||||
def apache_restart(apache_init_script):
|
||||
"""Restarts the Apache Server.
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class IPluginProxy(zope.interface.Interface):
|
|||
def cleanup_from_tests():
|
||||
"""Performs any necessary cleanup from running plugin tests.
|
||||
|
||||
This is guarenteed to be called before the program exits.
|
||||
This is guaranteed to be called before the program exits.
|
||||
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@ from setuptools import find_packages
|
|||
install_requires = [
|
||||
'acme',
|
||||
'letsencrypt',
|
||||
'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary?
|
||||
'mock<1.1.0', # py26
|
||||
'PyOpenSSL',
|
||||
'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary?
|
||||
'zope.interface',
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ class Account(object): # pylint: disable=too-few-public-methods
|
|||
# Implementation note: Email? Multiple accounts can have the
|
||||
# same email address. Registration URI? Assigned by the
|
||||
# server, not guaranteed to be stable over time, nor
|
||||
# cannonical URI can be generated. ACME protocol doesn't allow
|
||||
# canonical URI can be generated. ACME protocol doesn't allow
|
||||
# account key (and thus its fingerprint) to be updated...
|
||||
|
||||
@property
|
||||
|
|
|
|||
|
|
@ -16,12 +16,16 @@ import zope.component
|
|||
import zope.interface.exceptions
|
||||
import zope.interface.verify
|
||||
|
||||
from acme import client as acme_client
|
||||
from acme import jose
|
||||
|
||||
import letsencrypt
|
||||
|
||||
from letsencrypt import account
|
||||
from letsencrypt import configuration
|
||||
from letsencrypt import constants
|
||||
from letsencrypt import client
|
||||
from letsencrypt import crypto_util
|
||||
from letsencrypt import errors
|
||||
from letsencrypt import interfaces
|
||||
from letsencrypt import le_util
|
||||
|
|
@ -241,16 +245,20 @@ def install(args, config, plugins):
|
|||
le_client.enhance_config(domains, args.redirect)
|
||||
|
||||
|
||||
def revoke(args, unused_config, unused_plugins):
|
||||
def revoke(args, config, unused_plugins): # TODO: coop with renewal config
|
||||
"""Revoke a previously obtained certificate."""
|
||||
if args.cert_path is None and args.key_path is None:
|
||||
return "At least one of --cert-path or --key-path is required"
|
||||
|
||||
# This depends on the renewal config and cannot be completed yet.
|
||||
zope.component.getUtility(interfaces.IDisplay).notification(
|
||||
"Revocation is not available with the new Boulder server yet.")
|
||||
#client.revoke(args.installer, config, plugins, args.no_confirm,
|
||||
# args.cert_path, args.key_path)
|
||||
if args.key_path is not None: # revocation by cert key
|
||||
logger.debug("Revoking %s using cert key %s",
|
||||
args.cert_path[0], args.key_path[0])
|
||||
acme = acme_client.Client(
|
||||
config.server, key=jose.JWK.load(args.key_path[1]))
|
||||
else: # revocation by account key
|
||||
logger.debug("Revoking %s using Account Key", args.cert_path[0])
|
||||
acc, _ = _determine_account(args, config)
|
||||
# pylint: disable=protected-access
|
||||
acme = client._acme_from_config_key(config, acc.key)
|
||||
acme.revoke(jose.ComparableX509(crypto_util.pyopenssl_load_certificate(
|
||||
args.cert_path[1])[0]))
|
||||
|
||||
|
||||
def rollback(args, config, plugins):
|
||||
|
|
@ -578,14 +586,16 @@ def _create_subparsers(helpful):
|
|||
"--cert-path", required=True, help="Path to a certificate that "
|
||||
"is going to be installed.")
|
||||
parser_install.add_argument(
|
||||
"--key-path", required=True, help="Accompynying private key")
|
||||
"--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.")
|
||||
"--cert-path", type=read_file, help="Revoke a specific certificate.",
|
||||
required=True)
|
||||
parser_revoke.add_argument(
|
||||
"--key-path", type=read_file,
|
||||
help="Revoke all certs generated by the provided authorized key.")
|
||||
help="Revoke certificate using its accompanying key. Useful if "
|
||||
"Account Key is lost.")
|
||||
|
||||
parser_rollback.add_argument(
|
||||
"--checkpoints", type=int, metavar="N",
|
||||
|
|
@ -625,7 +635,7 @@ def _plugins_parsing(helpful, plugins):
|
|||
"plugins", description="Let's Encrypt client supports an "
|
||||
"extensible plugins architecture. See '%(prog)s plugins' for a "
|
||||
"list of all available plugins and their names. You can force "
|
||||
"a particular plugin by setting options provided below. Futher "
|
||||
"a particular plugin by setting options provided below. Further "
|
||||
"down this help message you will find plugin-specific options "
|
||||
"(prefixed by --{plugin_name}).")
|
||||
helpful.add(
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
def _acme_from_config_key(config, key):
|
||||
# TODO: Allow for other alg types besides RS256
|
||||
return acme_client.Client(new_reg_uri=config.server, key=key,
|
||||
return acme_client.Client(directory=config.server, key=key,
|
||||
verify_ssl=(not config.no_verify_ssl))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ CLI_DEFAULTS = dict(
|
|||
"letsencrypt", "cli.ini"),
|
||||
],
|
||||
verbose_count=-(logging.WARNING / 10),
|
||||
server="https://acme-staging.api.letsencrypt.org/acme/new-reg",
|
||||
server="https://acme-staging.api.letsencrypt.org/directory",
|
||||
rsa_key_size=2048,
|
||||
rollback_checkpoints=1,
|
||||
config_dir="/etc/letsencrypt",
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ util = zope.component.getUtility # pylint: disable=invalid-name
|
|||
|
||||
|
||||
def choose_plugin(prepared, question):
|
||||
"""Allow the user to choose ther plugin.
|
||||
"""Allow the user to choose their plugin.
|
||||
|
||||
:param list prepared: List of `~.PluginEntryPoint`.
|
||||
:param str question: Question to be presented to the user.
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ class IAuthenticator(IPlugin):
|
|||
|
||||
:param str domain: Domain for which challenge preferences are sought.
|
||||
|
||||
:returns: List of challege types (subclasses of
|
||||
:returns: List of challenge types (subclasses of
|
||||
:class:`acme.challenges.Challenge`) with the most
|
||||
preferred challenges first. If a type is not specified, it means the
|
||||
Authenticator cannot perform the challenge.
|
||||
|
|
@ -194,8 +194,7 @@ class IConfig(zope.interface.Interface):
|
|||
filtered, stripped or sanitized.
|
||||
|
||||
"""
|
||||
server = zope.interface.Attribute(
|
||||
"ACME new registration URI (including /acme/new-reg).")
|
||||
server = zope.interface.Attribute("ACME Directory Resource URI.")
|
||||
email = zope.interface.Attribute(
|
||||
"Email used for registration and recovery contact.")
|
||||
rsa_key_size = zope.interface.Attribute("Size of the RSA key.")
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import logging
|
|||
import pipes
|
||||
import shutil
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
|
@ -37,7 +38,7 @@ class ManualAuthenticator(common.Plugin):
|
|||
Make sure your web server displays the following content at
|
||||
{uri} before continuing:
|
||||
|
||||
{achall.token}
|
||||
{validation}
|
||||
|
||||
Content-Type header MUST be set to {ct}.
|
||||
|
||||
|
|
@ -122,6 +123,20 @@ binary for temporary key/certificate generation.""".replace("\n", "")
|
|||
responses.append(self._perform_single(achall))
|
||||
return responses
|
||||
|
||||
@classmethod
|
||||
def _test_mode_busy_wait(cls, port):
|
||||
while True:
|
||||
time.sleep(1)
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
sock.connect(("localhost", port))
|
||||
except socket.error: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
break
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
def _perform_single(self, achall):
|
||||
# same path for each challenge response would be easier for
|
||||
# users, but will not work if multiple domains point at the
|
||||
|
|
@ -129,13 +144,13 @@ binary for temporary key/certificate generation.""".replace("\n", "")
|
|||
response, validation = achall.gen_response_and_validation(
|
||||
tls=(not self.config.no_simple_http_tls))
|
||||
|
||||
port = (response.port if self.config.simple_http_port is None
|
||||
else int(self.config.simple_http_port))
|
||||
command = self.template.format(
|
||||
root=self._root, achall=achall, response=response,
|
||||
validation=pipes.quote(validation.json_dumps()),
|
||||
encoded_token=achall.chall.encode("token"),
|
||||
ct=response.CONTENT_TYPE, port=(
|
||||
response.port if self.config.simple_http_port is None
|
||||
else self.config.simple_http_port))
|
||||
ct=response.CONTENT_TYPE, port=port)
|
||||
if self.conf("test-mode"):
|
||||
logger.debug("Test mode. Executing the manual command: %s", command)
|
||||
try:
|
||||
|
|
@ -153,12 +168,12 @@ binary for temporary key/certificate generation.""".replace("\n", "")
|
|||
logger.debug("Manual command running as PID %s.", self._httpd.pid)
|
||||
# give it some time to bootstrap, before we try to verify
|
||||
# (cert generation in case of simpleHttpS might take time)
|
||||
time.sleep(4) # XXX
|
||||
self._test_mode_busy_wait(port)
|
||||
if self._httpd.poll() is not None:
|
||||
raise errors.Error("Couldn't execute manual command")
|
||||
else:
|
||||
self._notify_and_wait(self.MESSAGE_TEMPLATE.format(
|
||||
achall=achall, response=response,
|
||||
validation=validation.json_dumps(), response=response,
|
||||
uri=response.uri(achall.domain, achall.challb.chall),
|
||||
ct=response.CONTENT_TYPE, command=command))
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,27 @@ class ManualAuthenticatorTest(unittest.TestCase):
|
|||
self.achalls[0].challb.chall, "foo.com", KEY.public_key(), 4430)
|
||||
|
||||
message = mock_stdout.write.mock_calls[0][1][0]
|
||||
self.assertTrue(self.achalls[0].token in message)
|
||||
self.assertEqual(message, """\
|
||||
Make sure your web server displays the following content at
|
||||
http://foo.com/.well-known/acme-challenge/ZXZhR3hmQURzNnBTUmIyTEF2OUlaZjE3RHQzanV4R0orUEN0OTJ3citvQQ before continuing:
|
||||
|
||||
{"header": {"alg": "RS256", "jwk": {"e": "AQAB", "kty": "RSA", "n": "rHVztFHtH92ucFJD_N_HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2-6QWE30cWgdmJS86ObRz6lUTor4R0T-3C5Q"}}, "payload": "eyJ0bHMiOiBmYWxzZSwgInRva2VuIjogIlpYWmhSM2htUVVSek5uQlRVbUl5VEVGMk9VbGFaakUzUkhRemFuVjRSMG9yVUVOME9USjNjaXR2UVEiLCAidHlwZSI6ICJzaW1wbGVIdHRwIn0", "signature": "jFPJFC-2eRyBw7Sl0wyEBhsdvRZtKk8hc6HykEPAiofZlIwdIu76u2xHqMVZWSZdpxwMNUnnawTEAqgMWFydMA"}
|
||||
|
||||
Content-Type header MUST be set to application/jose+json.
|
||||
|
||||
If you don\'t have HTTP server configured, you can run the following
|
||||
command on the target server (as root):
|
||||
|
||||
mkdir -p /tmp/letsencrypt/public_html/.well-known/acme-challenge
|
||||
cd /tmp/letsencrypt/public_html
|
||||
echo -n \'{"header": {"alg": "RS256", "jwk": {"e": "AQAB", "kty": "RSA", "n": "rHVztFHtH92ucFJD_N_HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2-6QWE30cWgdmJS86ObRz6lUTor4R0T-3C5Q"}}, "payload": "eyJ0bHMiOiBmYWxzZSwgInRva2VuIjogIlpYWmhSM2htUVVSek5uQlRVbUl5VEVGMk9VbGFaakUzUkhRemFuVjRSMG9yVUVOME9USjNjaXR2UVEiLCAidHlwZSI6ICJzaW1wbGVIdHRwIn0", "signature": "jFPJFC-2eRyBw7Sl0wyEBhsdvRZtKk8hc6HykEPAiofZlIwdIu76u2xHqMVZWSZdpxwMNUnnawTEAqgMWFydMA"}\' > .well-known/acme-challenge/ZXZhR3hmQURzNnBTUmIyTEF2OUlaZjE3RHQzanV4R0orUEN0OTJ3citvQQ
|
||||
# run only once per server:
|
||||
$(command -v python2 || command -v python2.7 || command -v python2.6) -c \\
|
||||
"import BaseHTTPServer, SimpleHTTPServer; \\
|
||||
SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map = {\'\': \'application/jose+json\'}; \\
|
||||
s = BaseHTTPServer.HTTPServer((\'\', 4430), SimpleHTTPServer.SimpleHTTPRequestHandler); \\
|
||||
s.serve_forever()" \n""")
|
||||
#self.assertTrue(validation in message)
|
||||
|
||||
mock_verify.return_value = False
|
||||
self.assertEqual([None], self.auth.perform(self.achalls))
|
||||
|
|
@ -71,25 +91,29 @@ 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.time.sleep", autospec=True)
|
||||
@mock.patch("letsencrypt.plugins.manual.subprocess.Popen", autospec=True)
|
||||
def test_perform_test_command_run_failure(
|
||||
self, mock_popen, unused_mock_sleep):
|
||||
self, mock_popen, unused_mock_sleep, unused_mock_socket):
|
||||
mock_popen.poll.return_value = 10
|
||||
mock_popen.return_value.pid = 1234
|
||||
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.time.sleep", autospec=True)
|
||||
@mock.patch("acme.challenges.SimpleHTTPResponse.simple_verify",
|
||||
autospec=True)
|
||||
@mock.patch("letsencrypt.plugins.manual.subprocess.Popen", autospec=True)
|
||||
def test_perform_test_mode(self, mock_popen, mock_verify, mock_sleep):
|
||||
def test_perform_test_mode(self, mock_popen, mock_verify, mock_sleep,
|
||||
mock_socket):
|
||||
mock_popen.return_value.poll.side_effect = [None, 10]
|
||||
mock_popen.return_value.pid = 1234
|
||||
mock_verify.return_value = False
|
||||
self.assertEqual([False], self.auth_test_mode.perform(self.achalls))
|
||||
self.assertEqual(1, mock_sleep.call_count)
|
||||
self.assertEqual(1, mock_socket.call_count)
|
||||
|
||||
def test_cleanup_test_mode_already_terminated(self):
|
||||
# pylint: disable=protected-access
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class Revoker(object):
|
|||
"""
|
||||
def __init__(self, installer, config, no_confirm=False):
|
||||
# XXX
|
||||
self.acme = acme_client.Client(new_reg_uri=None, key=None, alg=None)
|
||||
self.acme = acme_client.Client(directory=None, key=None, alg=None)
|
||||
|
||||
self.installer = installer
|
||||
self.config = config
|
||||
|
|
|
|||
|
|
@ -625,7 +625,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
|
||||
"""
|
||||
# XXX: assumes official archive location rather than examining links
|
||||
# XXX: consider using os.open for availablity of os.O_EXCL
|
||||
# XXX: consider using os.open for availability of os.O_EXCL
|
||||
# XXX: ensure file permissions are correct; also create directories
|
||||
# if needed (ensuring their permissions are correct)
|
||||
# Figure out what the new version is and hence where to save things
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ class ClientTest(unittest.TestCase):
|
|||
|
||||
def test_init_acme_verify_ssl(self):
|
||||
self.acme_client.assert_called_once_with(
|
||||
new_reg_uri=mock.ANY, key=mock.ANY, verify_ssl=True)
|
||||
directory=mock.ANY, key=mock.ANY, verify_ssl=True)
|
||||
|
||||
def _mock_obtain_certificate(self):
|
||||
self.client.auth_handler = mock.MagicMock()
|
||||
|
|
|
|||
1
setup.py
1
setup.py
|
|
@ -41,6 +41,7 @@ install_requires = [
|
|||
'pyrfc3339',
|
||||
'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280
|
||||
'pytz',
|
||||
'requests',
|
||||
'zope.component',
|
||||
'zope.interface',
|
||||
]
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@
|
|||
# instance (see ./boulder-start.sh).
|
||||
#
|
||||
# Environment variables:
|
||||
# SERVER: Passed as "letsencrypt --server" argument. Boulder
|
||||
# monolithic defaults to :4000, AMQP defaults to :4300. This
|
||||
# script defaults to monolithic.
|
||||
# SERVER: Passed as "letsencrypt --server" argument.
|
||||
#
|
||||
# Note: this script is called by Boulder integration test suite!
|
||||
|
||||
|
|
@ -54,6 +52,13 @@ do
|
|||
[ "${dir}/${latest}" = "$live" ] # renewer fails this test
|
||||
done
|
||||
|
||||
# revoke by account key
|
||||
common revoke --cert-path "$root/conf/live/le.wtf/cert.pem"
|
||||
# revoke renewed
|
||||
common revoke --cert-path "$root/conf/live/le1.wtf/cert.pem"
|
||||
# revoke by cert key
|
||||
common revoke --cert-path "$root/conf/live/le2.wtf/cert.pem" \
|
||||
--key-path "$root/conf/live/le2.wtf/privkey.pem"
|
||||
|
||||
if type nginx;
|
||||
then
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export root store_flags
|
|||
|
||||
letsencrypt_test () {
|
||||
letsencrypt \
|
||||
--server "${SERVER:-http://localhost:4000/acme/new-reg}" \
|
||||
--server "${SERVER:-http://localhost:4000/directory}" \
|
||||
--no-verify-ssl \
|
||||
--dvsni-port 5001 \
|
||||
--simple-http-port 5001 \
|
||||
|
|
|
|||
15
tools/deps.sh
Executable file
15
tools/deps.sh
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# Find all Python imports.
|
||||
#
|
||||
# ./deps.sh letsencrypt
|
||||
# ./deps.sh acme
|
||||
# ./deps.sh letsencrypt-apache
|
||||
# ...
|
||||
#
|
||||
# Manually compare the output with deps in setup.py.
|
||||
|
||||
git grep -h -E '^(import|from.*import)' $1/ | \
|
||||
awk '{print $2}' | \
|
||||
grep -vE "^$1" | \
|
||||
sort -u
|
||||
42
tox.cover.sh
42
tox.cover.sh
|
|
@ -1,10 +1,35 @@
|
|||
#!/bin/sh
|
||||
#!/bin/sh -xe
|
||||
|
||||
# USAGE: ./tox.cover.sh [package]
|
||||
#
|
||||
# This script is used by tox.ini (and thus Travis CI) in order to
|
||||
# generate separate stats for each package. It should be removed once
|
||||
# those packages are moved to separate repo.
|
||||
#
|
||||
# -e makes sure we fail fast and don't submit coveralls submit
|
||||
|
||||
if [ "xxx$1" = "xxx" ]; then
|
||||
pkgs="letsencrypt acme letsencrypt_apache letsencrypt_nginx letshelp_letsencrypt"
|
||||
else
|
||||
pkgs="$@"
|
||||
fi
|
||||
|
||||
cover () {
|
||||
if [ "$1" = "letsencrypt" ]; then
|
||||
min=97
|
||||
elif [ "$1" = "acme" ]; then
|
||||
min=100
|
||||
elif [ "$1" = "letsencrypt_apache" ]; then
|
||||
min=100
|
||||
elif [ "$1" = "letsencrypt_nginx" ]; then
|
||||
min=96
|
||||
elif [ "$1" = "letshelp_letsencrypt" ]; then
|
||||
min=100
|
||||
else
|
||||
echo "Unrecognized package: $1"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# "-c /dev/null" makes sure setup.cfg is not loaded (multiple
|
||||
# --with-cover add up, --cover-erase must not be set for coveralls
|
||||
# to get all the data); --with-cover scopes coverage to only
|
||||
|
|
@ -12,16 +37,11 @@ cover () {
|
|||
# specific package directory; --cover-tests makes sure every tests
|
||||
# is run (c.f. #403)
|
||||
nosetests -c /dev/null --with-cover --cover-tests --cover-package \
|
||||
"$1" --cover-min-percentage="$2" "$1"
|
||||
"$1" --cover-min-percentage="$min" "$1"
|
||||
}
|
||||
|
||||
rm -f .coverage # --cover-erase is off, make sure stats are correct
|
||||
|
||||
# don't use sequential composition (;), if letsencrypt_nginx returns
|
||||
# 0, coveralls submit will be triggered (c.f. .travis.yml,
|
||||
# after_success)
|
||||
cover letsencrypt 97 && \
|
||||
cover acme 100 && \
|
||||
cover letsencrypt_apache 100 && \
|
||||
cover letsencrypt_nginx 96 && \
|
||||
cover letshelp_letsencrypt 100
|
||||
for pkg in $pkgs
|
||||
do
|
||||
cover $pkg
|
||||
done
|
||||
|
|
|
|||
12
tox.ini
12
tox.ini
|
|
@ -6,7 +6,7 @@
|
|||
# acme and letsencrypt are not yet on pypi, so when Tox invokes
|
||||
# "install *.zip", it will not find deps
|
||||
skipsdist = true
|
||||
envlist = py26,py27,cover,lint
|
||||
envlist = py26,py27,py33,py34,cover,lint
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
|
|
@ -23,6 +23,16 @@ setenv =
|
|||
PYTHONHASHSEED = 0
|
||||
# https://testrun.org/tox/latest/example/basic.html#special-handling-of-pythonhas
|
||||
|
||||
[testenv:py33]
|
||||
commands =
|
||||
pip install -e acme[testing]
|
||||
nosetests acme
|
||||
|
||||
[testenv:py34]
|
||||
commands =
|
||||
pip install -e acme[testing]
|
||||
nosetests acme
|
||||
|
||||
[testenv:cover]
|
||||
basepython = python2.7
|
||||
commands =
|
||||
|
|
|
|||
Loading…
Reference in a new issue