mirror of
https://github.com/certbot/certbot.git
synced 2026-06-04 22:33:00 -04:00
Merge branch 'master' into ecdsa
This commit is contained in:
commit
342838bacd
165 changed files with 4136 additions and 2871 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -33,3 +33,5 @@ tags
|
|||
tests/letstest/letest-*/
|
||||
tests/letstest/*.pem
|
||||
tests/letstest/venv/
|
||||
|
||||
.venv
|
||||
|
|
|
|||
4
.pep8
4
.pep8
|
|
@ -1,4 +0,0 @@
|
|||
[pep8]
|
||||
# E265 block comment should start with '# '
|
||||
# E501 line too long (X > 79 characters)
|
||||
ignore = E265,E501
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
[MASTER]
|
||||
|
||||
# use as many jobs as there are cores
|
||||
jobs=0
|
||||
|
||||
# Specify a configuration file.
|
||||
#rcfile=
|
||||
|
||||
|
|
|
|||
12
.travis.yml
12
.travis.yml
|
|
@ -10,10 +10,6 @@ before_install:
|
|||
# using separate envs with different TOXENVs creates 4x1 Travis build
|
||||
# matrix, which allows us to clearly distinguish which component under
|
||||
# test has failed
|
||||
env:
|
||||
global:
|
||||
- BOULDERPATH=$PWD/boulder/
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- python: "2.7"
|
||||
|
|
@ -87,16 +83,20 @@ matrix:
|
|||
env: TOXENV=py34
|
||||
- python: "3.5"
|
||||
env: TOXENV=py35
|
||||
- python: "3.6"
|
||||
env: TOXENV=py36
|
||||
- python: "2.7"
|
||||
env: TOXENV=nginxroundtrip
|
||||
|
||||
|
||||
# Only build pushes to the master branch, PRs, and branches beginning with
|
||||
# `test-`. This reduces the number of simultaneous Travis runs, which speeds
|
||||
# turnaround time on review since there is a cap of 5 simultaneous runs.
|
||||
# `test-` or of the form `digit(s).digit(s).x`. This reduces the number of
|
||||
# simultaneous Travis runs, which speeds turnaround time on review since there
|
||||
# is a cap of on the number of simultaneous runs.
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^\d+\.\d+\.x$/
|
||||
- /^test-.*$/
|
||||
|
||||
# container-based infrastructure
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ RUN /opt/certbot/src/letsencrypt-auto-source/letsencrypt-auto --os-packages-only
|
|||
# the above is not likely to change, so by putting it further up the
|
||||
# Dockerfile we make sure we cache as much as possible
|
||||
|
||||
COPY setup.py README.rst CHANGES.rst MANIFEST.in linter_plugin.py tox.cover.sh tox.ini pep8.travis.sh .pep8 .pylintrc /opt/certbot/src/
|
||||
COPY setup.py README.rst CHANGES.rst MANIFEST.in linter_plugin.py tox.cover.sh tox.ini .pylintrc /opt/certbot/src/
|
||||
|
||||
# all above files are necessary for setup.py, however, package source
|
||||
# code directory has to be copied separately to a subdirectory...
|
||||
|
|
|
|||
41
Vagrantfile
vendored
41
Vagrantfile
vendored
|
|
@ -1,41 +0,0 @@
|
|||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
|
||||
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
|
||||
VAGRANTFILE_API_VERSION = "2"
|
||||
|
||||
# Setup instructions from docs/contributing.rst
|
||||
# Script installs dependencies for tox and boulder integration
|
||||
$ubuntu_setup_script = <<SETUP_SCRIPT
|
||||
cd /vagrant
|
||||
./letsencrypt-auto-source/letsencrypt-auto --os-packages-only
|
||||
./tools/venv.sh
|
||||
wget https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz -P /tmp/
|
||||
sudo tar -C /usr/local -xzf /tmp/go1.5.3.linux-amd64.tar.gz
|
||||
if ! grep -Fxq "export GOROOT=/usr/local/go" /home/vagrant/.profile ; then echo "export GOROOT=/usr/local/go" >> /home/vagrant/.profile; fi
|
||||
if ! grep -Fxq "export PATH=\\$GOROOT/bin:\\$PATH" /home/vagrant/.profile ; then echo "export PATH=\\$GOROOT/bin:\\$PATH" >> /home/vagrant/.profile; fi
|
||||
if ! grep -Fxq "export GOPATH=\\$HOME/go" /home/vagrant/.profile ; then echo "export GOPATH=\\$HOME/go" >> /home/vagrant/.profile; fi
|
||||
if ! grep -Fxq "cd /vagrant/; ./tests/boulder-start.sh &" /etc/rc.local ; then sed -i -e '$i \cd /vagrant/; ./tests/boulder-start.sh &\n' /etc/rc.local; fi
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
sudo -E apt-get -q -y install git make libltdl-dev mariadb-server rabbitmq-server nginx-light
|
||||
SETUP_SCRIPT
|
||||
|
||||
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
||||
|
||||
config.vm.define "ubuntu-trusty", primary: true do |ubuntu_trusty|
|
||||
ubuntu_trusty.vm.box = "ubuntu/trusty64"
|
||||
ubuntu_trusty.vm.provision "shell", inline: $ubuntu_setup_script
|
||||
ubuntu_trusty.vm.provider "virtualbox" do |v|
|
||||
# VM needs more memory to run test suite, got "OSError: [Errno 12]
|
||||
# Cannot allocate memory" when running
|
||||
# letsencrypt.client.tests.display.util_test.NcursesDisplayTest
|
||||
# We may no longer need this.
|
||||
v.memory = 1024
|
||||
|
||||
# Handle cases when the host is behind a private network by making the
|
||||
# NAT engine use the host's resolver mechanisms to handle DNS requests.
|
||||
v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
[pep8]
|
||||
# E265 block comment should start with '# '
|
||||
# E501 line too long (X > 79 characters)
|
||||
ignore = E265,E501
|
||||
377
acme/.pylintrc
377
acme/.pylintrc
|
|
@ -1,377 +0,0 @@
|
|||
[MASTER]
|
||||
|
||||
# Specify a configuration file.
|
||||
#rcfile=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Profiled execution.
|
||||
profile=no
|
||||
|
||||
# Add files or directories to the blacklist. They should be base names, not
|
||||
# paths.
|
||||
ignore=CVS
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# List of plugins (as comma separated values of python modules names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=linter_plugin
|
||||
|
||||
# Use multiple processes to speed up Pylint.
|
||||
jobs=1
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code
|
||||
extension-pkg-whitelist=
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
|
||||
confidence=
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time. See also the "--disable" option for examples.
|
||||
#enable=
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once).You can also use "--disable=all" to
|
||||
# disable everything first and then reenable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --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
|
||||
# bstract-class-not-used cannot be disabled locally (at least in
|
||||
# pylint 1.4.1/2)
|
||||
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, msvs
|
||||
# (visual studio) and html. You can also give a reporter class, eg
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
output-format=text
|
||||
|
||||
# Put messages in a separate file for each module / package specified on the
|
||||
# command line instead of printing them on stdout. Reports (if any) will be
|
||||
# written in a file name "pylint_global.[txt|html]".
|
||||
files-output=no
|
||||
|
||||
# Tells whether to display a full report or only the messages
|
||||
reports=yes
|
||||
|
||||
# Python expression which should return a note less than 10 (10 is the highest
|
||||
# note). You have access to the variables errors warning, statement which
|
||||
# respectively contain the number of errors / warnings messages and the total
|
||||
# number of statements analyzed. This is used by the global evaluation report
|
||||
# (RP0004).
|
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||
|
||||
# Add a comment according to your evaluation note. This is used by the global
|
||||
# evaluation report (RP0004).
|
||||
comment=no
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details
|
||||
#msg-template=
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=80
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
# List of optional constructs for which whitespace checking is disabled
|
||||
no-space-check=trailing-comma,dict-separator
|
||||
|
||||
# Maximum number of lines in a module
|
||||
max-module-lines=1000
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
expected-line-ending-format=
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,XXX,TODO
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format
|
||||
logging-modules=logging,logger
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Spelling dictionary name. Available dictionaries: none. To make it working
|
||||
# install python-enchant package.
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to indicated private dictionary in
|
||||
# --spelling-private-dict-file option instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis
|
||||
ignored-modules=
|
||||
|
||||
# List of classes names for which member attributes should not be checked
|
||||
# (useful for classes with attributes dynamically set).
|
||||
ignored-classes=SQLObject
|
||||
|
||||
# When zope mode is activated, add a predefined set of Zope acquired attributes
|
||||
# to generated-members.
|
||||
zope=no
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E0201 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=REQUEST,acl_users,aq_parent
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
# Ignore comments when computing similarities.
|
||||
ignore-comments=yes
|
||||
|
||||
# Ignore docstrings when computing similarities.
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Ignore imports when computing similarities.
|
||||
ignore-imports=no
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expectedly
|
||||
# not used).
|
||||
dummy-variables-rgx=_$|dummy|unused
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid to define new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,_cb
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Required attributes for module, separated by a comma
|
||||
required-attributes=
|
||||
|
||||
# List of builtins function names that should not be used, separated by a comma
|
||||
bad-functions=map,filter,input
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma
|
||||
good-names=i,j,k,ex,Run,_,logger
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma
|
||||
bad-names=foo,bar,baz,toto,tutu,tata
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name
|
||||
include-naming-hint=no
|
||||
|
||||
# Regular expression matching correct function names
|
||||
function-rgx=[a-z_][a-z0-9_]{2,40}$
|
||||
|
||||
# Naming hint for function names
|
||||
function-name-hint=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression matching correct variable names
|
||||
variable-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Naming hint for variable names
|
||||
variable-name-hint=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression matching correct constant names
|
||||
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
|
||||
|
||||
# Naming hint for constant names
|
||||
const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
|
||||
|
||||
# Regular expression matching correct attribute names
|
||||
attr-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Naming hint for attribute names
|
||||
attr-name-hint=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression matching correct argument names
|
||||
argument-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Naming hint for argument names
|
||||
argument-name-hint=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression matching correct class attribute names
|
||||
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
|
||||
|
||||
# Naming hint for class attribute names
|
||||
class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
|
||||
|
||||
# Regular expression matching correct inline iteration names
|
||||
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
|
||||
|
||||
# Naming hint for inline iteration names
|
||||
inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
|
||||
|
||||
# Regular expression matching correct class names
|
||||
class-rgx=[A-Z_][a-zA-Z0-9]+$
|
||||
|
||||
# Naming hint for class names
|
||||
class-name-hint=[A-Z_][a-zA-Z0-9]+$
|
||||
|
||||
# Regular expression matching correct module names
|
||||
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
|
||||
|
||||
# Naming hint for module names
|
||||
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,49}$
|
||||
|
||||
# Naming hint for method names
|
||||
method-name-hint=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=__.*__|test_[A-Za-z0-9_]*|_.*|.*Test
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
|
||||
[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
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,__new__,setUp
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=mcs
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,_fields,_replace,_source,_make
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# Maximum number of arguments for function / method
|
||||
max-args=6
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore
|
||||
ignored-argument-names=_.*
|
||||
|
||||
# Maximum number of locals for function / method body
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of return / yield for function / method body
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of branch for function / method body
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of statements in function / method body
|
||||
max-statements=50
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=12
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma
|
||||
deprecated-modules=regsub,TERMIOS,Bastion,rexec
|
||||
|
||||
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||
# given file (report RP0402 must not be disabled)
|
||||
import-graph=
|
||||
|
||||
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
ext-import-graph=
|
||||
|
||||
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
int-import-graph=
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "Exception"
|
||||
overgeneral-exceptions=Exception
|
||||
|
|
@ -9,7 +9,6 @@ from cryptography.hazmat.primitives import hashes
|
|||
import OpenSSL
|
||||
import requests
|
||||
|
||||
from acme import dns_resolver
|
||||
from acme import errors
|
||||
from acme import crypto_util
|
||||
from acme import fields
|
||||
|
|
@ -183,7 +182,7 @@ class KeyAuthorizationChallenge(_TokenChallenge):
|
|||
|
||||
Subclasses must implement this method, but they are likely to
|
||||
return completely different data structures, depending on what's
|
||||
necessary to complete the challenge. Interepretation of that
|
||||
necessary to complete the challenge. Interpretation of that
|
||||
return value must be known to the caller.
|
||||
|
||||
:param JWK account_key:
|
||||
|
|
@ -214,36 +213,24 @@ class DNS01Response(KeyAuthorizationChallengeResponse):
|
|||
def simple_verify(self, chall, domain, account_public_key):
|
||||
"""Simple verify.
|
||||
|
||||
This method no longer checks DNS records and is a simple wrapper
|
||||
around `KeyAuthorizationChallengeResponse.verify`.
|
||||
|
||||
:param challenges.DNS01 chall: Corresponding challenge.
|
||||
:param unicode domain: Domain name being verified.
|
||||
:param JWK account_public_key: Public key for the key pair
|
||||
being authorized.
|
||||
|
||||
:returns: ``True`` iff validation with the TXT records resolved from a
|
||||
DNS server is successful.
|
||||
:return: ``True`` iff verification of the key authorization was
|
||||
successful.
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
if not self.verify(chall, account_public_key):
|
||||
# pylint: disable=unused-argument
|
||||
verified = self.verify(chall, account_public_key)
|
||||
if not verified:
|
||||
logger.debug("Verification of key authorization in response failed")
|
||||
return False
|
||||
|
||||
validation_domain_name = chall.validation_domain_name(domain)
|
||||
validation = chall.validation(account_public_key)
|
||||
logger.debug("Verifying %s at %s...", chall.typ, validation_domain_name)
|
||||
|
||||
try:
|
||||
txt_records = dns_resolver.txt_records_for_name(
|
||||
validation_domain_name)
|
||||
except errors.DependencyError:
|
||||
raise errors.DependencyError("Local validation for 'dns-01' "
|
||||
"challenges requires 'dnspython'")
|
||||
exists = validation in txt_records
|
||||
if not exists:
|
||||
logger.debug("Key authorization from response (%r) doesn't match "
|
||||
"any DNS response in %r", self.key_authorization,
|
||||
txt_records)
|
||||
return exists
|
||||
return verified
|
||||
|
||||
|
||||
@Challenge.register # pylint: disable=too-many-ancestors
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ from six.moves.urllib import parse as urllib_parse # pylint: disable=import-err
|
|||
from acme import errors
|
||||
from acme import jose
|
||||
from acme import test_util
|
||||
from acme.dns_resolver import DNS_REQUIREMENT
|
||||
|
||||
CERT = test_util.load_comparable_cert('cert.pem')
|
||||
KEY = jose.JWKRSA(key=test_util.load_rsa_private_key('rsa512_key.pem'))
|
||||
|
|
@ -92,7 +91,6 @@ class DNS01ResponseTest(unittest.TestCase):
|
|||
from acme.challenges import DNS01
|
||||
self.chall = DNS01(token=(b'x' * 16))
|
||||
self.response = self.chall.response(KEY)
|
||||
self.records_for_name_path = "acme.dns_resolver.txt_records_for_name"
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_partial_json())
|
||||
|
|
@ -105,45 +103,16 @@ class DNS01ResponseTest(unittest.TestCase):
|
|||
from acme.challenges import DNS01Response
|
||||
hash(DNS01Response.from_json(self.jmsg))
|
||||
|
||||
def test_simple_verify_bad_key_authorization(self):
|
||||
def test_simple_verify_failure(self):
|
||||
key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem'))
|
||||
self.response.simple_verify(self.chall, "local", key2.public_key())
|
||||
public_key = key2.public_key()
|
||||
verified = self.response.simple_verify(self.chall, "local", public_key)
|
||||
self.assertFalse(verified)
|
||||
|
||||
@mock.patch('acme.dns_resolver.DNS_AVAILABLE', False)
|
||||
def test_simple_verify_without_dns(self):
|
||||
self.assertRaises(
|
||||
errors.DependencyError, self.response.simple_verify,
|
||||
self.chall, 'local', KEY.public_key())
|
||||
|
||||
@test_util.skip_unless(test_util.requirement_available(DNS_REQUIREMENT),
|
||||
"optional dependency dnspython is not available")
|
||||
def test_simple_verify_good_validation(self): # pragma: no cover
|
||||
with mock.patch(self.records_for_name_path) as mock_resolver:
|
||||
mock_resolver.return_value = [
|
||||
self.chall.validation(KEY.public_key())]
|
||||
self.assertTrue(self.response.simple_verify(
|
||||
self.chall, "local", KEY.public_key()))
|
||||
mock_resolver.assert_called_once_with(
|
||||
self.chall.validation_domain_name("local"))
|
||||
|
||||
@test_util.skip_unless(test_util.requirement_available(DNS_REQUIREMENT),
|
||||
"optional dependency dnspython is not available")
|
||||
def test_simple_verify_good_validation_multitxts(self): # pragma: no cover
|
||||
with mock.patch(self.records_for_name_path) as mock_resolver:
|
||||
mock_resolver.return_value = [
|
||||
"!", self.chall.validation(KEY.public_key())]
|
||||
self.assertTrue(self.response.simple_verify(
|
||||
self.chall, "local", KEY.public_key()))
|
||||
mock_resolver.assert_called_once_with(
|
||||
self.chall.validation_domain_name("local"))
|
||||
|
||||
@test_util.skip_unless(test_util.requirement_available(DNS_REQUIREMENT),
|
||||
"optional dependency dnspython is not available")
|
||||
def test_simple_verify_bad_validation(self): # pragma: no cover
|
||||
with mock.patch(self.records_for_name_path) as mock_resolver:
|
||||
mock_resolver.return_value = ["!"]
|
||||
self.assertFalse(self.response.simple_verify(
|
||||
self.chall, "local", KEY.public_key()))
|
||||
def test_simple_verify_success(self):
|
||||
public_key = KEY.public_key()
|
||||
verified = self.response.simple_verify(self.chall, "local", public_key)
|
||||
self.assertTrue(verified)
|
||||
|
||||
|
||||
class DNS01Test(unittest.TestCase):
|
||||
|
|
|
|||
|
|
@ -27,7 +27,11 @@ logger = logging.getLogger(__name__)
|
|||
# for SSL, which does allow these options to be configured.
|
||||
# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning
|
||||
if sys.version_info < (2, 7, 9): # pragma: no cover
|
||||
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3()
|
||||
try:
|
||||
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3()
|
||||
except AttributeError:
|
||||
import urllib3.contrib.pyopenssl # pylint: disable=import-error
|
||||
urllib3.contrib.pyopenssl.inject_into_urllib3()
|
||||
|
||||
DER_CONTENT_TYPE = 'application/pkix-cert'
|
||||
|
||||
|
|
@ -67,20 +71,13 @@ class Client(object): # pylint: disable=too-many-instance-attributes
|
|||
self.directory = directory
|
||||
|
||||
@classmethod
|
||||
def _regr_from_response(cls, response, uri=None, new_authzr_uri=None,
|
||||
terms_of_service=None):
|
||||
def _regr_from_response(cls, response, uri=None, terms_of_service=None):
|
||||
if 'terms-of-service' in response.links:
|
||||
terms_of_service = response.links['terms-of-service']['url']
|
||||
if 'next' in response.links:
|
||||
new_authzr_uri = response.links['next']['url']
|
||||
|
||||
if new_authzr_uri is None:
|
||||
raise errors.ClientError('"next" link missing')
|
||||
|
||||
return messages.RegistrationResource(
|
||||
body=messages.Registration.from_json(response.json()),
|
||||
uri=response.headers.get('Location', uri),
|
||||
new_authzr_uri=new_authzr_uri,
|
||||
terms_of_service=terms_of_service)
|
||||
|
||||
def register(self, new_reg=None):
|
||||
|
|
@ -113,7 +110,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
|
|||
# (c.f. acme-spec #94)
|
||||
|
||||
return self._regr_from_response(
|
||||
response, uri=regr.uri, new_authzr_uri=regr.new_authzr_uri,
|
||||
response, uri=regr.uri,
|
||||
terms_of_service=regr.terms_of_service)
|
||||
|
||||
def update_registration(self, regr, update=None):
|
||||
|
|
@ -128,12 +125,24 @@ class Client(object): # pylint: disable=too-many-instance-attributes
|
|||
|
||||
"""
|
||||
update = regr.body if update is None else update
|
||||
updated_regr = self._send_recv_regr(
|
||||
regr, body=messages.UpdateRegistration(**dict(update)))
|
||||
body = messages.UpdateRegistration(**dict(update))
|
||||
updated_regr = self._send_recv_regr(regr, body=body)
|
||||
if updated_regr != regr:
|
||||
raise errors.UnexpectedUpdate(regr)
|
||||
return updated_regr
|
||||
|
||||
def deactivate_registration(self, regr):
|
||||
"""Deactivate registration.
|
||||
|
||||
:param messages.RegistrationResource regr: The Registration Resource
|
||||
to be deactivated.
|
||||
|
||||
:returns: The Registration resource that was deactivated.
|
||||
:rtype: `.RegistrationResource`
|
||||
|
||||
"""
|
||||
return self.update_registration(regr, update={'status': 'deactivated'})
|
||||
|
||||
def query_registration(self, regr):
|
||||
"""Query server about registration.
|
||||
|
||||
|
|
@ -158,43 +167,30 @@ class Client(object): # pylint: disable=too-many-instance-attributes
|
|||
return self.update_registration(
|
||||
regr.update(body=regr.body.update(agreement=regr.terms_of_service)))
|
||||
|
||||
def _authzr_from_response(self, response, identifier,
|
||||
uri=None, new_cert_uri=None):
|
||||
# pylint: disable=no-self-use
|
||||
if new_cert_uri is None:
|
||||
try:
|
||||
new_cert_uri = response.links['next']['url']
|
||||
except KeyError:
|
||||
raise errors.ClientError('"next" link missing')
|
||||
|
||||
def _authzr_from_response(self, response, identifier, uri=None):
|
||||
authzr = messages.AuthorizationResource(
|
||||
body=messages.Authorization.from_json(response.json()),
|
||||
uri=response.headers.get('Location', uri),
|
||||
new_cert_uri=new_cert_uri)
|
||||
uri=response.headers.get('Location', uri))
|
||||
if authzr.body.identifier != identifier:
|
||||
raise errors.UnexpectedUpdate(authzr)
|
||||
return authzr
|
||||
|
||||
def request_challenges(self, identifier, new_authzr_uri=None):
|
||||
def request_challenges(self, identifier):
|
||||
"""Request challenges.
|
||||
|
||||
:param .messages.Identifier identifier: Identifier to be challenged.
|
||||
:param str new_authzr_uri: ``new-authorization`` URI. If omitted,
|
||||
will default to value found in ``directory``.
|
||||
|
||||
:returns: Authorization Resource.
|
||||
:rtype: `.AuthorizationResource`
|
||||
|
||||
"""
|
||||
new_authz = messages.NewAuthorization(identifier=identifier)
|
||||
response = self.net.post(self.directory.new_authz
|
||||
if new_authzr_uri is None else new_authzr_uri,
|
||||
new_authz)
|
||||
response = self.net.post(self.directory.new_authz, new_authz)
|
||||
# TODO: handle errors
|
||||
assert response.status_code == http_client.CREATED
|
||||
return self._authzr_from_response(response, identifier)
|
||||
|
||||
def request_domain_challenges(self, domain, new_authzr_uri=None):
|
||||
def request_domain_challenges(self, domain):
|
||||
"""Request challenges for domain names.
|
||||
|
||||
This is simply a convenience function that wraps around
|
||||
|
|
@ -209,7 +205,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
|
|||
|
||||
"""
|
||||
return self.request_challenges(messages.Identifier(
|
||||
typ=messages.IDENTIFIER_FQDN, value=domain), new_authzr_uri)
|
||||
typ=messages.IDENTIFIER_FQDN, value=domain))
|
||||
|
||||
def answer_challenge(self, challb, response):
|
||||
"""Answer challenge.
|
||||
|
|
@ -284,7 +280,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
|
|||
"""
|
||||
response = self.net.get(authzr.uri)
|
||||
updated_authzr = self._authzr_from_response(
|
||||
response, authzr.body.identifier, authzr.uri, authzr.new_cert_uri)
|
||||
response, authzr.body.identifier, authzr.uri)
|
||||
# TODO: check and raise UnexpectedUpdate
|
||||
return updated_authzr, response
|
||||
|
||||
|
|
@ -308,7 +304,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
|
|||
|
||||
content_type = DER_CONTENT_TYPE # TODO: add 'cert_type 'argument
|
||||
response = self.net.post(
|
||||
authzrs[0].new_cert_uri, # TODO: acme-spec #90
|
||||
self.directory.new_cert,
|
||||
req,
|
||||
content_type=content_type,
|
||||
headers={'Accept': content_type})
|
||||
|
|
@ -477,17 +473,21 @@ class Client(object): # pylint: disable=too-many-instance-attributes
|
|||
"Recursion limit reached. Didn't get {0}".format(uri))
|
||||
return chain
|
||||
|
||||
def revoke(self, cert):
|
||||
def revoke(self, cert, rsn):
|
||||
"""Revoke certificate.
|
||||
|
||||
:param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in
|
||||
`.ComparableX509`
|
||||
|
||||
:param int rsn: Reason code for certificate revocation.
|
||||
|
||||
:raises .ClientError: If revocation is unsuccessful.
|
||||
|
||||
"""
|
||||
response = self.net.post(self.directory[messages.Revocation],
|
||||
messages.Revocation(certificate=cert),
|
||||
messages.Revocation(
|
||||
certificate=cert,
|
||||
reason=rsn),
|
||||
content_type=None)
|
||||
if response.status_code != http_client.OK:
|
||||
raise errors.ClientError(
|
||||
|
|
@ -654,8 +654,23 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
|
|||
self._add_nonce(self.head(url))
|
||||
return self._nonces.pop()
|
||||
|
||||
def post(self, url, obj, content_type=JOSE_CONTENT_TYPE, **kwargs):
|
||||
"""POST object wrapped in `.JWS` and check response."""
|
||||
def post(self, *args, **kwargs):
|
||||
"""POST object wrapped in `.JWS` and check response.
|
||||
|
||||
If the server responded with a badNonce error, the request will
|
||||
be retried once.
|
||||
|
||||
"""
|
||||
try:
|
||||
return self._post_once(*args, **kwargs)
|
||||
except messages.Error as error:
|
||||
if error.code == 'badNonce':
|
||||
logger.debug('Retrying request after error:\n%s', error)
|
||||
return self._post_once(*args, **kwargs)
|
||||
else:
|
||||
raise
|
||||
|
||||
def _post_once(self, url, obj, content_type=JOSE_CONTENT_TYPE, **kwargs):
|
||||
data = self._wrap_in_jws(obj, self._get_nonce(url))
|
||||
kwargs.setdefault('headers', {'Content-Type': content_type})
|
||||
response = self._send_request('POST', url, data=data, **kwargs)
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ class ClientTest(unittest.TestCase):
|
|||
'https://www.letsencrypt-demo.org/acme/revoke-cert',
|
||||
messages.NewAuthorization:
|
||||
'https://www.letsencrypt-demo.org/acme/new-authz',
|
||||
messages.CertificateRequest:
|
||||
'https://www.letsencrypt-demo.org/acme/new-cert',
|
||||
})
|
||||
|
||||
from acme.client import Client
|
||||
|
|
@ -56,7 +58,6 @@ class ClientTest(unittest.TestCase):
|
|||
self.new_reg = messages.NewRegistration(**dict(reg))
|
||||
self.regr = messages.RegistrationResource(
|
||||
body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1',
|
||||
new_authzr_uri='https://www.letsencrypt-demo.org/acme/new-reg',
|
||||
terms_of_service='https://www.letsencrypt-demo.org/tos')
|
||||
|
||||
# Authorization
|
||||
|
|
@ -72,8 +73,7 @@ class ClientTest(unittest.TestCase):
|
|||
typ=messages.IDENTIFIER_FQDN, value='example.com'),
|
||||
challenges=(challb,), combinations=None)
|
||||
self.authzr = messages.AuthorizationResource(
|
||||
body=self.authz, uri=authzr_uri,
|
||||
new_cert_uri='https://www.letsencrypt-demo.org/acme/new-cert')
|
||||
body=self.authz, uri=authzr_uri)
|
||||
|
||||
# Request issuance
|
||||
self.certr = messages.CertificateResource(
|
||||
|
|
@ -81,6 +81,9 @@ class ClientTest(unittest.TestCase):
|
|||
uri='https://www.letsencrypt-demo.org/acme/cert/1',
|
||||
cert_chain_uri='https://www.letsencrypt-demo.org/ca')
|
||||
|
||||
# Reason code for revocation
|
||||
self.rsn = 1
|
||||
|
||||
def test_init_downloads_directory(self):
|
||||
uri = 'http://www.letsencrypt-demo.org/directory'
|
||||
from acme.client import Client
|
||||
|
|
@ -95,18 +98,12 @@ class ClientTest(unittest.TestCase):
|
|||
self.response.json.return_value = self.regr.body.to_json()
|
||||
self.response.headers['Location'] = self.regr.uri
|
||||
self.response.links.update({
|
||||
'next': {'url': self.regr.new_authzr_uri},
|
||||
'terms-of-service': {'url': self.regr.terms_of_service},
|
||||
})
|
||||
|
||||
self.assertEqual(self.regr, self.client.register(self.new_reg))
|
||||
# TODO: test POST call arguments
|
||||
|
||||
def test_register_missing_next(self):
|
||||
self.response.status_code = http_client.CREATED
|
||||
self.assertRaises(
|
||||
errors.ClientError, self.client.register, self.new_reg)
|
||||
|
||||
def test_update_registration(self):
|
||||
# "Instance of 'Field' has no to_json/update member" bug:
|
||||
# pylint: disable=no-member
|
||||
|
|
@ -121,17 +118,24 @@ class ClientTest(unittest.TestCase):
|
|||
self.assertRaises(
|
||||
errors.UnexpectedUpdate, self.client.update_registration, self.regr)
|
||||
|
||||
def test_deactivate_account(self):
|
||||
self.response.headers['Location'] = self.regr.uri
|
||||
self.response.json.return_value = self.regr.body.to_json()
|
||||
self.assertEqual(self.regr,
|
||||
self.client.deactivate_registration(self.regr))
|
||||
|
||||
def test_deactivate_account_bad_registration_returned(self):
|
||||
self.response.headers['Location'] = self.regr.uri
|
||||
self.response.json.return_value = "some wrong registration thing"
|
||||
self.assertRaises(
|
||||
errors.UnexpectedUpdate,
|
||||
self.client.deactivate_registration,
|
||||
self.regr)
|
||||
|
||||
def test_query_registration(self):
|
||||
self.response.json.return_value = self.regr.body.to_json()
|
||||
self.assertEqual(self.regr, self.client.query_registration(self.regr))
|
||||
|
||||
def test_query_registration_updates_new_authzr_uri(self):
|
||||
self.response.json.return_value = self.regr.body.to_json()
|
||||
self.response.links = {'next': {'url': 'UPDATED'}}
|
||||
self.assertEqual(
|
||||
'UPDATED',
|
||||
self.client.query_registration(self.regr).new_authzr_uri)
|
||||
|
||||
def test_agree_to_tos(self):
|
||||
self.client.update_registration = mock.Mock()
|
||||
self.client.agree_to_tos(self.regr)
|
||||
|
|
@ -142,9 +146,6 @@ class ClientTest(unittest.TestCase):
|
|||
self.response.status_code = http_client.CREATED
|
||||
self.response.headers['Location'] = self.authzr.uri
|
||||
self.response.json.return_value = self.authz.to_json()
|
||||
self.response.links = {
|
||||
'next': {'url': self.authzr.new_cert_uri},
|
||||
}
|
||||
|
||||
def test_request_challenges(self):
|
||||
self._prepare_response_for_request_challenges()
|
||||
|
|
@ -153,10 +154,11 @@ class ClientTest(unittest.TestCase):
|
|||
self.directory.new_authz,
|
||||
messages.NewAuthorization(identifier=self.identifier))
|
||||
|
||||
def test_requets_challenges_custom_uri(self):
|
||||
def test_request_challenges_custom_uri(self):
|
||||
self._prepare_response_for_request_challenges()
|
||||
self.client.request_challenges(self.identifier, 'URI')
|
||||
self.net.post.assert_called_once_with('URI', mock.ANY)
|
||||
self.client.request_challenges(self.identifier)
|
||||
self.net.post.assert_called_once_with(
|
||||
'https://www.letsencrypt-demo.org/acme/new-authz', mock.ANY)
|
||||
|
||||
def test_request_challenges_unexpected_update(self):
|
||||
self._prepare_response_for_request_challenges()
|
||||
|
|
@ -164,12 +166,7 @@ class ClientTest(unittest.TestCase):
|
|||
identifier=self.identifier.update(value='foo')).to_json()
|
||||
self.assertRaises(
|
||||
errors.UnexpectedUpdate, self.client.request_challenges,
|
||||
self.identifier, self.authzr.uri)
|
||||
|
||||
def test_request_challenges_missing_next(self):
|
||||
self.response.status_code = http_client.CREATED
|
||||
self.assertRaises(errors.ClientError, self.client.request_challenges,
|
||||
self.identifier)
|
||||
self.identifier)
|
||||
|
||||
def test_request_domain_challenges(self):
|
||||
self.client.request_challenges = mock.MagicMock()
|
||||
|
|
@ -177,12 +174,6 @@ class ClientTest(unittest.TestCase):
|
|||
self.client.request_challenges(self.identifier),
|
||||
self.client.request_domain_challenges('example.com'))
|
||||
|
||||
def test_request_domain_challenges_custom_uri(self):
|
||||
self.client.request_challenges = mock.MagicMock()
|
||||
self.assertEqual(
|
||||
self.client.request_challenges(self.identifier, 'URI'),
|
||||
self.client.request_domain_challenges('example.com', 'URI'))
|
||||
|
||||
def test_answer_challenge(self):
|
||||
self.response.links['up'] = {'url': self.challr.authzr_uri}
|
||||
self.response.json.return_value = self.challr.body.to_json()
|
||||
|
|
@ -371,7 +362,7 @@ class ClientTest(unittest.TestCase):
|
|||
errors.PollError, self.client.poll_and_request_issuance,
|
||||
csr, authzrs=(invalid_authzr,), mintime=mintime)
|
||||
|
||||
# exceeded max_attemps | TODO: move to a separate test
|
||||
# exceeded max_attempts | TODO: move to a separate test
|
||||
self.assertRaises(
|
||||
errors.PollError, self.client.poll_and_request_issuance,
|
||||
csr, authzrs, mintime=mintime, max_attempts=2)
|
||||
|
|
@ -427,13 +418,22 @@ class ClientTest(unittest.TestCase):
|
|||
self.assertRaises(errors.Error, self.client.fetch_chain, self.certr)
|
||||
|
||||
def test_revoke(self):
|
||||
self.client.revoke(self.certr.body)
|
||||
self.client.revoke(self.certr.body, self.rsn)
|
||||
self.net.post.assert_called_once_with(
|
||||
self.directory[messages.Revocation], mock.ANY, content_type=None)
|
||||
|
||||
def test_revocation_payload(self):
|
||||
obj = messages.Revocation(certificate=self.certr.body, reason=self.rsn)
|
||||
self.assertTrue('reason' in obj.to_partial_json().keys())
|
||||
self.assertEquals(self.rsn, obj.to_partial_json()['reason'])
|
||||
|
||||
def test_revoke_bad_status_raises_error(self):
|
||||
self.response.status_code = http_client.METHOD_NOT_ALLOWED
|
||||
self.assertRaises(errors.ClientError, self.client.revoke, self.certr)
|
||||
self.assertRaises(
|
||||
errors.ClientError,
|
||||
self.client.revoke,
|
||||
self.certr,
|
||||
self.rsn)
|
||||
|
||||
|
||||
class ClientNetworkTest(unittest.TestCase):
|
||||
|
|
@ -616,7 +616,9 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
|
|||
self.wrapped_obj = mock.MagicMock()
|
||||
self.content_type = mock.sentinel.content_type
|
||||
|
||||
self.all_nonces = [jose.b64encode(b'Nonce'), jose.b64encode(b'Nonce2')]
|
||||
self.all_nonces = [
|
||||
jose.b64encode(b'Nonce'),
|
||||
jose.b64encode(b'Nonce2'), jose.b64encode(b'Nonce3')]
|
||||
self.available_nonces = self.all_nonces[:]
|
||||
|
||||
def send_request(*args, **kwargs):
|
||||
|
|
@ -664,7 +666,7 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
|
|||
self.net._wrap_in_jws.assert_called_once_with(
|
||||
self.obj, jose.b64decode(self.all_nonces.pop()))
|
||||
|
||||
assert not self.available_nonces
|
||||
self.available_nonces = []
|
||||
self.assertRaises(errors.MissingNonce, self.net.post,
|
||||
'uri', self.obj, content_type=self.content_type)
|
||||
self.net._wrap_in_jws.assert_called_with(
|
||||
|
|
@ -680,6 +682,35 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
|
|||
self.assertRaises(errors.BadNonce, self.net.post, 'uri',
|
||||
self.obj, content_type=self.content_type)
|
||||
|
||||
def test_post_failed_retry(self):
|
||||
check_response = mock.MagicMock()
|
||||
check_response.side_effect = messages.Error.with_code('badNonce')
|
||||
|
||||
# pylint: disable=protected-access
|
||||
self.net._check_response = check_response
|
||||
self.assertRaises(messages.Error, self.net.post, 'uri',
|
||||
self.obj, content_type=self.content_type)
|
||||
|
||||
def test_post_not_retried(self):
|
||||
check_response = mock.MagicMock()
|
||||
check_response.side_effect = [messages.Error.with_code('malformed'),
|
||||
self.checked_response]
|
||||
|
||||
# pylint: disable=protected-access
|
||||
self.net._check_response = check_response
|
||||
self.assertRaises(messages.Error, self.net.post, 'uri',
|
||||
self.obj, content_type=self.content_type)
|
||||
|
||||
def test_post_successful_retry(self):
|
||||
check_response = mock.MagicMock()
|
||||
check_response.side_effect = [messages.Error.with_code('badNonce'),
|
||||
self.checked_response]
|
||||
|
||||
# pylint: disable=protected-access
|
||||
self.net._check_response = check_response
|
||||
self.assertEqual(self.checked_response, self.net.post(
|
||||
'uri', self.obj, content_type=self.content_type))
|
||||
|
||||
def test_head_get_post_error_passthrough(self):
|
||||
self.send_request.side_effect = requests.exceptions.RequestException
|
||||
for method in self.net.head, self.net.get:
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class SSLSocketAndProbeSNITest(unittest.TestCase):
|
|||
def test_probe_not_recognized_name(self):
|
||||
self.assertRaises(errors.Error, self._probe, b'bar')
|
||||
|
||||
# TODO: py33/py34 tox hangs forever on do_hendshake in second probe
|
||||
# TODO: py33/py34 tox hangs forever on do_handshake in second probe
|
||||
#def probe_connection_error(self):
|
||||
# self._probe(b'foo')
|
||||
# #time.sleep(1) # TODO: avoid race conditions in other way
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
"""DNS Resolver for ACME client.
|
||||
Required only for local validation of 'dns-01' challenges.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from acme import errors
|
||||
from acme import util
|
||||
|
||||
DNS_REQUIREMENT = 'dnspython>=1.12'
|
||||
|
||||
try:
|
||||
util.activate(DNS_REQUIREMENT)
|
||||
# pragma: no cover
|
||||
import dns.exception
|
||||
import dns.resolver
|
||||
DNS_AVAILABLE = True
|
||||
except errors.DependencyError: # pragma: no cover
|
||||
DNS_AVAILABLE = False
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def txt_records_for_name(name):
|
||||
"""Resolve the name and return the TXT records.
|
||||
|
||||
:param unicode name: Domain name being verified.
|
||||
|
||||
:returns: A list of txt records, if empty the name could not be resolved
|
||||
:rtype: list of unicode
|
||||
|
||||
"""
|
||||
if not DNS_AVAILABLE:
|
||||
raise errors.DependencyError(
|
||||
'{0} is required to use this function'.format(DNS_REQUIREMENT))
|
||||
try:
|
||||
dns_response = dns.resolver.query(name, 'TXT')
|
||||
except dns.resolver.NXDOMAIN as error:
|
||||
return []
|
||||
except dns.exception.DNSException as error:
|
||||
logger.error("Error resolving %s: %s", name, str(error))
|
||||
return []
|
||||
|
||||
return [txt_rec.decode("utf-8") for rdata in dns_response
|
||||
for txt_rec in rdata.strings]
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
"""Tests for acme.dns_resolver."""
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
from six.moves import reload_module # pylint: disable=import-error
|
||||
|
||||
from acme import errors
|
||||
from acme import test_util
|
||||
from acme.dns_resolver import DNS_REQUIREMENT
|
||||
|
||||
|
||||
if test_util.requirement_available(DNS_REQUIREMENT):
|
||||
import dns
|
||||
|
||||
|
||||
def create_txt_response(name, txt_records):
|
||||
"""
|
||||
Returns an RRSet containing the 'txt_records' as the result of a DNS
|
||||
query for 'name'.
|
||||
|
||||
This takes advantage of the fact that an Answer object mostly behaves
|
||||
like an RRset.
|
||||
"""
|
||||
return dns.rrset.from_text_list(name, 60, "IN", "TXT", txt_records)
|
||||
|
||||
|
||||
class TxtRecordsForNameTest(unittest.TestCase):
|
||||
"""Tests for acme.dns_resolver.txt_records_for_name."""
|
||||
@classmethod
|
||||
def _call(cls, *args, **kwargs):
|
||||
from acme.dns_resolver import txt_records_for_name
|
||||
return txt_records_for_name(*args, **kwargs)
|
||||
|
||||
|
||||
@test_util.skip_unless(test_util.requirement_available(DNS_REQUIREMENT),
|
||||
"optional dependency dnspython is not available")
|
||||
class TxtRecordsForNameWithDnsTest(TxtRecordsForNameTest):
|
||||
"""Tests for acme.dns_resolver.txt_records_for_name with dns."""
|
||||
@mock.patch("acme.dns_resolver.dns.resolver.query")
|
||||
def test_txt_records_for_name_with_single_response(self, mock_dns):
|
||||
mock_dns.return_value = create_txt_response('name', ['response'])
|
||||
self.assertEqual(['response'], self._call('name'))
|
||||
|
||||
@mock.patch("acme.dns_resolver.dns.resolver.query")
|
||||
def test_txt_records_for_name_with_multiple_responses(self, mock_dns):
|
||||
mock_dns.return_value = create_txt_response(
|
||||
'name', ['response1', 'response2'])
|
||||
self.assertEqual(['response1', 'response2'], self._call('name'))
|
||||
|
||||
@mock.patch("acme.dns_resolver.dns.resolver.query")
|
||||
def test_txt_records_for_name_domain_not_found(self, mock_dns):
|
||||
mock_dns.side_effect = dns.resolver.NXDOMAIN
|
||||
self.assertEquals([], self._call('name'))
|
||||
|
||||
@mock.patch("acme.dns_resolver.dns.resolver.query")
|
||||
def test_txt_records_for_name_domain_other_error(self, mock_dns):
|
||||
mock_dns.side_effect = dns.exception.DNSException
|
||||
self.assertEquals([], self._call('name'))
|
||||
|
||||
|
||||
class TxtRecordsForNameWithoutDnsTest(TxtRecordsForNameTest):
|
||||
"""Tests for acme.dns_resolver.txt_records_for_name without dns."""
|
||||
def setUp(self):
|
||||
from acme import dns_resolver
|
||||
dns_resolver.DNS_AVAILABLE = False
|
||||
|
||||
def tearDown(self):
|
||||
from acme import dns_resolver
|
||||
reload_module(dns_resolver)
|
||||
|
||||
def test_exception_raised(self):
|
||||
self.assertRaises(
|
||||
errors.DependencyError, self._call, "example.org")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
"""Javascript Object Signing and Encryption (jose).
|
||||
|
||||
This package is a Python implementation of the stadards developed by
|
||||
This package is a Python implementation of the standards developed by
|
||||
IETF `Javascript Object Signing and Encryption (Active WG)`_, in
|
||||
particular the following RFCs:
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ class Field(object):
|
|||
|
||||
@classmethod
|
||||
def _empty(cls, value):
|
||||
"""Is the provided value cosidered "empty" for this field?
|
||||
"""Is the provided value considered "empty" for this field?
|
||||
|
||||
This is useful for subclasses that might want to override the
|
||||
definition of being empty, e.g. for some more exotic data types.
|
||||
|
|
@ -267,7 +267,7 @@ class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable):
|
|||
|
||||
if missing:
|
||||
raise errors.DeserializationError(
|
||||
'The following field are required: {0}'.format(
|
||||
'The following fields are required: {0}'.format(
|
||||
','.join(missing)))
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ 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, asymmetric failed: %s', error)
|
||||
return JWKOct(key=data)
|
||||
|
||||
if cls.typ is not NotImplemented and not isinstance(
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ class Directory(jose.JSONDeSerializable):
|
|||
try:
|
||||
return self[name.replace('_', '-')]
|
||||
except KeyError as error:
|
||||
raise AttributeError(str(error))
|
||||
raise AttributeError(str(error) + ': ' + name)
|
||||
|
||||
def __getitem__(self, name):
|
||||
try:
|
||||
|
|
@ -250,6 +250,7 @@ class Registration(ResourceBody):
|
|||
agreement = jose.Field('agreement', omitempty=True)
|
||||
authorizations = jose.Field('authorizations', omitempty=True)
|
||||
certificates = jose.Field('certificates', omitempty=True)
|
||||
status = jose.Field('status', omitempty=True)
|
||||
|
||||
class Authorizations(jose.JSONObjectWithFields):
|
||||
"""Authorizations granted to Account in the process of registration.
|
||||
|
|
@ -314,12 +315,10 @@ class RegistrationResource(ResourceWithURI):
|
|||
"""Registration Resource.
|
||||
|
||||
:ivar acme.messages.Registration body:
|
||||
:ivar unicode new_authzr_uri: URI found in the 'next' ``Link`` header
|
||||
:ivar unicode terms_of_service: URL for the CA TOS.
|
||||
|
||||
"""
|
||||
body = jose.Field('body', decoder=Registration.from_json)
|
||||
new_authzr_uri = jose.Field('new_authzr_uri')
|
||||
terms_of_service = jose.Field('terms_of_service', omitempty=True)
|
||||
|
||||
|
||||
|
|
@ -424,11 +423,9 @@ class AuthorizationResource(ResourceWithURI):
|
|||
"""Authorization Resource.
|
||||
|
||||
:ivar acme.messages.Authorization body:
|
||||
:ivar unicode new_cert_uri: URI found in the 'next' ``Link`` header
|
||||
|
||||
"""
|
||||
body = jose.Field('body', decoder=Authorization.from_json)
|
||||
new_cert_uri = jose.Field('new_cert_uri')
|
||||
|
||||
|
||||
@Directory.register
|
||||
|
|
@ -469,3 +466,4 @@ class Revocation(jose.JSONObjectWithFields):
|
|||
resource = fields.Resource(resource_type)
|
||||
certificate = jose.Field(
|
||||
'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert)
|
||||
reason = jose.Field('reason')
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class ErrorTest(unittest.TestCase):
|
|||
'type': ERROR_PREFIX + 'malformed',
|
||||
}
|
||||
self.error_custom = Error(typ='custom', detail='bar')
|
||||
self.jobj_cusom = {'type': 'custom', 'detail': 'bar'}
|
||||
self.jobj_custom = {'type': 'custom', 'detail': 'bar'}
|
||||
|
||||
def test_default_typ(self):
|
||||
from acme.messages import Error
|
||||
|
|
@ -225,14 +225,12 @@ class RegistrationResourceTest(unittest.TestCase):
|
|||
from acme.messages import RegistrationResource
|
||||
self.regr = RegistrationResource(
|
||||
body=mock.sentinel.body, uri=mock.sentinel.uri,
|
||||
new_authzr_uri=mock.sentinel.new_authzr_uri,
|
||||
terms_of_service=mock.sentinel.terms_of_service)
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.regr.to_json(), {
|
||||
'body': mock.sentinel.body,
|
||||
'uri': mock.sentinel.uri,
|
||||
'new_authzr_uri': mock.sentinel.new_authzr_uri,
|
||||
'terms_of_service': mock.sentinel.terms_of_service,
|
||||
})
|
||||
|
||||
|
|
@ -346,9 +344,7 @@ class AuthorizationResourceTest(unittest.TestCase):
|
|||
from acme.messages import AuthorizationResource
|
||||
authzr = AuthorizationResource(
|
||||
uri=mock.sentinel.uri,
|
||||
body=mock.sentinel.body,
|
||||
new_cert_uri=mock.sentinel.new_cert_uri,
|
||||
)
|
||||
body=mock.sentinel.body)
|
||||
self.assertTrue(isinstance(authzr, jose.JSONDeSerializable))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@ from cryptography.hazmat.backends import default_backend
|
|||
from cryptography.hazmat.primitives import serialization
|
||||
import OpenSSL
|
||||
|
||||
from acme import errors
|
||||
from acme import jose
|
||||
from acme import util
|
||||
|
||||
|
||||
def vector_path(*names):
|
||||
|
|
@ -78,20 +76,6 @@ def load_pyopenssl_private_key(*names):
|
|||
return OpenSSL.crypto.load_privatekey(loader, load_vector(*names))
|
||||
|
||||
|
||||
def requirement_available(requirement):
|
||||
"""Checks if requirement can be imported.
|
||||
|
||||
:rtype: bool
|
||||
:returns: ``True`` iff requirement can be imported
|
||||
|
||||
"""
|
||||
try:
|
||||
util.activate(requirement)
|
||||
except errors.DependencyError: # pragma: no cover
|
||||
return False
|
||||
return True # pragma: no cover
|
||||
|
||||
|
||||
def skip_unless(condition, reason): # pragma: no cover
|
||||
"""Skip tests unless a condition holds.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,7 @@
|
|||
"""ACME utilities."""
|
||||
import pkg_resources
|
||||
import six
|
||||
|
||||
from acme import errors
|
||||
|
||||
|
||||
def map_keys(dikt, func):
|
||||
"""Map dictionary keys."""
|
||||
return dict((func(key), value) for key, value in six.iteritems(dikt))
|
||||
|
||||
|
||||
def activate(requirement):
|
||||
"""Make requirement importable.
|
||||
|
||||
:param str requirement: the distribution and version to activate
|
||||
|
||||
:raises acme.errors.DependencyError: if cannot activate requirement
|
||||
|
||||
"""
|
||||
try:
|
||||
for distro in pkg_resources.require(requirement): # pylint: disable=not-callable
|
||||
distro.activate()
|
||||
except (pkg_resources.DistributionNotFound, pkg_resources.VersionConflict):
|
||||
raise errors.DependencyError('{0} is unavailable'.format(requirement))
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
"""Tests for acme.util."""
|
||||
import unittest
|
||||
|
||||
from acme import errors
|
||||
|
||||
|
||||
class MapKeysTest(unittest.TestCase):
|
||||
"""Tests for acme.util.map_keys."""
|
||||
|
|
@ -14,21 +12,5 @@ class MapKeysTest(unittest.TestCase):
|
|||
self.assertEqual({2: 2, 4: 4}, map_keys({1: 2, 3: 4}, lambda x: x + 1))
|
||||
|
||||
|
||||
class ActivateTest(unittest.TestCase):
|
||||
"""Tests for acme.util.activate."""
|
||||
|
||||
@classmethod
|
||||
def _call(cls, *args, **kwargs):
|
||||
from acme.util import activate
|
||||
return activate(*args, **kwargs)
|
||||
|
||||
def test_failure(self):
|
||||
self.assertRaises(errors.DependencyError, self._call, 'acme>99.0.0')
|
||||
|
||||
def test_success(self):
|
||||
self._call('acme')
|
||||
import acme as unused_acme
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ acme.agree_to_tos(regr)
|
|||
logging.debug(regr)
|
||||
|
||||
authzr = acme.request_challenges(
|
||||
identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=DOMAIN),
|
||||
new_authzr_uri=regr.new_authzr_uri)
|
||||
identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=DOMAIN))
|
||||
logging.debug(authzr)
|
||||
|
||||
authzr, authzr_response = acme.poll(authzr)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from setuptools import setup
|
|||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.10.0.dev0'
|
||||
version = '0.12.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
|
|
@ -33,14 +33,8 @@ if sys.version_info < (2, 7):
|
|||
else:
|
||||
install_requires.append('mock')
|
||||
|
||||
# dnspython 1.12 is required to support both Python 2 and Python 3.
|
||||
dns_extras = [
|
||||
'dnspython>=1.12',
|
||||
]
|
||||
|
||||
dev_extras = [
|
||||
'nose',
|
||||
'pep8',
|
||||
'tox',
|
||||
]
|
||||
|
||||
|
|
@ -78,7 +72,6 @@ setup(
|
|||
include_package_data=True,
|
||||
install_requires=install_requires,
|
||||
extras_require={
|
||||
'dns': dns_extras,
|
||||
'dev': dev_extras,
|
||||
'docs': docs_extras,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -68,9 +68,12 @@ class AugeasConfigurator(common.Plugin):
|
|||
# As aug.get may return null
|
||||
if lens_path and lens in lens_path:
|
||||
msg = (
|
||||
"There has been an error in parsing the file (%s): %s",
|
||||
"There has been an error in parsing the file {0} on line {1}: "
|
||||
"{2}".format(
|
||||
# Strip off /augeas/files and /error
|
||||
path[13:len(path) - 6], self.aug.get(path + "/message"))
|
||||
path[13:len(path) - 6],
|
||||
self.aug.get(path + "/line"),
|
||||
self.aug.get(path + "/message")))
|
||||
raise errors.PluginError(msg)
|
||||
|
||||
# TODO: Cleanup this function
|
||||
|
|
|
|||
|
|
@ -472,7 +472,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
"\n\nUnfortunately mod_macro is not yet supported".format(
|
||||
"\n ".join(vhost_macro)), force_interactive=True)
|
||||
|
||||
return all_names
|
||||
return util.get_filtered_names(all_names)
|
||||
|
||||
def get_name_from_ip(self, addr): # pylint: disable=no-self-use
|
||||
"""Returns a reverse dns name if available.
|
||||
|
|
@ -1807,7 +1807,7 @@ def get_file_path(vhost_path):
|
|||
else:
|
||||
return None
|
||||
except AttributeError:
|
||||
# If we recieved a None path
|
||||
# If we received a None path
|
||||
return None
|
||||
|
||||
last_good = ""
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ class Addr(common.Addr):
|
|||
"""Represents an Apache address."""
|
||||
|
||||
def __eq__(self, other):
|
||||
"""This is defined as equalivalent within Apache.
|
||||
"""This is defined as equivalent within Apache.
|
||||
|
||||
ip_addr:* == ip_addr
|
||||
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@
|
|||
#
|
||||
# Set the following policy settings here and they will be propagated to the 30 rules
|
||||
# file (modsecurity_crs_30_http_policy.conf) by using macro expansion.
|
||||
# If you run into false positves, you can adjust the settings here.
|
||||
# If you run into false positives, you can adjust the settings here.
|
||||
#
|
||||
#SecAction \
|
||||
"id:'900012', \
|
||||
|
|
@ -349,7 +349,7 @@
|
|||
|
||||
|
||||
#
|
||||
# -- [[ Check UTF enconding ]] -----------------------------------------------------------
|
||||
# -- [[ Check UTF encoding ]] -----------------------------------------------------------
|
||||
#
|
||||
# We only want to apply this check if UTF-8 encoding is actually used by the site, otherwise
|
||||
# it will result in false positives.
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from certbot import achallenges
|
|||
from certbot import errors
|
||||
|
||||
from certbot.tests import acme_util
|
||||
from certbot.tests import util as certbot_util
|
||||
|
||||
from certbot_apache import configurator
|
||||
from certbot_apache import parser
|
||||
|
|
@ -97,15 +98,15 @@ class MultipleVhostsTest(util.ApacheTest):
|
|||
# Weak test..
|
||||
ApacheConfigurator.add_parser_arguments(mock.MagicMock())
|
||||
|
||||
@mock.patch("zope.component.getUtility")
|
||||
@certbot_util.patch_get_utility()
|
||||
def test_get_all_names(self, mock_getutility):
|
||||
mock_getutility.notification = mock.MagicMock(return_value=True)
|
||||
names = self.config.get_all_names()
|
||||
self.assertEqual(names, set(
|
||||
["certbot.demo", "ocspvhost.com", "encryption-example.demo",
|
||||
"ip-172-30-0-17", "*.blue.purple.com"]))
|
||||
["certbot.demo", "ocspvhost.com", "encryption-example.demo"]
|
||||
))
|
||||
|
||||
@mock.patch("zope.component.getUtility")
|
||||
@certbot_util.patch_get_utility()
|
||||
@mock.patch("certbot_apache.configurator.socket.gethostbyaddr")
|
||||
def test_get_all_names_addrs(self, mock_gethost, mock_getutility):
|
||||
mock_gethost.side_effect = [("google.com", "", ""), socket.error]
|
||||
|
|
@ -122,7 +123,8 @@ class MultipleVhostsTest(util.ApacheTest):
|
|||
self.config.vhosts.append(vhost)
|
||||
|
||||
names = self.config.get_all_names()
|
||||
self.assertEqual(len(names), 7)
|
||||
# Names get filtered, only 5 are returned
|
||||
self.assertEqual(len(names), 5)
|
||||
self.assertTrue("zombo.com" in names)
|
||||
self.assertTrue("google.com" in names)
|
||||
self.assertTrue("certbot.demo" in names)
|
||||
|
|
@ -1117,7 +1119,7 @@ class MultipleVhostsTest(util.ApacheTest):
|
|||
not_rewriterule = "NotRewriteRule ^ ..."
|
||||
self.assertFalse(self.config._sift_rewrite_rule(not_rewriterule))
|
||||
|
||||
@mock.patch("certbot_apache.configurator.zope.component.getUtility")
|
||||
@certbot_util.patch_get_utility()
|
||||
def test_make_vhost_ssl_with_existing_rewrite_rule(self, mock_get_utility):
|
||||
self.config.parser.modules.add("rewrite_module")
|
||||
|
||||
|
|
@ -1146,7 +1148,7 @@ class MultipleVhostsTest(util.ApacheTest):
|
|||
mock_get_utility().add_message.assert_called_once_with(mock.ANY,
|
||||
|
||||
mock.ANY)
|
||||
@mock.patch("certbot_apache.configurator.zope.component.getUtility")
|
||||
@certbot_util.patch_get_utility()
|
||||
def test_make_vhost_ssl_with_existing_rewrite_conds(self, mock_get_utility):
|
||||
self.config.parser.modules.add("rewrite_module")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
"""Test certbot_apache.display_ops."""
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
import zope.component
|
||||
|
||||
from certbot import errors
|
||||
|
||||
from certbot.display import util as display_util
|
||||
from certbot import errors
|
||||
|
||||
from certbot.tests import util as certbot_util
|
||||
|
||||
from certbot_apache import obj
|
||||
|
||||
|
|
@ -17,8 +18,6 @@ class SelectVhostTest(unittest.TestCase):
|
|||
"""Tests for certbot_apache.display_ops.select_vhost."""
|
||||
|
||||
def setUp(self):
|
||||
zope.component.provideUtility(display_util.FileDisplay(sys.stdout,
|
||||
False))
|
||||
self.base_dir = "/example_path"
|
||||
self.vhosts = util.get_vh_truth(
|
||||
self.base_dir, "debian_apache_2_4/multiple_vhosts")
|
||||
|
|
@ -28,12 +27,12 @@ class SelectVhostTest(unittest.TestCase):
|
|||
from certbot_apache.display_ops import select_vhost
|
||||
return select_vhost("example.com", vhosts)
|
||||
|
||||
@mock.patch("certbot_apache.display_ops.zope.component.getUtility")
|
||||
@certbot_util.patch_get_utility()
|
||||
def test_successful_choice(self, mock_util):
|
||||
mock_util().menu.return_value = (display_util.OK, 3)
|
||||
self.assertEqual(self.vhosts[3], self._call(self.vhosts))
|
||||
|
||||
@mock.patch("certbot_apache.display_ops.zope.component.getUtility")
|
||||
@certbot_util.patch_get_utility()
|
||||
def test_noninteractive(self, mock_util):
|
||||
mock_util().menu.side_effect = errors.MissingCommandlineFlag("no vhost default")
|
||||
try:
|
||||
|
|
@ -41,7 +40,7 @@ class SelectVhostTest(unittest.TestCase):
|
|||
except errors.MissingCommandlineFlag as e:
|
||||
self.assertTrue("vhost ambiguity" in e.message)
|
||||
|
||||
@mock.patch("certbot_apache.display_ops.zope.component.getUtility")
|
||||
@certbot_util.patch_get_utility()
|
||||
def test_more_info_cancel(self, mock_util):
|
||||
mock_util().menu.side_effect = [
|
||||
(display_util.HELP, 1),
|
||||
|
|
@ -56,7 +55,7 @@ class SelectVhostTest(unittest.TestCase):
|
|||
self.assertEqual(self._call([]), None)
|
||||
|
||||
@mock.patch("certbot_apache.display_ops.display_util")
|
||||
@mock.patch("certbot_apache.display_ops.zope.component.getUtility")
|
||||
@certbot_util.patch_get_utility()
|
||||
@mock.patch("certbot_apache.display_ops.logger")
|
||||
def test_small_display(self, mock_logger, mock_util, mock_display_util):
|
||||
mock_display_util.WIDTH = 20
|
||||
|
|
@ -65,7 +64,7 @@ class SelectVhostTest(unittest.TestCase):
|
|||
|
||||
self.assertEqual(mock_logger.debug.call_count, 1)
|
||||
|
||||
@mock.patch("certbot_apache.display_ops.zope.component.getUtility")
|
||||
@certbot_util.patch_get_utility()
|
||||
def test_multiple_names(self, mock_util):
|
||||
mock_util().menu.return_value = (display_util.OK, 5)
|
||||
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ class ParserInitTest(util.ApacheTest):
|
|||
shutil.rmtree(self.work_dir)
|
||||
|
||||
@mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg")
|
||||
def test_unparsable(self, mock_cfg):
|
||||
def test_unparseable(self, mock_cfg):
|
||||
from certbot_apache.parser import ApacheParser
|
||||
mock_cfg.return_value = ('Define: TEST')
|
||||
self.assertRaises(
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ class ApacheTlsSni01(common.TLSSNI01):
|
|||
ips = " ".join(str(i) for i in ip_addrs)
|
||||
document_root = os.path.join(
|
||||
self.configurator.config.work_dir, "tls_sni_01_page/")
|
||||
# TODO: Python docs is not clear how mutliline string literal
|
||||
# TODO: Python docs is not clear how multiline string literal
|
||||
# newlines are parsed on different platforms. At least on
|
||||
# Linux (Debian sid), when source file uses CRLF, Python still
|
||||
# parses it as "\n"... c.f.:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from setuptools import setup
|
|||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.10.0.dev0'
|
||||
version = '0.12.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
|
|
|
|||
451
certbot-auto
451
certbot-auto
|
|
@ -15,11 +15,15 @@ set -e # Work even if somebody does "sh thisscript.sh".
|
|||
|
||||
# Note: you can set XDG_DATA_HOME or VENV_PATH before running this script,
|
||||
# if you want to change where the virtual environment will be installed
|
||||
XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share}
|
||||
if [ -z "$XDG_DATA_HOME" ]; then
|
||||
XDG_DATA_HOME=~/.local/share
|
||||
fi
|
||||
VENV_NAME="letsencrypt"
|
||||
VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"}
|
||||
if [ -z "$VENV_PATH" ]; then
|
||||
VENV_PATH="$XDG_DATA_HOME/$VENV_NAME"
|
||||
fi
|
||||
VENV_BIN="$VENV_PATH/bin"
|
||||
LE_AUTO_VERSION="0.9.3"
|
||||
LE_AUTO_VERSION="0.11.1"
|
||||
BASENAME=$(basename $0)
|
||||
USAGE="Usage: $BASENAME [OPTIONS]
|
||||
A self-updating wrapper script for the Certbot ACME client. When run, updates
|
||||
|
|
@ -34,8 +38,9 @@ Help for certbot itself cannot be provided until it is installed.
|
|||
-n, --non-interactive, --noninteractive run without asking for user input
|
||||
--no-self-upgrade do not download updates
|
||||
--os-packages-only install OS dependencies and exit
|
||||
-q, --quiet provide only update/error output
|
||||
-v, --verbose provide more output
|
||||
-q, --quiet provide only update/error output;
|
||||
implies --non-interactive
|
||||
|
||||
All arguments are accepted and forwarded to the Certbot client when run."
|
||||
|
||||
|
|
@ -58,6 +63,7 @@ for arg in "$@" ; do
|
|||
--verbose)
|
||||
VERBOSE=1;;
|
||||
-[!-]*)
|
||||
OPTIND=1
|
||||
while getopts ":hnvq" short_arg $arg; do
|
||||
case "$short_arg" in
|
||||
h)
|
||||
|
|
@ -79,43 +85,79 @@ if [ $BASENAME = "letsencrypt-auto" ]; then
|
|||
HELP=0
|
||||
fi
|
||||
|
||||
# Set ASSUME_YES to 1 if QUIET (i.e. --quiet implies --non-interactive)
|
||||
if [ "$QUIET" = 1 ]; then
|
||||
ASSUME_YES=1
|
||||
fi
|
||||
|
||||
# Support for busybox and others where there is no "command",
|
||||
# but "which" instead
|
||||
if command -v command > /dev/null 2>&1 ; then
|
||||
export EXISTS="command -v"
|
||||
elif which which > /dev/null 2>&1 ; then
|
||||
export EXISTS="which"
|
||||
else
|
||||
echo "Cannot find command nor which... please install one!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# certbot-auto needs root access to bootstrap OS dependencies, and
|
||||
# certbot itself needs root access for almost all modes of operation
|
||||
# The "normal" case is that sudo is used for the steps that need root, but
|
||||
# this script *can* be run as root (not recommended), or fall back to using
|
||||
# `su`
|
||||
# `su`. Auto-detection can be overridden by explicitly setting the
|
||||
# environment variable LE_AUTO_SUDO to 'sudo', 'sudo_su' or '' as used below.
|
||||
|
||||
# Because the parameters in `su -c` has to be a string,
|
||||
# we need to properly escape it.
|
||||
su_sudo() {
|
||||
args=""
|
||||
# This `while` loop iterates over all parameters given to this function.
|
||||
# For each parameter, all `'` will be replace by `'"'"'`, and the escaped string
|
||||
# will be wrapped in a pair of `'`, then appended to `$args` string
|
||||
# For example, `echo "It's only 1\$\!"` will be escaped to:
|
||||
# 'echo' 'It'"'"'s only 1$!'
|
||||
# │ │└┼┘│
|
||||
# │ │ │ └── `'s only 1$!'` the literal string
|
||||
# │ │ └── `\"'\"` is a single quote (as a string)
|
||||
# │ └── `'It'`, to be concatenated with the strings following it
|
||||
# └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself
|
||||
while [ $# -ne 0 ]; do
|
||||
args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' "
|
||||
shift
|
||||
done
|
||||
su root -c "$args"
|
||||
}
|
||||
|
||||
SUDO_ENV=""
|
||||
export CERTBOT_AUTO="$0"
|
||||
if test "`id -u`" -ne "0" ; then
|
||||
if command -v sudo 1>/dev/null 2>&1; then
|
||||
SUDO=sudo
|
||||
SUDO_ENV="CERTBOT_AUTO=$0"
|
||||
else
|
||||
echo \"sudo\" is not available, will use \"su\" for installation steps...
|
||||
# Because the parameters in `su -c` has to be a string,
|
||||
# we need properly escape it
|
||||
su_sudo() {
|
||||
args=""
|
||||
# This `while` loop iterates over all parameters given to this function.
|
||||
# For each parameter, all `'` will be replace by `'"'"'`, and the escaped string
|
||||
# will be wrapped in a pair of `'`, then appended to `$args` string
|
||||
# For example, `echo "It's only 1\$\!"` will be escaped to:
|
||||
# 'echo' 'It'"'"'s only 1$!'
|
||||
# │ │└┼┘│
|
||||
# │ │ │ └── `'s only 1$!'` the literal string
|
||||
# │ │ └── `\"'\"` is a single quote (as a string)
|
||||
# │ └── `'It'`, to be concatenated with the strings following it
|
||||
# └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself
|
||||
while [ $# -ne 0 ]; do
|
||||
args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' "
|
||||
shift
|
||||
done
|
||||
su root -c "$args"
|
||||
}
|
||||
SUDO=su_sudo
|
||||
fi
|
||||
if [ -n "${LE_AUTO_SUDO+x}" ]; then
|
||||
case "$LE_AUTO_SUDO" in
|
||||
su_sudo|su)
|
||||
SUDO=su_sudo
|
||||
;;
|
||||
sudo)
|
||||
SUDO=sudo
|
||||
SUDO_ENV="CERTBOT_AUTO=$0"
|
||||
;;
|
||||
'') ;; # Nothing to do for plain root method.
|
||||
*)
|
||||
echo "Error: unknown root authorization mechanism '$LE_AUTO_SUDO'."
|
||||
exit 1
|
||||
esac
|
||||
echo "Using preset root authorization mechanism '$LE_AUTO_SUDO'."
|
||||
else
|
||||
SUDO=
|
||||
if test "`id -u`" -ne "0" ; then
|
||||
if $EXISTS sudo 1>/dev/null 2>&1; then
|
||||
SUDO=sudo
|
||||
SUDO_ENV="CERTBOT_AUTO=$0"
|
||||
else
|
||||
echo \"sudo\" is not available, will use \"su\" for installation steps...
|
||||
SUDO=su_sudo
|
||||
fi
|
||||
else
|
||||
SUDO=
|
||||
fi
|
||||
fi
|
||||
|
||||
ExperimentalBootstrap() {
|
||||
|
|
@ -136,7 +178,7 @@ ExperimentalBootstrap() {
|
|||
DeterminePythonVersion() {
|
||||
for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do
|
||||
# Break (while keeping the LE_PYTHON value) if found.
|
||||
command -v "$LE_PYTHON" > /dev/null && break
|
||||
$EXISTS "$LE_PYTHON" > /dev/null && break
|
||||
done
|
||||
if [ "$?" != "0" ]; then
|
||||
echo "Cannot find any Pythons; please install one!"
|
||||
|
|
@ -171,14 +213,21 @@ BootstrapDebCommon() {
|
|||
#
|
||||
# - Debian 6.0.10 "squeeze" (x64)
|
||||
|
||||
$SUDO apt-get update || echo apt-get update hit problems but continuing anyway...
|
||||
if [ "$QUIET" = 1 ]; then
|
||||
QUIET_FLAG='-qq'
|
||||
fi
|
||||
|
||||
$SUDO apt-get $QUIET_FLAG update || echo apt-get update hit problems but continuing anyway...
|
||||
|
||||
# virtualenv binary can be found in different packages depending on
|
||||
# distro version (#346)
|
||||
|
||||
virtualenv=
|
||||
if apt-cache show virtualenv > /dev/null 2>&1 && ! apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then
|
||||
virtualenv="virtualenv"
|
||||
# virtual env is known to apt and is installable
|
||||
if apt-cache show virtualenv > /dev/null 2>&1 ; then
|
||||
if ! LC_ALL=C apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then
|
||||
virtualenv="virtualenv"
|
||||
fi
|
||||
fi
|
||||
|
||||
if apt-cache show python-virtualenv > /dev/null 2>&1; then
|
||||
|
|
@ -186,77 +235,76 @@ BootstrapDebCommon() {
|
|||
fi
|
||||
|
||||
augeas_pkg="libaugeas0 augeas-lenses"
|
||||
AUGVERSION=`apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2`
|
||||
AUGVERSION=`LC_ALL=C apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2`
|
||||
|
||||
if [ "$ASSUME_YES" = 1 ]; then
|
||||
YES_FLAG="-y"
|
||||
fi
|
||||
|
||||
AddBackportRepo() {
|
||||
# ARGS:
|
||||
BACKPORT_NAME="$1"
|
||||
BACKPORT_SOURCELINE="$2"
|
||||
echo "To use the Apache Certbot plugin, augeas needs to be installed from $BACKPORT_NAME."
|
||||
if ! grep -v -e ' *#' /etc/apt/sources.list | grep -q "$BACKPORT_NAME" ; then
|
||||
# This can theoretically error if sources.list.d is empty, but in that case we don't care.
|
||||
if ! grep -v -e ' *#' /etc/apt/sources.list.d/* 2>/dev/null | grep -q "$BACKPORT_NAME"; then
|
||||
if [ "$ASSUME_YES" = 1 ]; then
|
||||
/bin/echo -n "Installing augeas from $BACKPORT_NAME in 3 seconds..."
|
||||
sleep 1s
|
||||
/bin/echo -ne "\e[0K\rInstalling augeas from $BACKPORT_NAME in 2 seconds..."
|
||||
sleep 1s
|
||||
/bin/echo -e "\e[0K\rInstalling augeas from $BACKPORT_NAME in 1 second ..."
|
||||
sleep 1s
|
||||
add_backports=1
|
||||
else
|
||||
read -p "Would you like to enable the $BACKPORT_NAME repository [Y/n]? " response
|
||||
case $response in
|
||||
[yY][eE][sS]|[yY]|"")
|
||||
add_backports=1;;
|
||||
*)
|
||||
add_backports=0;;
|
||||
esac
|
||||
fi
|
||||
if [ "$add_backports" = 1 ]; then
|
||||
$SUDO sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list"
|
||||
$SUDO apt-get update
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [ "$add_backports" != 0 ]; then
|
||||
$SUDO apt-get install $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg
|
||||
augeas_pkg=
|
||||
# ARGS:
|
||||
BACKPORT_NAME="$1"
|
||||
BACKPORT_SOURCELINE="$2"
|
||||
echo "To use the Apache Certbot plugin, augeas needs to be installed from $BACKPORT_NAME."
|
||||
if ! grep -v -e ' *#' /etc/apt/sources.list | grep -q "$BACKPORT_NAME" ; then
|
||||
# This can theoretically error if sources.list.d is empty, but in that case we don't care.
|
||||
if ! grep -v -e ' *#' /etc/apt/sources.list.d/* 2>/dev/null | grep -q "$BACKPORT_NAME"; then
|
||||
if [ "$ASSUME_YES" = 1 ]; then
|
||||
/bin/echo -n "Installing augeas from $BACKPORT_NAME in 3 seconds..."
|
||||
sleep 1s
|
||||
/bin/echo -ne "\e[0K\rInstalling augeas from $BACKPORT_NAME in 2 seconds..."
|
||||
sleep 1s
|
||||
/bin/echo -e "\e[0K\rInstalling augeas from $BACKPORT_NAME in 1 second ..."
|
||||
sleep 1s
|
||||
add_backports=1
|
||||
else
|
||||
read -p "Would you like to enable the $BACKPORT_NAME repository [Y/n]? " response
|
||||
case $response in
|
||||
[yY][eE][sS]|[yY]|"")
|
||||
add_backports=1;;
|
||||
*)
|
||||
add_backports=0;;
|
||||
esac
|
||||
fi
|
||||
if [ "$add_backports" = 1 ]; then
|
||||
$SUDO sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list"
|
||||
$SUDO apt-get $QUIET_FLAG update
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [ "$add_backports" != 0 ]; then
|
||||
$SUDO apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg
|
||||
augeas_pkg=
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
if dpkg --compare-versions 1.0 gt "$AUGVERSION" ; then
|
||||
if lsb_release -a | grep -q wheezy ; then
|
||||
AddBackportRepo wheezy-backports "deb http://http.debian.net/debian wheezy-backports main"
|
||||
elif lsb_release -a | grep -q precise ; then
|
||||
# XXX add ARM case
|
||||
AddBackportRepo precise-backports "deb http://archive.ubuntu.com/ubuntu precise-backports main restricted universe multiverse"
|
||||
else
|
||||
echo "No libaugeas0 version is available that's new enough to run the"
|
||||
echo "Certbot apache plugin..."
|
||||
fi
|
||||
# XXX add a case for ubuntu PPAs
|
||||
if lsb_release -a | grep -q wheezy ; then
|
||||
AddBackportRepo wheezy-backports "deb http://http.debian.net/debian wheezy-backports main"
|
||||
elif lsb_release -a | grep -q precise ; then
|
||||
# XXX add ARM case
|
||||
AddBackportRepo precise-backports "deb http://archive.ubuntu.com/ubuntu precise-backports main restricted universe multiverse"
|
||||
else
|
||||
echo "No libaugeas0 version is available that's new enough to run the"
|
||||
echo "Certbot apache plugin..."
|
||||
fi
|
||||
# XXX add a case for ubuntu PPAs
|
||||
fi
|
||||
|
||||
$SUDO apt-get install $YES_FLAG --no-install-recommends \
|
||||
$SUDO apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends \
|
||||
python \
|
||||
python-dev \
|
||||
$virtualenv \
|
||||
gcc \
|
||||
dialog \
|
||||
$augeas_pkg \
|
||||
libssl-dev \
|
||||
openssl \
|
||||
libffi-dev \
|
||||
ca-certificates \
|
||||
|
||||
|
||||
|
||||
if ! command -v virtualenv > /dev/null ; then
|
||||
if ! $EXISTS virtualenv > /dev/null ; then
|
||||
echo Failed to install a working \"virtualenv\" command, exiting
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -284,6 +332,9 @@ BootstrapRpmCommon() {
|
|||
if [ "$ASSUME_YES" = 1 ]; then
|
||||
yes_flag="-y"
|
||||
fi
|
||||
if [ "$QUIET" = 1 ]; then
|
||||
QUIET_FLAG='--quiet'
|
||||
fi
|
||||
|
||||
if ! $SUDO $tool list *virtualenv >/dev/null 2>&1; then
|
||||
echo "To use Certbot, packages from the EPEL repository need to be installed."
|
||||
|
|
@ -292,14 +343,14 @@ BootstrapRpmCommon() {
|
|||
exit 1
|
||||
fi
|
||||
if [ "$ASSUME_YES" = 1 ]; then
|
||||
/bin/echo -n "Enabling the EPEL repository in 3 seconds..."
|
||||
sleep 1s
|
||||
/bin/echo -ne "\e[0K\rEnabling the EPEL repository in 2 seconds..."
|
||||
sleep 1s
|
||||
/bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..."
|
||||
sleep 1s
|
||||
/bin/echo -n "Enabling the EPEL repository in 3 seconds..."
|
||||
sleep 1s
|
||||
/bin/echo -ne "\e[0K\rEnabling the EPEL repository in 2 seconds..."
|
||||
sleep 1s
|
||||
/bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..."
|
||||
sleep 1s
|
||||
fi
|
||||
if ! $SUDO $tool install $yes_flag epel-release; then
|
||||
if ! $SUDO $tool install $yes_flag $QUIET_FLAG epel-release; then
|
||||
echo "Could not enable EPEL. Aborting bootstrap!"
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -307,7 +358,6 @@ BootstrapRpmCommon() {
|
|||
|
||||
pkgs="
|
||||
gcc
|
||||
dialog
|
||||
augeas-libs
|
||||
openssl
|
||||
openssl-devel
|
||||
|
|
@ -342,9 +392,9 @@ BootstrapRpmCommon() {
|
|||
"
|
||||
fi
|
||||
|
||||
if ! $SUDO $tool install $yes_flag $pkgs; then
|
||||
echo "Could not install OS dependencies. Aborting bootstrap!"
|
||||
exit 1
|
||||
if ! $SUDO $tool install $yes_flag $QUIET_FLAG $pkgs; then
|
||||
echo "Could not install OS dependencies. Aborting bootstrap!"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
|
@ -356,12 +406,15 @@ BootstrapSuseCommon() {
|
|||
install_flags="-l"
|
||||
fi
|
||||
|
||||
$SUDO zypper $zypper_flags in $install_flags \
|
||||
if [ "$QUIET" = 1 ]; then
|
||||
QUIET_FLAG='-qq'
|
||||
fi
|
||||
|
||||
$SUDO zypper $QUIET_FLAG $zypper_flags in $install_flags \
|
||||
python \
|
||||
python-devel \
|
||||
python-virtualenv \
|
||||
gcc \
|
||||
dialog \
|
||||
augeas-lenses \
|
||||
libopenssl-devel \
|
||||
libffi-devel \
|
||||
|
|
@ -380,7 +433,6 @@ BootstrapArchCommon() {
|
|||
python2
|
||||
python-virtualenv
|
||||
gcc
|
||||
dialog
|
||||
augeas
|
||||
openssl
|
||||
libffi
|
||||
|
|
@ -396,7 +448,11 @@ BootstrapArchCommon() {
|
|||
fi
|
||||
|
||||
if [ "$missing" ]; then
|
||||
$SUDO pacman -S --needed $missing $noconfirm
|
||||
if [ "$QUIET" = 1]; then
|
||||
$SUDO pacman -S --needed $missing $noconfirm > /dev/null
|
||||
else
|
||||
$SUDO pacman -S --needed $missing $noconfirm
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
|
|
@ -404,28 +460,36 @@ BootstrapGentooCommon() {
|
|||
PACKAGES="
|
||||
dev-lang/python:2.7
|
||||
dev-python/virtualenv
|
||||
dev-util/dialog
|
||||
app-admin/augeas
|
||||
dev-libs/openssl
|
||||
dev-libs/libffi
|
||||
app-misc/ca-certificates
|
||||
virtual/pkgconfig"
|
||||
|
||||
ASK_OPTION="--ask"
|
||||
if [ "$ASSUME_YES" = 1 ]; then
|
||||
ASK_OPTION=""
|
||||
fi
|
||||
|
||||
case "$PACKAGE_MANAGER" in
|
||||
(paludis)
|
||||
$SUDO cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x
|
||||
;;
|
||||
(pkgcore)
|
||||
$SUDO pmerge --noreplace --oneshot $PACKAGES
|
||||
$SUDO pmerge --noreplace --oneshot $ASK_OPTION $PACKAGES
|
||||
;;
|
||||
(portage|*)
|
||||
$SUDO emerge --noreplace --oneshot $PACKAGES
|
||||
$SUDO emerge --noreplace --oneshot $ASK_OPTION $PACKAGES
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
BootstrapFreeBsd() {
|
||||
$SUDO pkg install -Ay \
|
||||
if [ "$QUIET" = 1 ]; then
|
||||
QUIET_FLAG="--quiet"
|
||||
fi
|
||||
|
||||
$SUDO pkg install -Ay $QUIET_FLAG \
|
||||
python \
|
||||
py27-virtualenv \
|
||||
augeas \
|
||||
|
|
@ -449,7 +513,6 @@ BootstrapMac() {
|
|||
fi
|
||||
|
||||
$pkgcmd augeas
|
||||
$pkgcmd dialog
|
||||
if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" \
|
||||
-o "$(which python)" = "/usr/bin/python" ]; then
|
||||
# We want to avoid using the system Python because it requires root to use pip.
|
||||
|
|
@ -458,7 +521,7 @@ BootstrapMac() {
|
|||
$pkgcmd python
|
||||
fi
|
||||
|
||||
# Workaround for _dlopen not finding augeas on OS X
|
||||
# Workaround for _dlopen not finding augeas on macOS
|
||||
if [ "$pkgman" = "port" ] && ! [ -e "/usr/local/lib/libaugeas.dylib" ] && [ -e "/opt/local/lib/libaugeas.dylib" ]; then
|
||||
echo "Applying augeas workaround"
|
||||
$SUDO mkdir -p /usr/local/lib/
|
||||
|
|
@ -466,15 +529,15 @@ BootstrapMac() {
|
|||
fi
|
||||
|
||||
if ! hash pip 2>/dev/null; then
|
||||
echo "pip not installed"
|
||||
echo "Installing pip..."
|
||||
curl --silent --show-error --retry 5 https://bootstrap.pypa.io/get-pip.py | python
|
||||
echo "pip not installed"
|
||||
echo "Installing pip..."
|
||||
curl --silent --show-error --retry 5 https://bootstrap.pypa.io/get-pip.py | python
|
||||
fi
|
||||
|
||||
if ! hash virtualenv 2>/dev/null; then
|
||||
echo "virtualenv not installed."
|
||||
echo "Installing with pip..."
|
||||
pip install virtualenv
|
||||
echo "virtualenv not installed."
|
||||
echo "Installing with pip..."
|
||||
pip install virtualenv
|
||||
fi
|
||||
}
|
||||
|
||||
|
|
@ -484,26 +547,29 @@ BootstrapSmartOS() {
|
|||
}
|
||||
|
||||
BootstrapMageiaCommon() {
|
||||
if ! $SUDO urpmi --force \
|
||||
python \
|
||||
libpython-devel \
|
||||
python-virtualenv
|
||||
if [ "$QUIET" = 1 ]; then
|
||||
QUIET_FLAG='--quiet'
|
||||
fi
|
||||
|
||||
if ! $SUDO urpmi --force $QUIET_FLAG \
|
||||
python \
|
||||
libpython-devel \
|
||||
python-virtualenv
|
||||
then
|
||||
echo "Could not install Python dependencies. Aborting bootstrap!"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! $SUDO urpmi --force \
|
||||
git \
|
||||
gcc \
|
||||
cdialog \
|
||||
python-augeas \
|
||||
libopenssl-devel \
|
||||
libffi-devel \
|
||||
rootcerts
|
||||
if ! $SUDO urpmi --force $QUIET_FLAG \
|
||||
git \
|
||||
gcc \
|
||||
python-augeas \
|
||||
libopenssl-devel \
|
||||
libffi-devel \
|
||||
rootcerts
|
||||
then
|
||||
echo "Could not install additional dependencies. Aborting bootstrap!"
|
||||
exit 1
|
||||
echo "Could not install additional dependencies. Aborting bootstrap!"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
|
@ -541,7 +607,7 @@ Bootstrap() {
|
|||
elif uname | grep -iq FreeBSD ; then
|
||||
ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd
|
||||
elif uname | grep -iq Darwin ; then
|
||||
ExperimentalBootstrap "Mac OS X" BootstrapMac
|
||||
ExperimentalBootstrap "macOS" BootstrapMac
|
||||
elif [ -f /etc/issue ] && grep -iq "Amazon Linux" /etc/issue ; then
|
||||
ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon
|
||||
elif [ -f /etc/product ] && grep -q "Joyent Instance" /etc/product ; then
|
||||
|
|
@ -557,7 +623,7 @@ Bootstrap() {
|
|||
}
|
||||
|
||||
TempDir() {
|
||||
mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || OS X
|
||||
mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || macOS
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -570,6 +636,11 @@ if [ "$1" = "--le-auto-phase2" ]; then
|
|||
# --version output ran through grep due to python-cryptography DeprecationWarnings
|
||||
# grep for both certbot and letsencrypt until certbot and shim packages have been released
|
||||
INSTALLED_VERSION=$("$VENV_BIN/letsencrypt" --version 2>&1 | grep "^certbot\|^letsencrypt" | cut -d " " -f 2)
|
||||
if [ -z "$INSTALLED_VERSION" ]; then
|
||||
echo "Error: couldn't get currently installed version for $VENV_BIN/letsencrypt: " 1>&2
|
||||
"$VENV_BIN/letsencrypt" --version
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
INSTALLED_VERSION="none"
|
||||
fi
|
||||
|
|
@ -594,6 +665,11 @@ if [ "$1" = "--le-auto-phase2" ]; then
|
|||
# `pip install --no-cache-dir -e acme -e . -e certbot-apache -e certbot-nginx`,
|
||||
# and then use `hashin` or a more secure method to gather the hashes.
|
||||
|
||||
# Hashin example:
|
||||
# pip install hashin
|
||||
# hashin -r letsencrypt-auto-requirements.txt cryptography==1.5.2
|
||||
# sets the new certbot-auto pinned version of cryptography to 1.5.2
|
||||
|
||||
argparse==1.4.0 \
|
||||
--hash=sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314 \
|
||||
--hash=sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4
|
||||
|
|
@ -601,7 +677,8 @@ argparse==1.4.0 \
|
|||
# This comes before cffi because cffi will otherwise install an unchecked
|
||||
# version via setup_requires.
|
||||
pycparser==2.14 \
|
||||
--hash=sha256:7959b4a74abdc27b312fed1c21e6caf9309ce0b29ea86b591fd2e99ecdf27f73
|
||||
--hash=sha256:7959b4a74abdc27b312fed1c21e6caf9309ce0b29ea86b591fd2e99ecdf27f73 \
|
||||
--no-binary pycparser
|
||||
|
||||
cffi==1.4.2 \
|
||||
--hash=sha256:53c1c9ddb30431513eb7f3cdef0a3e06b0f1252188aaa7744af0f5a4cd45dbaf \
|
||||
|
|
@ -624,29 +701,29 @@ ConfigArgParse==0.10.0 \
|
|||
--hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7
|
||||
configobj==5.0.6 \
|
||||
--hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902
|
||||
cryptography==1.3.4 \
|
||||
--hash=sha256:bede00edd11a2a62c8c98c271cc103fa3a3d72acf64f6e5e4eaf251128897b17 \
|
||||
--hash=sha256:53b39e687b744bb548a98f40736cc529d9f60959b4e6cc551322cf9505d35eb3 \
|
||||
--hash=sha256:474b73ad1139b4e423e46bbd818efd0d5c0df1c65d9f7c957d64c9215d77afde \
|
||||
--hash=sha256:aaddf9592d5b99e32dd518bb4a25b147c124f9d6b4ad64b94f01b15d1666b8c8 \
|
||||
--hash=sha256:6dcad2f407db8c3cd6ecd78361439c449a4f94786b46c54507e7e68f51e1709d \
|
||||
--hash=sha256:475c153fc622e656f1f10a9c9941d0ac7ab18df7c38d35d563a437c1c0e34f24 \
|
||||
--hash=sha256:86dd61df581cba04e89e45081efbc531faff1c9d99c77b1ce97f87216c356353 \
|
||||
--hash=sha256:75cc697e4ef5fdd0102ca749114c6370dbd11db0c9132a18834858c2566247e3 \
|
||||
--hash=sha256:ea03ad5b9df6d79fc9fc1ab23729e01e1c920d2974c5e3c634ccf45a5c378452 \
|
||||
--hash=sha256:c8872b8fe4f3416d6338ab99612f49ab314f7856cb43bffab2a32d28a6267be8 \
|
||||
--hash=sha256:468fc6e16eaec6ceaa6bc341273e6e9912d01b42b740f8cf896ace7fcd6a321d \
|
||||
--hash=sha256:d6fea3c6502735011c5d61a62aef1c1d770fc6a2def45d9e6c0d94c9651e3317 \
|
||||
--hash=sha256:3cf95f179f4bead3d5649b91860ef4cf60ad4244209190fc405908272576d961 \
|
||||
--hash=sha256:141f77e60a5b9158309b2b60288c7f81d37faa15c22a69b94c190ceefaaa6236 \
|
||||
--hash=sha256:87b7a1fe703c6424451f3372d1879dae91c7fe5e13375441a72833db76fee30e \
|
||||
--hash=sha256:f5ee3cb0cf1a6550bf483ccffa6608db267a377b45f7e3a8201a86d1d8feb19f \
|
||||
--hash=sha256:4e097286651ea318300af3251375d48b71b8228481c56cd617ddd4459a1ff261 \
|
||||
--hash=sha256:1e3d3ae3f22f22d50d340f47f25227511326f3f1396c6d2446a5b45b516c4313 \
|
||||
--hash=sha256:6a057941cb64d79834ea3cf99093fcc4787c2a5d44f686c4f297361ddc419bcd \
|
||||
--hash=sha256:68b3d5390b92559ddd3353c73ab2dfcff758f9c4ec4f5d5226ccede0e5d779f4 \
|
||||
--hash=sha256:545dc003b4b6081f9c3e452da15d819b04b696f49484aff64c0a2aedf766bef8 \
|
||||
--hash=sha256:423ff890c01be7c70dbfeaa967eeef5146f1a43a5f810ffdc07b178e48a105a9
|
||||
cryptography==1.5.3 \
|
||||
--hash=sha256:e514d92086246b53ae9b048df652cf3036b462e50a6ce9fac6b6253502679991 \
|
||||
--hash=sha256:10ee414f4b5af403a0d8f20dfa80f7dad1fc7ae5452ec5af03712d5b6e78c664 \
|
||||
--hash=sha256:7234456d1f4345a144ed07af2416c7c0659d4bb599dd1a963103dc8c183b370e \
|
||||
--hash=sha256:d3b9587406f94642bd70b3d666b813f446e95f84220c9e416ad94cbfb6be2eaa \
|
||||
--hash=sha256:b15fc6b59f1474eef62207c85888afada8acc47fae8198ba2b0197d54538961a \
|
||||
--hash=sha256:3b62d65d342704fc07ed171598db2a2775bdf587b1b6abd2cba2261bfe3ccde3 \
|
||||
--hash=sha256:059343022ec904c867a13bc55d2573e36c8cfb2c250e30d8a2e9825f253b07ba \
|
||||
--hash=sha256:c7897cf13bc8b4ee0215d83cbd51766d87c06b277fcca1f9108595508e5bcfb4 \
|
||||
--hash=sha256:9b69e983e5bf83039ddd52e52a28c7faedb2b22bdfb5876377b95aac7d3be63e \
|
||||
--hash=sha256:61e40905c426d02b3fae38088dc66ce4ef84830f7eb223dec6b3ac3ccdc676fb \
|
||||
--hash=sha256:00783a32bcd91a12177230d35bfcf70a2333ade4a6b607fac94a633a7971c671 \
|
||||
--hash=sha256:d11973f49b648cde1ea1a30e496d7557dbfeccd08b3cd9ba58d286a9c274ff8e \
|
||||
--hash=sha256:f24bedf28b81932ba6063aec9a826669f5237ea3b755efe04d98b072faa053a5 \
|
||||
--hash=sha256:3ab5725367239e3deb9b92e917aa965af3fef008f25b96a3000821869e208181 \
|
||||
--hash=sha256:8a53209de822e22b5f73bf4b99e68ac4ccc91051fd6751c8252982983e86a77d \
|
||||
--hash=sha256:5a07439d4b1e4197ac202b7eea45e26a6fd65757652dc50f1a63367f711df933 \
|
||||
--hash=sha256:26b1c4b40aec7b0074bceabe6e06565aa28176eca7323a31df66ebf89fe916d3 \
|
||||
--hash=sha256:eaa4a7b5a6682adcf8d6ebb2a08a008802657643655bb527c95c8a3860253d8e \
|
||||
--hash=sha256:8156927dcf8da274ff205ad0612f75c380df45385bacf98531a5b3348c88d135 \
|
||||
--hash=sha256:61ec0d792749d0e91e84b1d58b6dfd204806b10b5811f846c2ceca0de028c53a \
|
||||
--hash=sha256:26330c88041569ca621cc42274d0ea2667a48b6deab41467272c3aba0b6e8f07 \
|
||||
--hash=sha256:cf82ddac919b587f5e44247579b433224cc2e03332d2ea4d89aa70d7e6b64ae5
|
||||
enum34==1.1.2 \
|
||||
--hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \
|
||||
--hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501
|
||||
|
|
@ -662,8 +739,6 @@ ipaddress==1.0.16 \
|
|||
linecache2==1.0.0 \
|
||||
--hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \
|
||||
--hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c
|
||||
ndg-httpsclient==0.4.0 \
|
||||
--hash=sha256:e8c155fdebd9c4bcb0810b4ed01ae1987554b1ee034dd7532d7b8fdae38a6274
|
||||
ordereddict==1.1 \
|
||||
--hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f
|
||||
parsedatetime==2.1 \
|
||||
|
|
@ -684,9 +759,9 @@ pyasn1==0.1.9 \
|
|||
--hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \
|
||||
--hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \
|
||||
--hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f
|
||||
pyopenssl==16.0.0 \
|
||||
--hash=sha256:5add70cf00273bf957ca31fdb0df9b0ae4639e081897d5f86a0ae1f104901230 \
|
||||
--hash=sha256:363d10ee43d062285facf4e465f4f5163f9f702f9134f0a5896f134cbb92d17d
|
||||
pyOpenSSL==16.2.0 \
|
||||
--hash=sha256:26ca380ddf272f7556e48064bbcd5bd71f83dfc144f3583501c7ddbd9434ee17 \
|
||||
--hash=sha256:7779a3bbb74e79db234af6a08775568c6769b5821faecf6e2f4143edb227516e
|
||||
pyparsing==2.1.8 \
|
||||
--hash=sha256:2f0f5ceb14eccd5aef809d6382e87df22ca1da583c79f6db01675ce7d7f49c18 \
|
||||
--hash=sha256:03a4869b9f3493807ee1f1cb405e6d576a1a2ca4d81a982677c0c1ad6177c56b \
|
||||
|
|
@ -701,9 +776,6 @@ pyRFC3339==1.0 \
|
|||
--hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535
|
||||
python-augeas==0.5.0 \
|
||||
--hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2
|
||||
python2-pythondialog==3.3.0 \
|
||||
--hash=sha256:04e93f24995c43dd90f338d5d865ca72ce3fb5a5358d4daa4965571db35fc3ec \
|
||||
--hash=sha256:3e6f593fead98f8a526bc3e306933533236e33729f552f52896ea504f55313fa
|
||||
pytz==2015.7 \
|
||||
--hash=sha256:3abe6a6d3fc2fbbe4c60144211f45da2edbe3182a6f6511af6bbba0598b1f992 \
|
||||
--hash=sha256:939ef9c1e1224d980405689a97ffcf7828c56d1517b31d73464356c1f2b7769e \
|
||||
|
|
@ -718,9 +790,9 @@ pytz==2015.7 \
|
|||
--hash=sha256:fbd26746772c24cb93c8b97cbdad5cb9e46c86bbdb1b9d8a743ee00e2fb1fc5d \
|
||||
--hash=sha256:99266ef30a37e43932deec2b7ca73e83c8dbc3b9ff703ec73eca6b1dae6befea \
|
||||
--hash=sha256:8b6ce1c993909783bc96e0b4f34ea223bff7a4df2c90bdb9c4e0f1ac928689e3
|
||||
requests==2.9.1 \
|
||||
--hash=sha256:113fbba5531a9e34945b7d36b33a084e8ba5d0664b703c81a7c572d91919a5b8 \
|
||||
--hash=sha256:c577815dd00f1394203fc44eb979724b098f88264a9ef898ee45b8e5e9cf587f
|
||||
requests==2.12.1 \
|
||||
--hash=sha256:3f3f27a9d0f9092935efc78054ef324eb9f8166718270aefe036dfa1e4f68e1e \
|
||||
--hash=sha256:2109ecea94df90980be040490ff1d879971b024861539abb00054062388b612e
|
||||
six==1.10.0 \
|
||||
--hash=sha256:0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1 \
|
||||
--hash=sha256:105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a
|
||||
|
|
@ -761,18 +833,18 @@ letsencrypt==0.7.0 \
|
|||
|
||||
# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE.
|
||||
|
||||
acme==0.9.3 \
|
||||
--hash=sha256:d18ce17a75ad24d27981dfaef0524aa905eab757b267e027162b56a8967ab8fb \
|
||||
--hash=sha256:a6eff1f955eb2e4316abd9aa2fedb6d9345e6b5b8a2d64ea0ad35e05d6124099
|
||||
certbot==0.9.3 \
|
||||
--hash=sha256:a87ef4c53c018df4e52ee2f2e906ad16bbb37789f29e6f284c495a2eb4d9b243 \
|
||||
--hash=sha256:68149cb8392b29f5d5246e7226d25f913f2b10482bf3bc7368e8c8821d25f3b0
|
||||
certbot-apache==0.9.3 \
|
||||
--hash=sha256:f379b1053e10709692654d7a6fcea9eaed19b66c49a753b61e31bd06a04b0aac \
|
||||
--hash=sha256:a5d98cf972072de08f984db4e6a7f20269f3f023c43f6d4e781fe43be7c10086
|
||||
certbot-nginx==0.9.3 \
|
||||
--hash=sha256:3c26f18f0b57550f069263bd9b2984ef33eab6693e7796611c1b2cc16574069c \
|
||||
--hash=sha256:7337a2e90e0b28a1ab09e31d9fb81c6d78e6453500c824c0f18bab5d31b63058
|
||||
acme==0.11.1 \
|
||||
--hash=sha256:9f4efac6dc4477a3baa7eb2392d4f7583f974e4ad336439aa1961ef805622a77 \
|
||||
--hash=sha256:db35258edfc13dfe5839215898fe2d5d3caafc9a084f631a032f3fdf712c694e
|
||||
certbot==0.11.1 \
|
||||
--hash=sha256:ba80552df0f390dbc5fcd14b4ea4b1499ea866f5f78c8c1a375abc25101dedf1 \
|
||||
--hash=sha256:6c1724486d500c5163c9313d6a14af5af9f4515f79553627303a6b86df2c3af2
|
||||
certbot-apache==0.11.1 \
|
||||
--hash=sha256:70132d9013509011b9edeba64fc208961f50ef78457f58d3b80a61094102efcd \
|
||||
--hash=sha256:efe2224b531595edee366423c115e2874a3c9011890321d3ccda0367efc776c0
|
||||
certbot-nginx==0.11.1 \
|
||||
--hash=sha256:1895eea1de92ab3dfd762998a4be7868ec3ec4d42cce7772995e4e9b2e488e6a \
|
||||
--hash=sha256:e5e5ffe8930ba10139bb61c2a05a30e84d9a69a7d8fc6a7b391f707eae8bfce5
|
||||
|
||||
UNLIKELY_EOF
|
||||
# -------------------------------------------------------------------------
|
||||
|
|
@ -940,7 +1012,28 @@ UNLIKELY_EOF
|
|||
# Report error. (Otherwise, be quiet.)
|
||||
echo "Had a problem while installing Python packages."
|
||||
if [ "$VERBOSE" != 1 ]; then
|
||||
echo
|
||||
echo "pip prints the following errors: "
|
||||
echo "====================================================="
|
||||
echo "$PIP_OUT"
|
||||
echo "====================================================="
|
||||
echo
|
||||
echo "Certbot has problem setting up the virtual environment."
|
||||
|
||||
if `echo $PIP_OUT | grep -q Killed` || `echo $PIP_OUT | grep -q "allocate memory"` ; then
|
||||
echo
|
||||
echo "Based on your pip output, the problem can likely be fixed by "
|
||||
echo "increasing the available memory."
|
||||
else
|
||||
echo
|
||||
echo "We were not be able to guess the right solution from your pip "
|
||||
echo "output."
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Consult https://certbot.eff.org/docs/install.html#problems-with-python-virtual-environment"
|
||||
echo "for possible solutions."
|
||||
echo "You may also find some support resources at https://certbot.eff.org/support/ ."
|
||||
fi
|
||||
rm -rf "$VENV_PATH"
|
||||
exit 1
|
||||
|
|
@ -963,7 +1056,7 @@ UNLIKELY_EOF
|
|||
fi
|
||||
|
||||
else
|
||||
# Phase 1: Upgrade certbot-auto if neceesary, then self-invoke.
|
||||
# Phase 1: Upgrade certbot-auto if necessary, then self-invoke.
|
||||
#
|
||||
# Each phase checks the version of only the thing it is responsible for
|
||||
# upgrading. Phase 1 checks the version of the latest release of
|
||||
|
|
@ -1132,7 +1225,7 @@ UNLIKELY_EOF
|
|||
# TODO: Deal with quotes in pathnames.
|
||||
echo "Replacing certbot-auto..."
|
||||
# Clone permissions with cp. chmod and chown don't have a --reference
|
||||
# option on OS X or BSD, and stat -c on Linux is stat -f on OS X and BSD:
|
||||
# option on macOS or BSD, and stat -c on Linux is stat -f on macOS and BSD:
|
||||
$SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone"
|
||||
$SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone"
|
||||
# Using mv rather than cp leaves the old file descriptor pointing to the
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ RUN /opt/certbot/src/certbot-auto -n --os-packages-only
|
|||
# the above is not likely to change, so by putting it further up the
|
||||
# Dockerfile we make sure we cache as much as possible
|
||||
|
||||
COPY setup.py README.rst CHANGES.rst MANIFEST.in linter_plugin.py tox.cover.sh tox.ini pep8.travis.sh .pep8 .pylintrc /opt/certbot/src/
|
||||
COPY setup.py README.rst CHANGES.rst MANIFEST.in linter_plugin.py tox.cover.sh tox.ini .pylintrc /opt/certbot/src/
|
||||
|
||||
# all above files are necessary for setup.py, however, package source
|
||||
# code directory has to be copied separately to a subdirectory...
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import zope.interface
|
|||
|
||||
from certbot import configuration
|
||||
from certbot import errors as le_errors
|
||||
from certbot import util as certbot_util
|
||||
from certbot_apache import configurator
|
||||
from certbot_apache import constants
|
||||
from certbot_compatibility_test import errors
|
||||
|
|
@ -106,4 +107,7 @@ def _get_names(config):
|
|||
not util.IP_REGEX.match(words[1]) and
|
||||
words[1].find(".") != -1):
|
||||
all_names.add(words[1])
|
||||
return all_names, non_ip_names
|
||||
return (
|
||||
certbot_util.get_filtered_names(all_names),
|
||||
certbot_util.get_filtered_names(non_ip_names)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ def test_deploy_cert(plugin, temp_dir, domains):
|
|||
plugin.deploy_cert(domain, cert_path, util.KEY_PATH, cert_path, cert_path)
|
||||
plugin.save() # Needed by the Apache plugin
|
||||
except le_errors.Error as error:
|
||||
logger.error("Plugin failed to deploy ceritificate for %s:", domain)
|
||||
logger.error("Plugin failed to deploy certificate for %s:", domain)
|
||||
logger.exception(error)
|
||||
return False
|
||||
|
||||
|
|
@ -202,7 +202,7 @@ def test_enhancements(plugin, domains):
|
|||
success = False
|
||||
|
||||
if success:
|
||||
logger.info("Enhancments test succeeded")
|
||||
logger.info("Enhancements test succeeded")
|
||||
|
||||
return success
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import socket
|
|||
import requests
|
||||
import zope.interface
|
||||
|
||||
import six
|
||||
|
||||
from acme import crypto_util
|
||||
from acme import errors as acme_errors
|
||||
from certbot import interfaces
|
||||
|
|
@ -19,7 +21,14 @@ class Validator(object):
|
|||
|
||||
def certificate(self, cert, name, alt_host=None, port=443):
|
||||
"""Verifies the certificate presented at name is cert"""
|
||||
host = alt_host if alt_host else socket.gethostbyname(name)
|
||||
if alt_host is None:
|
||||
host = socket.gethostbyname(name)
|
||||
elif isinstance(alt_host, six.binary_type):
|
||||
host = alt_host
|
||||
else:
|
||||
host = alt_host.encode()
|
||||
name = name if isinstance(name, six.binary_type) else name.encode()
|
||||
|
||||
try:
|
||||
presented_cert = crypto_util.probe_sni(name, host, port)
|
||||
except acme_errors.Error as error:
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ http {
|
|||
## Define a zone for limiting the number of simultaneous
|
||||
## connections nginx accepts. 1m means 32000 simultaneous
|
||||
## sessions. We need to define for each server the limit_conn
|
||||
## value refering to this or other zones.
|
||||
## value referring to this or other zones.
|
||||
## ** This syntax requires nginx version >=
|
||||
## ** 1.1.8. Cf. http://nginx.org/en/CHANGES. If using an older
|
||||
## ** version then use the limit_zone directive below
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ charset_map windows-1251 utf-8 {
|
|||
AA D084 ; # capital Ukrainian YE
|
||||
AB C2AB ; # left-pointing double angle quotation mark
|
||||
AC C2AC ; # not sign
|
||||
AD C2AD ; # soft hypen
|
||||
AD C2AD ; # soft hyphen
|
||||
AE C2AE ; # (R)
|
||||
AF D087 ; # capital Ukrainian YI
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ http {
|
|||
keepalive_timeout 60;
|
||||
|
||||
# http-redirects to https; even if using of hsts;
|
||||
# usefull if users are typing your server-name w/out https://
|
||||
# useful if users are typing your server-name w/out https://
|
||||
|
||||
|
||||
# logjam and a good idea anyway
|
||||
|
|
@ -98,7 +98,7 @@ http {
|
|||
#ssl_ciphers ALL:!ADH:!EXP:!LOW:!RC2:!3DES:!SEED:!RC4:+HIGH:+MEDIUM;
|
||||
|
||||
#
|
||||
# suggestions by mozilla-server-team - good compatibility, pfs, preferrable ciphers
|
||||
# suggestions by mozilla-server-team - good compatibility, pfs, preferable ciphers
|
||||
#
|
||||
# modern ciphers
|
||||
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK';
|
||||
|
|
@ -106,7 +106,7 @@ http {
|
|||
# intermediate ciphers
|
||||
#ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
|
||||
|
||||
# old ciphers (would need SSLv3, but is not recommende as of oct 2014
|
||||
# old ciphers (would need SSLv3, but is not recommended as of oct 2014
|
||||
#ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
|
||||
|
||||
# logjam / cipher suggested from weakdh.org
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ server {
|
|||
location ~* (index\.php|upload\.php|connector\.php|dl\.php|ut\.php|lt\.php|download\.php)$ {
|
||||
fastcgi_split_path_info ^(.|\.php)(/.+)$;
|
||||
|
||||
include /etc/nginx/fastcgi_params.conf; #standar fastcgi config file
|
||||
include /etc/nginx/fastcgi_params.conf; #standard fastcgi config file
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_intercept_errors on;
|
||||
fastcgi_pass 127.0.0.1:9000;
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ from setuptools import setup
|
|||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.10.0.dev0'
|
||||
version = '0.12.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
'certbot',
|
||||
'certbot-apache',
|
||||
'six',
|
||||
'requests',
|
||||
'zope.interface',
|
||||
]
|
||||
|
|
|
|||
|
|
@ -191,11 +191,9 @@ class NginxConfigurator(common.Plugin):
|
|||
vhost.filep, vhost.names)
|
||||
except errors.MisconfigurationError as error:
|
||||
logger.debug(error)
|
||||
logger.warning(
|
||||
"Cannot find a cert or key directive in %s for %s. "
|
||||
"VirtualHost was not modified.", vhost.filep, vhost.names)
|
||||
# Presumably break here so that the virtualhost is not modified
|
||||
return False
|
||||
raise errors.PluginError("Cannot find a cert or key directive in {0} for {1}. "
|
||||
"VirtualHost was not modified.".format(vhost.filep, vhost.names))
|
||||
|
||||
self.save_notes += ("Changed vhost at %s with addresses of %s\n" %
|
||||
(vhost.filep,
|
||||
|
|
@ -405,25 +403,7 @@ class NginxConfigurator(common.Plugin):
|
|||
except (socket.error, socket.herror, socket.timeout):
|
||||
continue
|
||||
|
||||
return self._get_filtered_names(all_names)
|
||||
|
||||
def _get_filtered_names(self, all_names):
|
||||
"""Removes names that aren't considered valid by Let's Encrypt.
|
||||
|
||||
:param set all_names: all names found in the Nginx configuration
|
||||
|
||||
:returns: all found names that are considered valid by LE
|
||||
:rtype: set
|
||||
|
||||
"""
|
||||
filtered_names = set()
|
||||
for name in all_names:
|
||||
try:
|
||||
filtered_names.add(util.enforce_le_validity(name))
|
||||
except errors.ConfigurationError as error:
|
||||
logger.debug('Not suggesting name "%s"', name)
|
||||
logger.debug(error)
|
||||
return filtered_names
|
||||
return util.get_filtered_names(all_names)
|
||||
|
||||
def _get_snakeoil_paths(self):
|
||||
# TODO: generate only once
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class RawNginxParser(object):
|
|||
modifier = Literal("=") | Literal("~*") | Literal("~") | Literal("^~")
|
||||
|
||||
# rules
|
||||
comment = space + Literal('#') + restOfLine()
|
||||
comment = space + Literal('#') + restOfLine
|
||||
|
||||
assignment = space + key + Optional(space + value, default=None) + semicolon
|
||||
location_statement = space + Optional(modifier) + Optional(space + location + space)
|
||||
|
|
@ -52,10 +52,10 @@ class RawNginxParser(object):
|
|||
|
||||
map_statement = space + Literal("map") + space + nonspace + space + dollar_var + space
|
||||
# This is NOT an accurate way to parse nginx map entries; it's almost
|
||||
# certianly too permissive and may be wrong in other ways, but it should
|
||||
# certainly too permissive and may be wrong in other ways, but it should
|
||||
# preserve things correctly in mmmmost or all cases.
|
||||
#
|
||||
# - I can neither prove nor disprove that it is corect wrt all escaped
|
||||
# - I can neither prove nor disprove that it is correct wrt all escaped
|
||||
# semicolon situations
|
||||
# Addresses https://github.com/fatiherikli/nginxparser/issues/19
|
||||
map_pattern = Regex(r'".*"') | Regex(r"'.*'") | nonspace
|
||||
|
|
@ -143,7 +143,7 @@ class RawNginxDumper(object):
|
|||
def loads(source):
|
||||
"""Parses from a string.
|
||||
|
||||
:param str souce: The string to parse
|
||||
:param str source: The string to parse
|
||||
:returns: The parsed tree
|
||||
:rtype: list
|
||||
|
||||
|
|
|
|||
|
|
@ -158,6 +158,18 @@ class NginxConfiguratorTest(util.NginxTest):
|
|||
"example/chain.pem",
|
||||
None)
|
||||
|
||||
@mock.patch('certbot_nginx.parser.NginxParser.add_server_directives')
|
||||
def test_deploy_cert_raise_on_add_error(self, mock_add_server_directives):
|
||||
mock_add_server_directives.side_effect = errors.MisconfigurationError()
|
||||
self.assertRaises(
|
||||
errors.PluginError,
|
||||
self.config.deploy_cert,
|
||||
"migration.com",
|
||||
"example/cert.pem",
|
||||
"example/key.pem",
|
||||
"example/chain.pem",
|
||||
"example/fullchain.pem")
|
||||
|
||||
def test_deploy_cert(self):
|
||||
server_conf = self.config.parser.abs_path('server.conf')
|
||||
nginx_conf = self.config.parser.abs_path('nginx.conf')
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ MainRule "rx:%[2|3]." "msg:double encoding !" "mz:ARGS|URL|BODY|$HEADERS_VAR:Co
|
|||
####################################
|
||||
MainRule "str:&#" "msg: utf7/8 encoding" "mz:ARGS|BODY|URL|$HEADERS_VAR:Cookie" "s:$EVADE:4" id:1400;
|
||||
MainRule "str:%U" "msg: M$ encoding" "mz:ARGS|BODY|URL|$HEADERS_VAR:Cookie" "s:$EVADE:4" id:1401;
|
||||
MainRule negative "rx:multipart/form-data|application/x-www-form-urlencoded" "msg:Content is neither mulipart/x-www-form.." "mz:$HEADERS_VAR:Content-type" "s:$EVADE:4" id:1402;
|
||||
MainRule negative "rx:multipart/form-data|application/x-www-form-urlencoded" "msg:Content is neither multipart/x-www-form.." "mz:$HEADERS_VAR:Content-type" "s:$EVADE:4" id:1402;
|
||||
|
||||
#############################
|
||||
## File uploads: 1500-1600 ##
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ charset_map windows-1251 utf-8 {
|
|||
AA D084; # capital Ukrainian YE
|
||||
AB C2AB; # left-pointing double angle quotation mark
|
||||
AC C2AC; # not sign
|
||||
AD C2AD; # soft hypen
|
||||
AD C2AD; # soft hyphen
|
||||
AE C2AE; # (R)
|
||||
AF D087; # capital Ukrainian YI
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from setuptools import setup
|
|||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.10.0.dev0'
|
||||
version = '0.12.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Certbot client."""
|
||||
|
||||
# version number like 1.2.3a0, must have at least 2 parts, like 1.2
|
||||
__version__ = '0.10.0.dev0'
|
||||
__version__ = '0.12.0.dev0'
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import datetime
|
|||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
|
|
@ -81,7 +82,7 @@ class Account(object): # pylint: disable=too-few-public-methods
|
|||
self.meta == other.meta)
|
||||
|
||||
|
||||
def report_new_account(acc, config):
|
||||
def report_new_account(config):
|
||||
"""Informs the user about their new ACME account."""
|
||||
reporter = zope.component.queryUtility(interfaces.IReporter)
|
||||
if reporter is None:
|
||||
|
|
@ -95,12 +96,6 @@ def report_new_account(acc, config):
|
|||
config.config_dir),
|
||||
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.MEDIUM_PRIORITY)
|
||||
|
||||
|
||||
class AccountMemoryStorage(interfaces.AccountStorage):
|
||||
"""In-memory account strage."""
|
||||
|
|
@ -197,6 +192,18 @@ class AccountFileStorage(interfaces.AccountStorage):
|
|||
"""
|
||||
self._save(account, regr_only=True)
|
||||
|
||||
def delete(self, account_id):
|
||||
"""Delete registration info from disk
|
||||
|
||||
:param account_id: id of account which should be deleted
|
||||
|
||||
"""
|
||||
account_dir_path = self._account_dir_path(account_id)
|
||||
if not os.path.isdir(account_dir_path):
|
||||
raise errors.AccountNotFound(
|
||||
"Account at %s does not exist" % account_dir_path)
|
||||
shutil.rmtree(account_dir_path)
|
||||
|
||||
def _save(self, account, regr_only):
|
||||
account_dir_path = self._account_dir_path(account.id)
|
||||
util.make_or_verify_dir(account_dir_path, 0o700, os.geteuid(),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""Client annotated ACME challenges.
|
||||
|
||||
Please use names such as ``achall`` to distiguish from variables "of type"
|
||||
Please use names such as ``achall`` to distinguish from variables "of type"
|
||||
:class:`acme.challenges.Challenge` (denoted by ``chall``)
|
||||
and :class:`.ChallengeBody` (denoted by ``challb``)::
|
||||
|
||||
|
|
|
|||
|
|
@ -34,8 +34,7 @@ class AuthHandler(object):
|
|||
:ivar list achalls: DV challenges in the form of
|
||||
:class:`certbot.achallenges.AnnotatedChallenge`
|
||||
:ivar list pref_challs: sorted user specified preferred challenges
|
||||
in the form of subclasses of :class:`acme.challenges.Challenge`
|
||||
with the most preferred challenge listed first
|
||||
type strings with the most preferred challenge listed first
|
||||
|
||||
"""
|
||||
def __init__(self, auth, acme, account, pref_challs):
|
||||
|
|
@ -64,8 +63,7 @@ class AuthHandler(object):
|
|||
|
||||
"""
|
||||
for domain in domains:
|
||||
self.authzr[domain] = self.acme.request_domain_challenges(
|
||||
domain, self.account.regr.new_authzr_uri)
|
||||
self.authzr[domain] = self.acme.request_domain_challenges(domain)
|
||||
|
||||
self._choose_challenges(domains)
|
||||
|
||||
|
|
@ -252,8 +250,10 @@ class AuthHandler(object):
|
|||
# Make sure to make a copy...
|
||||
plugin_pref = self.auth.get_chall_pref(domain)
|
||||
if self.pref_challs:
|
||||
chall_prefs.extend(pref for pref in self.pref_challs
|
||||
if pref in plugin_pref)
|
||||
plugin_pref_types = set(chall.typ for chall in plugin_pref)
|
||||
for typ in self.pref_challs:
|
||||
if typ in plugin_pref_types:
|
||||
chall_prefs.append(challenges.Challenge.TYPES[typ])
|
||||
if chall_prefs:
|
||||
return chall_prefs
|
||||
raise errors.AuthorizationError(
|
||||
|
|
|
|||
|
|
@ -95,27 +95,23 @@ def delete(config):
|
|||
# Public Helpers
|
||||
###################
|
||||
|
||||
def lineage_for_certname(config, certname):
|
||||
def lineage_for_certname(cli_config, certname):
|
||||
"""Find a lineage object with name certname."""
|
||||
def update_cert_for_name_match(candidate_lineage, rv):
|
||||
"""Return cert if it has name certname, else return rv
|
||||
"""
|
||||
matching_lineage_name_cert = rv
|
||||
if candidate_lineage.lineagename == certname:
|
||||
matching_lineage_name_cert = candidate_lineage
|
||||
return matching_lineage_name_cert
|
||||
return _search_lineages(config, update_cert_for_name_match, None)
|
||||
configs_dir = cli_config.renewal_configs_dir
|
||||
# Verify the directory is there
|
||||
util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid())
|
||||
renewal_file = storage.renewal_file_for_certname(cli_config, certname)
|
||||
try:
|
||||
return storage.RenewableCert(renewal_file, cli_config)
|
||||
except (errors.CertStorageError, IOError):
|
||||
logger.debug("Renewal conf file %s is broken.", renewal_file)
|
||||
logger.debug("Traceback was:\n%s", traceback.format_exc())
|
||||
return None
|
||||
|
||||
def domains_for_certname(config, certname):
|
||||
"""Find the domains in the cert with name certname."""
|
||||
def update_domains_for_name_match(candidate_lineage, rv):
|
||||
"""Return domains if certname matches, else return rv
|
||||
"""
|
||||
matching_domains = rv
|
||||
if candidate_lineage.lineagename == certname:
|
||||
matching_domains = candidate_lineage.names()
|
||||
return matching_domains
|
||||
return _search_lineages(config, update_domains_for_name_match, None)
|
||||
lineage = lineage_for_certname(config, certname)
|
||||
return lineage.names() if lineage else None
|
||||
|
||||
def find_duplicative_certs(config, domains):
|
||||
"""Find existing certs that duplicate the request."""
|
||||
|
|
@ -156,7 +152,8 @@ def _get_certname(config, verb):
|
|||
if not choices:
|
||||
raise errors.Error("No existing certificates found.")
|
||||
code, index = disp.menu("Which certificate would you like to {0}?".format(verb),
|
||||
choices, ok_label="Select", flag="--cert-name")
|
||||
choices, ok_label="Select", flag="--cert-name",
|
||||
force_interactive=True)
|
||||
if code != display_util.OK or not index in range(0, len(choices)):
|
||||
raise errors.Error("User ended interaction.")
|
||||
certname = choices[index]
|
||||
|
|
|
|||
151
certbot/cli.py
151
certbot/cli.py
|
|
@ -1,4 +1,5 @@
|
|||
"""Certbot command line argument & config processing."""
|
||||
# pylint: disable=too-many-lines
|
||||
from __future__ import print_function
|
||||
import argparse
|
||||
import copy
|
||||
|
|
@ -18,7 +19,6 @@ import certbot
|
|||
from certbot import constants
|
||||
from certbot import crypto_util
|
||||
from certbot import errors
|
||||
from certbot import hooks
|
||||
from certbot import interfaces
|
||||
from certbot import util
|
||||
|
||||
|
|
@ -81,7 +81,6 @@ obtain, install, and renew certificates:
|
|||
manage certificates:
|
||||
certificates Display information about certs you have from Certbot
|
||||
revoke Revoke a certificate (supply --cert-path)
|
||||
rename Rename a certificate
|
||||
delete Delete a certificate
|
||||
|
||||
manage your account with Let's Encrypt:
|
||||
|
|
@ -151,7 +150,7 @@ def possible_deprecation_warning(config):
|
|||
if cli_command != LEAUTO:
|
||||
return
|
||||
if config.no_self_upgrade:
|
||||
# users setting --no-self-upgrade might be hanging on a clent version like 0.3.0
|
||||
# users setting --no-self-upgrade might be hanging on a client version like 0.3.0
|
||||
# or 0.5.0 which is the new script, but doesn't set CERTBOT_AUTO; they don't
|
||||
# need warnings
|
||||
return
|
||||
|
|
@ -319,7 +318,7 @@ class CustomHelpFormatter(argparse.HelpFormatter):
|
|||
# The attributes here are:
|
||||
# short: a string that will be displayed by "certbot -h commands"
|
||||
# opts: a string that heads the section of flags with which this command is documented,
|
||||
# both for "cerbot -h SUBCOMMAND" and "certbot -h all"
|
||||
# both for "certbot -h SUBCOMMAND" and "certbot -h all"
|
||||
# usage: an optional string that overrides the header of "certbot -h SUBCOMMAND"
|
||||
VERB_HELP = [
|
||||
("run (default)", {
|
||||
|
|
@ -335,7 +334,7 @@ VERB_HELP = [
|
|||
"This command obtains a TLS/SSL certificate without installing it anywhere.")
|
||||
}),
|
||||
("renew", {
|
||||
"short": "Renew all certificates (or one specifed with --cert-name)",
|
||||
"short": "Renew all certificates (or one specified with --cert-name)",
|
||||
"opts": ("The 'renew' subcommand will attempt to renew all"
|
||||
" certificates (or more precisely, certificate lineages) you have"
|
||||
" previously obtained if they are close to expiry, and print a"
|
||||
|
|
@ -361,16 +360,17 @@ VERB_HELP = [
|
|||
}),
|
||||
("revoke", {
|
||||
"short": "Revoke a certificate specified with --cert-path",
|
||||
"opts": "Options for revocation of certs"
|
||||
}),
|
||||
("rename", {
|
||||
"short": "Change a certificate's name (for management purposes)",
|
||||
"opts": "Options for changing certificate names"
|
||||
"opts": "Options for revocation of certs",
|
||||
"usage": "\n\n certbot revoke --cert-path /path/to/fullchain.pem [options]\n\n"
|
||||
}),
|
||||
("register", {
|
||||
"short": "Register for account with Let's Encrypt / other ACME server",
|
||||
"opts": "Options for account registration & modification"
|
||||
}),
|
||||
("unregister", {
|
||||
"short": "Irrevocably deactivate your account",
|
||||
"opts": "Options for account deactivation."
|
||||
}),
|
||||
("install", {
|
||||
"short": "Install an arbitrary cert in a server",
|
||||
"opts": "Options for modifying how a cert is deployed"
|
||||
|
|
@ -388,7 +388,7 @@ VERB_HELP = [
|
|||
"opts": 'Options for for the "plugins" subcommand'
|
||||
}),
|
||||
("update_symlinks", {
|
||||
"short": "Recreate symlinks in your /live/ directory",
|
||||
"short": "Recreate symlinks in your /etc/letsencrypt/live/ directory",
|
||||
"opts": ("Recreates cert and key symlinks in {0}, if you changed them by hand "
|
||||
"or edited a renewal configuration file".format(
|
||||
os.path.join(flag_default("config_dir"), "live")))
|
||||
|
|
@ -411,14 +411,23 @@ class HelpfulArgumentParser(object):
|
|||
|
||||
def __init__(self, args, plugins, detect_defaults=False):
|
||||
from certbot import main
|
||||
self.VERBS = {"auth": main.obtain_cert, "certonly": main.obtain_cert,
|
||||
"config_changes": main.config_changes, "run": main.run,
|
||||
"install": main.install, "plugins": main.plugins_cmd,
|
||||
"register": main.register, "renew": main.renew,
|
||||
"revoke": main.revoke, "rollback": main.rollback,
|
||||
"everything": main.run, "update_symlinks": main.update_symlinks,
|
||||
"certificates": main.certificates, "rename": main.rename,
|
||||
"delete": main.delete}
|
||||
self.VERBS = {
|
||||
"auth": main.certonly,
|
||||
"certonly": main.certonly,
|
||||
"config_changes": main.config_changes,
|
||||
"run": main.run,
|
||||
"install": main.install,
|
||||
"plugins": main.plugins_cmd,
|
||||
"register": main.register,
|
||||
"unregister": main.unregister,
|
||||
"renew": main.renew,
|
||||
"revoke": main.revoke,
|
||||
"rollback": main.rollback,
|
||||
"everything": main.run,
|
||||
"update_symlinks": main.update_symlinks,
|
||||
"certificates": main.certificates,
|
||||
"delete": main.delete,
|
||||
}
|
||||
|
||||
# List of topics for which additional help can be provided
|
||||
HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + list(self.VERBS)
|
||||
|
|
@ -429,6 +438,10 @@ class HelpfulArgumentParser(object):
|
|||
|
||||
self.detect_defaults = detect_defaults
|
||||
self.args = args
|
||||
|
||||
if self.args and self.args[0] == 'help':
|
||||
self.args[0] = '--help'
|
||||
|
||||
self.determine_verb()
|
||||
help1 = self.prescan_for_flag("-h", self.help_topics)
|
||||
help2 = self.prescan_for_flag("--help", self.help_topics)
|
||||
|
|
@ -483,7 +496,7 @@ class HelpfulArgumentParser(object):
|
|||
if "apache" in plugins:
|
||||
apache_doc = "--apache Use the Apache plugin for authentication & installation"
|
||||
else:
|
||||
apache_doc = "(the cerbot apache plugin is not installed)"
|
||||
apache_doc = "(the certbot apache plugin is not installed)"
|
||||
|
||||
usage = SHORT_USAGE
|
||||
if help_arg == True:
|
||||
|
|
@ -543,9 +556,6 @@ class HelpfulArgumentParser(object):
|
|||
if parsed_args.must_staple:
|
||||
parsed_args.staple = True
|
||||
|
||||
if parsed_args.validate_hooks:
|
||||
hooks.validate_hooks(parsed_args)
|
||||
|
||||
return parsed_args
|
||||
|
||||
def set_test_server(self, parsed_args):
|
||||
|
|
@ -780,7 +790,7 @@ def _add_all_groups(helpful):
|
|||
helpful.add_group("paths", description="Arguments changing execution paths & servers")
|
||||
helpful.add_group("manage",
|
||||
description="Various subcommands and flags are available for managing your certificates:",
|
||||
verbs=["certificates", "delete", "renew", "revoke", "rename"])
|
||||
verbs=["certificates", "delete", "renew", "revoke", "update_symlinks"])
|
||||
|
||||
# VERBS
|
||||
for verb, docs in VERB_HELP:
|
||||
|
|
@ -833,17 +843,12 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
|
|||
"multiple -d flags or enter a comma separated list of domains "
|
||||
"as a parameter. (default: Ask)")
|
||||
helpful.add(
|
||||
[None, "run", "certonly", "manage", "rename", "delete", "certificates"],
|
||||
[None, "run", "certonly", "manage", "delete", "certificates"],
|
||||
"--cert-name", dest="certname",
|
||||
metavar="CERTNAME", default=None,
|
||||
help="Certificate name to apply. Only one certificate name can be used "
|
||||
"per Certbot run. To see certificate names, run 'certbot certificates'. "
|
||||
"When creating a new certificate, specifies the new certificate's name.")
|
||||
helpful.add(
|
||||
["rename", "manage"],
|
||||
"--updated-cert-name", dest="new_certname",
|
||||
metavar="NEW_CERTNAME", default=None,
|
||||
help="New name for the certificate. Must be a valid filename.")
|
||||
helpful.add(
|
||||
[None, "testing", "renew", "certonly"],
|
||||
"--dry-run", action="store_true", dest="dry_run",
|
||||
|
|
@ -872,7 +877,15 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
|
|||
help="With the register verb, indicates that details associated "
|
||||
"with an existing registration, such as the e-mail address, "
|
||||
"should be updated, rather than registering a new account.")
|
||||
helpful.add(["register", "automation"], "-m", "--email", help=config_help("email"))
|
||||
helpful.add(
|
||||
["register", "unregister", "automation"], "-m", "--email",
|
||||
help=config_help("email"))
|
||||
helpful.add(["register", "automation"], "--eff-email", action="store_true",
|
||||
default=None, dest="eff_email",
|
||||
help="Share your e-mail address with EFF")
|
||||
helpful.add(["register", "automation"], "--no-eff-email", action="store_false",
|
||||
default=None, dest="eff_email",
|
||||
help="Don't share your e-mail address with EFF")
|
||||
helpful.add(
|
||||
["automation", "certonly", "run"],
|
||||
"--keep-until-expiring", "--keep", "--reinstall",
|
||||
|
|
@ -914,7 +927,7 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
|
|||
"automation", "--agree-tos", dest="tos", action="store_true",
|
||||
help="Agree to the ACME Subscriber Agreement (default: Ask)")
|
||||
helpful.add(
|
||||
"automation", "--account", metavar="ACCOUNT_ID",
|
||||
["unregister", "automation"], "--account", metavar="ACCOUNT_ID",
|
||||
help="Account ID to use")
|
||||
helpful.add(
|
||||
"automation", "--duplicate", dest="duplicate", action="store_true",
|
||||
|
|
@ -934,8 +947,9 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
|
|||
help="Silence all output except errors. Useful for automation via cron."
|
||||
" Implies --non-interactive.")
|
||||
# overwrites server, handled in HelpfulArgumentParser.parse_args()
|
||||
helpful.add("testing", "--test-cert", "--staging", action='store_true', dest='staging',
|
||||
help='Use the staging server to obtain test (invalid) certs; equivalent'
|
||||
helpful.add(["testing", "revoke", "run"], "--test-cert", "--staging",
|
||||
action='store_true', dest='staging',
|
||||
help='Use the staging server to obtain or revoke test (invalid) certs; equivalent'
|
||||
' to --server ' + constants.STAGING_URI)
|
||||
helpful.add(
|
||||
"testing", "--debug", action="store_true",
|
||||
|
|
@ -1023,13 +1037,16 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
|
|||
" Intended primarily for renewal, where it can be used to temporarily"
|
||||
" shut down a webserver that might conflict with the standalone"
|
||||
" plugin. This will only be called if a certificate is actually to be"
|
||||
" obtained/renewed.")
|
||||
" obtained/renewed. When renewing several certificates that have"
|
||||
" identical pre-hooks, only the first will be executed.")
|
||||
helpful.add(
|
||||
"renew", "--post-hook",
|
||||
help="Command to be run in a shell after attempting to obtain/renew"
|
||||
" certificates. Can be used to deploy renewed certificates, or to"
|
||||
" restart any servers that were stopped by --pre-hook. This is only"
|
||||
" run if an attempt was made to obtain/renew a certificate.")
|
||||
" run if an attempt was made to obtain/renew a certificate. If"
|
||||
" multiple renewed certificates have identical post-hooks, only"
|
||||
" one will be run.")
|
||||
helpful.add(
|
||||
"renew", "--renew-hook",
|
||||
help="Command to be run in a shell once for each successfully renewed"
|
||||
|
|
@ -1079,6 +1096,11 @@ def _create_subparsers(helpful):
|
|||
"--csr", type=read_file,
|
||||
help="Path to a Certificate Signing Request (CSR) in DER or PEM format."
|
||||
" Currently --csr only works with the 'certonly' subcommand.")
|
||||
helpful.add("revoke",
|
||||
"--reason", dest="reason",
|
||||
choices=CaseInsensitiveList(constants.REVOCATION_REASONS.keys()),
|
||||
action=_EncodeReasonAction, default=0,
|
||||
help="Specify reason for revoking certificate.")
|
||||
helpful.add("rollback",
|
||||
"--checkpoints", type=int, metavar="N",
|
||||
default=flag_default("rollback_checkpoints"),
|
||||
|
|
@ -1095,6 +1117,16 @@ def _create_subparsers(helpful):
|
|||
const=interfaces.IInstaller, help="Limit to installer plugins only.")
|
||||
|
||||
|
||||
class CaseInsensitiveList(list):
|
||||
"""A list that will ignore case when searching.
|
||||
|
||||
This class is passed to the `choices` argument of `argparse.add_arguments`
|
||||
through the `helpful` wrapper. It is necessary due to special handling of
|
||||
command line arguments by `set_by_cli` in which the `type_func` is not applied."""
|
||||
def __contains__(self, element):
|
||||
return super(CaseInsensitiveList, self).__contains__(element.lower())
|
||||
|
||||
|
||||
def _paths_parser(helpful):
|
||||
add = helpful.add
|
||||
verb = helpful.verb
|
||||
|
|
@ -1173,6 +1205,15 @@ def _plugins_parsing(helpful, plugins):
|
|||
helpful.add_plugin_args(plugins)
|
||||
|
||||
|
||||
class _EncodeReasonAction(argparse.Action):
|
||||
"""Action class for parsing revocation reason."""
|
||||
|
||||
def __call__(self, parser, namespace, reason, option_string=None):
|
||||
"""Encodes the reason for certificate revocation."""
|
||||
code = constants.REVOCATION_REASONS[reason.lower()]
|
||||
setattr(namespace, self.dest, code)
|
||||
|
||||
|
||||
class _DomainsAction(argparse.Action):
|
||||
"""Action class for parsing domains."""
|
||||
|
||||
|
|
@ -1208,13 +1249,31 @@ class _PrefChallAction(argparse.Action):
|
|||
"""Action class for parsing preferred challenges."""
|
||||
|
||||
def __call__(self, parser, namespace, pref_challs, option_string=None):
|
||||
aliases = {"dns": "dns-01", "http": "http-01", "tls-sni": "tls-sni-01"}
|
||||
challs = [c.strip() for c in pref_challs.split(",")]
|
||||
challs = [aliases[c] if c in aliases else c for c in challs]
|
||||
unrecognized = ", ".join(name for name in challs
|
||||
if name not in challenges.Challenge.TYPES)
|
||||
if unrecognized:
|
||||
raise argparse.ArgumentTypeError(
|
||||
"Unrecognized challenges: {0}".format(unrecognized))
|
||||
namespace.pref_challs.extend(challenges.Challenge.TYPES[name]
|
||||
for name in challs)
|
||||
try:
|
||||
challs = parse_preferred_challenges(pref_challs.split(","))
|
||||
except errors.Error as error:
|
||||
raise argparse.ArgumentTypeError(str(error))
|
||||
namespace.pref_challs.extend(challs)
|
||||
|
||||
|
||||
def parse_preferred_challenges(pref_challs):
|
||||
"""Translate and validate preferred challenges.
|
||||
|
||||
:param pref_challs: list of preferred challenge types
|
||||
:type pref_challs: `list` of `str`
|
||||
|
||||
:returns: validated list of preferred challenge types
|
||||
:rtype: `list` of `str`
|
||||
|
||||
:raises errors.Error: if pref_challs is invalid
|
||||
|
||||
"""
|
||||
aliases = {"dns": "dns-01", "http": "http-01", "tls-sni": "tls-sni-01"}
|
||||
challs = [c.strip() for c in pref_challs]
|
||||
challs = [aliases.get(c, c) for c in challs]
|
||||
unrecognized = ", ".join(name for name in challs
|
||||
if name not in challenges.Challenge.TYPES)
|
||||
if unrecognized:
|
||||
raise errors.Error(
|
||||
"Unrecognized challenges: {0}".format(unrecognized))
|
||||
return challs
|
||||
|
|
|
|||
|
|
@ -15,15 +15,16 @@ import certbot
|
|||
|
||||
from certbot import account
|
||||
from certbot import auth_handler
|
||||
from certbot import cli
|
||||
from certbot import constants
|
||||
from certbot import crypto_util
|
||||
from certbot import errors
|
||||
from certbot import eff
|
||||
from certbot import error_handler
|
||||
from certbot import errors
|
||||
from certbot import interfaces
|
||||
from certbot import util
|
||||
from certbot import reverter
|
||||
from certbot import storage
|
||||
from certbot import cli
|
||||
from certbot import util
|
||||
|
||||
from certbot.display import ops as display_ops
|
||||
from certbot.display import enhancements
|
||||
|
|
@ -93,7 +94,7 @@ def register(config, account_storage, tos_cb=None):
|
|||
Terms of Service present in the contained
|
||||
`.Registration.terms_of_service` is accepted by the client, and
|
||||
``False`` otherwise. ``tos_cb`` will be called only if the
|
||||
client acction is necessary, i.e. when ``terms_of_service is not
|
||||
client action is necessary, i.e. when ``terms_of_service is not
|
||||
None``. This argument is optional, if not supplied it will
|
||||
default to automatic acceptance!
|
||||
|
||||
|
|
@ -136,9 +137,11 @@ def register(config, account_storage, tos_cb=None):
|
|||
regr = acme.agree_to_tos(regr)
|
||||
|
||||
acc = account.Account(regr, key)
|
||||
account.report_new_account(acc, config)
|
||||
account.report_new_account(config)
|
||||
account_storage.save(acc)
|
||||
|
||||
eff.handle_subscription(config)
|
||||
|
||||
return acc, acme
|
||||
|
||||
|
||||
|
|
@ -172,7 +175,7 @@ def perform_registration(acme, config):
|
|||
|
||||
|
||||
class Client(object):
|
||||
"""ACME protocol client.
|
||||
"""Certbot's client.
|
||||
|
||||
:ivar .IConfig config: Client configuration.
|
||||
:ivar .Account account: Account registered with `register`.
|
||||
|
|
@ -410,7 +413,8 @@ class Client(object):
|
|||
|
||||
chain_path = None if chain_path is None else os.path.abspath(chain_path)
|
||||
|
||||
with error_handler.ErrorHandler(self.installer.recovery_routine):
|
||||
msg = ("Unable to install the certificate")
|
||||
with error_handler.ErrorHandler(self._recovery_routine_with_msg, msg):
|
||||
for dom in domains:
|
||||
self.installer.deploy_cert(
|
||||
domain=dom, cert_path=os.path.abspath(cert_path),
|
||||
|
|
@ -471,7 +475,7 @@ class Client(object):
|
|||
self.installer.restart()
|
||||
|
||||
def apply_enhancement(self, domains, enhancement, options=None):
|
||||
"""Applies an enhacement on all domains.
|
||||
"""Applies an enhancement on all domains.
|
||||
|
||||
:param domains: list of ssl_vhosts
|
||||
:type list of str
|
||||
|
|
@ -527,7 +531,7 @@ class Client(object):
|
|||
self.installer.rollback_checkpoints()
|
||||
self.installer.restart()
|
||||
except:
|
||||
# TODO: suggest letshelp-letsencypt here
|
||||
# TODO: suggest letshelp-letsencrypt here
|
||||
reporter.add_message(
|
||||
"An error occurred and we failed to restore your config and "
|
||||
"restart your server. Please submit a bug report to "
|
||||
|
|
|
|||
|
|
@ -37,6 +37,17 @@ CLI_DEFAULTS = dict(
|
|||
)
|
||||
STAGING_URI = "https://acme-staging.api.letsencrypt.org/directory"
|
||||
|
||||
# The set of reasons for revoking a certificate is defined in RFC 5280 in
|
||||
# section 5.3.1. The reasons that users are allowed to submit are restricted to
|
||||
# those accepted by the ACME server implementation. They are listed in
|
||||
# `letsencrypt.boulder.revocation.reasons.go`.
|
||||
REVOCATION_REASONS = {
|
||||
"unspecified": 0,
|
||||
"keycompromise": 1,
|
||||
"affiliationchanged": 3,
|
||||
"superseded": 4,
|
||||
"cessationofoperation": 5}
|
||||
|
||||
"""Defaults for CLI flags and `.IConfig` attributes."""
|
||||
|
||||
QUIET_LOGGING_LEVEL = logging.WARNING
|
||||
|
|
@ -97,3 +108,6 @@ RENEWAL_CONFIGS_DIR = "renewal"
|
|||
|
||||
FORCE_INTERACTIVE_FLAG = "--force-interactive"
|
||||
"""Flag to disable TTY checking in IDisplay."""
|
||||
|
||||
EFF_SUBSCRIBE_URI = "https://supporters.eff.org/subscribe/certbot"
|
||||
"""EFF URI used to submit the e-mail address of users who opt-in."""
|
||||
|
|
|
|||
|
|
@ -253,14 +253,16 @@ class FileDisplay(object):
|
|||
:rtype: bool
|
||||
|
||||
"""
|
||||
msg = "Invalid IDisplay call for this prompt:\n{0}".format(prompt)
|
||||
if cli_flag:
|
||||
msg += ("\nYou can set an answer to "
|
||||
"this prompt with the {0} flag".format(cli_flag))
|
||||
assert default is not None or force_interactive, msg
|
||||
|
||||
# assert_valid_call(prompt, default, cli_flag, force_interactive)
|
||||
if self._can_interact(force_interactive):
|
||||
return False
|
||||
elif default is None:
|
||||
msg = "Unable to get an answer for the question:\n{0}".format(prompt)
|
||||
if cli_flag:
|
||||
msg += (
|
||||
"\nYou can provide an answer on the "
|
||||
"command line with the {0} flag.".format(cli_flag))
|
||||
raise errors.Error(msg)
|
||||
else:
|
||||
logger.debug(
|
||||
"Falling back to default %s for the prompt:\n%s",
|
||||
|
|
@ -403,6 +405,24 @@ class FileDisplay(object):
|
|||
return OK, selection
|
||||
|
||||
|
||||
def assert_valid_call(prompt, default, cli_flag, force_interactive):
|
||||
"""Verify that provided arguments is a valid IDisplay call.
|
||||
|
||||
:param str prompt: prompt for the user
|
||||
:param default: default answer to prompt
|
||||
:param str cli_flag: command line option for setting an answer
|
||||
to this question
|
||||
:param bool force_interactive: if interactivity is forced by the
|
||||
IDisplay call
|
||||
|
||||
"""
|
||||
msg = "Invalid IDisplay call for this prompt:\n{0}".format(prompt)
|
||||
if cli_flag:
|
||||
msg += ("\nYou can set an answer to "
|
||||
"this prompt with the {0} flag".format(cli_flag))
|
||||
assert default is not None or force_interactive, msg
|
||||
|
||||
|
||||
@zope.interface.implementer(interfaces.IDisplay)
|
||||
class NoninteractiveDisplay(object):
|
||||
"""An iDisplay implementation that never asks for interactive user input"""
|
||||
|
|
@ -438,7 +458,7 @@ class NoninteractiveDisplay(object):
|
|||
line=os.linesep, frame=side_frame, msg=message))
|
||||
|
||||
def menu(self, message, choices, ok_label=None, cancel_label=None,
|
||||
help_label=None, default=None, cli_flag=None, *unused_kwargs):
|
||||
help_label=None, default=None, cli_flag=None, **unused_kwargs):
|
||||
# pylint: disable=unused-argument,too-many-arguments
|
||||
"""Avoid displaying a menu.
|
||||
|
||||
|
|
|
|||
95
certbot/eff.py
Normal file
95
certbot/eff.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
"""Subscribes users to the EFF newsletter."""
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import zope.component
|
||||
|
||||
from certbot import constants
|
||||
from certbot import interfaces
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def handle_subscription(config):
|
||||
"""High level function to take care of EFF newsletter subscriptions.
|
||||
|
||||
The user may be asked if they want to sign up for the newsletter if
|
||||
they have not already specified.
|
||||
|
||||
:param .IConfig config: Client configuration.
|
||||
|
||||
"""
|
||||
if config.email is None:
|
||||
if config.eff_email:
|
||||
_report_failure("you didn't provide an e-mail address")
|
||||
return
|
||||
if config.eff_email is None:
|
||||
config.eff_email = _want_subscription()
|
||||
if config.eff_email:
|
||||
subscribe(config.email)
|
||||
|
||||
|
||||
def _want_subscription():
|
||||
"""Does the user want to be subscribed to the EFF newsletter?
|
||||
|
||||
:returns: True if we should subscribe the user, otherwise, False
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
prompt = (
|
||||
'Would you be willing to share your email address with the '
|
||||
"Electronic Frontier Foundation, a founding partner of the Let's "
|
||||
'Encrypt project and the non-profit organization that develops '
|
||||
"Certbot? We'd like to send you email about EFF and our work to "
|
||||
'encrypt the web, protect its users and defend digital rights.')
|
||||
display = zope.component.getUtility(interfaces.IDisplay)
|
||||
return display.yesno(prompt, default=False)
|
||||
|
||||
|
||||
def subscribe(email):
|
||||
"""Subscribe the user to the EFF mailing list.
|
||||
|
||||
:param str email: the e-mail address to subscribe
|
||||
|
||||
"""
|
||||
url = constants.EFF_SUBSCRIBE_URI
|
||||
data = {'data_type': 'json',
|
||||
'email': email,
|
||||
'form_id': 'eff_supporters_library_subscribe_form'}
|
||||
logger.debug('Sending POST request to %s:\n%s', url, data)
|
||||
_check_response(requests.post(url, data=data))
|
||||
|
||||
|
||||
def _check_response(response):
|
||||
"""Check for errors in the server's response.
|
||||
|
||||
If an error occurred, it will be reported to the user.
|
||||
|
||||
:param requests.Response response: the server's response to the
|
||||
subscription request
|
||||
|
||||
"""
|
||||
logger.debug('Received response:\n%s', response.content)
|
||||
if response.ok:
|
||||
if not response.json()['status']:
|
||||
_report_failure('your e-mail address appears to be invalid')
|
||||
else:
|
||||
_report_failure()
|
||||
|
||||
|
||||
def _report_failure(reason=None):
|
||||
"""Notify the user of failing to sign them up for the newsletter.
|
||||
|
||||
:param reason: a phrase describing what the problem was
|
||||
beginning with a lowercase letter and no closing punctuation
|
||||
:type reason: `str` or `None`
|
||||
|
||||
"""
|
||||
msg = ['We were unable to subscribe you the EFF mailing list']
|
||||
if reason is not None:
|
||||
msg.append(' because ')
|
||||
msg.append(reason)
|
||||
msg.append('. You can try again later by visiting https://act.eff.org.')
|
||||
reporter = zope.component.getUtility(interfaces.IReporter)
|
||||
reporter.add_message(''.join(msg), reporter.LOW_PRIORITY)
|
||||
|
|
@ -118,9 +118,9 @@ class ErrorHandler(object):
|
|||
self.prev_handlers.clear()
|
||||
|
||||
def _signal_handler(self, signum, unused_frame):
|
||||
"""Replacement function for handling recieved signals.
|
||||
"""Replacement function for handling received signals.
|
||||
|
||||
Store the recieved signal. If we are executing the code block in
|
||||
Store the received signal. If we are executing the code block in
|
||||
the body of the context manager, stop by raising signal exit.
|
||||
|
||||
:param int signum: number of current signal
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class HookCommandNotFound(Error):
|
|||
|
||||
|
||||
class SignalExit(Error):
|
||||
"""A Unix signal was recieved while in the ErrorHandler context manager."""
|
||||
"""A Unix signal was received while in the ErrorHandler context manager."""
|
||||
|
||||
|
||||
# Auth Handler Errors
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import os
|
|||
from subprocess import Popen, PIPE
|
||||
|
||||
from certbot import errors
|
||||
from certbot import util
|
||||
|
||||
from certbot.plugins import util as plug_util
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -17,9 +20,20 @@ def validate_hooks(config):
|
|||
validate_hook(config.renew_hook, "renew")
|
||||
|
||||
def _prog(shell_cmd):
|
||||
"""Extract the program run by a shell command"""
|
||||
cmd = _which(shell_cmd)
|
||||
return os.path.basename(cmd) if cmd else None
|
||||
"""Extract the program run by a shell command.
|
||||
|
||||
:param str shell_cmd: command to be executed
|
||||
|
||||
:returns: basename of command or None if the command isn't found
|
||||
:rtype: str or None
|
||||
|
||||
"""
|
||||
if not util.exe_exists(shell_cmd):
|
||||
plug_util.path_surgery(shell_cmd)
|
||||
if not util.exe_exists(shell_cmd):
|
||||
return None
|
||||
return os.path.basename(shell_cmd)
|
||||
|
||||
|
||||
def validate_hook(shell_cmd, hook_name):
|
||||
"""Check that a command provided as a hook is plausibly executable.
|
||||
|
|
@ -36,35 +50,50 @@ def validate_hook(shell_cmd, hook_name):
|
|||
|
||||
def pre_hook(config):
|
||||
"Run pre-hook if it's defined and hasn't been run."
|
||||
if config.pre_hook and not pre_hook.already:
|
||||
logger.info("Running pre-hook command: %s", config.pre_hook)
|
||||
_run_hook(config.pre_hook)
|
||||
pre_hook.already = True
|
||||
cmd = config.pre_hook
|
||||
if cmd and cmd not in pre_hook.already:
|
||||
logger.info("Running pre-hook command: %s", cmd)
|
||||
_run_hook(cmd)
|
||||
pre_hook.already.add(cmd)
|
||||
elif cmd:
|
||||
logger.info("Pre-hook command already run, skipping: %s", cmd)
|
||||
|
||||
pre_hook.already = False
|
||||
pre_hook.already = set()
|
||||
|
||||
def post_hook(config, final=False):
|
||||
|
||||
def post_hook(config):
|
||||
"""Run post hook if defined.
|
||||
|
||||
If the verb is renew, we might have more certs to renew, so we wait until
|
||||
we're called with final=True before actually doing anything.
|
||||
run_saved_post_hooks() is called.
|
||||
"""
|
||||
if config.post_hook:
|
||||
if not pre_hook.already:
|
||||
logger.info("No renewals attempted, so not running post-hook")
|
||||
if config.verb != "renew":
|
||||
logger.warning("Sanity failure in renewal hooks")
|
||||
return
|
||||
if final or config.verb != "renew":
|
||||
logger.info("Running post-hook command: %s", config.post_hook)
|
||||
_run_hook(config.post_hook)
|
||||
|
||||
cmd = config.post_hook
|
||||
# In the "renew" case, we save these up to run at the end
|
||||
if config.verb == "renew":
|
||||
if cmd and cmd not in post_hook.eventually:
|
||||
post_hook.eventually.append(cmd)
|
||||
# certonly / run
|
||||
elif cmd:
|
||||
logger.info("Running post-hook command: %s", cmd)
|
||||
_run_hook(cmd)
|
||||
|
||||
post_hook.eventually = []
|
||||
|
||||
def run_saved_post_hooks():
|
||||
"""Run any post hooks that were saved up in the course of the 'renew' verb"""
|
||||
for cmd in post_hook.eventually:
|
||||
logger.info("Running post-hook command: %s", cmd)
|
||||
_run_hook(cmd)
|
||||
|
||||
|
||||
def renew_hook(config, domains, lineage_path):
|
||||
"Run post-renewal hook if defined."
|
||||
"""Run post-renewal hook if defined."""
|
||||
if config.renew_hook:
|
||||
if not config.dry_run:
|
||||
os.environ["RENEWED_DOMAINS"] = " ".join(domains)
|
||||
os.environ["RENEWED_LINEAGE"] = lineage_path
|
||||
logger.info("Running renew-hook command: %s", config.renew_hook)
|
||||
_run_hook(config.renew_hook)
|
||||
else:
|
||||
logger.warning("Dry run: skipping renewal hook command: %s", config.renew_hook)
|
||||
|
|
@ -93,27 +122,7 @@ def execute(shell_cmd):
|
|||
logger.error('Hook command "%s" returned error code %d',
|
||||
shell_cmd, cmd.returncode)
|
||||
if err:
|
||||
logger.error('Error output from %s:\n%s', _prog(shell_cmd), err)
|
||||
base_cmd = os.path.basename(shell_cmd.split(None, 1)[0])
|
||||
logger.error('Error output from %s:\n%s', base_cmd, err)
|
||||
return (err, out)
|
||||
|
||||
|
||||
def _is_exe(fpath):
|
||||
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
|
||||
|
||||
def _which(program):
|
||||
"""Test if program is in the path."""
|
||||
# Borrowed from:
|
||||
# https://stackoverflow.com/questions/377017/test-if-executable-exists-in-python
|
||||
# XXX May need more porting to handle .exe extensions on Windows
|
||||
|
||||
fpath, _fname = os.path.split(program)
|
||||
if fpath:
|
||||
if _is_exe(program):
|
||||
return program
|
||||
else:
|
||||
for path in os.environ["PATH"].split(os.pathsep):
|
||||
exe_file = os.path.join(path, program)
|
||||
if _is_exe(exe_file):
|
||||
return exe_file
|
||||
|
||||
return None
|
||||
|
|
|
|||
274
certbot/main.py
274
certbot/main.py
|
|
@ -24,6 +24,7 @@ from certbot import crypto_util
|
|||
from certbot import colored_logging
|
||||
from certbot import configuration
|
||||
from certbot import constants
|
||||
from certbot import eff
|
||||
from certbot import errors
|
||||
from certbot import hooks
|
||||
from certbot import interfaces
|
||||
|
|
@ -39,7 +40,7 @@ from certbot.plugins import selection as plug_sel
|
|||
_PERM_ERR_FMT = os.linesep.join((
|
||||
"The following error was encountered:", "{0}",
|
||||
"If running as non-root, set --config-dir, "
|
||||
"--logs-dir, and --work-dir to writeable paths."))
|
||||
"--work-dir, and --logs-dir to writeable paths."))
|
||||
|
||||
USER_CANCELLED = ("User chose to cancel the operation and may "
|
||||
"reinvoke the client.")
|
||||
|
|
@ -48,72 +49,53 @@ USER_CANCELLED = ("User chose to cancel the operation and may "
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _suggest_donation_if_appropriate(config, action):
|
||||
def _suggest_donation_if_appropriate(config):
|
||||
"""Potentially suggest a donation to support Certbot."""
|
||||
if config.staging or config.verb == "renew":
|
||||
assert config.verb != "renew"
|
||||
if config.staging:
|
||||
# --dry-run implies --staging
|
||||
return
|
||||
if action not in ["renew", "newcert"]:
|
||||
return
|
||||
reporter_util = zope.component.getUtility(interfaces.IReporter)
|
||||
msg = ("If you like Certbot, please consider supporting our work by:\n\n"
|
||||
"Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate\n"
|
||||
"Donating to EFF: https://eff.org/donate-le\n\n")
|
||||
reporter_util.add_message(msg, reporter_util.LOW_PRIORITY)
|
||||
|
||||
|
||||
|
||||
def _report_successful_dry_run(config):
|
||||
reporter_util = zope.component.getUtility(interfaces.IReporter)
|
||||
if config.verb != "renew":
|
||||
reporter_util.add_message("The dry run was successful.",
|
||||
reporter_util.HIGH_PRIORITY, on_crash=False)
|
||||
assert config.verb != "renew"
|
||||
reporter_util.add_message("The dry run was successful.",
|
||||
reporter_util.HIGH_PRIORITY, on_crash=False)
|
||||
|
||||
|
||||
def _auth_from_available(le_client, config, domains=None, certname=None, lineage=None):
|
||||
def _get_and_save_cert(le_client, config, domains=None, certname=None, lineage=None):
|
||||
"""Authenticate and enroll certificate.
|
||||
|
||||
This method finds the relevant lineage, figures out what to do with it,
|
||||
then performs that action. Includes calls to hooks, various reports,
|
||||
checks, and requests for user input.
|
||||
|
||||
:returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_domains_and_certname
|
||||
action can be: "newcert" | "renew" | "reinstall"
|
||||
:returns: the issued certificate or `None` if doing a dry run
|
||||
:rtype: `storage.RenewableCert` or `None`
|
||||
"""
|
||||
# If lineage is specified, use that one instead of looking around for
|
||||
# a matching one.
|
||||
if lineage is None:
|
||||
# This will find a relevant matching lineage that exists
|
||||
action, lineage = _find_lineage_for_domains_and_certname(config, domains, certname)
|
||||
else:
|
||||
# Renewal, where we already know the specific lineage we're
|
||||
# interested in
|
||||
action = "renew"
|
||||
|
||||
if action == "reinstall":
|
||||
# The lineage already exists; allow the caller to try installing
|
||||
# it without getting a new certificate at all.
|
||||
logger.info("Keeping the existing certificate")
|
||||
return "reinstall", lineage
|
||||
|
||||
hooks.pre_hook(config)
|
||||
try:
|
||||
if action == "renew":
|
||||
if lineage is not None:
|
||||
# Renewal, where we already know the specific lineage we're
|
||||
# interested in
|
||||
logger.info("Renewing an existing certificate")
|
||||
renewal.renew_cert(config, le_client, lineage)
|
||||
elif action == "newcert":
|
||||
renewal.renew_cert(config, domains, le_client, lineage)
|
||||
else:
|
||||
# TREAT AS NEW REQUEST
|
||||
assert domains is not None
|
||||
logger.info("Obtaining a new certificate")
|
||||
lineage = le_client.obtain_and_enroll_certificate(domains, certname)
|
||||
if lineage is False:
|
||||
raise errors.Error("Certificate could not be obtained")
|
||||
finally:
|
||||
hooks.post_hook(config, final=False)
|
||||
hooks.post_hook(config)
|
||||
|
||||
if not config.dry_run and not config.verb == "renew":
|
||||
_report_new_cert(config, lineage.cert, lineage.fullchain)
|
||||
|
||||
return action, lineage
|
||||
return lineage
|
||||
|
||||
|
||||
def _handle_subset_cert_request(config, domains, cert):
|
||||
|
|
@ -235,6 +217,18 @@ def _find_lineage_for_domains(config, domains):
|
|||
elif subset_names_cert is not None:
|
||||
return _handle_subset_cert_request(config, domains, subset_names_cert)
|
||||
|
||||
def _find_cert(config, domains, certname):
|
||||
"""Finds an existing certificate object given domains and/or a certificate name.
|
||||
|
||||
:returns: Two-element tuple of a boolean that indicates if this function should be
|
||||
followed by a call to fetch a certificate from the server, and either a
|
||||
RenewableCert instance or None.
|
||||
"""
|
||||
action, lineage = _find_lineage_for_domains_and_certname(config, domains, certname)
|
||||
if action == "reinstall":
|
||||
logger.info("Keeping the existing certificate")
|
||||
return (action != "reinstall"), lineage
|
||||
|
||||
def _find_lineage_for_domains_and_certname(config, domains, certname):
|
||||
"""Find appropriate lineage based on given domains and/or certname.
|
||||
|
||||
|
|
@ -313,26 +307,25 @@ def _report_new_cert(config, cert_path, fullchain_path):
|
|||
:param str fullchain_path: path to full chain
|
||||
|
||||
"""
|
||||
if config.dry_run:
|
||||
_report_successful_dry_run(config)
|
||||
return
|
||||
|
||||
assert cert_path and fullchain_path, "No certificates saved to report."
|
||||
|
||||
expiry = crypto_util.notAfter(cert_path).date()
|
||||
reporter_util = zope.component.getUtility(interfaces.IReporter)
|
||||
if fullchain_path:
|
||||
# Print the path to fullchain.pem because that's what modern webservers
|
||||
# (Nginx and Apache2.4) will want.
|
||||
and_chain = "and chain have"
|
||||
path = fullchain_path
|
||||
else:
|
||||
# Unless we're in .csr mode and there really isn't one
|
||||
and_chain = "has "
|
||||
path = cert_path
|
||||
# Print the path to fullchain.pem because that's what modern webservers
|
||||
# (Nginx and Apache2.4) will want.
|
||||
|
||||
verbswitch = ' with the "certonly" option' if config.verb == "run" else ""
|
||||
# XXX Perhaps one day we could detect the presence of known old webservers
|
||||
# and say something more informative here.
|
||||
msg = ('Congratulations! Your certificate {0} been saved at {1}.'
|
||||
' Your cert will expire on {2}. To obtain a new or tweaked version of this '
|
||||
'certificate in the future, simply run {3} again{4}. '
|
||||
'To non-interactively renew *all* of your certificates, run "{3} renew"'
|
||||
.format(and_chain, path, expiry, cli.cli_command, verbswitch))
|
||||
msg = ('Congratulations! Your certificate and chain have been saved at {0}.'
|
||||
' Your cert will expire on {1}. To obtain a new or tweaked version of this '
|
||||
'certificate in the future, simply run {2} again{3}. '
|
||||
'To non-interactively renew *all* of your certificates, run "{2} renew"'
|
||||
.format(fullchain_path, expiry, cli.cli_command, verbswitch))
|
||||
reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY)
|
||||
|
||||
|
||||
|
|
@ -406,6 +399,35 @@ def _init_le_client(config, authenticator, installer):
|
|||
return client.Client(config, acc, authenticator, installer, acme=acme)
|
||||
|
||||
|
||||
def unregister(config, unused_plugins):
|
||||
"""Deactivate account on server"""
|
||||
account_storage = account.AccountFileStorage(config)
|
||||
accounts = account_storage.find_all()
|
||||
reporter_util = zope.component.getUtility(interfaces.IReporter)
|
||||
|
||||
if not accounts:
|
||||
return "Could not find existing account to deactivate."
|
||||
yesno = zope.component.getUtility(interfaces.IDisplay).yesno
|
||||
prompt = ("Are you sure you would like to irrevocably deactivate "
|
||||
"your account?")
|
||||
wants_deactivate = yesno(prompt, yes_label='Deactivate', no_label='Abort',
|
||||
default=True)
|
||||
|
||||
if not wants_deactivate:
|
||||
return "Deactivation aborted."
|
||||
|
||||
acc, acme = _determine_account(config)
|
||||
acme_client = client.Client(config, acc, None, None, acme=acme)
|
||||
|
||||
# delete on boulder
|
||||
acme_client.acme.deactivate_registration(acc.regr)
|
||||
account_files = account.AccountFileStorage(config)
|
||||
# delete local account files
|
||||
account_files.delete(config.account)
|
||||
|
||||
reporter_util.add_message("Account deactivated.", reporter_util.MEDIUM_PRIORITY)
|
||||
|
||||
|
||||
def register(config, unused_plugins):
|
||||
"""Create or modify accounts on the server."""
|
||||
|
||||
|
|
@ -413,6 +435,8 @@ def register(config, unused_plugins):
|
|||
# exist or not.
|
||||
account_storage = account.AccountFileStorage(config)
|
||||
accounts = account_storage.find_all()
|
||||
reporter_util = zope.component.getUtility(interfaces.IReporter)
|
||||
add_msg = lambda m: reporter_util.add_message(m, reporter_util.MEDIUM_PRIORITY)
|
||||
|
||||
# registering a new account
|
||||
if not config.update_registration:
|
||||
|
|
@ -443,10 +467,16 @@ def register(config, unused_plugins):
|
|||
acc.regr = acme_client.acme.update_registration(acc.regr.update(
|
||||
body=acc.regr.body.update(contact=('mailto:' + config.email,))))
|
||||
account_storage.save_regr(acc)
|
||||
reporter_util = zope.component.getUtility(interfaces.IReporter)
|
||||
msg = "Your e-mail address was updated to {0}.".format(config.email)
|
||||
reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY)
|
||||
eff.handle_subscription(config)
|
||||
add_msg("Your e-mail address was updated to {0}.".format(config.email))
|
||||
|
||||
def _install_cert(config, le_client, domains, lineage=None):
|
||||
path_provider = lineage if lineage else config
|
||||
assert path_provider.cert_path is not None
|
||||
|
||||
le_client.deploy_certificate(domains, path_provider.key_path,
|
||||
path_provider.cert_path, path_provider.chain_path, path_provider.fullchain_path)
|
||||
le_client.enhance_config(domains, path_provider.chain_path)
|
||||
|
||||
def install(config, plugins):
|
||||
"""Install a previously obtained cert in a server."""
|
||||
|
|
@ -461,11 +491,7 @@ def install(config, plugins):
|
|||
|
||||
domains, _ = _find_domains_or_certname(config, installer)
|
||||
le_client = _init_le_client(config, authenticator=None, installer=installer)
|
||||
assert config.cert_path is not None # required=True in the subparser
|
||||
le_client.deploy_certificate(
|
||||
domains, config.key_path, config.cert_path, config.chain_path,
|
||||
config.fullchain_path)
|
||||
le_client.enhance_config(domains, config.chain_path)
|
||||
_install_cert(config, le_client, domains)
|
||||
|
||||
|
||||
def plugins_cmd(config, plugins): # TODO: Use IDisplay rather than print
|
||||
|
|
@ -550,8 +576,10 @@ def revoke(config, unused_plugins): # TODO: coop with renewal config
|
|||
key = acc.key
|
||||
acme = client.acme_from_config_key(config, key)
|
||||
cert = crypto_util.pyopenssl_load_certificate(config.cert_path[1])[0]
|
||||
logger.debug("Reason code for revocation: %s", config.reason)
|
||||
|
||||
try:
|
||||
acme.revoke(jose.ComparableX509(cert))
|
||||
acme.revoke(jose.ComparableX509(cert), config.reason)
|
||||
except acme_errors.ClientError as e:
|
||||
return e.message
|
||||
|
||||
|
|
@ -567,28 +595,32 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals
|
|||
except errors.PluginSelectionError as e:
|
||||
return e.message
|
||||
|
||||
domains, certname = _find_domains_or_certname(config, installer)
|
||||
|
||||
# TODO: Handle errors from _init_le_client?
|
||||
le_client = _init_le_client(config, authenticator, installer)
|
||||
|
||||
action, lineage = _auth_from_available(le_client, config, domains, certname)
|
||||
domains, certname = _find_domains_or_certname(config, installer)
|
||||
should_get_cert, lineage = _find_cert(config, domains, certname)
|
||||
|
||||
le_client.deploy_certificate(
|
||||
domains, lineage.privkey, lineage.cert,
|
||||
lineage.chain, lineage.fullchain)
|
||||
new_lineage = lineage
|
||||
if should_get_cert:
|
||||
new_lineage = _get_and_save_cert(le_client, config, domains,
|
||||
certname, lineage)
|
||||
|
||||
le_client.enhance_config(domains, lineage.chain)
|
||||
cert_path = new_lineage.cert_path if new_lineage else None
|
||||
fullchain_path = new_lineage.fullchain_path if new_lineage else None
|
||||
_report_new_cert(config, cert_path, fullchain_path)
|
||||
|
||||
if action in ("newcert", "reinstall",):
|
||||
_install_cert(config, le_client, domains, new_lineage)
|
||||
|
||||
if lineage is None or not should_get_cert:
|
||||
display_ops.success_installation(domains)
|
||||
else:
|
||||
display_ops.success_renewal(domains)
|
||||
|
||||
_suggest_donation_if_appropriate(config, action)
|
||||
_suggest_donation_if_appropriate(config)
|
||||
|
||||
|
||||
def _csr_obtain_cert(config, le_client):
|
||||
def _csr_get_and_save_cert(config, le_client):
|
||||
"""Obtain a cert using a user-supplied CSR
|
||||
|
||||
This works differently in the CSR case (for now) because we don't
|
||||
|
|
@ -600,16 +632,39 @@ def _csr_obtain_cert(config, le_client):
|
|||
if config.dry_run:
|
||||
logger.debug(
|
||||
"Dry run: skipping saving certificate to %s", config.cert_path)
|
||||
else:
|
||||
cert_path, _, cert_fullchain = le_client.save_certificate(
|
||||
return None, None
|
||||
cert_path, _, fullchain_path = le_client.save_certificate(
|
||||
certr, chain, config.cert_path, config.chain_path, config.fullchain_path)
|
||||
_report_new_cert(config, cert_path, cert_fullchain)
|
||||
return cert_path, fullchain_path
|
||||
|
||||
def obtain_cert(config, plugins, lineage=None):
|
||||
def renew_cert(config, plugins, lineage):
|
||||
"""Renew & save an existing cert. Do not install it."""
|
||||
try:
|
||||
# installers are used in auth mode to determine domain names
|
||||
installer, auth = plug_sel.choose_configurator_plugins(config, plugins, "certonly")
|
||||
except errors.PluginSelectionError as e:
|
||||
logger.info("Could not choose appropriate plugin: %s", e)
|
||||
raise
|
||||
le_client = _init_le_client(config, auth, installer)
|
||||
|
||||
_get_and_save_cert(le_client, config, lineage=lineage)
|
||||
|
||||
notify = zope.component.getUtility(interfaces.IDisplay).notification
|
||||
if installer is None:
|
||||
notify("new certificate deployed without reload, fullchain is {0}".format(
|
||||
lineage.fullchain), pause=False)
|
||||
else:
|
||||
# In case of a renewal, reload server to pick up new certificate.
|
||||
# In principle we could have a configuration option to inhibit this
|
||||
# from happening.
|
||||
installer.restart()
|
||||
notify("new certificate deployed with reload of {0} server; fullchain is {1}".format(
|
||||
config.installer, lineage.fullchain), pause=False)
|
||||
|
||||
def certonly(config, plugins):
|
||||
"""Authenticate & obtain cert, but do not install it.
|
||||
|
||||
This implements the 'certonly' subcommand, and is also called from within the
|
||||
'renew' command."""
|
||||
This implements the 'certonly' subcommand."""
|
||||
|
||||
# SETUP: Select plugins and construct a client instance
|
||||
try:
|
||||
|
|
@ -620,41 +675,33 @@ def obtain_cert(config, plugins, lineage=None):
|
|||
raise
|
||||
le_client = _init_le_client(config, auth, installer)
|
||||
|
||||
# SHOWTIME: Possibly obtain/renew a cert, and set action to renew | newcert | reinstall
|
||||
if config.csr is None: # the common case
|
||||
domains, certname = _find_domains_or_certname(config, installer)
|
||||
action, _ = _auth_from_available(le_client, config, domains, certname, lineage)
|
||||
else:
|
||||
assert lineage is None, "Did not expect a CSR with a RenewableCert"
|
||||
_csr_obtain_cert(config, le_client)
|
||||
action = "newcert"
|
||||
if config.csr:
|
||||
cert_path, fullchain_path = _csr_get_and_save_cert(config, le_client)
|
||||
_report_new_cert(config, cert_path, fullchain_path)
|
||||
_suggest_donation_if_appropriate(config)
|
||||
return
|
||||
|
||||
# POSTPRODUCTION: Cleanup, deployment & reporting
|
||||
notify = zope.component.getUtility(interfaces.IDisplay).notification
|
||||
if config.dry_run:
|
||||
_report_successful_dry_run(config)
|
||||
elif config.verb == "renew":
|
||||
if installer is None:
|
||||
notify("new certificate deployed without reload, fullchain is {0}".format(
|
||||
lineage.fullchain), pause=False)
|
||||
else:
|
||||
# In case of a renewal, reload server to pick up new certificate.
|
||||
# In principle we could have a configuration option to inhibit this
|
||||
# from happening.
|
||||
installer.restart()
|
||||
notify("new certificate deployed with reload of {0} server; fullchain is {1}".format(
|
||||
config.installer, lineage.fullchain), pause=False)
|
||||
elif action == "reinstall" and config.verb == "certonly":
|
||||
domains, certname = _find_domains_or_certname(config, installer)
|
||||
should_get_cert, lineage = _find_cert(config, domains, certname)
|
||||
|
||||
if not should_get_cert:
|
||||
notify = zope.component.getUtility(interfaces.IDisplay).notification
|
||||
notify("Certificate not yet due for renewal; no action taken.", pause=False)
|
||||
_suggest_donation_if_appropriate(config, action)
|
||||
return
|
||||
|
||||
lineage = _get_and_save_cert(le_client, config, domains, certname, lineage)
|
||||
|
||||
cert_path = lineage.cert_path if lineage else None
|
||||
fullchain_path = lineage.fullchain_path if lineage else None
|
||||
_report_new_cert(config, cert_path, fullchain_path)
|
||||
_suggest_donation_if_appropriate(config)
|
||||
|
||||
def renew(config, unused_plugins):
|
||||
"""Renew previously-obtained certificates."""
|
||||
try:
|
||||
renewal.handle_renewal_request(config)
|
||||
finally:
|
||||
hooks.post_hook(config, final=True)
|
||||
hooks.run_saved_post_hooks()
|
||||
|
||||
|
||||
def setup_log_file_handler(config, logfile, fmt):
|
||||
|
|
@ -781,7 +828,7 @@ def make_or_verify_core_dir(directory, mode, uid, strict):
|
|||
raise errors.Error(_PERM_ERR_FMT.format(error))
|
||||
|
||||
def make_or_verify_needed_dirs(config):
|
||||
"""Create or verify existance of config, work, or logs directories"""
|
||||
"""Create or verify existence of config, work, or logs directories"""
|
||||
make_or_verify_core_dir(config.config_dir, constants.CONFIG_DIRS_MODE,
|
||||
os.geteuid(), config.strict_permissions)
|
||||
make_or_verify_core_dir(config.work_dir, constants.CONFIG_DIRS_MODE,
|
||||
|
|
@ -804,6 +851,20 @@ def set_displayer(config):
|
|||
config.force_interactive)
|
||||
zope.component.provideUtility(displayer)
|
||||
|
||||
def _post_logging_setup(config, plugins, cli_args):
|
||||
"""Perform any setup or configuration tasks that require a logger."""
|
||||
|
||||
# This needs logging, but would otherwise be in HelpfulArgumentParser
|
||||
if config.validate_hooks:
|
||||
hooks.validate_hooks(config)
|
||||
|
||||
cli.possible_deprecation_warning(config)
|
||||
|
||||
logger.debug("certbot version: %s", certbot.__version__)
|
||||
# do not log `config`, as it contains sensitive data (e.g. revoke --key)!
|
||||
logger.debug("Arguments: %r", cli_args)
|
||||
logger.debug("Discovered plugins: %r", plugins)
|
||||
|
||||
|
||||
def main(cli_args=sys.argv[1:]):
|
||||
"""Command line argument parsing and main script execution."""
|
||||
|
|
@ -821,12 +882,7 @@ def main(cli_args=sys.argv[1:]):
|
|||
# logger ..." TODO: this should be done before plugins discovery
|
||||
setup_logging(config)
|
||||
|
||||
cli.possible_deprecation_warning(config)
|
||||
|
||||
logger.debug("certbot version: %s", certbot.__version__)
|
||||
# do not log `config`, as it contains sensitive data (e.g. revoke --key)!
|
||||
logger.debug("Arguments: %r", cli_args)
|
||||
logger.debug("Discovered plugins: %r", plugins)
|
||||
_post_logging_setup(config, plugins, cli_args)
|
||||
|
||||
sys.excepthook = functools.partial(_handle_exception, config=config)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"""Tools for checking certificate revocation."""
|
||||
import logging
|
||||
import re
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
|
|
@ -44,7 +45,6 @@ class RevocationChecker(object):
|
|||
return False
|
||||
|
||||
|
||||
logger.debug("Querying OCSP for %s", cert_path)
|
||||
url, host = self.determine_ocsp_server(cert_path)
|
||||
if not host:
|
||||
return False
|
||||
|
|
@ -56,12 +56,14 @@ class RevocationChecker(object):
|
|||
"-url", url,
|
||||
"-CAfile", chain_path,
|
||||
"-verify_other", chain_path,
|
||||
"-trust_other",
|
||||
"-header"] + self.host_args(host)
|
||||
logger.debug("Querying OCSP for %s", cert_path)
|
||||
logger.debug(" ".join(cmd))
|
||||
try:
|
||||
output, err = util.run_script(cmd, log=logging.debug)
|
||||
except errors.SubprocessError as e:
|
||||
except errors.SubprocessError:
|
||||
logger.info("OCSP check failed for %s (are we offline?)", cert_path)
|
||||
logger.debug("Command was:\n%s\nError was:\n%s", " ".join(cmd), e)
|
||||
return False
|
||||
|
||||
return _translate_ocsp_query(cert_path, output, err)
|
||||
|
|
@ -79,9 +81,8 @@ class RevocationChecker(object):
|
|||
url, _err = util.run_script(
|
||||
["openssl", "x509", "-in", cert_path, "-noout", "-ocsp_uri"],
|
||||
log=logging.debug)
|
||||
except errors.SubprocessError as e:
|
||||
except errors.SubprocessError:
|
||||
logger.info("Cannot extract OCSP URI from %s", cert_path)
|
||||
logger.debug("Error was:\n%s", e)
|
||||
return None, None
|
||||
|
||||
url = url.rstrip()
|
||||
|
|
@ -95,15 +96,25 @@ class RevocationChecker(object):
|
|||
def _translate_ocsp_query(cert_path, ocsp_output, ocsp_errors):
|
||||
"""Parse openssl's weird output to work out what it means."""
|
||||
|
||||
if not "Response verify OK" in ocsp_errors:
|
||||
states = ("good", "revoked", "unknown")
|
||||
patterns = [r"{0}: (WARNING.*)?{1}".format(cert_path, s) for s in states]
|
||||
good, revoked, unknown = (re.search(p, ocsp_output, flags=re.DOTALL) for p in patterns)
|
||||
|
||||
warning = good.group(1) if good else None
|
||||
|
||||
if (not "Response verify OK" in ocsp_errors) or (good and warning) or unknown:
|
||||
logger.info("Revocation status for %s is unknown", cert_path)
|
||||
logger.debug("Uncertain ouput:\n%s\nstderr:\n%s", ocsp_output, ocsp_errors)
|
||||
logger.debug("Uncertain output:\n%s\nstderr:\n%s", ocsp_output, ocsp_errors)
|
||||
return False
|
||||
if cert_path + ": good" in ocsp_output:
|
||||
elif good and not warning:
|
||||
return False
|
||||
elif cert_path + ": revoked" in ocsp_output:
|
||||
elif revoked:
|
||||
warning = revoked.group(1)
|
||||
if warning:
|
||||
logger.info("OCSP revocation warning: %s", warning)
|
||||
return True
|
||||
else:
|
||||
logger.warn("Unable to properly parse OCSP output: %s", ocsp_output)
|
||||
logger.warn("Unable to properly parse OCSP output: %s\nstderr:%s",
|
||||
ocsp_output, ocsp_errors)
|
||||
return False
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
"""Tests for certbot.plugins.common."""
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
|
@ -170,8 +173,16 @@ class TLSSNI01Test(unittest.TestCase):
|
|||
]
|
||||
|
||||
def setUp(self):
|
||||
self.tempdir = tempfile.mkdtemp()
|
||||
configurator = mock.MagicMock()
|
||||
configurator.config.config_dir = os.path.join(self.tempdir, "config")
|
||||
configurator.config.work_dir = os.path.join(self.tempdir, "work")
|
||||
|
||||
from certbot.plugins.common import TLSSNI01
|
||||
self.sni = TLSSNI01(configurator=mock.MagicMock())
|
||||
self.sni = TLSSNI01(configurator=configurator)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.tempdir)
|
||||
|
||||
def test_add_chall(self):
|
||||
self.sni.add_chall(self.achalls[0], 0)
|
||||
|
|
@ -187,6 +198,7 @@ class TLSSNI01Test(unittest.TestCase):
|
|||
|
||||
response = challenges.TLSSNI01Response()
|
||||
achall = mock.MagicMock()
|
||||
achall.chall.encode.return_value = "token"
|
||||
key = test_util.load_pyopenssl_private_key("rsa512_key.pem")
|
||||
achall.response_and_validation.return_value = (
|
||||
response, (test_util.load_cert("cert.pem"), key))
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ class PluginEntryPoint(object):
|
|||
return self._initialized is not None
|
||||
|
||||
def init(self, config=None):
|
||||
"""Memoized plugin inititialization."""
|
||||
"""Memoized plugin initialization."""
|
||||
if not self.initialized:
|
||||
self.entry_point.require() # fetch extras!
|
||||
self._initialized = self.plugin_cls(config, self.name)
|
||||
|
|
@ -230,7 +230,7 @@ class PluginsRegistry(collections.Mapping):
|
|||
def available(self):
|
||||
"""Filter plugins based on availability."""
|
||||
return self.filter(lambda p_ep: p_ep.available)
|
||||
# succefully prepared + misconfigured
|
||||
# successfully prepared + misconfigured
|
||||
|
||||
def find_init(self, plugin):
|
||||
"""Find an initialized plugin.
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ class PluginsRegistryTest(unittest.TestCase):
|
|||
|
||||
def test_find_init(self):
|
||||
self.assertTrue(self.reg.find_init(mock.Mock()) is None)
|
||||
self.plugin_ep.initalized = True
|
||||
self.plugin_ep.initialized = True
|
||||
self.assertTrue(
|
||||
self.reg.find_init(self.plugin_ep.init()) is self.plugin_ep)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ import mock
|
|||
from acme import challenges
|
||||
|
||||
from certbot import errors
|
||||
|
||||
from certbot.tests import acme_util
|
||||
from certbot.tests import util as test_util
|
||||
|
||||
|
||||
class AuthenticatorTest(unittest.TestCase):
|
||||
|
|
@ -42,12 +44,12 @@ class AuthenticatorTest(unittest.TestCase):
|
|||
self.assertEqual(self.auth.get_chall_pref('example.org'),
|
||||
[challenges.HTTP01, challenges.DNS01])
|
||||
|
||||
@mock.patch('certbot.plugins.manual.zope.component.getUtility')
|
||||
@test_util.patch_get_utility()
|
||||
def test_ip_logging_not_ok(self, mock_get_utility):
|
||||
mock_get_utility().yesno.return_value = False
|
||||
self.assertRaises(errors.PluginError, self.auth.perform, [])
|
||||
|
||||
@mock.patch('certbot.plugins.manual.zope.component.getUtility')
|
||||
@test_util.patch_get_utility()
|
||||
def test_ip_logging_ok(self, mock_get_utility):
|
||||
mock_get_utility().yesno.return_value = True
|
||||
self.auth.perform([])
|
||||
|
|
@ -75,7 +77,7 @@ class AuthenticatorTest(unittest.TestCase):
|
|||
self.auth.env[self.http_achall.domain]['CERTBOT_AUTH_OUTPUT'],
|
||||
http_expected)
|
||||
|
||||
@mock.patch('certbot.plugins.manual.zope.component.getUtility')
|
||||
@test_util.patch_get_utility()
|
||||
def test_manual_perform(self, mock_get_utility):
|
||||
self.config.manual_public_ip_logging_ok = True
|
||||
self.assertEqual(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Tests for letsenecrypt.plugins.selection"""
|
||||
"""Tests for letsencrypt.plugins.selection"""
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
|
@ -6,6 +6,7 @@ import mock
|
|||
import zope.component
|
||||
|
||||
from certbot.display import util as display_util
|
||||
from certbot.tests import util as test_util
|
||||
from certbot import interfaces
|
||||
|
||||
|
||||
|
|
@ -126,14 +127,14 @@ class ChoosePluginTest(unittest.TestCase):
|
|||
from certbot.plugins.selection import choose_plugin
|
||||
return choose_plugin(self.plugins, "Question?")
|
||||
|
||||
@mock.patch("certbot.plugins.selection.z_util")
|
||||
@test_util.patch_get_utility("certbot.plugins.selection.z_util")
|
||||
def test_selection(self, mock_util):
|
||||
mock_util().menu.side_effect = [(display_util.OK, 0),
|
||||
(display_util.OK, 1)]
|
||||
self.assertEqual(self.mock_stand, self._call())
|
||||
self.assertEqual(mock_util().notification.call_count, 1)
|
||||
|
||||
@mock.patch("certbot.plugins.selection.z_util")
|
||||
@test_util.patch_get_utility("certbot.plugins.selection.z_util")
|
||||
def test_more_info(self, mock_util):
|
||||
mock_util().menu.side_effect = [
|
||||
(display_util.HELP, 0),
|
||||
|
|
@ -144,7 +145,7 @@ class ChoosePluginTest(unittest.TestCase):
|
|||
self.assertEqual(self.mock_stand, self._call())
|
||||
self.assertEqual(mock_util().notification.call_count, 2)
|
||||
|
||||
@mock.patch("certbot.plugins.selection.z_util")
|
||||
@test_util.patch_get_utility("certbot.plugins.selection.z_util")
|
||||
def test_no_choice(self, mock_util):
|
||||
mock_util().menu.return_value = (display_util.CANCEL, 0)
|
||||
self.assertTrue(self._call() is None)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ from certbot import errors
|
|||
from certbot import interfaces
|
||||
|
||||
from certbot.plugins import common
|
||||
from certbot.plugins import util
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -208,74 +207,38 @@ class Authenticator(common.Plugin):
|
|||
# pylint: disable=unused-argument,missing-docstring
|
||||
return self.supported_challenges
|
||||
|
||||
def _verify_ports_are_available(self, achalls):
|
||||
"""Confirm the ports are available to solve all achalls.
|
||||
|
||||
:param list achalls: list of
|
||||
:class:`~certbot.achallenges.AnnotatedChallenge`
|
||||
|
||||
:raises .errors.MisconfigurationError: if required port is
|
||||
unavailable
|
||||
|
||||
"""
|
||||
ports = []
|
||||
if any(isinstance(ac.chall, challenges.HTTP01) for ac in achalls):
|
||||
ports.append(self.config.http01_port)
|
||||
if any(isinstance(ac.chall, challenges.TLSSNI01) for ac in achalls):
|
||||
ports.append(self.config.tls_sni_01_port)
|
||||
|
||||
renewer = (self.config.verb == "renew")
|
||||
|
||||
if any(util.already_listening(port, renewer) for port in ports):
|
||||
raise errors.MisconfigurationError(
|
||||
"At least one of the required ports is already taken.")
|
||||
|
||||
def perform(self, achalls): # pylint: disable=missing-docstring
|
||||
self._verify_ports_are_available(achalls)
|
||||
return [self._try_perform_single(achall) for achall in achalls]
|
||||
|
||||
try:
|
||||
return self.perform2(achalls)
|
||||
except errors.StandaloneBindError as error:
|
||||
display = zope.component.getUtility(interfaces.IDisplay)
|
||||
def _try_perform_single(self, achall):
|
||||
while True:
|
||||
try:
|
||||
return self._perform_single(achall)
|
||||
except errors.StandaloneBindError as error:
|
||||
_handle_perform_error(error)
|
||||
|
||||
if error.socket_error.errno == socket.errno.EACCES:
|
||||
display.notification(
|
||||
"Could not bind TCP port {0} because you don't have "
|
||||
"the appropriate permissions (for example, you "
|
||||
"aren't running this program as "
|
||||
"root).".format(error.port), force_interactive=True)
|
||||
elif error.socket_error.errno == socket.errno.EADDRINUSE:
|
||||
display.notification(
|
||||
"Could not bind TCP port {0} because it is already in "
|
||||
"use by another process on this system (such as a web "
|
||||
"server). Please stop the program in question and then "
|
||||
"try again.".format(error.port), force_interactive=True)
|
||||
else:
|
||||
raise # XXX: How to handle unknown errors in binding?
|
||||
def _perform_single(self, achall):
|
||||
if isinstance(achall.chall, challenges.HTTP01):
|
||||
server, response = self._perform_http_01(achall)
|
||||
else: # tls-sni-01
|
||||
server, response = self._perform_tls_sni_01(achall)
|
||||
self.served[server].add(achall)
|
||||
return response
|
||||
|
||||
def perform2(self, achalls):
|
||||
"""Perform achallenges without IDisplay interaction."""
|
||||
responses = []
|
||||
def _perform_http_01(self, achall):
|
||||
server = self.servers.run(self.config.http01_port, challenges.HTTP01)
|
||||
response, validation = achall.response_and_validation()
|
||||
resource = acme_standalone.HTTP01RequestHandler.HTTP01Resource(
|
||||
chall=achall.chall, response=response, validation=validation)
|
||||
self.http_01_resources.add(resource)
|
||||
return server, response
|
||||
|
||||
for achall in achalls:
|
||||
if isinstance(achall.chall, challenges.HTTP01):
|
||||
server = self.servers.run(
|
||||
self.config.http01_port, challenges.HTTP01)
|
||||
response, validation = achall.response_and_validation()
|
||||
self.http_01_resources.add(
|
||||
acme_standalone.HTTP01RequestHandler.HTTP01Resource(
|
||||
chall=achall.chall, response=response,
|
||||
validation=validation))
|
||||
else: # tls-sni-01
|
||||
server = self.servers.run(
|
||||
self.config.tls_sni_01_port, challenges.TLSSNI01)
|
||||
response, (cert, _) = achall.response_and_validation(
|
||||
cert_key=self.key)
|
||||
self.certs[response.z_domain] = (self.key, cert)
|
||||
self.served[server].add(achall)
|
||||
responses.append(response)
|
||||
|
||||
return responses
|
||||
def _perform_tls_sni_01(self, achall):
|
||||
port = self.config.tls_sni_01_port
|
||||
server = self.servers.run(port, challenges.TLSSNI01)
|
||||
response, (cert, _) = achall.response_and_validation(cert_key=self.key)
|
||||
self.certs[response.z_domain] = (self.key, cert)
|
||||
return server, response
|
||||
|
||||
def cleanup(self, achalls): # pylint: disable=missing-docstring
|
||||
# reduce self.served and close servers if none challenges are served
|
||||
|
|
@ -286,3 +249,25 @@ class Authenticator(common.Plugin):
|
|||
for port, server in six.iteritems(self.servers.running()):
|
||||
if not self.served[server]:
|
||||
self.servers.stop(port)
|
||||
|
||||
|
||||
def _handle_perform_error(error):
|
||||
if error.socket_error.errno == socket.errno.EACCES:
|
||||
raise errors.PluginError(
|
||||
"Could not bind TCP port {0} because you don't have "
|
||||
"the appropriate permissions (for example, you "
|
||||
"aren't running this program as "
|
||||
"root).".format(error.port))
|
||||
elif error.socket_error.errno == socket.errno.EADDRINUSE:
|
||||
display = zope.component.getUtility(interfaces.IDisplay)
|
||||
msg = (
|
||||
"Could not bind TCP port {0} because it is already in "
|
||||
"use by another process on this system (such as a web "
|
||||
"server). Please stop the program in question and "
|
||||
"then try again.".format(error.port))
|
||||
should_retry = display.yesno(msg, "Retry",
|
||||
"Cancel", default=False)
|
||||
if not should_retry:
|
||||
raise errors.PluginError(msg)
|
||||
else:
|
||||
raise
|
||||
|
|
|
|||
|
|
@ -8,11 +8,9 @@ import six
|
|||
|
||||
from acme import challenges
|
||||
from acme import jose
|
||||
from acme import standalone as acme_standalone
|
||||
|
||||
from certbot import achallenges
|
||||
from certbot import errors
|
||||
from certbot import interfaces
|
||||
|
||||
from certbot.tests import acme_util
|
||||
from certbot.tests import util as test_util
|
||||
|
|
@ -114,6 +112,7 @@ def get_open_port():
|
|||
open_socket.close()
|
||||
return port
|
||||
|
||||
|
||||
class AuthenticatorTest(unittest.TestCase):
|
||||
"""Tests for certbot.plugins.standalone.Authenticator."""
|
||||
|
||||
|
|
@ -124,6 +123,7 @@ class AuthenticatorTest(unittest.TestCase):
|
|||
tls_sni_01_port=get_open_port(), http01_port=get_open_port(),
|
||||
standalone_supported_challenges="tls-sni-01,http-01")
|
||||
self.auth = Authenticator(self.config, name="standalone")
|
||||
self.auth.servers = mock.MagicMock()
|
||||
|
||||
def test_supported_challenges(self):
|
||||
self.assertEqual(self.auth.supported_challenges,
|
||||
|
|
@ -146,6 +146,52 @@ class AuthenticatorTest(unittest.TestCase):
|
|||
self.assertEqual(self.auth.get_chall_pref(domain=None),
|
||||
[challenges.TLSSNI01])
|
||||
|
||||
def test_perform(self):
|
||||
achalls = self._get_achalls()
|
||||
response = self.auth.perform(achalls)
|
||||
|
||||
expected = [achall.response(achall.account_key) for achall in achalls]
|
||||
self.assertEqual(response, expected)
|
||||
|
||||
@test_util.patch_get_utility()
|
||||
def test_perform_eaddrinuse_retry(self, mock_get_utility):
|
||||
errno = socket.errno.EADDRINUSE
|
||||
error = errors.StandaloneBindError(mock.MagicMock(errno=errno), -1)
|
||||
self.auth.servers.run.side_effect = [error] + 2 * [mock.MagicMock()]
|
||||
mock_yesno = mock_get_utility.return_value.yesno
|
||||
mock_yesno.return_value = True
|
||||
|
||||
self.test_perform()
|
||||
self._assert_correct_yesno_call(mock_yesno)
|
||||
|
||||
@test_util.patch_get_utility()
|
||||
def test_perform_eaddrinuse_no_retry(self, mock_get_utility):
|
||||
mock_yesno = mock_get_utility.return_value.yesno
|
||||
mock_yesno.return_value = False
|
||||
|
||||
errno = socket.errno.EADDRINUSE
|
||||
self.assertRaises(errors.PluginError, self._fail_perform, errno)
|
||||
self._assert_correct_yesno_call(mock_yesno)
|
||||
|
||||
def _assert_correct_yesno_call(self, mock_yesno):
|
||||
yesno_args, yesno_kwargs = mock_yesno.call_args
|
||||
self.assertTrue("in use" in yesno_args[0])
|
||||
self.assertFalse(yesno_kwargs.get("default", True))
|
||||
|
||||
def test_perform_eacces(self):
|
||||
errno = socket.errno.EACCES
|
||||
self.assertRaises(errors.PluginError, self._fail_perform, errno)
|
||||
|
||||
def test_perform_unexpected_socket_error(self):
|
||||
errno = socket.errno.ENOTCONN
|
||||
self.assertRaises(
|
||||
errors.StandaloneBindError, self._fail_perform, errno)
|
||||
|
||||
def _fail_perform(self, errno):
|
||||
error = errors.StandaloneBindError(mock.MagicMock(errno=errno), -1)
|
||||
self.auth.servers.run.side_effect = error
|
||||
self.auth.perform(self._get_achalls())
|
||||
|
||||
@classmethod
|
||||
def _get_achalls(cls):
|
||||
domain = b'localhost'
|
||||
|
|
@ -157,84 +203,7 @@ class AuthenticatorTest(unittest.TestCase):
|
|||
|
||||
return [http_01, tls_sni_01]
|
||||
|
||||
@mock.patch("certbot.plugins.standalone.util")
|
||||
def test_perform_already_listening(self, mock_util):
|
||||
http_01, tls_sni_01 = self._get_achalls()
|
||||
|
||||
for achall, port in ((http_01, self.config.http01_port,),
|
||||
(tls_sni_01, self.config.tls_sni_01_port)):
|
||||
mock_util.already_listening.return_value = True
|
||||
self.assertRaises(
|
||||
errors.MisconfigurationError, self.auth.perform, [achall])
|
||||
mock_util.already_listening.assert_called_once_with(port, False)
|
||||
mock_util.already_listening.reset_mock()
|
||||
|
||||
@mock.patch("certbot.plugins.standalone.zope.component.getUtility")
|
||||
def test_perform(self, unused_mock_get_utility):
|
||||
achalls = self._get_achalls()
|
||||
|
||||
self.auth.perform2 = mock.Mock(return_value=mock.sentinel.responses)
|
||||
self.assertEqual(mock.sentinel.responses, self.auth.perform(achalls))
|
||||
self.auth.perform2.assert_called_once_with(achalls)
|
||||
|
||||
@mock.patch("certbot.plugins.standalone.zope.component.getUtility")
|
||||
def _test_perform_bind_errors(self, errno, achalls, mock_get_utility):
|
||||
port = get_open_port()
|
||||
def _perform2(unused_achalls):
|
||||
raise errors.StandaloneBindError(mock.Mock(errno=errno), port)
|
||||
|
||||
self.auth.perform2 = mock.MagicMock(side_effect=_perform2)
|
||||
self.auth.perform(achalls)
|
||||
mock_get_utility.assert_called_once_with(interfaces.IDisplay)
|
||||
notification = mock_get_utility.return_value.notification
|
||||
self.assertEqual(1, notification.call_count)
|
||||
self.assertTrue(str(port) in notification.call_args[0][0])
|
||||
|
||||
def test_perform_eacces(self):
|
||||
# pylint: disable=no-value-for-parameter
|
||||
self._test_perform_bind_errors(socket.errno.EACCES, [])
|
||||
|
||||
def test_perform_eaddrinuse(self):
|
||||
# pylint: disable=no-value-for-parameter
|
||||
self._test_perform_bind_errors(socket.errno.EADDRINUSE, [])
|
||||
|
||||
def test_perfom_unknown_bind_error(self):
|
||||
self.assertRaises(
|
||||
errors.StandaloneBindError, self._test_perform_bind_errors,
|
||||
socket.errno.ENOTCONN, [])
|
||||
|
||||
def test_perform2(self):
|
||||
http_01, tls_sni_01 = self._get_achalls()
|
||||
|
||||
self.auth.servers = mock.MagicMock()
|
||||
|
||||
def _run(port, tls): # pylint: disable=unused-argument
|
||||
return "server{0}".format(port)
|
||||
|
||||
self.auth.servers.run.side_effect = _run
|
||||
responses = self.auth.perform2([http_01, tls_sni_01])
|
||||
|
||||
self.assertTrue(isinstance(responses, list))
|
||||
self.assertEqual(2, len(responses))
|
||||
self.assertTrue(isinstance(responses[0], challenges.HTTP01Response))
|
||||
self.assertTrue(isinstance(responses[1], challenges.TLSSNI01Response))
|
||||
|
||||
self.assertEqual(self.auth.servers.run.mock_calls, [
|
||||
mock.call(self.config.http01_port, challenges.HTTP01),
|
||||
mock.call(self.config.tls_sni_01_port, challenges.TLSSNI01),
|
||||
])
|
||||
self.assertEqual(self.auth.served, {
|
||||
"server" + str(self.config.tls_sni_01_port): set([tls_sni_01]),
|
||||
"server" + str(self.config.http01_port): set([http_01]),
|
||||
})
|
||||
self.assertEqual(1, len(self.auth.http_01_resources))
|
||||
self.assertEqual(1, len(self.auth.certs))
|
||||
self.assertEqual(list(self.auth.http_01_resources), [
|
||||
acme_standalone.HTTP01RequestHandler.HTTP01Resource(
|
||||
acme_util.HTTP01, responses[0], mock.ANY)])
|
||||
|
||||
def test_cleanup(self):
|
||||
self.auth.servers = mock.Mock()
|
||||
self.auth.servers.running.return_value = {
|
||||
1: "server1",
|
||||
2: "server2",
|
||||
|
|
|
|||
|
|
@ -1,41 +1,18 @@
|
|||
"""Plugin utilities."""
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
|
||||
import zope.component
|
||||
|
||||
from acme import errors as acme_errors
|
||||
from acme import util as acme_util
|
||||
|
||||
from certbot import interfaces
|
||||
from certbot import util
|
||||
|
||||
PSUTIL_REQUIREMENT = "psutil>=2.2.1"
|
||||
|
||||
try:
|
||||
acme_util.activate(PSUTIL_REQUIREMENT)
|
||||
import psutil # pragma: no cover
|
||||
USE_PSUTIL = True
|
||||
except acme_errors.DependencyError: # pragma: no cover
|
||||
USE_PSUTIL = False
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
RENEWER_EXTRA_MSG = (
|
||||
" For automated renewal, you may want to use a script that stops"
|
||||
" and starts your webserver. You can find an example at"
|
||||
" https://certbot.eff.org/docs/using.html#renewal ."
|
||||
" Alternatively you can use the webroot plugin to renew without"
|
||||
" needing to stop and start your webserver.")
|
||||
|
||||
|
||||
def path_surgery(restart_cmd):
|
||||
"""Attempt to perform PATH surgery to find restart_cmd
|
||||
def path_surgery(cmd):
|
||||
"""Attempt to perform PATH surgery to find cmd
|
||||
|
||||
Mitigates https://github.com/certbot/certbot/issues/1833
|
||||
|
||||
:param str restart_cmd: the command that is being searched for in the PATH
|
||||
:param str cmd: the command that is being searched for in the PATH
|
||||
|
||||
:returns: True if the operation succeeded, False otherwise
|
||||
"""
|
||||
|
|
@ -49,115 +26,13 @@ def path_surgery(restart_cmd):
|
|||
|
||||
if any(added):
|
||||
logger.debug("Can't find %s, attempting PATH mitigation by adding %s",
|
||||
restart_cmd, os.pathsep.join(added))
|
||||
cmd, os.pathsep.join(added))
|
||||
os.environ["PATH"] = path
|
||||
|
||||
if util.exe_exists(restart_cmd):
|
||||
if util.exe_exists(cmd):
|
||||
return True
|
||||
else:
|
||||
expanded = " expanded" if any(added) else ""
|
||||
logger.warning("Failed to find %s in%s PATH: %s", restart_cmd,
|
||||
logger.warning("Failed to find %s in%s PATH: %s", cmd,
|
||||
expanded, path)
|
||||
return False
|
||||
|
||||
|
||||
def already_listening(port, renewer=False):
|
||||
"""Check if a process is already listening on the port.
|
||||
|
||||
If so, also tell the user via a display notification.
|
||||
|
||||
.. warning::
|
||||
On some operating systems, this function can only usefully be
|
||||
run as root.
|
||||
|
||||
:param int port: The TCP port in question.
|
||||
:returns: True or False.
|
||||
|
||||
"""
|
||||
|
||||
if USE_PSUTIL:
|
||||
return already_listening_psutil(port, renewer=renewer)
|
||||
else:
|
||||
logger.debug("Psutil not found, using simple socket check.")
|
||||
return already_listening_socket(port, renewer=renewer)
|
||||
|
||||
|
||||
def already_listening_socket(port, renewer=False):
|
||||
"""Simple socket based check to find out if port is already in use
|
||||
|
||||
:param int port: The TCP port in question.
|
||||
:returns: True or False
|
||||
"""
|
||||
|
||||
try:
|
||||
testsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
|
||||
testsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
testsocket.bind(("", port))
|
||||
except socket.error:
|
||||
display = zope.component.getUtility(interfaces.IDisplay)
|
||||
extra = ""
|
||||
if renewer:
|
||||
extra = RENEWER_EXTRA_MSG
|
||||
display.notification(
|
||||
"Port {0} is already in use by another process. This will "
|
||||
"prevent us from binding to that port. Please stop the "
|
||||
"process that is populating the port in question and try "
|
||||
"again. {1}".format(port, extra), force_interactive=True)
|
||||
return True
|
||||
finally:
|
||||
testsocket.close()
|
||||
except socket.error:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def already_listening_psutil(port, renewer=False):
|
||||
"""Psutil variant of the open port check
|
||||
|
||||
:param int port: The TCP port in question.
|
||||
:returns: True or False.
|
||||
|
||||
"""
|
||||
try:
|
||||
net_connections = psutil.net_connections()
|
||||
except psutil.AccessDenied as error:
|
||||
logger.info("Access denied when trying to list network "
|
||||
"connections: %s. Are you root?", error)
|
||||
# this function is just a pre-check that often causes false
|
||||
# positives and problems in testing (c.f. #680 on Mac, #255
|
||||
# generally); we will fail later in bind() anyway
|
||||
return False
|
||||
|
||||
listeners = [conn.pid for conn in net_connections
|
||||
if conn.status == 'LISTEN' and
|
||||
conn.type == socket.SOCK_STREAM and
|
||||
conn.laddr[1] == port]
|
||||
try:
|
||||
if listeners and listeners[0] is not None:
|
||||
# conn.pid may be None if the current process doesn't have
|
||||
# permission to identify the listening process! Additionally,
|
||||
# listeners may have more than one element if separate
|
||||
# sockets have bound the same port on separate interfaces.
|
||||
# We currently only have UI to notify the user about one
|
||||
# of them at a time.
|
||||
pid = listeners[0]
|
||||
name = psutil.Process(pid).name()
|
||||
display = zope.component.getUtility(interfaces.IDisplay)
|
||||
extra = ""
|
||||
if renewer:
|
||||
extra = RENEWER_EXTRA_MSG
|
||||
display.notification(
|
||||
"The program {0} (process ID {1}) is already listening "
|
||||
"on TCP port {2}. This will prevent us from binding to "
|
||||
"that port. Please stop the {0} program temporarily "
|
||||
"and then try again.{3}".format(name, pid, port, extra),
|
||||
force_interactive=True)
|
||||
return True
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
# Perhaps the result of a race where the process could have
|
||||
# exited or relinquished the port (NoSuchProcess), or the result
|
||||
# of an OS policy where we're not allowed to look up the process
|
||||
# name (AccessDenied).
|
||||
pass
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
"""Tests for certbot.plugins.util."""
|
||||
import os
|
||||
import socket
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from certbot.plugins.util import PSUTIL_REQUIREMENT
|
||||
from certbot.tests import util as test_util
|
||||
|
||||
|
||||
class PathSurgeryTest(unittest.TestCase):
|
||||
"""Tests for certbot.plugins.path_surgery."""
|
||||
|
|
@ -34,140 +30,5 @@ class PathSurgeryTest(unittest.TestCase):
|
|||
self.assertTrue("/tmp" in os.environ["PATH"])
|
||||
|
||||
|
||||
class AlreadyListeningTest(unittest.TestCase):
|
||||
"""Tests for certbot.plugins.already_listening."""
|
||||
@classmethod
|
||||
def _call(cls, *args, **kwargs):
|
||||
from certbot.plugins.util import already_listening
|
||||
return already_listening(*args, **kwargs)
|
||||
|
||||
|
||||
class AlreadyListeningTestNoPsutil(AlreadyListeningTest):
|
||||
"""Tests for certbot.plugins.already_listening when
|
||||
psutil is not available"""
|
||||
@classmethod
|
||||
def _call(cls, *args, **kwargs):
|
||||
with mock.patch("certbot.plugins.util.USE_PSUTIL", False):
|
||||
return super(
|
||||
AlreadyListeningTestNoPsutil, cls)._call(*args, **kwargs)
|
||||
|
||||
@mock.patch("certbot.plugins.util.zope.component.getUtility")
|
||||
def test_ports_available(self, mock_getutil):
|
||||
# Ensure we don't get error
|
||||
with mock.patch("socket.socket.bind"):
|
||||
self.assertFalse(self._call(80))
|
||||
self.assertFalse(self._call(80, True))
|
||||
self.assertEqual(mock_getutil.call_count, 0)
|
||||
|
||||
@mock.patch("certbot.plugins.util.zope.component.getUtility")
|
||||
def test_ports_blocked(self, mock_getutil):
|
||||
with mock.patch("certbot.plugins.util.socket.socket.bind") as mock_bind:
|
||||
mock_bind.side_effect = socket.error
|
||||
self.assertTrue(self._call(80))
|
||||
self.assertTrue(self._call(80, True))
|
||||
with mock.patch("certbot.plugins.util.socket.socket") as mock_socket:
|
||||
mock_socket.side_effect = socket.error
|
||||
self.assertFalse(self._call(80))
|
||||
self.assertEqual(mock_getutil.call_count, 2)
|
||||
|
||||
|
||||
@test_util.skip_unless(test_util.requirement_available(PSUTIL_REQUIREMENT),
|
||||
"optional dependency psutil is not available")
|
||||
class AlreadyListeningTestPsutil(AlreadyListeningTest):
|
||||
"""Tests for certbot.plugins.already_listening."""
|
||||
@mock.patch("certbot.plugins.util.psutil.net_connections")
|
||||
@mock.patch("certbot.plugins.util.psutil.Process")
|
||||
@mock.patch("certbot.plugins.util.zope.component.getUtility")
|
||||
def test_race_condition(self, mock_get_utility, mock_process, mock_net):
|
||||
# This tests a race condition, or permission problem, or OS
|
||||
# incompatibility in which, for some reason, no process name can be
|
||||
# found to match the identified listening PID.
|
||||
import psutil
|
||||
from psutil._common import sconn
|
||||
conns = [
|
||||
sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30),
|
||||
raddr=(), status="LISTEN", pid=None),
|
||||
sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783),
|
||||
raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234),
|
||||
sconn(fd=-1, family=10, type=1, laddr=("::1", 54321),
|
||||
raddr=("::1", 111), status="CLOSE_WAIT", pid=None),
|
||||
sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17),
|
||||
raddr=(), status="LISTEN", pid=4416)]
|
||||
mock_net.return_value = conns
|
||||
mock_process.side_effect = psutil.NoSuchProcess("No such PID")
|
||||
# We simulate being unable to find the process name of PID 4416,
|
||||
# which results in returning False.
|
||||
self.assertFalse(self._call(17))
|
||||
self.assertEqual(mock_get_utility.generic_notification.call_count, 0)
|
||||
mock_process.assert_called_once_with(4416)
|
||||
|
||||
@mock.patch("certbot.plugins.util.psutil.net_connections")
|
||||
@mock.patch("certbot.plugins.util.psutil.Process")
|
||||
@mock.patch("certbot.plugins.util.zope.component.getUtility")
|
||||
def test_not_listening(self, mock_get_utility, mock_process, mock_net):
|
||||
from psutil._common import sconn
|
||||
conns = [
|
||||
sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30),
|
||||
raddr=(), status="LISTEN", pid=None),
|
||||
sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783),
|
||||
raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234),
|
||||
sconn(fd=-1, family=10, type=1, laddr=("::1", 54321),
|
||||
raddr=("::1", 111), status="CLOSE_WAIT", pid=None)]
|
||||
mock_net.return_value = conns
|
||||
mock_process.name.return_value = "inetd"
|
||||
self.assertFalse(self._call(17))
|
||||
self.assertEqual(mock_get_utility.generic_notification.call_count, 0)
|
||||
self.assertEqual(mock_process.call_count, 0)
|
||||
|
||||
@mock.patch("certbot.plugins.util.psutil.net_connections")
|
||||
@mock.patch("certbot.plugins.util.psutil.Process")
|
||||
@mock.patch("certbot.plugins.util.zope.component.getUtility")
|
||||
def test_listening_ipv4(self, mock_get_utility, mock_process, mock_net):
|
||||
from psutil._common import sconn
|
||||
conns = [
|
||||
sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30),
|
||||
raddr=(), status="LISTEN", pid=None),
|
||||
sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783),
|
||||
raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234),
|
||||
sconn(fd=-1, family=10, type=1, laddr=("::1", 54321),
|
||||
raddr=("::1", 111), status="CLOSE_WAIT", pid=None),
|
||||
sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17),
|
||||
raddr=(), status="LISTEN", pid=4416)]
|
||||
mock_net.return_value = conns
|
||||
mock_process.name.return_value = "inetd"
|
||||
result = self._call(17, True)
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(mock_get_utility.call_count, 1)
|
||||
mock_process.assert_called_once_with(4416)
|
||||
|
||||
@mock.patch("certbot.plugins.util.psutil.net_connections")
|
||||
@mock.patch("certbot.plugins.util.psutil.Process")
|
||||
@mock.patch("certbot.plugins.util.zope.component.getUtility")
|
||||
def test_listening_ipv6(self, mock_get_utility, mock_process, mock_net):
|
||||
from psutil._common import sconn
|
||||
conns = [
|
||||
sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30),
|
||||
raddr=(), status="LISTEN", pid=None),
|
||||
sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783),
|
||||
raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234),
|
||||
sconn(fd=-1, family=10, type=1, laddr=("::1", 54321),
|
||||
raddr=("::1", 111), status="CLOSE_WAIT", pid=None),
|
||||
sconn(fd=3, family=10, type=1, laddr=("::", 12345), raddr=(),
|
||||
status="LISTEN", pid=4420),
|
||||
sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17),
|
||||
raddr=(), status="LISTEN", pid=4416)]
|
||||
mock_net.return_value = conns
|
||||
mock_process.name.return_value = "inetd"
|
||||
result = self._call(12345)
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(mock_get_utility.call_count, 1)
|
||||
mock_process.assert_called_once_with(4420)
|
||||
|
||||
@mock.patch("certbot.plugins.util.psutil.net_connections")
|
||||
def test_access_denied_exception(self, mock_net):
|
||||
import psutil
|
||||
mock_net.side_effect = psutil.AccessDenied("")
|
||||
self.assertFalse(self._call(12345))
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -110,12 +110,13 @@ to serve all files under specified web root ({0})."""
|
|||
|
||||
def _prompt_with_webroot_list(self, domain, known_webroots):
|
||||
display = zope.component.getUtility(interfaces.IDisplay)
|
||||
path_flag = "--" + self.option_name("path")
|
||||
|
||||
while True:
|
||||
code, index = display.menu(
|
||||
"Select the webroot for {0}:".format(domain),
|
||||
["Enter a new webroot"] + known_webroots,
|
||||
help_label="Help", cli_flag="--" + self.option_name("path"))
|
||||
help_label="Help", cli_flag=path_flag, force_interactive=True)
|
||||
if code == display_util.CANCEL:
|
||||
raise errors.PluginError(
|
||||
"Every requested domain must have a "
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class AuthenticatorTest(unittest.TestCase):
|
|||
def test_prepare(self):
|
||||
self.auth.prepare() # shouldn't raise any exceptions
|
||||
|
||||
@mock.patch("certbot.plugins.webroot.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_webroot_from_list(self, mock_get_utility):
|
||||
self.config.webroot_path = []
|
||||
self.config.webroot_map = {"otherthing.com": self.path}
|
||||
|
|
@ -78,7 +78,7 @@ class AuthenticatorTest(unittest.TestCase):
|
|||
self.assertEqual(self.config.webroot_map[self.achall.domain],
|
||||
self.path)
|
||||
|
||||
@mock.patch("certbot.plugins.webroot.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_webroot_from_list_help_and_cancel(self, mock_get_utility):
|
||||
self.config.webroot_path = []
|
||||
self.config.webroot_map = {"otherthing.com": self.path}
|
||||
|
|
@ -95,7 +95,7 @@ class AuthenticatorTest(unittest.TestCase):
|
|||
webroot in call[0][1]
|
||||
for webroot in six.itervalues(self.config.webroot_map)))
|
||||
|
||||
@mock.patch("certbot.plugins.webroot.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_new_webroot(self, mock_get_utility):
|
||||
self.config.webroot_path = []
|
||||
self.config.webroot_map = {}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""Functionality for autorenewal and associated juggling of configurations"""
|
||||
from __future__ import print_function
|
||||
import copy
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
import traceback
|
||||
|
|
@ -29,9 +30,13 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
STR_CONFIG_ITEMS = ["config_dir", "logs_dir", "work_dir", "user_agent",
|
||||
"server", "account", "authenticator", "installer",
|
||||
"standalone_supported_challenges", "ecdsa_curve",
|
||||
"key_types", "renew_hook"]
|
||||
"standalone_supported_challenges", "renew_hook",
|
||||
"pre_hook", "post_hook", "ecdsa_curve", "key_types"]
|
||||
INT_CONFIG_ITEMS = ["rsa_key_size", "tls_sni_01_port", "http01_port"]
|
||||
BOOL_CONFIG_ITEMS = ["must_staple", "allow_subset_of_names"]
|
||||
|
||||
CONFIG_ITEMS = set(itertools.chain(
|
||||
BOOL_CONFIG_ITEMS, INT_CONFIG_ITEMS, STR_CONFIG_ITEMS, ('pref_challs',)))
|
||||
|
||||
|
||||
def _reconstitute(config, full_path):
|
||||
|
|
@ -70,7 +75,7 @@ def _reconstitute(config, full_path):
|
|||
# Now restore specific values along with their data types, if
|
||||
# those elements are present.
|
||||
try:
|
||||
_restore_required_config_elements(config, renewalparams)
|
||||
restore_required_config_elements(config, renewalparams)
|
||||
_restore_plugin_configs(config, renewalparams)
|
||||
except (ValueError, errors.Error) as error:
|
||||
logger.warning(
|
||||
|
|
@ -150,7 +155,7 @@ def _restore_plugin_configs(config, renewalparams):
|
|||
setattr(config.namespace, config_item, cast(config_value))
|
||||
|
||||
|
||||
def _restore_required_config_elements(config, renewalparams):
|
||||
def restore_required_config_elements(config, renewalparams):
|
||||
"""Sets non-plugin specific values in config from renewalparams
|
||||
|
||||
:param configuration.NamespaceConfig config: configuration for the
|
||||
|
|
@ -159,30 +164,92 @@ def _restore_required_config_elements(config, renewalparams):
|
|||
configuration file that defines this lineage
|
||||
|
||||
"""
|
||||
# string-valued items to add if they're present
|
||||
for config_item in STR_CONFIG_ITEMS:
|
||||
if config_item in renewalparams and not cli.set_by_cli(config_item):
|
||||
value = renewalparams[config_item]
|
||||
# Unfortunately, we've lost type information from ConfigObj,
|
||||
# so we don't know if the original was NoneType or str!
|
||||
if value == "None":
|
||||
value = None
|
||||
setattr(config.namespace, config_item, value)
|
||||
# int-valued items to add if they're present
|
||||
for config_item in INT_CONFIG_ITEMS:
|
||||
if config_item in renewalparams and not cli.set_by_cli(config_item):
|
||||
config_value = renewalparams[config_item]
|
||||
# the default value for http01_port was None during private beta
|
||||
if config_item == "http01_port" and config_value == "None":
|
||||
logger.info("updating legacy http01_port value")
|
||||
int_value = cli.flag_default("http01_port")
|
||||
else:
|
||||
try:
|
||||
int_value = int(config_value)
|
||||
except ValueError:
|
||||
raise errors.Error(
|
||||
"Expected a numeric value for {0}".format(config_item))
|
||||
setattr(config.namespace, config_item, int_value)
|
||||
|
||||
required_items = itertools.chain(
|
||||
(("pref_challs", _restore_pref_challs),),
|
||||
six.moves.zip(BOOL_CONFIG_ITEMS, itertools.repeat(_restore_bool)),
|
||||
six.moves.zip(INT_CONFIG_ITEMS, itertools.repeat(_restore_int)),
|
||||
six.moves.zip(STR_CONFIG_ITEMS, itertools.repeat(_restore_str)))
|
||||
for item_name, restore_func in required_items:
|
||||
if item_name in renewalparams and not cli.set_by_cli(item_name):
|
||||
value = restore_func(item_name, renewalparams[item_name])
|
||||
setattr(config.namespace, item_name, value)
|
||||
|
||||
|
||||
def _restore_pref_challs(unused_name, value):
|
||||
"""Restores preferred challenges from a renewal config file.
|
||||
|
||||
If value is a `str`, it should be a single challenge type.
|
||||
|
||||
:param str unused_name: option name
|
||||
:param value: option value
|
||||
:type value: `list` of `str` or `str`
|
||||
|
||||
:returns: converted option value to be stored in the runtime config
|
||||
:rtype: `list` of `str`
|
||||
|
||||
:raises errors.Error: if value can't be converted to an bool
|
||||
|
||||
"""
|
||||
# If pref_challs has only one element, configobj saves the value
|
||||
# with a trailing comma so it's parsed as a list. If this comma is
|
||||
# removed by the user, the value is parsed as a str.
|
||||
value = [value] if isinstance(value, str) else value
|
||||
return cli.parse_preferred_challenges(value)
|
||||
|
||||
|
||||
def _restore_bool(name, value):
|
||||
"""Restores an boolean key-value pair from a renewal config file.
|
||||
|
||||
:param str name: option name
|
||||
:param str value: option value
|
||||
|
||||
:returns: converted option value to be stored in the runtime config
|
||||
:rtype: bool
|
||||
|
||||
:raises errors.Error: if value can't be converted to an bool
|
||||
|
||||
"""
|
||||
lowercase_value = value.lower()
|
||||
if lowercase_value not in ("true", "false"):
|
||||
raise errors.Error(
|
||||
"Expected True or False for {0} but found {1}".format(name, value))
|
||||
return lowercase_value == "true"
|
||||
|
||||
|
||||
def _restore_int(name, value):
|
||||
"""Restores an integer key-value pair from a renewal config file.
|
||||
|
||||
:param str name: option name
|
||||
:param str value: option value
|
||||
|
||||
:returns: converted option value to be stored in the runtime config
|
||||
:rtype: int
|
||||
|
||||
:raises errors.Error: if value can't be converted to an int
|
||||
|
||||
"""
|
||||
if name == "http01_port" and value == "None":
|
||||
logger.info("updating legacy http01_port value")
|
||||
return cli.flag_default("http01_port")
|
||||
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
raise errors.Error("Expected a numeric value for {0}".format(name))
|
||||
|
||||
|
||||
def _restore_str(unused_name, value):
|
||||
"""Restores an string key-value pair from a renewal config file.
|
||||
|
||||
:param str unused_name: option name
|
||||
:param str value: option value
|
||||
|
||||
:returns: converted option value to be stored in the runtime config
|
||||
:rtype: str or None
|
||||
|
||||
"""
|
||||
return None if value == "None" else value
|
||||
|
||||
|
||||
def should_renew(config, lineage):
|
||||
|
|
@ -220,12 +287,14 @@ def _avoid_invalidating_lineage(config, lineage, original_server):
|
|||
"unless you use the --break-my-certs flag!".format(names))
|
||||
|
||||
|
||||
def renew_cert(config, le_client, lineage):
|
||||
def renew_cert(config, domains, le_client, lineage):
|
||||
"Renew a certificate lineage."
|
||||
renewal_params = lineage.configuration["renewalparams"]
|
||||
original_server = renewal_params.get("server", cli.flag_default("server"))
|
||||
_avoid_invalidating_lineage(config, lineage, original_server)
|
||||
new_certr, new_chain, new_key, _ = le_client.obtain_certificate(lineage.names())
|
||||
if not domains:
|
||||
domains = lineage.names()
|
||||
new_certr, new_chain, new_key, _ = le_client.obtain_certificate(domains)
|
||||
if config.dry_run:
|
||||
logger.debug("Dry run: skipping updating lineage at %s",
|
||||
os.path.dirname(lineage.cert))
|
||||
|
|
@ -238,7 +307,7 @@ def renew_cert(config, le_client, lineage):
|
|||
lineage.save_successor(prior_version, new_cert, new_key.pem, new_chain, config)
|
||||
lineage.update_all_links_to(lineage.latest_common_version())
|
||||
|
||||
hooks.renew_hook(config, lineage.names(), lineage.live_dir)
|
||||
hooks.renew_hook(config, domains, lineage.live_dir)
|
||||
|
||||
|
||||
def report(msgs, category):
|
||||
|
|
@ -261,6 +330,9 @@ def _renew_describe_results(config, renew_successes, renew_failures,
|
|||
notify(report(renew_skipped, "skipped"))
|
||||
if not renew_successes and not renew_failures:
|
||||
notify("No renewals were attempted.")
|
||||
if (config.pre_hook is not None or
|
||||
config.renew_hook is not None or config.post_hook is not None):
|
||||
notify("No hooks were run.")
|
||||
elif renew_successes and not renew_failures:
|
||||
notify("Congratulations, all renewals succeeded. The following certs "
|
||||
"have been renewed:")
|
||||
|
|
@ -339,7 +411,12 @@ def handle_renewal_request(config):
|
|||
if should_renew(lineage_config, renewal_candidate):
|
||||
plugins = plugins_disco.PluginsRegistry.find_all()
|
||||
from certbot import main
|
||||
main.obtain_cert(lineage_config, plugins, renewal_candidate)
|
||||
# domains have been restored into lineage_config by reconstitute
|
||||
# but they're unnecessary anyway because renew_cert here
|
||||
# will just grab them from the certificate
|
||||
# we already know it's time to renew based on should_renew
|
||||
# and we have a lineage in renewal_candidate
|
||||
main.renew_cert(lineage_config, plugins, renewal_candidate)
|
||||
renew_successes.append(renewal_candidate.fullchain)
|
||||
else:
|
||||
renew_skipped.append(renewal_candidate.fullchain)
|
||||
|
|
|
|||
|
|
@ -183,9 +183,9 @@ def _relevant(option):
|
|||
from certbot import renewal
|
||||
from certbot.plugins import disco as plugins_disco
|
||||
plugins = list(plugins_disco.PluginsRegistry.find_all())
|
||||
return (option in renewal.STR_CONFIG_ITEMS
|
||||
or option in renewal.INT_CONFIG_ITEMS
|
||||
or any(option.startswith(x + "_") for x in plugins))
|
||||
|
||||
return (option in renewal.CONFIG_ITEMS or
|
||||
any(option.startswith(x + "_") for x in plugins))
|
||||
|
||||
|
||||
def relevant_values(all_values):
|
||||
|
|
@ -391,6 +391,26 @@ class RenewableCert(object):
|
|||
self._update_symlinks()
|
||||
self._check_symlinks()
|
||||
|
||||
@property
|
||||
def key_path(self):
|
||||
"""Duck type for self.privkey"""
|
||||
return self.privkey
|
||||
|
||||
@property
|
||||
def cert_path(self):
|
||||
"""Duck type for self.cert"""
|
||||
return self.cert
|
||||
|
||||
@property
|
||||
def chain_path(self):
|
||||
"""Duck type for self.chain"""
|
||||
return self.chain
|
||||
|
||||
@property
|
||||
def fullchain_path(self):
|
||||
"""Duck type for self.fullchain"""
|
||||
return self.fullchain
|
||||
|
||||
@property
|
||||
def target_expiry(self):
|
||||
"""The current target certificate's expiration datetime
|
||||
|
|
@ -716,7 +736,7 @@ class RenewableCert(object):
|
|||
|
||||
:returns: ``True`` if there is a complete version of this
|
||||
lineage with a larger version number than the current
|
||||
version, and ``False`` otherwis
|
||||
version, and ``False`` otherwise
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -62,13 +62,10 @@ class ReportNewAccountTest(unittest.TestCase):
|
|||
|
||||
def setUp(self):
|
||||
self.config = mock.MagicMock(config_dir="/etc/letsencrypt")
|
||||
reg = messages.Registration.from_data(email="rhino@jungle.io")
|
||||
self.acc = mock.MagicMock(regr=messages.RegistrationResource(
|
||||
uri=None, new_authzr_uri=None, body=reg))
|
||||
|
||||
def _call(self):
|
||||
from certbot.account import report_new_account
|
||||
report_new_account(self.acc, self.config)
|
||||
report_new_account(self.config)
|
||||
|
||||
@mock.patch("certbot.account.zope.component.queryUtility")
|
||||
def test_no_reporter(self, mock_zope):
|
||||
|
|
@ -80,8 +77,6 @@ class ReportNewAccountTest(unittest.TestCase):
|
|||
self._call()
|
||||
call_list = mock_zope().add_message.call_args_list
|
||||
self.assertTrue(self.config.config_dir in call_list[0][0][0])
|
||||
self.assertTrue(
|
||||
", ".join(self.acc.regr.body.emails) in call_list[1][0][0])
|
||||
|
||||
|
||||
class AccountMemoryStorageTest(unittest.TestCase):
|
||||
|
|
@ -115,7 +110,7 @@ class AccountFileStorageTest(unittest.TestCase):
|
|||
from certbot.account import Account
|
||||
self.acc = Account(
|
||||
regr=messages.RegistrationResource(
|
||||
uri=None, new_authzr_uri=None, body=messages.Registration()),
|
||||
uri=None, body=messages.Registration()),
|
||||
key=KEY)
|
||||
|
||||
def tearDown(self):
|
||||
|
|
@ -190,6 +185,14 @@ class AccountFileStorageTest(unittest.TestCase):
|
|||
self.assertRaises(
|
||||
errors.AccountStorageError, self.storage.save, self.acc)
|
||||
|
||||
def test_delete(self):
|
||||
self.storage.save(self.acc)
|
||||
self.storage.delete(self.acc.id)
|
||||
self.assertRaises(errors.AccountNotFound, self.storage.load, self.acc.id)
|
||||
|
||||
def test_delete_no_account(self):
|
||||
self.assertRaises(errors.AccountNotFound, self.storage.delete, self.acc.id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -96,6 +96,5 @@ def gen_authzr(authz_status, domain, challs, statuses, combos=True):
|
|||
# pylint: disable=star-args
|
||||
return messages.AuthorizationResource(
|
||||
uri="https://trusted.ca/new-authz-resource",
|
||||
new_cert_uri="https://trusted.ca/new-cert",
|
||||
body=messages.Authorization(**authz_kwargs)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from certbot import errors
|
|||
from certbot import util
|
||||
|
||||
from certbot.tests import acme_util
|
||||
from certbot.tests import util as test_util
|
||||
|
||||
|
||||
class ChallengeFactoryTest(unittest.TestCase):
|
||||
|
|
@ -175,7 +176,8 @@ class GetAuthorizationsTest(unittest.TestCase):
|
|||
mock_poll.side_effect = self._validate_all
|
||||
self.mock_auth.get_chall_pref.return_value.append(challenges.HTTP01)
|
||||
|
||||
self.handler.pref_challs.extend((challenges.HTTP01, challenges.DNS01,))
|
||||
self.handler.pref_challs.extend((challenges.HTTP01.typ,
|
||||
challenges.DNS01.typ,))
|
||||
|
||||
self.handler.get_authorizations(["0"])
|
||||
|
||||
|
|
@ -186,7 +188,7 @@ class GetAuthorizationsTest(unittest.TestCase):
|
|||
def test_preferred_challenges_not_supported(self):
|
||||
self.mock_net.request_domain_challenges.side_effect = functools.partial(
|
||||
gen_dom_authzr, challs=acme_util.CHALLENGES)
|
||||
self.handler.pref_challs.append(challenges.HTTP01)
|
||||
self.handler.pref_challs.append(challenges.HTTP01.typ)
|
||||
self.assertRaises(
|
||||
errors.AuthorizationError, self.handler.get_authorizations, ["0"])
|
||||
|
||||
|
|
@ -251,7 +253,7 @@ class PollChallengesTest(unittest.TestCase):
|
|||
self.assertEqual(authzr.body.status, messages.STATUS_PENDING)
|
||||
|
||||
@mock.patch("certbot.auth_handler.time")
|
||||
@mock.patch("certbot.auth_handler.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_poll_challenges_failure(self, unused_mock_time, unused_mock_zope):
|
||||
self.mock_net.poll.side_effect = self._mock_poll_solve_one_invalid
|
||||
self.assertRaises(
|
||||
|
|
@ -307,7 +309,6 @@ class PollChallengesTest(unittest.TestCase):
|
|||
|
||||
new_authzr = messages.AuthorizationResource(
|
||||
uri=authzr.uri,
|
||||
new_cert_uri=authzr.new_cert_uri,
|
||||
body=messages.Authorization(
|
||||
identifier=authzr.body.identifier,
|
||||
challenges=new_challbs,
|
||||
|
|
@ -412,7 +413,7 @@ class ReportFailedChallsTest(unittest.TestCase):
|
|||
domain="foo.bar",
|
||||
account_key="key")
|
||||
|
||||
@mock.patch("certbot.auth_handler.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_same_error_and_domain(self, mock_zope):
|
||||
from certbot import auth_handler
|
||||
|
||||
|
|
@ -421,7 +422,7 @@ class ReportFailedChallsTest(unittest.TestCase):
|
|||
self.assertTrue(len(call_list) == 1)
|
||||
self.assertTrue("Domain: example.com\nType: tls\nDetail: detail" in call_list[0][0][0])
|
||||
|
||||
@mock.patch("certbot.auth_handler.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_different_errors_and_domains(self, mock_zope):
|
||||
from certbot import auth_handler
|
||||
|
||||
|
|
@ -435,7 +436,7 @@ def gen_auth_resp(chall_list):
|
|||
for chall in chall_list]
|
||||
|
||||
|
||||
def gen_dom_authzr(domain, unused_new_authzr_uri, challs, combos=True):
|
||||
def gen_dom_authzr(domain, challs, combos=True):
|
||||
"""Generates new authzr for domains."""
|
||||
return acme_util.gen_authzr(
|
||||
messages.STATUS_PENDING, domain, challs,
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ class UpdateLiveSymlinksTest(BaseCertManagerTest):
|
|||
class DeleteTest(storage_test.BaseRenewableCertTest):
|
||||
"""Tests for certbot.cert_manager.delete
|
||||
"""
|
||||
@mock.patch('zope.component.getUtility')
|
||||
@test_util.patch_get_utility()
|
||||
@mock.patch('certbot.cert_manager.lineage_for_certname')
|
||||
@mock.patch('certbot.storage.delete_files')
|
||||
def test_delete(self, mock_delete_files, mock_lineage_for_certname, unused_get_utility):
|
||||
|
|
@ -135,14 +135,14 @@ class CertificatesTest(BaseCertManagerTest):
|
|||
return certificates(*args, **kwargs)
|
||||
|
||||
@mock.patch('certbot.cert_manager.logger')
|
||||
@mock.patch('zope.component.getUtility')
|
||||
@test_util.patch_get_utility()
|
||||
def test_certificates_parse_fail(self, mock_utility, mock_logger):
|
||||
self._certificates(self.cli_config)
|
||||
self.assertTrue(mock_logger.warning.called) #pylint: disable=no-member
|
||||
self.assertTrue(mock_utility.called)
|
||||
|
||||
@mock.patch('certbot.cert_manager.logger')
|
||||
@mock.patch('zope.component.getUtility')
|
||||
@test_util.patch_get_utility()
|
||||
def test_certificates_quiet(self, mock_utility, mock_logger):
|
||||
self.cli_config.quiet = True
|
||||
self._certificates(self.cli_config)
|
||||
|
|
@ -150,7 +150,7 @@ class CertificatesTest(BaseCertManagerTest):
|
|||
self.assertTrue(mock_logger.warning.called) #pylint: disable=no-member
|
||||
|
||||
@mock.patch('certbot.cert_manager.logger')
|
||||
@mock.patch('zope.component.getUtility')
|
||||
@test_util.patch_get_utility()
|
||||
@mock.patch("certbot.storage.RenewableCert")
|
||||
@mock.patch('certbot.cert_manager._report_human_readable')
|
||||
def test_certificates_parse_success(self, mock_report, mock_renewable_cert,
|
||||
|
|
@ -163,7 +163,7 @@ class CertificatesTest(BaseCertManagerTest):
|
|||
self.assertTrue(mock_renewable_cert.called)
|
||||
|
||||
@mock.patch('certbot.cert_manager.logger')
|
||||
@mock.patch('zope.component.getUtility')
|
||||
@test_util.patch_get_utility()
|
||||
def test_certificates_no_files(self, mock_utility, mock_logger):
|
||||
tempdir = tempfile.mkdtemp()
|
||||
|
||||
|
|
@ -268,11 +268,11 @@ class LineageForCertnameTest(BaseCertManagerTest):
|
|||
"""Tests for certbot.cert_manager.lineage_for_certname"""
|
||||
|
||||
@mock.patch('certbot.util.make_or_verify_dir')
|
||||
@mock.patch('certbot.storage.renewal_conf_files')
|
||||
@mock.patch('certbot.storage.renewal_file_for_certname')
|
||||
@mock.patch('certbot.storage.RenewableCert')
|
||||
def test_found_match(self, mock_renewable_cert, mock_renewal_conf_files,
|
||||
def test_found_match(self, mock_renewable_cert, mock_renewal_conf_file,
|
||||
mock_make_or_verify_dir):
|
||||
mock_renewal_conf_files.return_value = ["somefile.conf"]
|
||||
mock_renewal_conf_file.return_value = "somefile.conf"
|
||||
mock_match = mock.Mock(lineagename="example.com")
|
||||
mock_renewable_cert.return_value = mock_match
|
||||
from certbot import cert_manager
|
||||
|
|
@ -281,13 +281,10 @@ class LineageForCertnameTest(BaseCertManagerTest):
|
|||
self.assertTrue(mock_make_or_verify_dir.called)
|
||||
|
||||
@mock.patch('certbot.util.make_or_verify_dir')
|
||||
@mock.patch('certbot.storage.renewal_conf_files')
|
||||
@mock.patch('certbot.storage.RenewableCert')
|
||||
def test_no_match(self, mock_renewable_cert, mock_renewal_conf_files,
|
||||
@mock.patch('certbot.storage.renewal_file_for_certname')
|
||||
def test_no_match(self, mock_renewal_conf_file,
|
||||
mock_make_or_verify_dir):
|
||||
mock_renewal_conf_files.return_value = ["somefile.conf"]
|
||||
mock_match = mock.Mock(lineagename="other.com")
|
||||
mock_renewable_cert.return_value = mock_match
|
||||
mock_renewal_conf_file.return_value = "other.com.conf"
|
||||
from certbot import cert_manager
|
||||
self.assertEqual(cert_manager.lineage_for_certname(self.cli_config, "example.com"),
|
||||
None)
|
||||
|
|
@ -298,11 +295,11 @@ class DomainsForCertnameTest(BaseCertManagerTest):
|
|||
"""Tests for certbot.cert_manager.domains_for_certname"""
|
||||
|
||||
@mock.patch('certbot.util.make_or_verify_dir')
|
||||
@mock.patch('certbot.storage.renewal_conf_files')
|
||||
@mock.patch('certbot.storage.renewal_file_for_certname')
|
||||
@mock.patch('certbot.storage.RenewableCert')
|
||||
def test_found_match(self, mock_renewable_cert, mock_renewal_conf_files,
|
||||
def test_found_match(self, mock_renewable_cert, mock_renewal_conf_file,
|
||||
mock_make_or_verify_dir):
|
||||
mock_renewal_conf_files.return_value = ["somefile.conf"]
|
||||
mock_renewal_conf_file.return_value = "somefile.conf"
|
||||
mock_match = mock.Mock(lineagename="example.com")
|
||||
domains = ["example.com", "example.org"]
|
||||
mock_match.names.return_value = domains
|
||||
|
|
@ -313,15 +310,10 @@ class DomainsForCertnameTest(BaseCertManagerTest):
|
|||
self.assertTrue(mock_make_or_verify_dir.called)
|
||||
|
||||
@mock.patch('certbot.util.make_or_verify_dir')
|
||||
@mock.patch('certbot.storage.renewal_conf_files')
|
||||
@mock.patch('certbot.storage.RenewableCert')
|
||||
def test_no_match(self, mock_renewable_cert, mock_renewal_conf_files,
|
||||
@mock.patch('certbot.storage.renewal_file_for_certname')
|
||||
def test_no_match(self, mock_renewal_conf_file,
|
||||
mock_make_or_verify_dir):
|
||||
mock_renewal_conf_files.return_value = ["somefile.conf"]
|
||||
mock_match = mock.Mock(lineagename="example.com")
|
||||
domains = ["example.com", "example.org"]
|
||||
mock_match.names.return_value = domains
|
||||
mock_renewable_cert.return_value = mock_match
|
||||
mock_renewal_conf_file.return_value = "somefile.conf"
|
||||
from certbot import cert_manager
|
||||
self.assertEqual(cert_manager.domains_for_certname(self.cli_config, "other.com"),
|
||||
None)
|
||||
|
|
@ -348,7 +340,7 @@ class RenameLineageTest(BaseCertManagerTest):
|
|||
return cert_manager.rename_lineage(*args, **kwargs)
|
||||
|
||||
@mock.patch('certbot.storage.renewal_conf_files')
|
||||
@mock.patch('certbot.main.zope.component.getUtility')
|
||||
@test_util.patch_get_utility()
|
||||
def test_no_certname(self, mock_get_utility, mock_renewal_conf_files):
|
||||
mock_config = mock.Mock(certname=None, new_certname="two")
|
||||
|
||||
|
|
@ -365,7 +357,7 @@ class RenameLineageTest(BaseCertManagerTest):
|
|||
util_mock.menu.return_value = (display_util.OK, -1)
|
||||
self.assertRaises(errors.Error, self._call, mock_config)
|
||||
|
||||
@mock.patch('certbot.main.zope.component.getUtility')
|
||||
@test_util.patch_get_utility()
|
||||
def test_no_new_certname(self, mock_get_utility):
|
||||
mock_config = mock.Mock(certname="one", new_certname=None)
|
||||
|
||||
|
|
@ -379,7 +371,7 @@ class RenameLineageTest(BaseCertManagerTest):
|
|||
mock_get_utility.return_value = util_mock
|
||||
self.assertRaises(errors.Error, self._call, mock_config)
|
||||
|
||||
@mock.patch('certbot.main.zope.component.getUtility')
|
||||
@test_util.patch_get_utility()
|
||||
@mock.patch('certbot.cert_manager.lineage_for_certname')
|
||||
def test_no_existing_certname(self, mock_lineage_for_certname, unused_get_utility):
|
||||
mock_config = mock.Mock(certname="one", new_certname="two")
|
||||
|
|
@ -387,7 +379,7 @@ class RenameLineageTest(BaseCertManagerTest):
|
|||
self.assertRaises(errors.ConfigurationError,
|
||||
self._call, mock_config)
|
||||
|
||||
@mock.patch('certbot.main.zope.component.getUtility')
|
||||
@test_util.patch_get_utility()
|
||||
@mock.patch("certbot.storage.RenewableCert._check_symlinks")
|
||||
def test_rename_cert(self, mock_check, unused_get_utility):
|
||||
mock_check.return_value = True
|
||||
|
|
@ -398,7 +390,7 @@ class RenameLineageTest(BaseCertManagerTest):
|
|||
self.assertTrue(updated_lineage is not None)
|
||||
self.assertEqual(updated_lineage.lineagename, mock_config.new_certname)
|
||||
|
||||
@mock.patch('certbot.main.zope.component.getUtility')
|
||||
@test_util.patch_get_utility()
|
||||
@mock.patch("certbot.storage.RenewableCert._check_symlinks")
|
||||
def test_rename_cert_interactive_certname(self, mock_check, mock_get_utility):
|
||||
mock_check.return_value = True
|
||||
|
|
@ -413,7 +405,7 @@ class RenameLineageTest(BaseCertManagerTest):
|
|||
self.assertTrue(updated_lineage is not None)
|
||||
self.assertEqual(updated_lineage.lineagename, mock_config.new_certname)
|
||||
|
||||
@mock.patch('certbot.main.zope.component.getUtility')
|
||||
@test_util.patch_get_utility()
|
||||
@mock.patch("certbot.storage.RenewableCert._check_symlinks")
|
||||
def test_rename_cert_bad_new_certname(self, mock_check, unused_get_utility):
|
||||
mock_check.return_value = True
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
"""Tests for certbot.cli."""
|
||||
import argparse
|
||||
import functools
|
||||
import unittest
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import six
|
||||
import mock
|
||||
import six
|
||||
from six.moves import reload_module # pylint: disable=import-error
|
||||
|
||||
from acme import challenges
|
||||
|
||||
from certbot import cli
|
||||
from certbot import constants
|
||||
from certbot import errors
|
||||
from certbot.plugins import disco
|
||||
|
||||
def reset_set_by_cli():
|
||||
'''Reset the state of the `set_by_cli` function'''
|
||||
cli.set_by_cli.detector = None
|
||||
PLUGINS = disco.PluginsRegistry.find_all()
|
||||
|
||||
|
||||
class TestReadFile(unittest.TestCase):
|
||||
'''Test cli.read_file'''
|
||||
|
|
@ -43,16 +43,16 @@ class ParseTest(unittest.TestCase):
|
|||
|
||||
_multiprocess_can_split_ = True
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.plugins = disco.PluginsRegistry.find_all()
|
||||
cls.parse = functools.partial(cli.prepare_and_parse_args, cls.plugins)
|
||||
|
||||
def setUp(self):
|
||||
reset_set_by_cli()
|
||||
reload_module(cli)
|
||||
|
||||
@staticmethod
|
||||
def parse(*args, **kwargs):
|
||||
"""Get result of cli.prepare_and_parse_args."""
|
||||
return cli.prepare_and_parse_args(PLUGINS, *args, **kwargs)
|
||||
|
||||
def _help_output(self, args):
|
||||
"Run a command, and return the ouput string for scrutiny"
|
||||
"Run a command, and return the output string for scrutiny"
|
||||
|
||||
output = six.StringIO()
|
||||
with mock.patch('certbot.main.sys.stdout', new=output):
|
||||
|
|
@ -60,6 +60,11 @@ class ParseTest(unittest.TestCase):
|
|||
self.assertRaises(SystemExit, self.parse, args, output)
|
||||
return output.getvalue()
|
||||
|
||||
def test_no_args(self):
|
||||
namespace = self.parse([])
|
||||
for d in ('config_dir', 'logs_dir', 'work_dir'):
|
||||
self.assertEqual(getattr(namespace, d), cli.flag_default(d))
|
||||
|
||||
def test_install_abspath(self):
|
||||
cert = 'cert'
|
||||
key = 'key'
|
||||
|
|
@ -88,7 +93,7 @@ class ParseTest(unittest.TestCase):
|
|||
self.assertTrue("{0}" not in out)
|
||||
|
||||
out = self._help_output(['-h', 'nginx'])
|
||||
if "nginx" in self.plugins:
|
||||
if "nginx" in PLUGINS:
|
||||
# may be false while building distributions without plugins
|
||||
self.assertTrue("--nginx-ctl" in out)
|
||||
self.assertTrue("--webroot-path" not in out)
|
||||
|
|
@ -96,7 +101,7 @@ class ParseTest(unittest.TestCase):
|
|||
|
||||
out = self._help_output(['-h'])
|
||||
self.assertTrue("letsencrypt-auto" not in out) # test cli.cli_command
|
||||
if "nginx" in self.plugins:
|
||||
if "nginx" in PLUGINS:
|
||||
self.assertTrue("Use the Nginx plugin" in out)
|
||||
else:
|
||||
self.assertTrue("(the certbot nginx plugin is not" in out)
|
||||
|
|
@ -121,6 +126,7 @@ class ParseTest(unittest.TestCase):
|
|||
out = self._help_output(['--help', 'revoke'])
|
||||
self.assertTrue("--cert-path" in out)
|
||||
self.assertTrue("--key-path" in out)
|
||||
self.assertTrue("--reason" in out)
|
||||
|
||||
out = self._help_output(['-h', 'config_changes'])
|
||||
self.assertTrue("--cert-path" not in out)
|
||||
|
|
@ -132,6 +138,26 @@ class ParseTest(unittest.TestCase):
|
|||
self.assertTrue("%s" not in out)
|
||||
self.assertTrue("{0}" not in out)
|
||||
|
||||
def test_help_no_dashes(self):
|
||||
self._help_output(['help']) # assert SystemExit is raised here
|
||||
|
||||
out = self._help_output(['help', 'all'])
|
||||
self.assertTrue("--configurator" in out)
|
||||
self.assertTrue("how a cert is deployed" in out)
|
||||
self.assertTrue("--webroot-path" in out)
|
||||
self.assertTrue("--text" not in out)
|
||||
self.assertTrue("--dialog" not in out)
|
||||
self.assertTrue("%s" not in out)
|
||||
self.assertTrue("{0}" not in out)
|
||||
|
||||
out = self._help_output(['help', 'install'])
|
||||
self.assertTrue("--cert-path" in out)
|
||||
self.assertTrue("--key-path" in out)
|
||||
|
||||
out = self._help_output(['help', 'revoke'])
|
||||
self.assertTrue("--cert-path" in out)
|
||||
self.assertTrue("--key-path" in out)
|
||||
|
||||
def test_parse_domains(self):
|
||||
short_args = ['-d', 'example.com']
|
||||
namespace = self.parse(short_args)
|
||||
|
|
@ -159,12 +185,12 @@ class ParseTest(unittest.TestCase):
|
|||
self.assertEqual(namespace.domains, ['example.com', 'another.net'])
|
||||
|
||||
def test_preferred_challenges(self):
|
||||
from acme.challenges import HTTP01, TLSSNI01, DNS01
|
||||
|
||||
short_args = ['--preferred-challenges', 'http, tls-sni-01, dns']
|
||||
namespace = self.parse(short_args)
|
||||
|
||||
self.assertEqual(namespace.pref_challs, [HTTP01, TLSSNI01, DNS01])
|
||||
expected = [challenges.HTTP01.typ,
|
||||
challenges.TLSSNI01.typ, challenges.DNS01.typ]
|
||||
self.assertEqual(namespace.pref_challs, expected)
|
||||
|
||||
short_args = ['--preferred-challenges', 'jumping-over-the-moon']
|
||||
self.assertRaises(argparse.ArgumentTypeError, self.parse, short_args)
|
||||
|
|
@ -262,6 +288,14 @@ class ParseTest(unittest.TestCase):
|
|||
self.assertFalse(cli.option_was_set(
|
||||
config_dir_option, cli.flag_default(config_dir_option)))
|
||||
|
||||
def test_encode_revocation_reason(self):
|
||||
for reason, code in constants.REVOCATION_REASONS.items():
|
||||
namespace = self.parse(['--reason', reason])
|
||||
self.assertEqual(namespace.reason, code)
|
||||
for reason, code in constants.REVOCATION_REASONS.items():
|
||||
namespace = self.parse(['--reason', reason.upper()])
|
||||
self.assertEqual(namespace.reason, code)
|
||||
|
||||
def test_force_interactive(self):
|
||||
self.assertRaises(
|
||||
errors.Error, self.parse, "renew --force-interactive".split())
|
||||
|
|
|
|||
|
|
@ -45,20 +45,25 @@ class RegisterTest(unittest.TestCase):
|
|||
def test_no_tos(self):
|
||||
with mock.patch("certbot.client.acme_client.Client") as mock_client:
|
||||
mock_client.register().terms_of_service = "http://tos"
|
||||
with mock.patch("certbot.account.report_new_account"):
|
||||
self.tos_cb.return_value = False
|
||||
self.assertRaises(errors.Error, self._call)
|
||||
with mock.patch("certbot.eff.handle_subscription") as mock_handle:
|
||||
with mock.patch("certbot.account.report_new_account"):
|
||||
self.tos_cb.return_value = False
|
||||
self.assertRaises(errors.Error, self._call)
|
||||
self.assertFalse(mock_handle.called)
|
||||
|
||||
self.tos_cb.return_value = True
|
||||
self._call()
|
||||
self.tos_cb.return_value = True
|
||||
self._call()
|
||||
self.assertTrue(mock_handle.called)
|
||||
|
||||
self.tos_cb = None
|
||||
self._call()
|
||||
self.tos_cb = None
|
||||
self._call()
|
||||
self.assertEqual(mock_handle.call_count, 2)
|
||||
|
||||
def test_it(self):
|
||||
with mock.patch("certbot.client.acme_client.Client"):
|
||||
with mock.patch("certbot.account.report_new_account"):
|
||||
self._call()
|
||||
with mock.patch("certbot.eff.handle_subscription"):
|
||||
self._call()
|
||||
|
||||
@mock.patch("certbot.account.report_new_account")
|
||||
@mock.patch("certbot.client.display_ops.get_email")
|
||||
|
|
@ -68,9 +73,11 @@ class RegisterTest(unittest.TestCase):
|
|||
msg = "DNS problem: NXDOMAIN looking up MX for example.com"
|
||||
mx_err = messages.Error.with_code('invalidContact', detail=msg)
|
||||
with mock.patch("certbot.client.acme_client.Client") as mock_client:
|
||||
mock_client().register.side_effect = [mx_err, mock.MagicMock()]
|
||||
self._call()
|
||||
self.assertEqual(mock_get_email.call_count, 1)
|
||||
with mock.patch("certbot.eff.handle_subscription") as mock_handle:
|
||||
mock_client().register.side_effect = [mx_err, mock.MagicMock()]
|
||||
self._call()
|
||||
self.assertEqual(mock_get_email.call_count, 1)
|
||||
self.assertTrue(mock_handle.called)
|
||||
|
||||
@mock.patch("certbot.account.report_new_account")
|
||||
def test_email_invalid_noninteractive(self, _rep):
|
||||
|
|
@ -78,8 +85,9 @@ class RegisterTest(unittest.TestCase):
|
|||
msg = "DNS problem: NXDOMAIN looking up MX for example.com"
|
||||
mx_err = messages.Error.with_code('invalidContact', detail=msg)
|
||||
with mock.patch("certbot.client.acme_client.Client") as mock_client:
|
||||
mock_client().register.side_effect = [mx_err, mock.MagicMock()]
|
||||
self.assertRaises(errors.Error, self._call)
|
||||
with mock.patch("certbot.eff.handle_subscription"):
|
||||
mock_client().register.side_effect = [mx_err, mock.MagicMock()]
|
||||
self.assertRaises(errors.Error, self._call)
|
||||
|
||||
def test_needs_email(self):
|
||||
self.config.email = None
|
||||
|
|
@ -87,21 +95,25 @@ class RegisterTest(unittest.TestCase):
|
|||
|
||||
@mock.patch("certbot.client.logger")
|
||||
def test_without_email(self, mock_logger):
|
||||
with mock.patch("certbot.client.acme_client.Client"):
|
||||
with mock.patch("certbot.account.report_new_account"):
|
||||
self.config.email = None
|
||||
self.config.register_unsafely_without_email = True
|
||||
self.config.dry_run = False
|
||||
self._call()
|
||||
mock_logger.warning.assert_called_once_with(mock.ANY)
|
||||
with mock.patch("certbot.eff.handle_subscription") as mock_handle:
|
||||
with mock.patch("certbot.client.acme_client.Client"):
|
||||
with mock.patch("certbot.account.report_new_account"):
|
||||
self.config.email = None
|
||||
self.config.register_unsafely_without_email = True
|
||||
self.config.dry_run = False
|
||||
self._call()
|
||||
mock_logger.warning.assert_called_once_with(mock.ANY)
|
||||
self.assertTrue(mock_handle.called)
|
||||
|
||||
def test_unsupported_error(self):
|
||||
from acme import messages
|
||||
msg = "Test"
|
||||
mx_err = messages.Error(detail=msg, typ="malformed", title="title")
|
||||
with mock.patch("certbot.client.acme_client.Client") as mock_client:
|
||||
mock_client().register.side_effect = [mx_err, mock.MagicMock()]
|
||||
self.assertRaises(messages.Error, self._call)
|
||||
with mock.patch("certbot.eff.handle_subscription") as mock_handle:
|
||||
mock_client().register.side_effect = [mx_err, mock.MagicMock()]
|
||||
self.assertRaises(messages.Error, self._call)
|
||||
self.assertFalse(mock_handle.called)
|
||||
|
||||
|
||||
class ClientTestCommon(unittest.TestCase):
|
||||
|
|
@ -454,7 +466,7 @@ class ClientTest(ClientTestCommon):
|
|||
["foo.bar"], "key", "cert", "chain", "fullchain")
|
||||
installer.recovery_routine.assert_called_once_with()
|
||||
|
||||
@mock.patch("certbot.client.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_deploy_certificate_restart_failure(self, mock_get_utility):
|
||||
installer = mock.MagicMock()
|
||||
installer.restart.side_effect = [errors.PluginError, None]
|
||||
|
|
@ -466,7 +478,7 @@ class ClientTest(ClientTestCommon):
|
|||
installer.rollback_checkpoints.assert_called_once_with()
|
||||
self.assertEqual(installer.restart.call_count, 2)
|
||||
|
||||
@mock.patch("certbot.client.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_deploy_certificate_restart_failure2(self, mock_get_utility):
|
||||
installer = mock.MagicMock()
|
||||
installer.restart.side_effect = errors.PluginError
|
||||
|
|
@ -569,7 +581,7 @@ class EnhanceConfigTest(ClientTestCommon):
|
|||
|
||||
def _test_error(self):
|
||||
self.config.redirect = True
|
||||
with mock.patch("certbot.client.zope.component.getUtility") as mock_gu:
|
||||
with test_util.patch_get_utility() as mock_gu:
|
||||
self.assertRaises(
|
||||
errors.PluginError, self._test_with_all_supported)
|
||||
self.assertEqual(mock_gu().add_message.call_count, 1)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ class NamespaceConfigTest(unittest.TestCase):
|
|||
|
||||
def setUp(self):
|
||||
self.namespace = mock.MagicMock(
|
||||
config_dir='/tmp/config', work_dir='/tmp/foo', foo='bar',
|
||||
config_dir='/tmp/config', work_dir='/tmp/foo',
|
||||
logs_dir="/tmp/bar", foo='bar',
|
||||
server='https://acme-server.org:443/new',
|
||||
tls_sni_01_port=1234, http01_port=4321)
|
||||
from certbot.configuration import NamespaceConfig
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ from acme import messages
|
|||
|
||||
from certbot import account
|
||||
from certbot import errors
|
||||
from certbot import interfaces
|
||||
|
||||
from certbot.display import util as display_util
|
||||
|
||||
|
|
@ -26,59 +25,66 @@ KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
|
|||
class GetEmailTest(unittest.TestCase):
|
||||
"""Tests for certbot.display.ops.get_email."""
|
||||
|
||||
def setUp(self):
|
||||
mock_display = mock.MagicMock()
|
||||
self.input = mock_display.input
|
||||
zope.component.provideUtility(mock_display, interfaces.IDisplay)
|
||||
|
||||
@classmethod
|
||||
def _call(cls, **kwargs):
|
||||
from certbot.display.ops import get_email
|
||||
return get_email(**kwargs)
|
||||
|
||||
def test_cancel_none(self):
|
||||
self.input.return_value = (display_util.CANCEL, "foo@bar.baz")
|
||||
@test_util.patch_get_utility("certbot.display.ops.z_util")
|
||||
def test_cancel_none(self, mock_get_utility):
|
||||
mock_input = mock_get_utility().input
|
||||
mock_input.return_value = (display_util.CANCEL, "foo@bar.baz")
|
||||
self.assertRaises(errors.Error, self._call)
|
||||
self.assertRaises(errors.Error, self._call, optional=False)
|
||||
|
||||
def test_ok_safe(self):
|
||||
self.input.return_value = (display_util.OK, "foo@bar.baz")
|
||||
@test_util.patch_get_utility("certbot.display.ops.z_util")
|
||||
def test_ok_safe(self, mock_get_utility):
|
||||
mock_input = mock_get_utility().input
|
||||
mock_input.return_value = (display_util.OK, "foo@bar.baz")
|
||||
with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
|
||||
mock_safe_email.return_value = True
|
||||
self.assertTrue(self._call() is "foo@bar.baz")
|
||||
|
||||
def test_ok_not_safe(self):
|
||||
self.input.return_value = (display_util.OK, "foo@bar.baz")
|
||||
@test_util.patch_get_utility("certbot.display.ops.z_util")
|
||||
def test_ok_not_safe(self, mock_get_utility):
|
||||
mock_input = mock_get_utility().input
|
||||
mock_input.return_value = (display_util.OK, "foo@bar.baz")
|
||||
with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
|
||||
mock_safe_email.side_effect = [False, True]
|
||||
self.assertTrue(self._call() is "foo@bar.baz")
|
||||
|
||||
def test_invalid_flag(self):
|
||||
@test_util.patch_get_utility("certbot.display.ops.z_util")
|
||||
def test_invalid_flag(self, mock_get_utility):
|
||||
invalid_txt = "There seem to be problems"
|
||||
self.input.return_value = (display_util.OK, "foo@bar.baz")
|
||||
mock_input = mock_get_utility().input
|
||||
mock_input.return_value = (display_util.OK, "foo@bar.baz")
|
||||
with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
|
||||
mock_safe_email.return_value = True
|
||||
self._call()
|
||||
self.assertTrue(invalid_txt not in self.input.call_args[0][0])
|
||||
self.assertTrue(invalid_txt not in mock_input.call_args[0][0])
|
||||
self._call(invalid=True)
|
||||
self.assertTrue(invalid_txt in self.input.call_args[0][0])
|
||||
self.assertTrue(invalid_txt in mock_input.call_args[0][0])
|
||||
|
||||
def test_optional_flag(self):
|
||||
self.input.return_value = (display_util.OK, "foo@bar.baz")
|
||||
@test_util.patch_get_utility("certbot.display.ops.z_util")
|
||||
def test_optional_flag(self, mock_get_utility):
|
||||
mock_input = mock_get_utility().input
|
||||
mock_input.return_value = (display_util.OK, "foo@bar.baz")
|
||||
with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
|
||||
mock_safe_email.side_effect = [False, True]
|
||||
self._call(optional=False)
|
||||
for call in self.input.call_args_list:
|
||||
for call in mock_input.call_args_list:
|
||||
self.assertTrue(
|
||||
"--register-unsafely-without-email" not in call[0][0])
|
||||
|
||||
def test_optional_invalid_unsafe(self):
|
||||
@test_util.patch_get_utility("certbot.display.ops.z_util")
|
||||
def test_optional_invalid_unsafe(self, mock_get_utility):
|
||||
invalid_txt = "There seem to be problems"
|
||||
self.input.return_value = (display_util.OK, "foo@bar.baz")
|
||||
mock_input = mock_get_utility().input
|
||||
mock_input.return_value = (display_util.OK, "foo@bar.baz")
|
||||
with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
|
||||
mock_safe_email.side_effect = [False, True]
|
||||
self._call(invalid=True)
|
||||
self.assertTrue(invalid_txt in self.input.call_args[0][0])
|
||||
self.assertTrue(invalid_txt in mock_input.call_args[0][0])
|
||||
|
||||
|
||||
class ChooseAccountTest(unittest.TestCase):
|
||||
|
|
@ -98,10 +104,10 @@ class ChooseAccountTest(unittest.TestCase):
|
|||
self.key = KEY
|
||||
|
||||
self.acc1 = account.Account(messages.RegistrationResource(
|
||||
uri=None, new_authzr_uri=None, body=messages.Registration.from_data(
|
||||
uri=None, body=messages.Registration.from_data(
|
||||
email="email1@g.com")), self.key)
|
||||
self.acc2 = account.Account(messages.RegistrationResource(
|
||||
uri=None, new_authzr_uri=None, body=messages.Registration.from_data(
|
||||
uri=None, body=messages.Registration.from_data(
|
||||
email="email2@g.com", phone="phone")), self.key)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -109,17 +115,17 @@ class ChooseAccountTest(unittest.TestCase):
|
|||
from certbot.display import ops
|
||||
return ops.choose_account(accounts)
|
||||
|
||||
@mock.patch("certbot.display.ops.z_util")
|
||||
@test_util.patch_get_utility("certbot.display.ops.z_util")
|
||||
def test_one(self, mock_util):
|
||||
mock_util().menu.return_value = (display_util.OK, 0)
|
||||
self.assertEqual(self._call([self.acc1]), self.acc1)
|
||||
|
||||
@mock.patch("certbot.display.ops.z_util")
|
||||
@test_util.patch_get_utility("certbot.display.ops.z_util")
|
||||
def test_two(self, mock_util):
|
||||
mock_util().menu.return_value = (display_util.OK, 1)
|
||||
self.assertEqual(self._call([self.acc1, self.acc2]), self.acc2)
|
||||
|
||||
@mock.patch("certbot.display.ops.z_util")
|
||||
@test_util.patch_get_utility("certbot.display.ops.z_util")
|
||||
def test_cancel(self, mock_util):
|
||||
mock_util().menu.return_value = (display_util.CANCEL, 1)
|
||||
self.assertTrue(self._call([self.acc1, self.acc2]) is None)
|
||||
|
|
@ -210,12 +216,12 @@ class ChooseNamesTest(unittest.TestCase):
|
|||
self._call(None)
|
||||
self.assertEqual(mock_manual.call_count, 1)
|
||||
|
||||
@mock.patch("certbot.display.ops.z_util")
|
||||
@test_util.patch_get_utility("certbot.display.ops.z_util")
|
||||
def test_no_installer_cancel(self, mock_util):
|
||||
mock_util().input.return_value = (display_util.CANCEL, [])
|
||||
self.assertEqual(self._call(None), [])
|
||||
|
||||
@mock.patch("certbot.display.ops.z_util")
|
||||
@test_util.patch_get_utility("certbot.display.ops.z_util")
|
||||
def test_no_names_choose(self, mock_util):
|
||||
self.mock_install().get_all_names.return_value = set()
|
||||
domain = "example.com"
|
||||
|
|
@ -266,7 +272,7 @@ class ChooseNamesTest(unittest.TestCase):
|
|||
self.assertEqual(_sort_names(to_sort), sortd)
|
||||
|
||||
|
||||
@mock.patch("certbot.display.ops.z_util")
|
||||
@test_util.patch_get_utility("certbot.display.ops.z_util")
|
||||
def test_filter_names_valid_return(self, mock_util):
|
||||
self.mock_install.get_all_names.return_value = set(["example.com"])
|
||||
mock_util().checklist.return_value = (display_util.OK, ["example.com"])
|
||||
|
|
@ -275,14 +281,14 @@ class ChooseNamesTest(unittest.TestCase):
|
|||
self.assertEqual(names, ["example.com"])
|
||||
self.assertEqual(mock_util().checklist.call_count, 1)
|
||||
|
||||
@mock.patch("certbot.display.ops.z_util")
|
||||
@test_util.patch_get_utility("certbot.display.ops.z_util")
|
||||
def test_filter_names_nothing_selected(self, mock_util):
|
||||
self.mock_install.get_all_names.return_value = set(["example.com"])
|
||||
mock_util().checklist.return_value = (display_util.OK, [])
|
||||
|
||||
self.assertEqual(self._call(self.mock_install), [])
|
||||
|
||||
@mock.patch("certbot.display.ops.z_util")
|
||||
@test_util.patch_get_utility("certbot.display.ops.z_util")
|
||||
def test_filter_names_cancel(self, mock_util):
|
||||
self.mock_install.get_all_names.return_value = set(["example.com"])
|
||||
mock_util().checklist.return_value = (
|
||||
|
|
@ -301,7 +307,7 @@ class ChooseNamesTest(unittest.TestCase):
|
|||
self.assertEqual(get_valid_domains(all_invalid), [])
|
||||
self.assertEqual(len(get_valid_domains(two_valid)), 2)
|
||||
|
||||
@mock.patch("certbot.display.ops.z_util")
|
||||
@test_util.patch_get_utility("certbot.display.ops.z_util")
|
||||
def test_choose_manually(self, mock_util):
|
||||
from certbot.display.ops import _choose_names_manually
|
||||
# No retry
|
||||
|
|
@ -344,7 +350,7 @@ class SuccessInstallationTest(unittest.TestCase):
|
|||
from certbot.display.ops import success_installation
|
||||
success_installation(names)
|
||||
|
||||
@mock.patch("certbot.display.ops.z_util")
|
||||
@test_util.patch_get_utility("certbot.display.ops.z_util")
|
||||
def test_success_installation(self, mock_util):
|
||||
mock_util().notification.return_value = None
|
||||
names = ["example.com", "abc.com"]
|
||||
|
|
@ -366,7 +372,7 @@ class SuccessRenewalTest(unittest.TestCase):
|
|||
from certbot.display.ops import success_renewal
|
||||
success_renewal(names)
|
||||
|
||||
@mock.patch("certbot.display.ops.z_util")
|
||||
@test_util.patch_get_utility("certbot.display.ops.z_util")
|
||||
def test_success_renewal(self, mock_util):
|
||||
mock_util().notification.return_value = None
|
||||
names = ["example.com", "abc.com"]
|
||||
|
|
@ -387,12 +393,16 @@ class SuccessRevocationTest(unittest.TestCase):
|
|||
from certbot.display.ops import success_revocation
|
||||
success_revocation(path)
|
||||
|
||||
@mock.patch("certbot.display.ops.z_util")
|
||||
@test_util.patch_get_utility("certbot.display.ops.z_util")
|
||||
def test_success_revocation(self, mock_util):
|
||||
mock_util().notification.return_value = None
|
||||
path = "/path/to/cert.pem"
|
||||
self._call(path)
|
||||
mock_util().notification.assert_called_once()
|
||||
mock_util().notification.assert_called_once_with(
|
||||
"Congratulations! You have successfully revoked the certificate "
|
||||
"that was located at {0}{1}{1}".format(
|
||||
path,
|
||||
os.linesep), pause=False)
|
||||
self.assertTrue(path in mock_util().notification.call_args[0][0])
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
"""Test :mod:`certbot.display.util`."""
|
||||
import inspect
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
import certbot.errors as errors
|
||||
from certbot import errors
|
||||
from certbot import interfaces
|
||||
|
||||
from certbot.display import util as display_util
|
||||
|
||||
|
|
@ -91,9 +93,16 @@ class FileOutputDisplayTest(unittest.TestCase):
|
|||
self.assertEqual(input_, default)
|
||||
|
||||
def test_input_assertion_fail(self):
|
||||
self.assertRaises(AssertionError, self._force_noninteractive,
|
||||
# If the call to util.assert_valid_call is commented out, an
|
||||
# error.Error is raised, otherwise, an AssertionError is raised.
|
||||
self.assertRaises(Exception, self._force_noninteractive,
|
||||
self.displayer.input, "message", cli_flag="--flag")
|
||||
|
||||
def test_input_assertion_fail2(self):
|
||||
with mock.patch("certbot.display.util.assert_valid_call"):
|
||||
self.assertRaises(errors.Error, self._force_noninteractive,
|
||||
self.displayer.input, "msg", cli_flag="--flag")
|
||||
|
||||
def test_yesno(self):
|
||||
with mock.patch("six.moves.input", return_value="Yes"):
|
||||
self.assertTrue(self.displayer.yesno(
|
||||
|
|
@ -259,6 +268,13 @@ class FileOutputDisplayTest(unittest.TestCase):
|
|||
self.displayer._get_valid_int_ans(3),
|
||||
(display_util.CANCEL, -1))
|
||||
|
||||
def test_methods_take_force_interactive(self):
|
||||
# Every IDisplay method implemented by FileDisplay must take
|
||||
# force_interactive to prevent workflow regressions.
|
||||
for name in interfaces.IDisplay.names(): # pylint: disable=no-member
|
||||
arg_spec = inspect.getargspec(getattr(self.displayer, name))
|
||||
self.assertTrue("force_interactive" in arg_spec.args)
|
||||
|
||||
|
||||
class NoninteractiveDisplayTest(unittest.TestCase):
|
||||
"""Test non-interactive display.
|
||||
|
|
@ -309,6 +325,16 @@ class NoninteractiveDisplayTest(unittest.TestCase):
|
|||
self.assertRaises(
|
||||
errors.MissingCommandlineFlag, self.displayer.directory_select, "msg")
|
||||
|
||||
def test_methods_take_kwargs(self):
|
||||
# Every IDisplay method implemented by NoninteractiveDisplay
|
||||
# should take **kwargs because every method of FileDisplay must
|
||||
# take force_interactive which doesn't apply to
|
||||
# NoninteractiveDisplay.
|
||||
for name in interfaces.IDisplay.names(): # pylint: disable=no-member
|
||||
method = getattr(self.displayer, name)
|
||||
# asserts method accepts arbitrary keyword arguments
|
||||
self.assertFalse(inspect.getargspec(method).keywords is None)
|
||||
|
||||
|
||||
class SeparateListInputTest(unittest.TestCase):
|
||||
"""Test Module functions."""
|
||||
|
|
|
|||
135
certbot/tests/eff_test.py
Normal file
135
certbot/tests/eff_test.py
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
"""Tests for certbot.eff."""
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from certbot import constants
|
||||
from certbot.tests import util
|
||||
|
||||
|
||||
class HandleSubscriptionTest(unittest.TestCase):
|
||||
"""Tests for certbot.eff.handle_subscription."""
|
||||
def setUp(self):
|
||||
self.email = 'certbot@example.org'
|
||||
self.config = mock.Mock(email=self.email, eff_email=None)
|
||||
|
||||
def _call(self):
|
||||
from certbot.eff import handle_subscription
|
||||
return handle_subscription(self.config)
|
||||
|
||||
@util.patch_get_utility()
|
||||
@mock.patch('certbot.eff.subscribe')
|
||||
def test_failure(self, mock_subscribe, mock_get_utility):
|
||||
self.config.email = None
|
||||
self.config.eff_email = True
|
||||
self._call()
|
||||
self.assertFalse(mock_subscribe.called)
|
||||
self.assertFalse(mock_get_utility().yesno.called)
|
||||
actual = mock_get_utility().add_message.call_args[0][0]
|
||||
expected_part = "because you didn't provide an e-mail address"
|
||||
self.assertTrue(expected_part in actual)
|
||||
|
||||
@mock.patch('certbot.eff.subscribe')
|
||||
def test_no_subscribe_with_no_prompt(self, mock_subscribe):
|
||||
self.config.eff_email = False
|
||||
with util.patch_get_utility() as mock_get_utility:
|
||||
self._call()
|
||||
self.assertFalse(mock_subscribe.called)
|
||||
self._assert_no_get_utility_calls(mock_get_utility)
|
||||
|
||||
@util.patch_get_utility()
|
||||
@mock.patch('certbot.eff.subscribe')
|
||||
def test_subscribe_with_no_prompt(self, mock_subscribe, mock_get_utility):
|
||||
self.config.eff_email = True
|
||||
self._call()
|
||||
self._assert_subscribed(mock_subscribe)
|
||||
self._assert_no_get_utility_calls(mock_get_utility)
|
||||
|
||||
def _assert_no_get_utility_calls(self, mock_get_utility):
|
||||
self.assertFalse(mock_get_utility().yesno.called)
|
||||
self.assertFalse(mock_get_utility().add_message.called)
|
||||
|
||||
@util.patch_get_utility()
|
||||
@mock.patch('certbot.eff.subscribe')
|
||||
def test_subscribe_with_prompt(self, mock_subscribe, mock_get_utility):
|
||||
mock_get_utility().yesno.return_value = True
|
||||
self._call()
|
||||
self._assert_subscribed(mock_subscribe)
|
||||
self.assertFalse(mock_get_utility().add_message.called)
|
||||
self._assert_correct_yesno_call(mock_get_utility)
|
||||
|
||||
def _assert_subscribed(self, mock_subscribe):
|
||||
self.assertTrue(mock_subscribe.called)
|
||||
self.assertEqual(mock_subscribe.call_args[0][0], self.email)
|
||||
|
||||
@util.patch_get_utility()
|
||||
@mock.patch('certbot.eff.subscribe')
|
||||
def test_no_subscribe_with_prompt(self, mock_subscribe, mock_get_utility):
|
||||
mock_get_utility().yesno.return_value = False
|
||||
self._call()
|
||||
self.assertFalse(mock_subscribe.called)
|
||||
self.assertFalse(mock_get_utility().add_message.called)
|
||||
self._assert_correct_yesno_call(mock_get_utility)
|
||||
|
||||
def _assert_correct_yesno_call(self, mock_get_utility):
|
||||
self.assertTrue(mock_get_utility().yesno.called)
|
||||
call_args, call_kwargs = mock_get_utility().yesno.call_args
|
||||
actual = call_args[0]
|
||||
expected_part = 'Electronic Frontier Foundation'
|
||||
self.assertTrue(expected_part in actual)
|
||||
self.assertFalse(call_kwargs.get('default', True))
|
||||
|
||||
|
||||
class SubscribeTest(unittest.TestCase):
|
||||
"""Tests for certbot.eff.subscribe."""
|
||||
def setUp(self):
|
||||
self.email = 'certbot@example.org'
|
||||
self.json = {'status': True}
|
||||
self.response = mock.Mock(ok=True)
|
||||
self.response.json.return_value = self.json
|
||||
|
||||
@mock.patch('certbot.eff.requests.post')
|
||||
def _call(self, mock_post):
|
||||
mock_post.return_value = self.response
|
||||
|
||||
from certbot.eff import subscribe
|
||||
subscribe(self.email)
|
||||
self._check_post_call(mock_post)
|
||||
|
||||
def _check_post_call(self, mock_post):
|
||||
self.assertEqual(mock_post.call_count, 1)
|
||||
call_args, call_kwargs = mock_post.call_args
|
||||
self.assertEqual(call_args[0], constants.EFF_SUBSCRIBE_URI)
|
||||
|
||||
data = call_kwargs.get('data')
|
||||
self.assertFalse(data is None)
|
||||
self.assertEqual(data.get('email'), self.email)
|
||||
|
||||
@util.patch_get_utility()
|
||||
def test_bad_status(self, mock_get_utility):
|
||||
self.json['status'] = False
|
||||
self._call() # pylint: disable=no-value-for-parameter
|
||||
actual = self._get_reported_message(mock_get_utility)
|
||||
expected_part = 'because your e-mail address appears to be invalid.'
|
||||
self.assertTrue(expected_part in actual)
|
||||
|
||||
@util.patch_get_utility()
|
||||
def test_not_ok(self, mock_get_utility):
|
||||
self.response.ok = False
|
||||
self._call() # pylint: disable=no-value-for-parameter
|
||||
actual = self._get_reported_message(mock_get_utility)
|
||||
unexpected_part = 'because'
|
||||
self.assertFalse(unexpected_part in actual)
|
||||
|
||||
def _get_reported_message(self, mock_get_utility):
|
||||
self.assertTrue(mock_get_utility().add_message.called)
|
||||
return mock_get_utility().add_message.call_args[0][0]
|
||||
|
||||
@util.patch_get_utility()
|
||||
def test_subscribe(self, mock_get_utility):
|
||||
self._call() # pylint: disable=no-value-for-parameter
|
||||
self.assertFalse(mock_get_utility.called)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
|
|
@ -70,7 +70,7 @@ class ErrorHandlerTest(unittest.TestCase):
|
|||
send_signal(self.signals[0])
|
||||
should_be_42 *= 10
|
||||
|
||||
# check exectuion stoped when the signal was sent
|
||||
# check execution stoped when the signal was sent
|
||||
self.assertEqual(42, should_be_42)
|
||||
# assert signals were caught
|
||||
self.assertEqual([self.signals[0]], signals_received)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from certbot import achallenges
|
|||
from certbot.tests import acme_util
|
||||
|
||||
|
||||
class FaiiledChallengesTest(unittest.TestCase):
|
||||
class FailedChallengesTest(unittest.TestCase):
|
||||
"""Tests for certbot.errors.FailedChallenges."""
|
||||
|
||||
def setUp(self):
|
||||
|
|
|
|||
|
|
@ -5,16 +5,14 @@ import os
|
|||
import unittest
|
||||
|
||||
import mock
|
||||
from six.moves import reload_module # pylint: disable=import-error
|
||||
|
||||
from certbot import errors
|
||||
from certbot import hooks
|
||||
|
||||
class HookTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
reload_module(hooks)
|
||||
|
||||
@mock.patch('certbot.hooks._prog')
|
||||
def test_validate_hooks(self, mock_prog):
|
||||
|
|
@ -27,53 +25,56 @@ class HookTest(unittest.TestCase):
|
|||
config = mock.MagicMock(pre_hook="explodinator", post_hook="", renew_hook="")
|
||||
self.assertRaises(errors.HookCommandNotFound, hooks.validate_hooks, config)
|
||||
|
||||
@mock.patch('certbot.hooks._is_exe')
|
||||
def test_which(self, mock_is_exe):
|
||||
mock_is_exe.return_value = True
|
||||
self.assertEqual(hooks._which("/path/to/something"), "/path/to/something")
|
||||
|
||||
with mock.patch.dict('os.environ', {"PATH": "/floop:/fleep"}):
|
||||
mock_is_exe.return_value = True
|
||||
self.assertEqual(hooks._which("pingify"), "/floop/pingify")
|
||||
mock_is_exe.return_value = False
|
||||
self.assertEqual(hooks._which("pingify"), None)
|
||||
self.assertEqual(hooks._which("/path/to/something"), None)
|
||||
|
||||
@mock.patch('certbot.hooks._which')
|
||||
def test_prog(self, mockwhich):
|
||||
mockwhich.return_value = "/very/very/funky"
|
||||
@mock.patch('certbot.hooks.util.exe_exists')
|
||||
@mock.patch('certbot.hooks.plug_util.path_surgery')
|
||||
def test_prog(self, mock_ps, mock_exe_exists):
|
||||
mock_exe_exists.return_value = True
|
||||
self.assertEqual(hooks._prog("funky"), "funky")
|
||||
mockwhich.return_value = None
|
||||
self.assertEqual(mock_ps.call_count, 0)
|
||||
mock_exe_exists.return_value = False
|
||||
self.assertEqual(hooks._prog("funky"), None)
|
||||
self.assertEqual(mock_ps.call_count, 1)
|
||||
|
||||
def _test_a_hook(self, config, hook_function, calls_expected):
|
||||
def _test_a_hook(self, config, hook_function, calls_expected, **kwargs):
|
||||
with mock.patch('certbot.hooks.logger') as mock_logger:
|
||||
mock_logger.warning = mock.MagicMock()
|
||||
with mock.patch('certbot.hooks._run_hook') as mock_run_hook:
|
||||
hook_function(config)
|
||||
hook_function(config)
|
||||
hook_function(config, **kwargs)
|
||||
hook_function(config, **kwargs)
|
||||
self.assertEqual(mock_run_hook.call_count, calls_expected)
|
||||
return mock_logger.warning
|
||||
|
||||
def test_pre_hook(self):
|
||||
hooks.pre_hook.already = False
|
||||
config = mock.MagicMock(pre_hook="true")
|
||||
self._test_a_hook(config, hooks.pre_hook, 1)
|
||||
self._test_a_hook(config, hooks.pre_hook, 0)
|
||||
config = mock.MagicMock(pre_hook="more_true")
|
||||
self._test_a_hook(config, hooks.pre_hook, 1)
|
||||
self._test_a_hook(config, hooks.pre_hook, 0)
|
||||
config = mock.MagicMock(pre_hook="")
|
||||
self._test_a_hook(config, hooks.pre_hook, 0)
|
||||
|
||||
def test_post_hook(self):
|
||||
hooks.pre_hook.already = False
|
||||
# if pre-hook isn't called, post-hook shouldn't be
|
||||
config = mock.MagicMock(post_hook="true", verb="splonk")
|
||||
self._test_a_hook(config, hooks.post_hook, 0)
|
||||
def _test_renew_post_hooks(self, expected_count):
|
||||
with mock.patch('certbot.hooks.logger.info') as mock_info:
|
||||
with mock.patch('certbot.hooks._run_hook') as mock_run:
|
||||
hooks.run_saved_post_hooks()
|
||||
self.assertEqual(mock_run.call_count, expected_count)
|
||||
self.assertEqual(mock_info.call_count, expected_count)
|
||||
|
||||
def test_post_hooks(self):
|
||||
config = mock.MagicMock(post_hook="true", verb="splonk")
|
||||
self._test_a_hook(config, hooks.pre_hook, 1)
|
||||
self._test_a_hook(config, hooks.post_hook, 2)
|
||||
self._test_renew_post_hooks(0)
|
||||
|
||||
config = mock.MagicMock(post_hook="true", verb="renew")
|
||||
self._test_a_hook(config, hooks.post_hook, 0)
|
||||
self._test_renew_post_hooks(1)
|
||||
self._test_a_hook(config, hooks.post_hook, 0)
|
||||
self._test_renew_post_hooks(1)
|
||||
|
||||
config = mock.MagicMock(post_hook="more_true", verb="renew")
|
||||
self._test_a_hook(config, hooks.post_hook, 0)
|
||||
self._test_renew_post_hooks(2)
|
||||
|
||||
def test_renew_hook(self):
|
||||
with mock.patch.dict('os.environ', {}):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
"""Tests for certbot.main."""
|
||||
# pylint: disable=too-many-lines
|
||||
from __future__ import print_function
|
||||
|
||||
import itertools
|
||||
|
|
@ -12,6 +13,7 @@ import datetime
|
|||
import pytz
|
||||
|
||||
import six
|
||||
from six.moves import reload_module # pylint: disable=import-error
|
||||
|
||||
from acme import jose
|
||||
|
||||
|
|
@ -55,17 +57,21 @@ class RunTest(unittest.TestCase):
|
|||
def setUp(self):
|
||||
self.domain = 'example.org'
|
||||
self.patches = [
|
||||
mock.patch('certbot.main._auth_from_available'),
|
||||
mock.patch('certbot.main._get_and_save_cert'),
|
||||
mock.patch('certbot.main.display_ops.success_installation'),
|
||||
mock.patch('certbot.main.display_ops.success_renewal'),
|
||||
mock.patch('certbot.main._init_le_client'),
|
||||
mock.patch('certbot.main._suggest_donation_if_appropriate')]
|
||||
mock.patch('certbot.main._suggest_donation_if_appropriate'),
|
||||
mock.patch('certbot.main._report_new_cert'),
|
||||
mock.patch('certbot.main._find_cert')]
|
||||
|
||||
self.mock_auth = self.patches[0].start()
|
||||
self.mock_success_installation = self.patches[1].start()
|
||||
self.mock_success_renewal = self.patches[2].start()
|
||||
self.mock_init = self.patches[3].start()
|
||||
self.mock_suggest_donation = self.patches[4].start()
|
||||
self.mock_report_cert = self.patches[5].start()
|
||||
self.mock_find_cert = self.patches[6].start()
|
||||
|
||||
def tearDown(self):
|
||||
for patch in self.patches:
|
||||
|
|
@ -81,27 +87,29 @@ class RunTest(unittest.TestCase):
|
|||
run(config, plugins)
|
||||
|
||||
def test_newcert_success(self):
|
||||
self.mock_auth.return_value = ('newcert', mock.Mock())
|
||||
self.mock_auth.return_value = mock.Mock()
|
||||
self.mock_find_cert.return_value = True, None
|
||||
self._call()
|
||||
self.mock_success_installation.assert_called_once_with([self.domain])
|
||||
|
||||
def test_reinstall_success(self):
|
||||
self.mock_auth.return_value = ('reinstall', mock.Mock())
|
||||
self.mock_auth.return_value = mock.Mock()
|
||||
self.mock_find_cert.return_value = False, mock.Mock()
|
||||
self._call()
|
||||
self.mock_success_installation.assert_called_once_with([self.domain])
|
||||
|
||||
def test_renewal_success(self):
|
||||
self.mock_auth.return_value = ('renewal', mock.Mock())
|
||||
self.mock_auth.return_value = mock.Mock()
|
||||
self.mock_find_cert.return_value = True, mock.Mock()
|
||||
self._call()
|
||||
self.mock_success_renewal.assert_called_once_with([self.domain])
|
||||
|
||||
|
||||
class ObtainCertTest(unittest.TestCase):
|
||||
"""Tests for certbot.main.obtain_cert."""
|
||||
class CertonlyTest(unittest.TestCase):
|
||||
"""Tests for certbot.main.certonly."""
|
||||
|
||||
def setUp(self):
|
||||
self.get_utility_patch = mock.patch(
|
||||
'certbot.main.zope.component.getUtility')
|
||||
self.get_utility_patch = test_util.patch_get_utility()
|
||||
self.mock_get_utility = self.get_utility_patch.start()
|
||||
|
||||
def tearDown(self):
|
||||
|
|
@ -113,15 +121,20 @@ class ObtainCertTest(unittest.TestCase):
|
|||
cli.prepare_and_parse_args(plugins, args))
|
||||
|
||||
with mock.patch('certbot.main._init_le_client') as mock_init:
|
||||
main.obtain_cert(config, plugins)
|
||||
with mock.patch('certbot.main._suggest_donation_if_appropriate'):
|
||||
main.certonly(config, plugins)
|
||||
|
||||
return mock_init() # returns the client
|
||||
|
||||
@mock.patch('certbot.main._auth_from_available')
|
||||
def test_no_reinstall_text_pause(self, mock_auth):
|
||||
@mock.patch('certbot.main._find_cert')
|
||||
@mock.patch('certbot.main._get_and_save_cert')
|
||||
@mock.patch('certbot.main._report_new_cert')
|
||||
def test_no_reinstall_text_pause(self, unused_report, mock_auth,
|
||||
mock_find_cert):
|
||||
mock_notification = self.mock_get_utility().notification
|
||||
mock_notification.side_effect = self._assert_no_pause
|
||||
mock_auth.return_value = ('reinstall', mock.ANY)
|
||||
mock_auth.return_value = mock.Mock()
|
||||
mock_find_cert.return_value = False, None
|
||||
self._call('certonly --webroot -d example.com'.split())
|
||||
|
||||
def _assert_no_pause(self, message, pause=True):
|
||||
|
|
@ -216,7 +229,7 @@ class RevokeTest(unittest.TestCase):
|
|||
'cert.pem'))
|
||||
|
||||
self.patches = [
|
||||
mock.patch('acme.client.Client'),
|
||||
mock.patch('acme.client.Client', autospec=True),
|
||||
mock.patch('certbot.client.Client'),
|
||||
mock.patch('certbot.main._determine_account'),
|
||||
mock.patch('certbot.main.display_ops.success_revocation')
|
||||
|
|
@ -242,8 +255,9 @@ class RevokeTest(unittest.TestCase):
|
|||
for patch in self.patches:
|
||||
patch.stop()
|
||||
|
||||
def _call(self):
|
||||
args = 'revoke --cert-path={0}'.format(self.tmp_cert_path).split()
|
||||
def _call(self, extra_args=""):
|
||||
args = 'revoke --cert-path={0} ' + extra_args
|
||||
args = args.format(self.tmp_cert_path).split()
|
||||
plugins = disco.PluginsRegistry.find_all()
|
||||
config = configuration.NamespaceConfig(
|
||||
cli.prepare_and_parse_args(plugins, args))
|
||||
|
|
@ -251,6 +265,17 @@ class RevokeTest(unittest.TestCase):
|
|||
from certbot.main import revoke
|
||||
revoke(config, plugins)
|
||||
|
||||
@mock.patch('certbot.main.client.acme_client')
|
||||
def test_revoke_with_reason(self, mock_acme_client):
|
||||
mock_revoke = mock_acme_client.Client().revoke
|
||||
expected = []
|
||||
for reason, code in constants.REVOCATION_REASONS.items():
|
||||
self._call("--reason " + reason)
|
||||
expected.append(mock.call(mock.ANY, code))
|
||||
self._call("--reason " + reason.upper())
|
||||
expected.append(mock.call(mock.ANY, code))
|
||||
self.assertEqual(expected, mock_revoke.call_args_list)
|
||||
|
||||
def test_revocation_success(self):
|
||||
self._call()
|
||||
self.mock_success_revoke.assert_called_once_with(self.tmp_cert_path)
|
||||
|
|
@ -350,6 +375,9 @@ class DetermineAccountTest(unittest.TestCase):
|
|||
|
||||
def setUp(self):
|
||||
self.args = mock.MagicMock(account=None, email=None,
|
||||
config_dir="unused_config",
|
||||
logs_dir="unused_logs",
|
||||
work_dir="unused_work",
|
||||
register_unsafely_without_email=False)
|
||||
self.config = configuration.NamespaceConfig(self.args)
|
||||
self.accs = [mock.MagicMock(id='x'), mock.MagicMock(id='y')]
|
||||
|
|
@ -423,10 +451,9 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
'--logs-dir', self.logs_dir, '--text']
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.tmp_dir)
|
||||
# Reset globals in cli
|
||||
# pylint: disable=protected-access
|
||||
cli._parser = cli.set_by_cli.detector = None
|
||||
reload_module(cli)
|
||||
shutil.rmtree(self.tmp_dir)
|
||||
|
||||
def _call(self, args, stdout=None):
|
||||
"Run the cli with output streams and actual client mocked out"
|
||||
|
|
@ -480,22 +507,23 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
self._cli_missing_flag(args, "specify a plugin")
|
||||
args.extend(['--standalone', '-d', 'eg.is'])
|
||||
self._cli_missing_flag(args, "register before running")
|
||||
with mock.patch('certbot.main._auth_from_available'):
|
||||
with mock.patch('certbot.main._get_and_save_cert'):
|
||||
with mock.patch('certbot.main.client.acme_from_config_key'):
|
||||
args.extend(['--email', 'io@io.is'])
|
||||
self._cli_missing_flag(args, "--agree-tos")
|
||||
|
||||
@mock.patch('certbot.main._report_new_cert')
|
||||
@mock.patch('certbot.main.client.acme_client.Client')
|
||||
@mock.patch('certbot.main._determine_account')
|
||||
@mock.patch('certbot.main.client.Client.obtain_and_enroll_certificate')
|
||||
@mock.patch('certbot.main._auth_from_available')
|
||||
def test_user_agent(self, afa, _obt, det, _client):
|
||||
@mock.patch('certbot.main._get_and_save_cert')
|
||||
def test_user_agent(self, gsc, _obt, det, _client, unused_report):
|
||||
# Normally the client is totally mocked out, but here we need more
|
||||
# arguments to automate it...
|
||||
args = ["--standalone", "certonly", "-m", "none@none.com",
|
||||
"-d", "example.com", '--agree-tos'] + self.standard_args
|
||||
det.return_value = mock.MagicMock(), None
|
||||
afa.return_value = "newcert", mock.MagicMock()
|
||||
gsc.return_value = mock.MagicMock()
|
||||
|
||||
with mock.patch('certbot.main.client.acme_client.ClientNetwork') as acme_net:
|
||||
self._call_no_clientmock(args)
|
||||
|
|
@ -520,8 +548,9 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
'--key-path', 'key', '--chain-path', 'chain'])
|
||||
self.assertEqual(mock_pick_installer.call_count, 1)
|
||||
|
||||
@mock.patch('certbot.main._report_new_cert')
|
||||
@mock.patch('certbot.util.exe_exists')
|
||||
def test_configurator_selection(self, mock_exe_exists):
|
||||
def test_configurator_selection(self, mock_exe_exists, unused_report):
|
||||
mock_exe_exists.return_value = True
|
||||
real_plugins = disco.PluginsRegistry.find_all()
|
||||
args = ['--apache', '--authenticator', 'standalone']
|
||||
|
|
@ -547,13 +576,13 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
self._cli_missing_flag(["--standalone"], "With the standalone plugin, you probably")
|
||||
|
||||
with mock.patch("certbot.main._init_le_client") as mock_init:
|
||||
with mock.patch("certbot.main._auth_from_available") as mock_afa:
|
||||
mock_afa.return_value = (mock.MagicMock(), mock.MagicMock())
|
||||
with mock.patch("certbot.main._get_and_save_cert") as mock_gsc:
|
||||
mock_gsc.return_value = mock.MagicMock()
|
||||
self._call(["certonly", "--manual", "-d", "foo.bar"])
|
||||
unused_config, auth, unused_installer = mock_init.call_args[0]
|
||||
self.assertTrue(isinstance(auth, manual.Authenticator))
|
||||
|
||||
with mock.patch('certbot.main.obtain_cert') as mock_certonly:
|
||||
with mock.patch('certbot.main.certonly') as mock_certonly:
|
||||
self._call(["auth", "--standalone"])
|
||||
self.assertEqual(1, mock_certonly.call_count)
|
||||
|
||||
|
|
@ -641,12 +670,12 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
chain = 'chain'
|
||||
fullchain = 'fullchain'
|
||||
|
||||
with mock.patch('certbot.main.obtain_cert') as mock_obtaincert:
|
||||
with mock.patch('certbot.main.certonly') as mock_certonly:
|
||||
self._call(['certonly', '--cert-path', cert, '--key-path', 'key',
|
||||
'--chain-path', 'chain',
|
||||
'--fullchain-path', 'fullchain'])
|
||||
|
||||
config, unused_plugins = mock_obtaincert.call_args[0]
|
||||
config, unused_plugins = mock_certonly.call_args[0]
|
||||
self.assertEqual(config.cert_path, os.path.abspath(cert))
|
||||
self.assertEqual(config.key_path, os.path.abspath(key))
|
||||
self.assertEqual(config.chain_path, os.path.abspath(chain))
|
||||
|
|
@ -713,7 +742,7 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
args += '-d foo.bar -a standalone certonly'.split()
|
||||
self._call(args)
|
||||
|
||||
@mock.patch('certbot.main.zope.component.getUtility')
|
||||
@test_util.patch_get_utility()
|
||||
def test_certonly_dry_run_new_request_success(self, mock_get_utility):
|
||||
mock_client = mock.MagicMock()
|
||||
mock_client.obtain_and_enroll_certificate.return_value = None
|
||||
|
|
@ -726,13 +755,14 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
self.assertEqual(mock_get_utility().add_message.call_count, 1)
|
||||
|
||||
@mock.patch('certbot.crypto_util.notAfter')
|
||||
@mock.patch('certbot.main.zope.component.getUtility')
|
||||
@test_util.patch_get_utility()
|
||||
def test_certonly_new_request_success(self, mock_get_utility, mock_notAfter):
|
||||
cert_path = '/etc/letsencrypt/live/foo.bar'
|
||||
date = '1970-01-01'
|
||||
mock_notAfter().date.return_value = date
|
||||
|
||||
mock_lineage = mock.MagicMock(cert=cert_path, fullchain=cert_path)
|
||||
mock_lineage = mock.MagicMock(cert=cert_path, fullchain=cert_path,
|
||||
fullchain_path=cert_path)
|
||||
mock_client = mock.MagicMock()
|
||||
mock_client.obtain_and_enroll_certificate.return_value = mock_lineage
|
||||
self._certonly_new_request_common(mock_client)
|
||||
|
|
@ -755,7 +785,8 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
# pylint: disable=too-many-locals,too-many-arguments
|
||||
cert_path = test_util.vector_path('cert.pem')
|
||||
chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem'
|
||||
mock_lineage = mock.MagicMock(cert=cert_path, fullchain=chain_path)
|
||||
mock_lineage = mock.MagicMock(cert=cert_path, fullchain=chain_path,
|
||||
cert_path=cert_path, fullchain_path=chain_path)
|
||||
mock_lineage.should_autorenew.return_value = due_for_renewal
|
||||
mock_lineage.has_pending_deployment.return_value = False
|
||||
mock_lineage.names.return_value = ['isnot.org']
|
||||
|
|
@ -770,8 +801,7 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
mock_fdc.return_value = (mock_lineage, None)
|
||||
with mock.patch('certbot.main._init_le_client') as mock_init:
|
||||
mock_init.return_value = mock_client
|
||||
get_utility_path = 'certbot.main.zope.component.getUtility'
|
||||
with mock.patch(get_utility_path) as mock_get_utility:
|
||||
with test_util.patch_get_utility() as mock_get_utility:
|
||||
with mock.patch('certbot.main.renewal.OpenSSL') as mock_ssl:
|
||||
mock_latest = mock.MagicMock()
|
||||
mock_latest.get_issuer.return_value = "Fake fake"
|
||||
|
|
@ -807,7 +837,8 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
|
||||
return mock_lineage, mock_get_utility, stdout
|
||||
|
||||
def test_certonly_renewal(self):
|
||||
@mock.patch('certbot.crypto_util.notAfter')
|
||||
def test_certonly_renewal(self, unused_notafter):
|
||||
lineage, get_utility, _ = self._test_renewal_common(True, [])
|
||||
self.assertEqual(lineage.save_successor.call_count, 1)
|
||||
lineage.update_all_links_to.assert_called_once_with(
|
||||
|
|
@ -816,7 +847,8 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
self.assertTrue('fullchain.pem' in cert_msg)
|
||||
self.assertTrue('donate' in get_utility().add_message.call_args[0][0])
|
||||
|
||||
def test_certonly_renewal_triggers(self):
|
||||
@mock.patch('certbot.crypto_util.notAfter')
|
||||
def test_certonly_renewal_triggers(self, unused_notafter):
|
||||
# --dry-run should force renewal
|
||||
_, get_utility, _ = self._test_renewal_common(False, ['--dry-run', '--keep'],
|
||||
log_out="simulating renewal")
|
||||
|
|
@ -864,14 +896,17 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
test_util.make_lineage(self, 'sample-renewal.conf')
|
||||
args = ["renew", "--dry-run", "--post-hook=no-such-command",
|
||||
"--disable-hook-validation"]
|
||||
self._test_renewal_common(True, [], args=args, should_renew=True,
|
||||
error_expected=False)
|
||||
with mock.patch("certbot.hooks.post_hook"):
|
||||
self._test_renewal_common(True, [], args=args, should_renew=True,
|
||||
error_expected=False)
|
||||
|
||||
@mock.patch("certbot.cli.set_by_cli")
|
||||
def test_ancient_webroot_renewal_conf(self, mock_set_by_cli):
|
||||
mock_set_by_cli.return_value = False
|
||||
rc_path = test_util.make_lineage(self, 'sample-renewal-ancient.conf')
|
||||
args = mock.MagicMock(account=None, email=None, webroot_path=None)
|
||||
args = mock.MagicMock(account=None, config_dir=self.config_dir,
|
||||
logs_dir=self.logs_dir, work_dir=self.work_dir,
|
||||
email=None, webroot_path=None)
|
||||
config = configuration.NamespaceConfig(args)
|
||||
lineage = storage.RenewableCert(rc_path, config)
|
||||
renewalparams = lineage.configuration["renewalparams"]
|
||||
|
|
@ -915,15 +950,15 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
if names is not None:
|
||||
mock_lineage.names.return_value = names
|
||||
mock_rc.return_value = mock_lineage
|
||||
with mock.patch('certbot.main.obtain_cert') as mock_obtain_cert:
|
||||
with mock.patch('certbot.main.renew_cert') as mock_renew_cert:
|
||||
kwargs.setdefault('args', ['renew'])
|
||||
self._test_renewal_common(True, None, should_renew=False, **kwargs)
|
||||
|
||||
if assert_oc_called is not None:
|
||||
if assert_oc_called:
|
||||
self.assertTrue(mock_obtain_cert.called)
|
||||
self.assertTrue(mock_renew_cert.called)
|
||||
else:
|
||||
self.assertFalse(mock_obtain_cert.called)
|
||||
self.assertFalse(mock_renew_cert.called)
|
||||
|
||||
def test_renew_no_renewalparams(self):
|
||||
self._test_renew_common(assert_oc_called=False, error_expected=True)
|
||||
|
|
@ -983,8 +1018,8 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
mock_rc.return_value = mock_lineage
|
||||
mock_lineage.configuration = {
|
||||
'renewalparams': {'authenticator': 'webroot'}}
|
||||
with mock.patch('certbot.main.obtain_cert') as mock_obtain_cert:
|
||||
mock_obtain_cert.side_effect = Exception
|
||||
with mock.patch('certbot.main.renew_cert') as mock_renew_cert:
|
||||
mock_renew_cert.side_effect = Exception
|
||||
self._test_renewal_common(True, None, error_expected=True,
|
||||
args=['renew'], should_renew=False)
|
||||
|
||||
|
|
@ -994,7 +1029,13 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
self._test_renewal_common(True, None, args='renew --csr {0}'.format(CSR).split(),
|
||||
should_renew=False, error_expected=True)
|
||||
|
||||
@mock.patch('certbot.main.zope.component.getUtility')
|
||||
def test_no_renewal_with_hooks(self):
|
||||
_, _, stdout = self._test_renewal_common(
|
||||
due_for_renewal=False, extra_args=None, should_renew=False,
|
||||
args=['renew', '--post-hook', 'echo hello world'])
|
||||
self.assertTrue('No hooks were run.' in stdout.getvalue())
|
||||
|
||||
@test_util.patch_get_utility()
|
||||
@mock.patch('certbot.main._find_lineage_for_domains_and_certname')
|
||||
@mock.patch('certbot.main._init_le_client')
|
||||
def test_certonly_reinstall(self, mock_init, mock_renewal, mock_get_utility):
|
||||
|
|
@ -1012,13 +1053,12 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
mock_client = mock.MagicMock()
|
||||
mock_client.obtain_certificate_from_csr.return_value = (certr, chain)
|
||||
cert_path = '/etc/letsencrypt/live/example.com/cert.pem'
|
||||
mock_client.save_certificate.return_value = cert_path, None, None
|
||||
full_path = '/etc/letsencrypt/live/example.com/fullchain.pem'
|
||||
mock_client.save_certificate.return_value = cert_path, None, full_path
|
||||
with mock.patch('certbot.main._init_le_client') as mock_init:
|
||||
mock_init.return_value = mock_client
|
||||
get_utility_path = 'certbot.main.zope.component.getUtility'
|
||||
with mock.patch(get_utility_path) as mock_get_utility:
|
||||
with test_util.patch_get_utility() as mock_get_utility:
|
||||
chain_path = '/etc/letsencrypt/live/example.com/chain.pem'
|
||||
full_path = '/etc/letsencrypt/live/example.com/fullchain.pem'
|
||||
args = ('-a standalone certonly --csr {0} --cert-path {1} '
|
||||
'--chain-path {2} --fullchain-path {3}').format(
|
||||
CSR, cert_path, chain_path, full_path).split()
|
||||
|
|
@ -1038,7 +1078,7 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
def test_certonly_csr(self):
|
||||
mock_get_utility = self._test_certonly_csr_common()
|
||||
cert_msg = mock_get_utility().add_message.call_args_list[0][0][0]
|
||||
self.assertTrue('cert.pem' in cert_msg)
|
||||
self.assertTrue('fullchain.pem' in cert_msg)
|
||||
self.assertTrue(
|
||||
'donate' in mock_get_utility().add_message.call_args[0][0])
|
||||
|
||||
|
|
@ -1059,7 +1099,9 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
with open(CERT, 'rb') as f:
|
||||
cert = crypto_util.pyopenssl_load_certificate(f.read())[0]
|
||||
mock_revoke = mock_acme_client.Client().revoke
|
||||
mock_revoke.assert_called_once_with(jose.ComparableX509(cert))
|
||||
mock_revoke.assert_called_once_with(
|
||||
jose.ComparableX509(cert),
|
||||
mock.ANY)
|
||||
|
||||
@mock.patch('certbot.main._determine_account')
|
||||
def test_revoke_without_key(self, mock_determine_account):
|
||||
|
|
@ -1068,7 +1110,9 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
with open(CERT) as f:
|
||||
cert = crypto_util.pyopenssl_load_certificate(f.read())[0]
|
||||
mock_revoke = client.acme_from_config_key().revoke
|
||||
mock_revoke.assert_called_once_with(jose.ComparableX509(cert))
|
||||
mock_revoke.assert_called_once_with(
|
||||
jose.ComparableX509(cert),
|
||||
mock.ANY)
|
||||
|
||||
def test_agree_dev_preview_config(self):
|
||||
with mock.patch('certbot.main.run') as mocked_run:
|
||||
|
|
@ -1115,13 +1159,13 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
self.assertTrue("--register-unsafely-without-email" in x[0])
|
||||
|
||||
@mock.patch('certbot.main.display_ops.get_email')
|
||||
@mock.patch('certbot.main.zope.component.getUtility')
|
||||
@test_util.patch_get_utility()
|
||||
def test_update_registration_with_email(self, mock_utility, mock_email):
|
||||
email = "user@example.com"
|
||||
mock_email.return_value = email
|
||||
with mock.patch('certbot.main.client') as mocked_client:
|
||||
with mock.patch('certbot.main.account') as mocked_account:
|
||||
with mock.patch('certbot.main._determine_account') as mocked_det:
|
||||
with mock.patch('certbot.eff.handle_subscription') as mock_handle:
|
||||
with mock.patch('certbot.main._determine_account') as mocked_det:
|
||||
with mock.patch('certbot.main.account') as mocked_account:
|
||||
with mock.patch('certbot.main.client') as mocked_client:
|
||||
mocked_storage = mock.MagicMock()
|
||||
mocked_account.AccountFileStorage.return_value = mocked_storage
|
||||
|
|
@ -1142,6 +1186,69 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||
self.assertTrue(mocked_storage.save_regr.called)
|
||||
self.assertTrue(
|
||||
email in mock_utility().add_message.call_args[0][0])
|
||||
self.assertTrue(mock_handle.called)
|
||||
|
||||
|
||||
class UnregisterTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.patchers = {
|
||||
'_determine_account': mock.patch('certbot.main._determine_account'),
|
||||
'account': mock.patch('certbot.main.account'),
|
||||
'client': mock.patch('certbot.main.client'),
|
||||
'get_utility': test_util.patch_get_utility()}
|
||||
self.mocks = dict((k, v.start()) for k, v in self.patchers.items())
|
||||
|
||||
def tearDown(self):
|
||||
for patch in self.patchers.values():
|
||||
patch.stop()
|
||||
|
||||
def test_abort_unregister(self):
|
||||
self.mocks['account'].AccountFileStorage.return_value = mock.Mock()
|
||||
|
||||
util_mock = self.mocks['get_utility'].return_value
|
||||
util_mock.yesno.return_value = False
|
||||
|
||||
config = mock.Mock()
|
||||
unused_plugins = mock.Mock()
|
||||
|
||||
res = main.unregister(config, unused_plugins)
|
||||
self.assertEqual(res, "Deactivation aborted.")
|
||||
|
||||
def test_unregister(self):
|
||||
mocked_storage = mock.MagicMock()
|
||||
mocked_storage.find_all.return_value = ["an account"]
|
||||
|
||||
self.mocks['account'].AccountFileStorage.return_value = mocked_storage
|
||||
self.mocks['_determine_account'].return_value = (mock.MagicMock(), "foo")
|
||||
|
||||
acme_client = mock.MagicMock()
|
||||
self.mocks['client'].Client.return_value = acme_client
|
||||
|
||||
config = mock.MagicMock()
|
||||
unused_plugins = mock.MagicMock()
|
||||
|
||||
res = main.unregister(config, unused_plugins)
|
||||
|
||||
self.assertTrue(res is None)
|
||||
self.assertTrue(acme_client.acme.deactivate_registration.called)
|
||||
m = "Account deactivated."
|
||||
self.assertTrue(m in self.mocks['get_utility']().add_message.call_args[0][0])
|
||||
|
||||
def test_unregister_no_account(self):
|
||||
mocked_storage = mock.MagicMock()
|
||||
mocked_storage.find_all.return_value = []
|
||||
self.mocks['account'].AccountFileStorage.return_value = mocked_storage
|
||||
|
||||
acme_client = mock.MagicMock()
|
||||
self.mocks['client'].Client.return_value = acme_client
|
||||
|
||||
config = mock.MagicMock()
|
||||
unused_plugins = mock.MagicMock()
|
||||
|
||||
res = main.unregister(config, unused_plugins)
|
||||
m = "Could not find existing account to deactivate."
|
||||
self.assertEqual(res, m)
|
||||
self.assertFalse(acme_client.acme.deactivate_registration.called)
|
||||
|
||||
|
||||
class TestHandleException(unittest.TestCase):
|
||||
|
|
|
|||
|
|
@ -73,10 +73,9 @@ class OCSPTest(unittest.TestCase):
|
|||
self.assertEqual(mock_run.call_count, 2)
|
||||
|
||||
|
||||
@mock.patch('certbot.ocsp.logger.debug')
|
||||
@mock.patch('certbot.ocsp.logger.info')
|
||||
@mock.patch('certbot.util.run_script')
|
||||
def test_determine_ocsp_server(self, mock_run, mock_info, mock_debug):
|
||||
def test_determine_ocsp_server(self, mock_run, mock_info):
|
||||
uri = "http://ocsp.stg-int-x1.letsencrypt.org/"
|
||||
host = "ocsp.stg-int-x1.letsencrypt.org"
|
||||
mock_run.return_value = uri, ""
|
||||
|
|
@ -88,7 +87,6 @@ class OCSPTest(unittest.TestCase):
|
|||
c = "confusion"
|
||||
mock_run.side_effect = errors.SubprocessError(c)
|
||||
self.assertEqual(self.checker.determine_ocsp_server("beep"), (None, None))
|
||||
self.assertTrue(c in repr(mock_debug.call_args[0][1]))
|
||||
|
||||
@mock.patch('certbot.ocsp.logger')
|
||||
@mock.patch('certbot.util.run_script')
|
||||
|
|
@ -100,9 +98,19 @@ class OCSPTest(unittest.TestCase):
|
|||
self.assertEqual(ocsp._translate_ocsp_query(*openssl_confused), False)
|
||||
self.assertEqual(mock_log.debug.call_count, 1)
|
||||
self.assertEqual(mock_log.warn.call_count, 0)
|
||||
mock_log.debug.call_count = 0
|
||||
self.assertEqual(ocsp._translate_ocsp_query(*openssl_unknown), False)
|
||||
self.assertEqual(mock_log.debug.call_count, 1)
|
||||
self.assertEqual(mock_log.warn.call_count, 0)
|
||||
self.assertEqual(ocsp._translate_ocsp_query(*openssl_expired_ocsp), False)
|
||||
self.assertEqual(mock_log.debug.call_count, 2)
|
||||
self.assertEqual(ocsp._translate_ocsp_query(*openssl_broken), False)
|
||||
self.assertEqual(mock_log.warn.call_count, 1)
|
||||
mock_log.info.call_count = 0
|
||||
self.assertEqual(ocsp._translate_ocsp_query(*openssl_revoked), True)
|
||||
self.assertEqual(mock_log.info.call_count, 0)
|
||||
self.assertEqual(ocsp._translate_ocsp_query(*openssl_expired_ocsp_revoked), True)
|
||||
self.assertEqual(mock_log.info.call_count, 1)
|
||||
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
|
|
@ -131,7 +139,32 @@ blah.pem: revoked
|
|||
""",
|
||||
"""Response verify OK""")
|
||||
|
||||
openssl_unknown = ("blah.pem", """
|
||||
blah.pem: unknown
|
||||
This Update: Dec 20 18:00:00 2016 GMT
|
||||
Next Update: Dec 27 18:00:00 2016 GMT
|
||||
""",
|
||||
"Response verify OK")
|
||||
|
||||
openssl_broken = ("", "tentacles", "Response verify OK")
|
||||
|
||||
openssl_expired_ocsp = ("blah.pem", """
|
||||
blah.pem: WARNING: Status times invalid.
|
||||
140659132298912:error:2707307D:OCSP routines:OCSP_check_validity:status expired:ocsp_cl.c:372:
|
||||
good
|
||||
This Update: Apr 6 00:00:00 2016 GMT
|
||||
Next Update: Apr 13 00:00:00 2016 GMT
|
||||
""",
|
||||
"""Response verify OK""")
|
||||
|
||||
openssl_expired_ocsp_revoked = ("blah.pem", """
|
||||
blah.pem: WARNING: Status times invalid.
|
||||
140659132298912:error:2707307D:OCSP routines:OCSP_check_validity:status expired:ocsp_cl.c:372:
|
||||
revoked
|
||||
This Update: Apr 6 00:00:00 2016 GMT
|
||||
Next Update: Apr 13 00:00:00 2016 GMT
|
||||
""",
|
||||
"""Response verify OK""")
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -2,9 +2,13 @@
|
|||
import os
|
||||
import mock
|
||||
import unittest
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from acme import challenges
|
||||
|
||||
from certbot import configuration
|
||||
from certbot import errors
|
||||
from certbot import storage
|
||||
|
||||
from certbot.tests import util
|
||||
|
|
@ -15,15 +19,83 @@ class RenewalTest(unittest.TestCase):
|
|||
self.tmp_dir = tempfile.mkdtemp()
|
||||
self.config_dir = os.path.join(self.tmp_dir, 'config')
|
||||
|
||||
@mock.patch("certbot.cli.set_by_cli")
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.tmp_dir)
|
||||
|
||||
@mock.patch('certbot.cli.set_by_cli')
|
||||
def test_ancient_webroot_renewal_conf(self, mock_set_by_cli):
|
||||
mock_set_by_cli.return_value = False
|
||||
rc_path = util.make_lineage(self, 'sample-renewal-ancient.conf')
|
||||
args = mock.MagicMock(account=None, email=None, webroot_path=None)
|
||||
args = mock.MagicMock(account=None, config_dir=self.config_dir,
|
||||
logs_dir="logs", work_dir="work",
|
||||
email=None, webroot_path=None)
|
||||
config = configuration.NamespaceConfig(args)
|
||||
lineage = storage.RenewableCert(rc_path, config)
|
||||
renewalparams = lineage.configuration["renewalparams"]
|
||||
renewalparams = lineage.configuration['renewalparams']
|
||||
# pylint: disable=protected-access
|
||||
from certbot import renewal
|
||||
renewal._restore_webroot_config(config, renewalparams)
|
||||
self.assertEqual(config.webroot_path, ["/var/www/"])
|
||||
self.assertEqual(config.webroot_path, ['/var/www/'])
|
||||
|
||||
|
||||
class RestoreRequiredConfigElementsTest(unittest.TestCase):
|
||||
"""Tests for certbot.renewal.restore_required_config_elements."""
|
||||
def setUp(self):
|
||||
self.config = mock.MagicMock()
|
||||
|
||||
@classmethod
|
||||
def _call(cls, *args, **kwargs):
|
||||
from certbot.renewal import restore_required_config_elements
|
||||
return restore_required_config_elements(*args, **kwargs)
|
||||
|
||||
@mock.patch('certbot.renewal.cli.set_by_cli')
|
||||
def test_allow_subset_of_names_success(self, mock_set_by_cli):
|
||||
mock_set_by_cli.return_value = False
|
||||
self._call(self.config, {'allow_subset_of_names': 'True'})
|
||||
self.assertTrue(self.config.namespace.allow_subset_of_names is True)
|
||||
|
||||
@mock.patch('certbot.renewal.cli.set_by_cli')
|
||||
def test_allow_subset_of_names_failure(self, mock_set_by_cli):
|
||||
mock_set_by_cli.return_value = False
|
||||
renewalparams = {'allow_subset_of_names': 'maybe'}
|
||||
self.assertRaises(
|
||||
errors.Error, self._call, self.config, renewalparams)
|
||||
|
||||
@mock.patch('certbot.renewal.cli.set_by_cli')
|
||||
def test_pref_challs_list(self, mock_set_by_cli):
|
||||
mock_set_by_cli.return_value = False
|
||||
renewalparams = {'pref_challs': 'tls-sni, http-01, dns'.split(',')}
|
||||
self._call(self.config, renewalparams)
|
||||
expected = [challenges.TLSSNI01.typ,
|
||||
challenges.HTTP01.typ, challenges.DNS01.typ]
|
||||
self.assertEqual(self.config.namespace.pref_challs, expected)
|
||||
|
||||
@mock.patch('certbot.renewal.cli.set_by_cli')
|
||||
def test_pref_challs_str(self, mock_set_by_cli):
|
||||
mock_set_by_cli.return_value = False
|
||||
renewalparams = {'pref_challs': 'dns'}
|
||||
self._call(self.config, renewalparams)
|
||||
expected = [challenges.DNS01.typ]
|
||||
self.assertEqual(self.config.namespace.pref_challs, expected)
|
||||
|
||||
@mock.patch('certbot.renewal.cli.set_by_cli')
|
||||
def test_pref_challs_failure(self, mock_set_by_cli):
|
||||
mock_set_by_cli.return_value = False
|
||||
renewalparams = {'pref_challs': 'finding-a-shrubbery'}
|
||||
self.assertRaises(errors.Error, self._call, self.config, renewalparams)
|
||||
|
||||
@mock.patch('certbot.renewal.cli.set_by_cli')
|
||||
def test_must_staple_success(self, mock_set_by_cli):
|
||||
mock_set_by_cli.return_value = False
|
||||
self._call(self.config, {'must_staple': 'True'})
|
||||
self.assertTrue(self.config.namespace.must_staple is True)
|
||||
|
||||
@mock.patch('certbot.renewal.cli.set_by_cli')
|
||||
def test_must_staple_failure(self, mock_set_by_cli):
|
||||
mock_set_by_cli.return_value = False
|
||||
renewalparams = {'must_staple': 'maybe'}
|
||||
self.assertRaises(
|
||||
errors.Error, self._call, self.config, renewalparams)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import six
|
|||
|
||||
from certbot import errors
|
||||
|
||||
from certbot.tests import util as test_util
|
||||
|
||||
|
||||
class ReverterCheckpointLocalTest(unittest.TestCase):
|
||||
# pylint: disable=too-many-instance-attributes, too-many-public-methods
|
||||
|
|
@ -375,7 +377,7 @@ class TestFullCheckpointsReverter(unittest.TestCase):
|
|||
self.assertEqual(read_in(self.config2), "directive-dir2")
|
||||
self.assertFalse(os.path.isfile(config3))
|
||||
|
||||
@mock.patch("certbot.reverter.zope.component.getUtility")
|
||||
@test_util.patch_get_utility()
|
||||
def test_view_config_changes(self, mock_output):
|
||||
"""This is not strict as this is subject to change."""
|
||||
self._setup_three_checkpoints()
|
||||
|
|
@ -392,7 +394,7 @@ class TestFullCheckpointsReverter(unittest.TestCase):
|
|||
self.assertTrue(mock_logger.info.call_count > 0)
|
||||
|
||||
def test_view_config_changes_bad_backups_dir(self):
|
||||
# There shouldn't be any "in progess directories when this is called
|
||||
# There shouldn't be any "in progress directories when this is called
|
||||
# It must just be clean checkpoints
|
||||
os.makedirs(os.path.join(self.config.backup_dir, "in_progress"))
|
||||
|
||||
|
|
|
|||
|
|
@ -551,7 +551,8 @@ class RenewableCertTests(BaseRenewableCertTest):
|
|||
|
||||
from certbot.storage import relevant_values
|
||||
with mock.patch("certbot.cli.helpful_parser", mock_parser):
|
||||
return relevant_values(values)
|
||||
# make a copy to ensure values isn't modified
|
||||
return relevant_values(values.copy())
|
||||
|
||||
def test_relevant_values(self):
|
||||
"""Test that relevant_values() can reject an irrelevant value."""
|
||||
|
|
@ -567,10 +568,18 @@ class RenewableCertTests(BaseRenewableCertTest):
|
|||
def test_relevant_values_nondefault(self):
|
||||
"""Test that relevant_values() can retain a non-default value."""
|
||||
values = {"rsa_key_size": 12}
|
||||
# A copy is given to _test_relevant_values_common
|
||||
# to make sure values isn't modified by the method
|
||||
self.assertEqual(
|
||||
self._test_relevant_values_common(values.copy()), values)
|
||||
self._test_relevant_values_common(values), values)
|
||||
|
||||
def test_relevant_values_bool(self):
|
||||
values = {"allow_subset_of_names": True}
|
||||
self.assertEqual(
|
||||
self._test_relevant_values_common(values), values)
|
||||
|
||||
def test_relevant_values_str(self):
|
||||
values = {"authenticator": "apache"}
|
||||
self.assertEqual(
|
||||
self._test_relevant_values_common(values), values)
|
||||
|
||||
@mock.patch("certbot.storage.relevant_values")
|
||||
def test_new_lineage(self, mock_rv):
|
||||
|
|
|
|||
1
certbot/tests/testdata/sample-renewal.conf
vendored
1
certbot/tests/testdata/sample-renewal.conf
vendored
|
|
@ -73,4 +73,5 @@ tls_sni_01_port = 443
|
|||
logs_dir = /var/log/letsencrypt
|
||||
apache_vhost_root = /etc/apache2/sites-available
|
||||
configurator = None
|
||||
must_staple = True
|
||||
[[webroot_map]]
|
||||
|
|
|
|||
|
|
@ -10,15 +10,17 @@ import unittest
|
|||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
import mock
|
||||
import OpenSSL
|
||||
|
||||
from acme import errors
|
||||
from acme import jose
|
||||
from acme import util
|
||||
|
||||
from certbot import constants
|
||||
from certbot import interfaces
|
||||
from certbot import storage
|
||||
|
||||
from certbot.display import util as display_util
|
||||
|
||||
|
||||
def vector_path(*names):
|
||||
"""Path to a test vector."""
|
||||
|
|
@ -82,20 +84,6 @@ def load_pyopenssl_private_key(*names):
|
|||
return OpenSSL.crypto.load_privatekey(loader, load_vector(*names))
|
||||
|
||||
|
||||
def requirement_available(requirement):
|
||||
"""Checks if requirement can be imported.
|
||||
|
||||
:rtype: bool
|
||||
:returns: ``True`` iff requirement can be imported
|
||||
|
||||
"""
|
||||
try:
|
||||
util.activate(requirement)
|
||||
except errors.DependencyError: # pragma: no cover
|
||||
return False
|
||||
return True # pragma: no cover
|
||||
|
||||
|
||||
def skip_unless(condition, reason): # pragma: no cover
|
||||
"""Skip tests unless a condition holds.
|
||||
|
||||
|
|
@ -158,3 +146,87 @@ def make_lineage(self, testfile):
|
|||
line.replace('MAGICDIR', self.config_dir) for line in src)
|
||||
|
||||
return conf_path
|
||||
|
||||
|
||||
def patch_get_utility(target='zope.component.getUtility'):
|
||||
"""Patch zope.component.getUtility to use a special mock IDisplay.
|
||||
|
||||
The mock IDisplay works like a regular mock object, except it also
|
||||
also asserts that methods are called with valid arguments.
|
||||
|
||||
:param str target: path to patch
|
||||
|
||||
:returns: mock zope.component.getUtility
|
||||
:rtype: mock.MagicMock
|
||||
|
||||
"""
|
||||
return mock.patch(target, new_callable=_create_get_utility_mock)
|
||||
|
||||
|
||||
class FreezableMock(object):
|
||||
"""Mock object with the ability to freeze attributes.
|
||||
|
||||
This class works like a regular mock.MagicMock object, except
|
||||
attributes and behavior can be set and frozen so they cannot be
|
||||
changed during tests.
|
||||
|
||||
If a func argument is provided to the constructor, this function
|
||||
is called first when an instance of FreezableMock is called,
|
||||
followed by the usual behavior defined by MagicMock. The return
|
||||
value of func is ignored.
|
||||
|
||||
"""
|
||||
def __init__(self, frozen=False, func=None):
|
||||
self._frozen_set = set() if frozen else set(('freeze',))
|
||||
self._func = func
|
||||
self._mock = mock.MagicMock()
|
||||
self._frozen = frozen
|
||||
|
||||
def freeze(self):
|
||||
"""Freeze object preventing further changes."""
|
||||
self._frozen = True
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
if self._func is not None:
|
||||
self._func(*args, **kwargs)
|
||||
return self._mock(*args, **kwargs)
|
||||
|
||||
def __getattribute__(self, name):
|
||||
if name == '_frozen':
|
||||
try:
|
||||
return object.__getattribute__(self, name)
|
||||
except AttributeError:
|
||||
return False
|
||||
elif name == '_frozen_set' or name in self._frozen_set:
|
||||
return object.__getattribute__(self, name)
|
||||
else:
|
||||
return getattr(object.__getattribute__(self, '_mock'), name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if self._frozen:
|
||||
return setattr(self._mock, name, value)
|
||||
elif name != '_frozen_set':
|
||||
self._frozen_set.add(name)
|
||||
return object.__setattr__(self, name, value)
|
||||
|
||||
|
||||
def _create_get_utility_mock():
|
||||
display = FreezableMock()
|
||||
for name in interfaces.IDisplay.names(): # pylint: disable=no-member
|
||||
if name != 'notification':
|
||||
frozen_mock = FreezableMock(frozen=True, func=_assert_valid_call)
|
||||
setattr(display, name, frozen_mock)
|
||||
display.freeze()
|
||||
return mock.MagicMock(return_value=display)
|
||||
|
||||
|
||||
def _assert_valid_call(*args, **kwargs):
|
||||
assert_args = [args[0] if args else kwargs['message']]
|
||||
|
||||
assert_kwargs = {}
|
||||
assert_kwargs['default'] = kwargs.get('default', None)
|
||||
assert_kwargs['cli_flag'] = kwargs.get('cli_flag', None)
|
||||
assert_kwargs['force_interactive'] = kwargs.get('force_interactive', False)
|
||||
|
||||
# pylint: disable=star-args
|
||||
display_util.assert_valid_call(*assert_args, **assert_kwargs)
|
||||
|
|
|
|||
|
|
@ -374,6 +374,44 @@ class EnforceDomainSanityTest(unittest.TestCase):
|
|||
self.assertRaises(errors.ConfigurationError, self._call,
|
||||
u"eichh\u00f6rnchen.example.com")
|
||||
|
||||
def test_too_long(self):
|
||||
long_domain = u"a"*256
|
||||
self.assertRaises(errors.ConfigurationError, self._call,
|
||||
long_domain)
|
||||
|
||||
def test_not_too_long(self):
|
||||
not_too_long_domain = u"{0}.{1}.{2}.{3}".format("a"*63, "b"*63, "c"*63, "d"*63)
|
||||
self._call(not_too_long_domain)
|
||||
|
||||
def test_empty_label(self):
|
||||
empty_label_domain = u"fizz..example.com"
|
||||
self.assertRaises(errors.ConfigurationError, self._call,
|
||||
empty_label_domain)
|
||||
|
||||
def test_empty_trailing_label(self):
|
||||
empty_trailing_label_domain = u"example.com.."
|
||||
self.assertRaises(errors.ConfigurationError, self._call,
|
||||
empty_trailing_label_domain)
|
||||
|
||||
def test_long_label_1(self):
|
||||
long_label_domain = u"a"*64
|
||||
self.assertRaises(errors.ConfigurationError, self._call,
|
||||
long_label_domain)
|
||||
|
||||
def test_long_label_2(self):
|
||||
long_label_domain = u"{0}.{1}.com".format(u"a"*64, u"b"*63)
|
||||
self.assertRaises(errors.ConfigurationError, self._call,
|
||||
long_label_domain)
|
||||
|
||||
def test_not_long_label(self):
|
||||
not_too_long_label_domain = u"{0}.{1}.com".format(u"a"*63, u"b"*63)
|
||||
self._call(not_too_long_label_domain)
|
||||
|
||||
def test_empty_domain(self):
|
||||
empty_domain = u""
|
||||
self.assertRaises(errors.ConfigurationError, self._call,
|
||||
empty_domain)
|
||||
|
||||
def test_punycode_ok(self):
|
||||
# Punycode is now legal, so no longer an error; instead check
|
||||
# that it's _not_ an error (at the initial sanity check stage)
|
||||
|
|
|
|||
|
|
@ -219,6 +219,25 @@ def safely_remove(path):
|
|||
raise
|
||||
|
||||
|
||||
def get_filtered_names(all_names):
|
||||
"""Removes names that aren't considered valid by Let's Encrypt.
|
||||
|
||||
:param set all_names: all names found in the configuration
|
||||
|
||||
:returns: all found names that are considered valid by LE
|
||||
:rtype: set
|
||||
|
||||
"""
|
||||
filtered_names = set()
|
||||
for name in all_names:
|
||||
try:
|
||||
filtered_names.add(enforce_le_validity(name))
|
||||
except errors.ConfigurationError as error:
|
||||
logger.debug('Not suggesting name "%s"', name)
|
||||
logger.debug(error)
|
||||
return filtered_names
|
||||
|
||||
|
||||
def get_os_info(filepath="/etc/os-release"):
|
||||
"""
|
||||
Get OS name and version
|
||||
|
|
@ -478,13 +497,15 @@ def enforce_domain_sanity(domain):
|
|||
# FQDN checks according to RFC 2181: domain name should be less than 255
|
||||
# octets (inclusive). And each label is 1 - 63 octets (inclusive).
|
||||
# https://tools.ietf.org/html/rfc2181#section-11
|
||||
msg = "Requested domain {0} is not a FQDN because ".format(domain)
|
||||
msg = "Requested domain {0} is not a FQDN because".format(domain)
|
||||
if len(domain) > 255:
|
||||
raise errors.ConfigurationError("{0} it is too long.".format(msg))
|
||||
labels = domain.split('.')
|
||||
for l in labels:
|
||||
if not 0 < len(l) < 64:
|
||||
raise errors.ConfigurationError(msg + "label {0} is too long.".format(l))
|
||||
if len(domain) > 255:
|
||||
raise errors.ConfigurationError(msg + "it is too long.")
|
||||
if not l:
|
||||
raise errors.ConfigurationError("{0} it contains an empty label.".format(msg))
|
||||
elif len(l) > 63:
|
||||
raise errors.ConfigurationError("{0} label {1} is too long.".format(msg, l))
|
||||
|
||||
return domain
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue