Merge branch 'master' into reverter

Conflicts:
	letsencrypt/client/apache/configurator.py
	letsencrypt/client/crypto_util.py
	letsencrypt/client/display.py
	letsencrypt/client/revoker.py
	letsencrypt/client/tests/apache/configurator_test.py
	letsencrypt/client/tests/apache/dvsni_test.py
	letsencrypt/scripts/main.py
This commit is contained in:
James Kasten 2015-01-24 21:42:26 -08:00
commit 8c6cfaded0
27 changed files with 214 additions and 210 deletions

View file

@ -38,7 +38,7 @@ load-plugins=
# --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=
disable=fixme,locally-disabled
[REPORTS]
@ -81,7 +81,7 @@ required-attributes=
bad-functions=map,filter,apply,input,file
# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_
good-names=i,j,k,ex,Run,_,fd
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
@ -94,10 +94,10 @@ name-group=
include-naming-hint=no
# Regular expression matching correct function names
function-rgx=[a-z_][a-z0-9_]{2,30}$
function-rgx=[a-z_][a-z0-9_]{2,40}$
# Naming hint for function names
function-name-hint=[a-z_][a-z0-9_]{2,30}$
function-name-hint=[a-z_][a-z0-9_]{2,40}$
# Regular expression matching correct variable names
variable-rgx=[a-z_][a-z0-9_]{2,30}$
@ -148,14 +148,14 @@ module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression matching correct method names
method-rgx=[a-z_][a-z0-9_]{2,30}$
method-rgx=[a-z_][a-z0-9_]{2,40}$
# Naming hint for method names
method-name-hint=[a-z_][a-z0-9_]{2,30}$
method-name-hint=[a-z_][a-z0-9_]{2,40}$
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=(__.*__)|(test_[A-Za-z0-9_]*)|(_.*)
no-docstring-rgx=(__.*__)|(test_[A-Za-z0-9_]*)|(_.*)|(.*Test$)
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
@ -182,7 +182,7 @@ init-import=no
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=_$|dummy
dummy-variables-rgx=(unused)?_.*|dummy
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
@ -278,8 +278,8 @@ int-import-graph=
[CLASSES]
# List of interface methods to ignore, separated by a comma. This is used for
# instance to not check methods defines in Zope's Interface base class.
ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
# instance to not check methods defined in Zope's Interface base class.
ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by,implementedBy,providedBy
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp

View file

@ -36,7 +36,7 @@ In general:
```
sudo apt-get install python python-setuptools python-virtualenv \
python-dev gcc swig dialog libaugeas0 libssl-dev
python-dev gcc swig dialog libaugeas0 libssl-dev ca-certificates
```
#### Mac OSX
@ -77,7 +77,7 @@ In order to generate the sphinx documentation, run the following commands.
```
./venv/bin/python setup.py docs
cd docs
make html SPHINXBUILD='../venv/bin/python ../venv/bin/sphinx-build'
make clean html SPHINXBUILD=../venv/bin/sphinx-build
```
This should generate documentation in the /lets-encrypt-preview/docs/_build/html

0
docs/_static/.gitignore vendored Normal file
View file

View file

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

View file

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

View file

@ -35,7 +35,7 @@ CERT_KEY_BACKUP = os.path.join(WORK_DIR, "keys-certs/")
REV_TOKENS_DIR = os.path.join(WORK_DIR, "revocation_tokens/")
"""Directory where all revocation tokens are saved."""
KEY_DIR = os.path.join(SERVER_ROOT, "ssl/")
KEY_DIR = os.path.join(SERVER_ROOT, "keys/")
"""Where all keys should be stored"""
CERT_DIR = os.path.join(SERVER_ROOT, "certs/")

View file

@ -423,7 +423,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"based virtual host", addr)
self.add_name_vhost(addr)
def make_vhost_ssl(self, nonssl_vhost):
def make_vhost_ssl(self, nonssl_vhost): # pylint: disable=too-many-locals
"""Makes an ssl_vhost version of a nonssl_vhost.
Duplicates vhost and adds default ssl options
@ -519,8 +519,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
return ssl_vhost
# pylint: disable=no-method-argument,no-self-use,unused-argument
def supported_enhancements():
def supported_enhancements(self): # pylint: disable=no-self-use
"""Returns currently supported enhancements."""
return ["redirect"]
@ -529,7 +528,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:param str domain: domain to enhance
:param str enhancement: enhancement type defined in
:class:`letsencrypt.client.CONFIG.ENHANCEMENTS
:class:`letsencrypt.client.CONFIG.ENHANCEMENTS`
:param options: options for the enhancement
:type options: See :class:`letsencrypt.client.CONFIG.ENHANCEMENTS`
documentation for appropriate parameter.
@ -544,7 +543,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
except errors.LetsEncryptConfiguratorError:
logging.warn("Failed %s for %s", enhancement, domain)
def _enable_redirect(self, ssl_vhost, options):
def _enable_redirect(self, ssl_vhost, unused_options):
"""Redirect all equivalent HTTP traffic to ssl_vhost.
.. todo:: This enhancement should be rewritten and will unfortunately
@ -559,8 +558,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:param ssl_vhost: Destination of traffic, an ssl enabled vhost
:type ssl_vhost: :class:`letsencrypt.client.apache.obj.VirtualHost`
:param options: Not currently used
:type options: Not Available
:param unused_options: Not currently used
:type unused_options: Not Available
:returns: Success, general_vhost (HTTP vhost)
:rtype: (bool, :class:`letsencrypt.client.apache.obj.VirtualHost`)
@ -886,7 +885,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""
return apache_restart()
def config_test(self):
def config_test(self): # pylint: disable=no-self-use
"""Check the configuration of Apache for errors.
:returns: Success
@ -976,7 +975,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
fulfilled by configurator.
:returns: list of responses. All responses are returned in the same
order as received by the perform function. A None response
order as received by the perform function. A None response
indicates the challenge was not perfromed.
:rtype: list

View file

@ -17,7 +17,7 @@ class ApacheDvsni(object):
:ivar dvsni_chall: Data required for challenges.
where DvsniChall tuples have the following fields
`domain` (`str`), `r_b64` (base64 `str`), `nonce` (hex `str`)
`key` (:class:`letsencrypt.client.client.Client.Key`)
`key` (:class:`letsencrypt.client.client.Client.Key`)
:type dvsni_chall: `list` of
:class:`letsencrypt.client.challenge_util.DvsniChall`

View file

@ -2,7 +2,7 @@
class Addr(object):
"""Represents an Apache VirtualHost address.
r"""Represents an Apache VirtualHost address.
:param str addr: addr part of vhost address
:param str port: port number or \*, or ""
@ -58,7 +58,7 @@ class VirtualHost(object):
"""
def __init__(self, filep, path, addrs, ssl, enabled, names=None):
def __init__(self, filep, path, addrs, ssl, enabled, names=None): # pylint: disable=too-many-arguments
"""Initialize a VH."""
self.filep = filep
self.path = path

View file

@ -8,7 +8,7 @@ from letsencrypt.client import challenge_util
from letsencrypt.client import errors
class AuthHandler(object):
class AuthHandler(object): # pylint: disable=too-many-instance-attributes
"""ACME Authorization Handler for a client.
:ivar dv_auth: Authenticator capable of solving CONFIG.DV_CHALLENGES
@ -286,7 +286,7 @@ class AuthHandler(object):
raise errors.LetsEncryptClientError(
"Unimplemented Auth Challenge: %s" % chall["type"])
def _construct_client_chall(self, chall, domain):
def _construct_client_chall(self, chall, domain): # pylint: disable=no-self-use
"""Construct Client Type Challenges.
:param dict chall: Single challenge

View file

@ -226,6 +226,7 @@ def get_cert_info(filename):
.. todo:: Pub key is assumed to be RSA... find a good solution to allow EC.
:param str filename: Name of file containing certificate in PEM format.
:rtype: dict
"""
@ -233,8 +234,8 @@ def get_cert_info(filename):
cert = M2Crypto.X509.load_cert(filename)
try:
san = cert.get_ext('subjectAltName').get_value()
except:
san = cert.get_ext("subjectAltName").get_value()
except LookupError:
san = ""
return {
@ -251,5 +252,6 @@ def get_cert_info(filename):
def b64_cert_to_pem(b64_der_cert):
"""Convert JOSE Base-64 encoded DER cert to PEM."""
return M2Crypto.X509.load_cert_der_string(
le_util.jose_b64decode(b64_der_cert)).as_pem()

View file

@ -1,3 +1,4 @@
"""Lets Encrypt display."""
import textwrap
import dialog
@ -11,10 +12,10 @@ WIDTH = 72
HEIGHT = 20
class CommonDisplayMixin(object):
"""methods common to both NcursesDisplay and FileDisplay"""
class CommonDisplayMixin(object): # pylint: disable=too-few-public-methods
"""Mixin with methods common to classes implementing IDisplay."""
def redirect_by_default(self):
def redirect_by_default(self): # pylint: disable=missing-docstring
choices = [
("Easy", "Allow both HTTP and HTTPS access to these sites"),
("Secure", "Make all requests redirect to secure HTTPS access")]
@ -31,6 +32,8 @@ class CommonDisplayMixin(object):
class NcursesDisplay(CommonDisplayMixin):
"""Ncurses-based display."""
zope.interface.implements(interfaces.IDisplay)
def __init__(self, width=WIDTH, height=HEIGHT):
@ -39,10 +42,10 @@ class NcursesDisplay(CommonDisplayMixin):
self.width = width
self.height = height
def generic_notification(self, message):
def generic_notification(self, message): # pylint: disable=missing-docstring
self.dialog.msgbox(message, width=self.width)
def generic_menu(self, message, choices, input_text=""):
def generic_menu(self, message, choices, unused_input_text=""): # pylint: disable=missing-docstring
# Can accept either tuples or just the actual choices
if choices and isinstance(choices[0], tuple):
code, selection = self.dialog.menu(
@ -55,26 +58,27 @@ class NcursesDisplay(CommonDisplayMixin):
return code(int(tag) - 1)
def generic_input(self, message):
def generic_input(self, message): # pylint: disable=missing-docstring
return self.dialog.inputbox(message)
def generic_yesno(self, message, yes="Yes", no="No"):
def generic_yesno(self, message, yes_label="Yes", no_label="No"): # pylint: disable=missing-docstring
return self.dialog.DIALOG_OK == self.dialog.yesno(
message, self.height, self.width, yes_label=yes, no_label=no)
message, self.height, self.width,
yes_label=yes_label, no_label=no_label)
def filter_names(self, names):
def filter_names(self, names): # pylint: disable=missing-docstring
choices = [(n, "", 0) for n in names]
code, names = self.dialog.checklist(
"Which names would you like to activate HTTPS for?",
choices=choices)
return code, [str(s) for s in names]
def success_installation(self, domains):
def success_installation(self, domains): # pylint: disable=missing-docstring
self.dialog.msgbox(
"\nCongratulations! You have successfully enabled "
+ gen_https_names(domains) + "!", width=self.width)
def display_certs(self, certs):
def display_certs(self, certs): # pylint: disable=missing-docstring
list_choices = []
for i, c in enumerate(certs):
if c['installed']:
@ -99,7 +103,7 @@ class NcursesDisplay(CommonDisplayMixin):
tag = -1
return code, (int(tag) - 1)
def confirm_revocation(self, cert):
def confirm_revocation(self, cert): # pylint: disable=missing-docstring
text = ("Are you sure you would like to revoke the following "
"certificate:\n")
text += cert_info_frame(cert)
@ -107,7 +111,7 @@ class NcursesDisplay(CommonDisplayMixin):
return self.dialog.DIALOG_OK == self.dialog.yesno(
text, width=self.width, height=self.height)
def more_info_cert(self, cert):
def more_info_cert(self, cert): # pylint: disable=missing-docstring
text = "Certificate Information:\n"
text += cert_info_frame(cert)
print text
@ -115,20 +119,21 @@ class NcursesDisplay(CommonDisplayMixin):
class FileDisplay(CommonDisplayMixin):
"""File-based display."""
zope.interface.implements(interfaces.IDisplay)
def __init__(self, outfile):
super(FileDisplay, self).__init__()
self.outfile = outfile
def generic_notification(self, message):
def generic_notification(self, message): # pylint: disable=missing-docstring
side_frame = '-' * 79
wm = textwrap.fill(message, 80)
text = "\n%s\n%s\n%s\n" % (side_frame, wm, side_frame)
self.outfile.write(text)
msg = textwrap.fill(message, 80)
self.outfile.write("\n%s\n%s\n%s\n" % (side_frame, msg, side_frame))
raw_input("Press Enter to Continue")
def generic_menu(self, message, choices, input_text=""):
def generic_menu(self, message, choices, input_text=""): # pylint: disable=missing-docstring
# Can take either tuples or single items in choices list
if choices and isinstance(choices[0], tuple):
choices = ["%s - %s" % (c[0], c[1]) for c in choices]
@ -148,7 +153,7 @@ class FileDisplay(CommonDisplayMixin):
return code, (selection - 1)
def generic_input(self, message):
def generic_input(self, message): # pylint: disable=no-self-use,missing-docstring
ans = raw_input("%s (Enter c to cancel)\n" % message)
if ans.startswith('c') or ans.startswith('C'):
@ -156,12 +161,12 @@ class FileDisplay(CommonDisplayMixin):
else:
return OK, ans
def generic_yesno(self, message, yes_label="Yes", no_label="No"):
def generic_yesno(self, message, unused_yes_label="", unused_no_label=""): # pylint: disable=missing-docstring
self.outfile.write("\n%s\n" % textwrap.fill(message, 80))
ans = raw_input("y/n: ")
return ans.startswith('y') or ans.startswith('Y')
def filter_names(self, names):
def filter_names(self, names): # pylint: disable=missing-docstring
code, tag = self.generic_menu(
"Choose the names would you like to upgrade to HTTPS?",
names, "Select the number of the name: ")
@ -169,7 +174,7 @@ class FileDisplay(CommonDisplayMixin):
# Make sure to return a list...
return code, [names[tag]]
def display_certs(self, certs):
def display_certs(self, certs): # pylint: disable=missing-docstring
menu_choices = [(str(i+1), str(c["cn"]) + " - " + c["pub_key"] +
" - " + str(c["not_before"])[:-6])
for i, c in enumerate(certs)]
@ -206,14 +211,13 @@ class FileDisplay(CommonDisplayMixin):
return code, selection
def success_installation(self, domains):
s_f = '*' * 79
wm = textwrap.fill(("Congratulations! You have successfully " +
"enabled %s!") % gen_https_names(domains))
msg = "%s\n%s\n%s\n"
self.outfile.write(msg % (s_f, wm, s_f))
def success_installation(self, domains): # pylint: disable=missing-docstring
side_frame = '*' * 79
msg = textwrap.fill("Congratulations! You have successfully "
"enabled %s!" % gen_https_names(domains))
self.outfile.write("%s\n%s\n%s\n" % (side_frame, msg, side_frame))
def confirm_revocation(self, cert):
def confirm_revocation(self, cert): # pylint: disable=missing-docstring
self.outfile.write("Are you sure you would like to revoke "
"the following certificate:\n")
self.outfile.write(cert_info_frame(cert))
@ -221,7 +225,7 @@ class FileDisplay(CommonDisplayMixin):
ans = raw_input("y/n")
return ans.startswith('y') or ans.startswith('Y')
def more_info_cert(self, cert):
def more_info_cert(self, cert): # pylint: disable=missing-docstring
self.outfile.write("\nCertificate Information:\n")
self.outfile.write(cert_info_frame(cert))
@ -230,14 +234,14 @@ CANCEL = "cancel"
HELP = "help"
def cert_info_frame(cert):
def cert_info_frame(cert): # pylint: disable=missing-docstring
text = "-" * (WIDTH - 4) + "\n"
text += cert_info_string(cert)
text += "-" * (WIDTH - 4)
return text
def cert_info_string(cert):
def cert_info_string(cert): # pylint: disable=missing-docstring
text = "Subject: %s\n" % cert["subject"]
text += "SAN: %s\n" % cert["san"]
text += "Issuer: %s\n" % cert["issuer"]

View file

@ -1,3 +1,4 @@
"""Interactive challenge."""
import textwrap
import dialog
@ -23,7 +24,7 @@ class InteractiveChallenge(object):
super(InteractiveChallenge, self).__init__()
self.string = string
def perform(self, quiet=True):
def perform(self, quiet=True): # pylint: disable=missing-docstring
if quiet:
dialog.Dialog().msgbox(
self.get_display_string(), width=self.BOX_SIZE)
@ -33,7 +34,7 @@ class InteractiveChallenge(object):
return True
def get_display_string(self):
def get_display_string(self): # pylint: disable=missing-docstring
return (textwrap.fill(self.string, width=self.BOX_SIZE) +
"\n\nPlease Press Enter to Continue")

View file

@ -49,25 +49,24 @@ def check_permissions(filepath, mode, uid=0):
return stat.S_IMODE(file_stat.st_mode) == mode and file_stat.st_uid == uid
def unique_file(default_name, mode=0o777):
def unique_file(path, mode=0o777):
"""Safely finds a unique file for writing only (by default).
:param str default_name: Default file name
:param str path: path/filename.ext
:param int mode: File mode
:return: tuple of file object and file name
"""
count = 1
f_parsed = os.path.splitext(default_name)
while 1:
path, tail = os.path.split(path)
count = 0
while True:
fname = os.path.join(path, "%04d_%s" % (count, tail))
try:
file_d = os.open(
default_name, os.O_CREAT | os.O_EXCL | os.O_RDWR, mode)
return os.fdopen(file_d, 'w'), default_name
file_d = os.open(fname, os.O_CREAT | os.O_EXCL | os.O_RDWR, mode)
return os.fdopen(file_d, 'w'), fname
except OSError:
pass
default_name = f_parsed[0] + '_' + str(count) + f_parsed[1]
count += 1

View file

@ -30,8 +30,8 @@ class RecoveryContact(object):
self.contact = contact
self.poll_delay = poll_delay
def perform(self, quiet=True):
d = dialog.Dialog()
def perform(self, quiet=True): # pylint: disable=missing-docstring
d = dialog.Dialog() # pylint: disable=invalid-name
if quiet:
if self.success_url:
d.infobox(self.get_display_string())
@ -50,7 +50,7 @@ class RecoveryContact(object):
return True
def cleanup(self):
def cleanup(self): # pylint: disable=no-self-use,missing-docstring
return
def get_display_string(self):
@ -91,7 +91,7 @@ class RecoveryContact(object):
else:
return False
def prompt_continue(self, quiet=True):
def prompt_continue(self, quiet=True): # pylint: disable=no-self-use
"""Prompt user for continuation.
:param bool quiet: Display dialog box if True, raw prompt otherwise.
@ -109,7 +109,7 @@ class RecoveryContact(object):
return ans.startswith('y') or ans.startswith('Y')
def generate_response(self):
def generate_response(self): # pylint: disable=missing-docstring
if not self.token:
return {"type": "recoveryContact"}
return {

View file

@ -57,12 +57,13 @@ class Revoker(object):
return
c_sha1_vh = {}
if self.installer is not None:
for (cert, _, path) in self.installer.get_all_certs_keys():
try:
c_sha1_vh[M2Crypto.X509.load_cert(
cert).get_fingerprint(md='sha1')] = path
except:
except M2Crypto.X509.X509Error:
continue
with open(list_file, 'rb') as csvfile:

View file

@ -45,7 +45,8 @@ class ACMEObjectValidateTest(unittest.TestCase):
class PrettyTest(unittest.TestCase):
"""Tests for letsencrypt.client.acme.pretty."""
def _call(self, json_string):
@classmethod
def _call(cls, json_string):
from letsencrypt.client.acme import pretty
return pretty(json_string)
@ -64,31 +65,19 @@ class MessageFactoriesTest(unittest.TestCase):
self.nonce = '\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9'
self.b64nonce = '7Nbyb1lI6xPVI3Hg3aKSqQ'
def _validate(self, msg):
@classmethod
def _validate(cls, msg):
from letsencrypt.client.acme import SCHEMATA
jsonschema.validate(msg, SCHEMATA[msg['type']])
def _signature(self, sig):
return {
'nonce': self.b64nonce,
'alg': 'RS256',
'jwk': {
'kty': 'RSA',
'e': 'AQAB',
'n': 'rHVztFHtH92ucFJD_N_HW9AsdRsUuHUBBBDlHwNlRd3fp5'
'80rv2-6QWE30cWgdmJS86ObRz6lUTor4R0T-3C5Q',
},
'sig': sig,
}
def test_challenge_request(self):
from letsencrypt.client.acme import challenge_request
msg = challenge_request('example.com')
self._validate(msg)
self.assertEqual(msg, {
'type': 'challengeRequest',
'identifier': 'example.com',
})
self._validate(msg)
def test_authorization_request(self):
from letsencrypt.client.acme import authorization_request
@ -112,51 +101,57 @@ class MessageFactoriesTest(unittest.TestCase):
self.nonce,
)
self._validate(msg)
self.assertEqual(
msg.pop('signature')['sig'],
'VkpReso87ogwGul2MGck96TkYs4QoblIgNthgrm9O7EBGlzCRCnTHnx'
'bj6loqaC4f5bn1rgS927Gp1Kvbqnmqg'
)
self.assertEqual(msg, {
'type': 'authorizationRequest',
'sessionID': 'aefoGaavieG9Wihuk2aufai3aeZ5EeW4',
'nonce': 'czpsrF0KMH6dgajig3TGHw',
'signature': self._signature(
'VkpReso87ogwGul2MGck96TkYs4QoblIgNthgrm9O7EBGlzCRCnTHnx'
'bj6loqaC4f5bn1rgS927Gp1Kvbqnmqg'),
'responses': responses,
})
self._validate(msg)
def test_certificate_request(self):
from letsencrypt.client.acme import certificate_request
msg = certificate_request(
'TODO: real DER CSR?', self.privkey, self.nonce)
self._validate(msg)
self.assertEqual(
msg.pop('signature')['sig'],
'HEQVN4MU1yDrArP2T7WZQ12XlHCn5DgTPgb5eWT5_vjRPppLSNe6uWE'
'x9SFwG9d9umqn49nZCSW7uskA2lcW6Q'
)
self.assertEqual(msg, {
'type': 'certificateRequest',
'csr': 'VE9ETzogcmVhbCBERVIgQ1NSPw',
'signature': self._signature(
'HEQVN4MU1yDrArP2T7WZQ12XlHCn5DgTPgb5eWT5_vjRPppLSNe6uWE'
'x9SFwG9d9umqn49nZCSW7uskA2lcW6Q'),
})
self._validate(msg)
def test_revocation_request(self):
from letsencrypt.client.acme import revocation_request
msg = revocation_request(
'TODO: real DER cert?', self.privkey, self.nonce)
self._validate(msg)
self.assertEqual(
msg.pop('signature')['sig'],
'ABXA1IsyTalTXIojxmGnIUGyZASmvqEvTQ98jJ5KFs2FTswLEmsoqFX'
'fU6l5_fous-tsbXOfLN-7PjfZ5XWPvg'
)
self.assertEqual(msg, {
'type': 'revocationRequest',
'certificate': 'VE9ETzogcmVhbCBERVIgY2VydD8',
'signature': self._signature(
'ABXA1IsyTalTXIojxmGnIUGyZASmvqEvTQ98jJ5KFs2FTswLEmsoqFX'
'fU6l5_fous-tsbXOfLN-7PjfZ5XWPvg'),
})
self._validate(msg)
def test_status_request(self):
from letsencrypt.client.acme import status_request
msg = status_request(u'O7-s9MNq1siZHlgrMzi9_A')
self._validate(msg)
self.assertEqual(msg, {
'type': 'statusRequest',
'token': u'O7-s9MNq1siZHlgrMzi9_A',
})
self._validate(msg)
if __name__ == '__main__':

View file

@ -1,6 +1,5 @@
"""Test for letsencrypt.client.apache.configurator."""
import os
import pkg_resources
import re
import shutil
import unittest
@ -15,30 +14,19 @@ from letsencrypt.client.apache import configurator
from letsencrypt.client.apache import obj
from letsencrypt.client.apache import parser
from letsencrypt.client.tests.apache import config_util
from letsencrypt.client.tests.apache import util
class TwoVhost80Test(unittest.TestCase):
class TwoVhost80Test(util.ApacheTest):
"""Test two standard well configured HTTP vhosts."""
def setUp(self):
self.temp_dir, self.config_dir, self.work_dir = config_util.dir_setup(
"debian_apache_2_4/two_vhost_80")
super(TwoVhost80Test, self).setUp()
self.ssl_options = config_util.setup_apache_ssl_options(self.config_dir)
self.config = util.get_apache_configurator(
self.config_path, self.config_dir, self.work_dir, self.ssl_options)
# Final slash is currently important
self.config_path = os.path.join(
self.temp_dir, "debian_apache_2_4/two_vhost_80/apache2/")
with mock.patch("letsencrypt.client.apache.configurator."
"mod_loaded") as mock_load:
mock_load.return_value = True
self.config = config_util.get_apache_configurator(
self.config_path, self.config_dir, self.work_dir,
self.ssl_options)
self.vh_truth = config_util.get_vh_truth(
self.vh_truth = util.get_vh_truth(
self.temp_dir, "debian_apache_2_4/two_vhost_80")
def tearDown(self):
@ -172,12 +160,7 @@ class TwoVhost80Test(unittest.TestCase):
def test_perform(self, mock_restart, mock_dvsni_perform):
# Only tests functionality specific to configurator.perform
# Note: As more challenges are offered this will have to be expanded
rsa256_file = pkg_resources.resource_filename(
"letsencrypt.client.tests", 'testdata/rsa256_key.pem')
rsa256_pem = pkg_resources.resource_string(
"letsencrypt.client.tests", 'testdata/rsa256_key.pem')
auth_key = client.Client.Key(rsa256_file, rsa256_pem)
auth_key = client.Client.Key(self.rsa256_file, self.rsa256_pem)
chall1 = challenge_util.DvsniChall(
"encryption-example.demo",
"jIq_Xy1mXGN37tb4L6Xj_es58fW571ZNyXekdZzhh7Q",

View file

@ -1,5 +1,4 @@
"""Test for letsencrypt.client.apache.dvsni."""
import os
import pkg_resources
import unittest
import shutil
@ -10,30 +9,19 @@ from letsencrypt.client import challenge_util
from letsencrypt.client import client
from letsencrypt.client import CONFIG
from letsencrypt.client.tests.apache import config_util
from letsencrypt.client.tests.apache import util
class DvsniPerformTest(unittest.TestCase):
class DvsniPerformTest(util.ApacheTest):
"""Test the ApacheDVSNI challenge."""
def setUp(self):
super(DvsniPerformTest, self).setUp()
config = util.get_apache_configurator(
self.config_path, self.config_dir, self.work_dir, self.ssl_options)
from letsencrypt.client.apache import dvsni
self.temp_dir, self.config_dir, self.work_dir = config_util.dir_setup(
"debian_apache_2_4/two_vhost_80")
self.ssl_options = config_util.setup_apache_ssl_options(self.config_dir)
# Final slash is currently important
self.config_path = os.path.join(
self.temp_dir, "debian_apache_2_4/two_vhost_80/apache2/")
with mock.patch("letsencrypt.client.apache.configurator."
"mod_loaded") as mock_load:
mock_load.return_value = True
config = config_util.get_apache_configurator(
self.config_path, self.config_dir, self.work_dir,
self.ssl_options)
self.sni = dvsni.ApacheDvsni(config)
rsa256_file = pkg_resources.resource_filename(
@ -148,3 +136,7 @@ class DvsniPerformTest(unittest.TestCase):
self.assertEqual(
vhost.names,
set([str(self.challs[1].nonce + CONFIG.INVALID_EXT)]))
if __name__ == '__main__':
unittest.main()

View file

@ -11,23 +11,18 @@ import zope.component
from letsencrypt.client import display
from letsencrypt.client import errors
from letsencrypt.client.apache import parser
from letsencrypt.client.tests.apache import config_util
from letsencrypt.client.tests.apache import util
class ApacheParserTest(unittest.TestCase):
class ApacheParserTest(util.ApacheTest):
"""Apache Parser Test."""
def setUp(self):
super(ApacheParserTest, self).setUp()
zope.component.provideUtility(display.FileDisplay(sys.stdout))
self.temp_dir, self.config_dir, self.work_dir = config_util.dir_setup(
"debian_apache_2_4/two_vhost_80")
self.ssl_options = config_util.setup_apache_ssl_options(self.config_dir)
# Final slash is currently important
self.config_path = os.path.join(
self.temp_dir, "debian_apache_2_4/two_vhost_80/apache2/")
self.parser = parser.ApacheParser(
augeas.Augeas(flags=augeas.Augeas.NONE),
self.config_path, self.ssl_options)
@ -112,3 +107,7 @@ class ApacheParserTest(unittest.TestCase):
self.assertEqual(results["default"], results["listen"])
self.assertEqual(results["default"], results["name"])
if __name__ == '__main__':
unittest.main()

View file

@ -1,7 +1,9 @@
"""Common utilities for letsencrypt.client.apache."""
import os
import pkg_resources
import shutil
import tempfile
import unittest
import mock
@ -10,6 +12,26 @@ from letsencrypt.client.apache import configurator
from letsencrypt.client.apache import obj
class ApacheTest(unittest.TestCase):
def setUp(self):
super(ApacheTest, self).setUp()
self.temp_dir, self.config_dir, self.work_dir = dir_setup(
"debian_apache_2_4/two_vhost_80")
self.ssl_options = setup_apache_ssl_options(self.config_dir)
# Final slash is currently important
self.config_path = os.path.join(
self.temp_dir, "debian_apache_2_4/two_vhost_80/apache2/")
self.rsa256_file = pkg_resources.resource_filename(
"letsencrypt.client.tests", 'testdata/rsa256_key.pem')
self.rsa256_pem = pkg_resources.resource_string(
"letsencrypt.client.tests", 'testdata/rsa256_key.pem')
def dir_setup(test_dir="debian_apache_2_4/two_vhost_80"):
"""Setup the directories necessary for the configurator."""
temp_dir = tempfile.mkdtemp("temp")

View file

@ -1,21 +1,26 @@
"""Test auth_handler.py."""
"""Tests for letsencrypt.client.auth_handler."""
import unittest
import mock
from letsencrypt.client import errors
from letsencrypt.client.tests import acme_util
TRANSLATE = {"dvsni": "DvsniChall",
"simpleHttps": "SimpleHttpsChall",
"dns": "DnsChall",
"recoveryToken": "RecTokenChall",
"recoveryContact": "RecContactChall",
"proofOfPossession": "PopChall"}
TRANSLATE = {
"dvsni": "DvsniChall",
"simpleHttps": "SimpleHttpsChall",
"dns": "DnsChall",
"recoveryToken": "RecTokenChall",
"recoveryContact": "RecContactChall",
"proofOfPossession": "PopChall",
}
# pylint: disable=protected-access
class SatisfyChallengesTest(unittest.TestCase):
"""verify_identities test."""
def setUp(self):
from letsencrypt.client.auth_handler import AuthHandler
@ -225,7 +230,7 @@ class SatisfyChallengesTest(unittest.TestCase):
self.assertEqual(
type(self.handler.client_c["4"][0].chall).__name__, "RecTokenChall")
def _get_exp_response(self, domain, path, challenges):
def _get_exp_response(self, domain, path, challenges): # pylint: disable=no-self-use
exp_resp = ["null"] * len(challenges)
for i in path:
exp_resp[i] = TRANSLATE[challenges[i]["type"]] + str(domain)
@ -283,7 +288,6 @@ class GetAuthorizationsTest(unittest.TestCase):
self.handler.dv_c[dom], self.handler.client_c[dom] = dv_c, c_c
def test_progress_failure(self):
from letsencrypt.client.errors import LetsEncryptAuthHandlerError
challenges = acme_util.get_challenges()
self.handler.add_chall_msg(
"0",
@ -294,7 +298,7 @@ class GetAuthorizationsTest(unittest.TestCase):
self.mock_sat_chall.side_effect = self._sat_failure
self.assertRaises(
LetsEncryptAuthHandlerError, self.handler.get_authorizations)
errors.LetsEncryptAuthHandlerError, self.handler.get_authorizations)
# Check to make sure program didn't loop
self.assertEqual(self.mock_sat_chall.call_count, 1)
@ -327,8 +331,6 @@ class GetAuthorizationsTest(unittest.TestCase):
[mock.call("1"), mock.call("0")])
def _sat_incremental(self):
from letsencrypt.client.errors import LetsEncryptAuthHandlerError
# Exact responses don't matter, just path/response match
if self.iteration == 0:
# Only solve one of "0" required challs
@ -353,7 +355,7 @@ class GetAuthorizationsTest(unittest.TestCase):
self.handler.responses["0"][3] = "finally!"
else:
raise LetsEncryptAuthHandlerError(
raise errors.LetsEncryptAuthHandlerError(
"Failed incremental test: too many invocations")
def _test_finished(self):
@ -408,12 +410,12 @@ class PathSatisfiedTest(unittest.TestCase):
self.assertFalse(self.handler._path_satisfied(dom[i]))
def gen_auth_resp(chall_list):
def gen_auth_resp(chall_list): # pylint: disable=missing-docstring
return ["%s%s" % (type(chall).__name__, chall.domain)
for chall in chall_list]
def gen_path(str_list, challenges):
def gen_path(str_list, challenges): # pylint: disable=missing-docstring
path = []
for i, chall in enumerate(challenges):
for str_chall in str_list:

View file

@ -30,7 +30,8 @@ class CreateSigTest(unittest.TestCase):
'AX9AFJHk-bCMQPJbSzXKjG6H1IWbvxjS2Ew',
}
def _call(self, *args, **kwargs):
@classmethod
def _call(cls, *args, **kwargs):
from letsencrypt.client.crypto_util import create_sig
return create_sig(*args, **kwargs)
@ -50,7 +51,8 @@ class CreateSigTest(unittest.TestCase):
class ValidCSRTest(unittest.TestCase):
"""Tests for letsencrypt.client.crypto_util.valid_csr."""
def _call(self, csr):
@classmethod
def _call(cls, csr):
from letsencrypt.client.crypto_util import valid_csr
return valid_csr(csr)
@ -80,7 +82,8 @@ class ValidCSRTest(unittest.TestCase):
class CSRMatchesPubkeyTest(unittest.TestCase):
"""Tests for letsencrypt.client.crypto_util.csr_matches_pubkey."""
def _call_testdata(self, name, privkey):
@classmethod
def _call_testdata(cls, name, privkey):
from letsencrypt.client.crypto_util import csr_matches_pubkey
return csr_matches_pubkey(pkg_resources.resource_string(
__name__, os.path.join('testdata', name)), privkey)
@ -95,7 +98,7 @@ class CSRMatchesPubkeyTest(unittest.TestCase):
class MakeKeyTest(unittest.TestCase):
"""Tests for letsencrypt.client.crypto_util.make_key."""
def test_it(self):
def test_it(self): # pylint: disable=no-self-use
from letsencrypt.client.crypto_util import make_key
M2Crypto.RSA.load_key_string(make_key(1024))
M2Crypto.RSA.load_key_string(make_key(2048))
@ -105,7 +108,8 @@ class MakeKeyTest(unittest.TestCase):
class ValidPrivkeyTest(unittest.TestCase):
"""Tests for letsencrypt.client.crypto_util.valid_privkey."""
def _call(self, privkey):
@classmethod
def _call(cls, privkey):
from letsencrypt.client.crypto_util import valid_privkey
return valid_privkey(privkey)
@ -122,7 +126,7 @@ class ValidPrivkeyTest(unittest.TestCase):
class MakeSSCertTest(unittest.TestCase):
"""Tests for letsencrypt.client.crypto_util.make_ss_cert."""
def test_it(self):
def test_it(self): # pylint: disable=no-self-use
from letsencrypt.client.crypto_util import make_ss_cert
make_ss_cert(RSA256_KEY, ['example.com', 'www.example.com'])

View file

@ -100,26 +100,25 @@ class UniqueFileTest(unittest.TestCase):
self.assertEqual(0o700, os.stat(self._call(0o700)[1]).st_mode & 0o777)
self.assertEqual(0o100, os.stat(self._call(0o100)[1]).st_mode & 0o777)
def test_default_not_exists(self):
self.assertEqual(self._call()[1], self.default_name)
def test_default_exists(self):
name1 = self._call()[1] # create foo.txt
name1 = self._call()[1] # create 0000_foo.txt
name2 = self._call()[1]
name3 = self._call()[1]
self.assertNotEqual(name1, name2)
basename2 = os.path.basename(name2)
self.assertEqual(os.path.dirname(name2), self.root_path)
self.assertTrue(basename2.startswith('foo'))
self.assertTrue(basename2.endswith('.txt'))
self.assertNotEqual(name1, name3)
self.assertNotEqual(name2, name3)
basename3 = os.path.basename(name3)
self.assertEqual(os.path.dirname(name1), self.root_path)
self.assertEqual(os.path.dirname(name2), self.root_path)
self.assertEqual(os.path.dirname(name3), self.root_path)
self.assertTrue(basename3.startswith('foo'))
self.assertTrue(basename3.endswith('.txt'))
basename1 = os.path.basename(name2)
self.assertTrue(basename1.endswith('foo.txt'))
basename2 = os.path.basename(name2)
self.assertTrue(basename2.endswith('foo.txt'))
basename3 = os.path.basename(name3)
self.assertTrue(basename3.endswith('foo.txt'))
# https://en.wikipedia.org/wiki/Base64#Examples
@ -141,7 +140,8 @@ B64_URL_UNSAFE_EXAMPLES = {
class JOSEB64EncodeTest(unittest.TestCase):
"""Tests for letsencrypt.client.le_util.jose_b64encode."""
def _call(self, data):
@classmethod
def _call(cls, data):
from letsencrypt.client.le_util import jose_b64encode
return jose_b64encode(data)
@ -160,7 +160,8 @@ class JOSEB64EncodeTest(unittest.TestCase):
class JOSEB64DecodeTest(unittest.TestCase):
"""Tests for letsencrypt.client.le_util.jose_b64decode."""
def _call(self, data):
@classmethod
def _call(cls, data):
from letsencrypt.client.le_util import jose_b64decode
return jose_b64decode(data)

View file

@ -6,6 +6,7 @@ import os
import sys
import zope.component
import zope.interface
from letsencrypt.client import CONFIG
from letsencrypt.client import client
@ -18,7 +19,7 @@ from letsencrypt.client import revoker
from letsencrypt.client.apache import configurator
def main():
def main(): # pylint: disable=too-many-statements
"""Command line argument parsing and main script execution."""
if not os.geteuid() == 0:
sys.exit(
@ -41,7 +42,7 @@ def main():
help="Revert configuration N number of checkpoints.")
parser.add_argument("-B", "--keysize", dest="key_size", type=int,
default=CONFIG.RSA_KEY_SIZE, metavar="N",
help="RSA key shall be sized N bits. [%(default)s]")
help="RSA key shall be sized N bits. [%(default)d]")
parser.add_argument("-k", "--revoke", dest="revoke", action="store_true",
help="Revoke a certificate.")
parser.add_argument("-v", "--view-config-changes",
@ -102,7 +103,7 @@ def main():
sys.exit(1)
# Use the same object if possible
if interfaces.IAuthenticator.providedBy(installer):
if interfaces.IAuthenticator.providedBy(installer): # pylint: disable=no-member
auth = installer
else:
auth = determine_authenticator()
@ -181,9 +182,7 @@ def get_all_names(installer):
def determine_authenticator():
"""Returns a valid IAuthenticator."""
try:
if interfaces.IAuthenticator.implementedBy(
configurator.ApacheConfigurator):
return configurator.ApacheConfigurator()
return configurator.ApacheConfigurator()
except errors.LetsEncryptNoInstallationError:
logging.info("Unable to determine a way to authenticate the server")
@ -191,9 +190,7 @@ def determine_authenticator():
def determine_installer():
"""Returns a valid installer if one exists."""
try:
if interfaces.IInstaller.implementedBy(
configurator.ApacheConfigurator):
return configurator.ApacheConfigurator()
return configurator.ApacheConfigurator()
except errors.LetsEncryptNoInstallationError:
logging.info("Unable to find a way to install the certificate.")

View file

@ -5,7 +5,6 @@ from setuptools import setup
install_requires = [
'argparse',
'jsonschema',
'M2Crypto',
'mock',
'pycrypto',
'python-augeas',
@ -13,6 +12,9 @@ install_requires = [
'requests',
'zope.component',
'zope.interface',
# order of items in install_requires DOES matter and M2Crypto has
# to go last, see #152
'M2Crypto',
]
docs_extras = [
@ -24,6 +26,7 @@ testing_extras = [
'nose',
'nosexcover',
'pylint<1.4', # py2.6 compat, c.f #97
'astroid<1.3.0', # py2.6 compat, c.f. #187
'tox',
]

View file

@ -14,7 +14,7 @@ commands =
[testenv:cover]
commands =
python setup.py dev
python setup.py nosetests --with-coverage --cover-min-percentage=59
python setup.py nosetests --with-coverage --cover-min-percentage=61
[testenv:lint]
commands =