diff --git a/.azure-pipelines/templates/jobs/extended-tests-jobs.yml b/.azure-pipelines/templates/jobs/extended-tests-jobs.yml index 7c586ee5b..45873b835 100644 --- a/.azure-pipelines/templates/jobs/extended-tests-jobs.yml +++ b/.azure-pipelines/templates/jobs/extended-tests-jobs.yml @@ -4,7 +4,7 @@ jobs: - name: IMAGE_NAME value: ubuntu-22.04 - name: PYTHON_VERSION - value: 3.10 + value: 3.11 - group: certbot-common strategy: matrix: @@ -14,12 +14,13 @@ jobs: linux-py39: PYTHON_VERSION: 3.9 TOXENV: py39 + linux-py310: + PYTHON_VERSION: 3.10 + TOXENV: py310 linux-py37-nopin: PYTHON_VERSION: 3.7 TOXENV: py37 CERTBOT_NO_PIN: 1 - linux-external-mock: - TOXENV: external-mock linux-boulder-v2-integration-certbot-oldest: PYTHON_VERSION: 3.7 TOXENV: integration-certbot-oldest @@ -44,6 +45,10 @@ jobs: PYTHON_VERSION: 3.10 TOXENV: integration ACME_SERVER: boulder-v2 + linux-boulder-v2-py311-integration: + PYTHON_VERSION: 3.11 + TOXENV: integration + ACME_SERVER: boulder-v2 nginx-compat: TOXENV: nginx_compat linux-integration-rfc2136: diff --git a/.azure-pipelines/templates/jobs/standard-tests-jobs.yml b/.azure-pipelines/templates/jobs/standard-tests-jobs.yml index cf5e20c0b..c49e22bc1 100644 --- a/.azure-pipelines/templates/jobs/standard-tests-jobs.yml +++ b/.azure-pipelines/templates/jobs/standard-tests-jobs.yml @@ -1,17 +1,16 @@ jobs: - job: test variables: - PYTHON_VERSION: 3.10 + PYTHON_VERSION: 3.11 strategy: matrix: macos-py37-cover: IMAGE_NAME: macOS-12 PYTHON_VERSION: 3.7 TOXENV: py37-cover - macos-py310-cover: + macos-cover: IMAGE_NAME: macOS-12 - PYTHON_VERSION: 3.10 - TOXENV: py310-cover + TOXENV: py3-cover windows-py37: IMAGE_NAME: windows-2019 PYTHON_VERSION: 3.7 @@ -36,17 +35,14 @@ jobs: IMAGE_NAME: ubuntu-22.04 PYTHON_VERSION: 3.7 TOXENV: py37 - linux-py310-cover: + linux-cover: IMAGE_NAME: ubuntu-22.04 - PYTHON_VERSION: 3.10 - TOXENV: py310-cover - linux-py310-lint: + TOXENV: py3-cover + linux-lint: IMAGE_NAME: ubuntu-22.04 - PYTHON_VERSION: 3.10 TOXENV: lint-posix - linux-py310-mypy: + linux-mypy: IMAGE_NAME: ubuntu-22.04 - PYTHON_VERSION: 3.10 TOXENV: mypy-posix linux-integration: IMAGE_NAME: ubuntu-22.04 diff --git a/.pylintrc b/.pylintrc index d2730018d..194a8fe94 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,10 +1,65 @@ -[MASTER] +[MAIN] -# use as many jobs as there are cores -jobs=0 +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no -# Specify a configuration file. -#rcfile= +# Load and enable all available extensions. Use --list-extensions to see a list +# all available extensions. +#enable-all-extensions= + +# In error mode, messages with a category besides ERROR or FATAL are +# suppressed, and no reports are done by default. Error mode is compatible with +# disabling specific errors. +#errors-only= + +# Always return a 0 (non-error) status code, even if lint errors are found. +# This is primarily useful in continuous integration scripts. +#exit-zero= + +# 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-allow-list= + +# 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. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist=pywintypes,win32api,win32file,win32security + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold under which the program will exit with error. +fail-under=10 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +#from-stdin= + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\' represents the directory delimiter on Windows systems, it +# can't be used as an escape character. +ignore-paths= + +# Files or directories matching the regular expression patterns are skipped. +# The regex matches against base names, not paths. The default value ignores +# Emacs file locks +ignore-patterns=^\.# + +# 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). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). @@ -13,42 +68,303 @@ jobs=0 # https://github.com/PyCQA/pylint/pull/3396. init-hook="import pylint.config, os, sys; sys.path.append(os.path.dirname(pylint.config.PYLINTRC))" -# Profiled execution. -profile=no +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs=0 -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins=linter_plugin # 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 +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.10 -# 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=pywintypes,win32api,win32file,win32security +# Discover python modules and packages in the file system subtree. +recursive=no + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# In verbose mode, extra non-checker-related info will be displayed. +#verbose= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +function-rgx=[a-z_][a-z0-9_]{2,40}$ + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _, + fd, + logger + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +method-rgx=[a-z_][a-z0-9_]{2,50}$ + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# 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$) + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +variable-rgx=[a-z_][a-z0-9_]{1,30}$ + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# 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=cls + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=BaseException, + Exception + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +# git history told me that "This does something silly/broken..." +#indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1250 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# 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 + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging,logger [MESSAGES CONTROL] -# 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= +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence=HIGH, + CONTROL_FLOW, + INFERENCE, + INFERENCE_FAILURE, + UNDEFINED # 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 +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then re-enable 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" +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". # CERTBOT COMMENT # 1) Once certbot codebase is claimed to be compatible exclusively with Python 3, # the useless-object-inheritance check can be enabled again, and code fixed accordingly. @@ -74,261 +390,185 @@ extension-pkg-whitelist=pywintypes,win32api,win32file,win32security # not need to enforce encoding on files so we disable this check. # 7) consider-using-f-string is "suggesting" to move to f-string when possible with an error. This # clearly relates to code design and not to potential defects in the code, let's just ignore that. -disable=fixme,locally-disabled,locally-enabled,bad-continuation,no-self-use,invalid-name,cyclic-import,duplicate-code,design,import-outside-toplevel,useless-object-inheritance,unsubscriptable-object,no-value-for-parameter,no-else-return,no-else-raise,no-else-break,no-else-continue,raise-missing-from,wrong-import-order,unspecified-encoding,consider-using-f-string +disable=fixme,locally-disabled,invalid-name,cyclic-import,duplicate-code,design,import-outside-toplevel,useless-object-inheritance,unsubscriptable-object,no-value-for-parameter,no-else-return,no-else-raise,no-else-break,no-else-continue,raise-missing-from,wrong-import-order,unspecified-encoding,consider-using-f-string,raw-checker-failed,bad-inline-option,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,use-symbolic-message-instead -[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= +# 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 (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member -[BASIC] +[METHOD_ARGS] -# 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,apply,input,file - -# Good variable names which should always be accepted, separated by a comma -good-names=f,i,j,k,ex,Run,_,fd,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,40}$ - -# Regular expression matching correct variable names -variable-rgx=[a-z_][a-z0-9_]{1,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,50}$ - -# Naming hint for method names -method-name-hint=[a-z_][a-z0-9_]{2,50}$ - -# 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 +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. -notes=FIXME,XXX,TODO +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +notes-rgx= -[LOGGING] +[REFACTORING] -# Logging modules to check that the string format arguments are in logging -# function parameter format -logging-modules=logging,logger +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error -[VARIABLES] +[REPORTS] -# Tells whether we should check for unused import in __init__ files. -init-import=no +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=(unused)?_.*|dummy +# 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= -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +#output-format= + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes [SIMILARITIES] +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + # Minimum lines number of a similarity. min-similarity-lines=6 -# Ignore comments when computing similarities. -ignore-comments=yes -# Ignore docstrings when computing similarities. -ignore-docstrings=yes +[STRING] -# Ignore imports when computing similarities. -ignore-imports=yes +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no - -[FORMAT] - -# Maximum number of characters on a single line. -max-line-length=100 - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\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 - -# Maximum number of lines in a module -max-module-lines=1250 - -# 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. -# This does something silly/broken... -#indent-after-paren=4 +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=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 decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace,Field,Header,JWS,closing # 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=pkg_resources,confargparse,argparse -# import errors ignored only in 1.4.4 -# https://bitbucket.org/logilab/pylint/commits/cd000904c9e2 -# List of classes names for which member attributes should not be checked -# (useful for classes with attributes dynamically set). -ignored-classes=Field,Header,JWS,closing +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes -# When zope mode is activated, add a predefined set of Zope acquired attributes -# to generated-members. -zope=yes +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 -# 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 +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= -[IMPORTS] +[VARIABLES] -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub,TERMIOS,Bastion,rexec +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= +# List of names allowed to shadow builtins +allowed-redefined-builtins= -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= +# 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 +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ -[CLASSES] +# Argument names that match this expression will be ignored. +ignored-argument-names=_.*|^ignored_|^unused_ -# List of interface methods to ignore, separated by a comma. This is used for -# instance to not check methods defined in Zope's Interface base class. -ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by,implementedBy,providedBy +# Tells whether we should check for unused import in __init__ files. +init-import=no -# 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 - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=Exception +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 194f19e47..61af415bd 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -14,7 +14,6 @@ from typing import Tuple from typing import Type from typing import TypeVar from typing import Union -import warnings from cryptography.hazmat.primitives import hashes import josepy as jose @@ -24,12 +23,6 @@ import requests from acme import crypto_util from acme import errors -from acme import fields - -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - from acme.mixins import ResourceMixin - from acme.mixins import TypeMixin logger = logging.getLogger(__name__) @@ -51,14 +44,17 @@ class Challenge(jose.TypedJSONObjectWithFields): return UnrecognizedChallenge.from_json(jobj) -class ChallengeResponse(ResourceMixin, TypeMixin, jose.TypedJSONObjectWithFields): +class ChallengeResponse(jose.TypedJSONObjectWithFields): # _fields_to_partial_json """ACME challenge response.""" TYPES: Dict[str, Type['ChallengeResponse']] = {} - resource_type = 'challenge' - with warnings.catch_warnings(): - warnings.filterwarnings('ignore', 'resource attribute in acme.fields', DeprecationWarning) - resource: str = fields.resource(resource_type) + + def to_partial_json(self) -> Dict[str, Any]: + # Removes the `type` field which is inserted by TypedJSONObjectWithFields.to_partial_json. + # This field breaks RFC8555 compliance. + jobj = super().to_partial_json() + jobj.pop(self.type_field_name, None) + return jobj class UnrecognizedChallenge(Challenge): @@ -305,7 +301,7 @@ class HTTP01Response(KeyAuthorizationChallengeResponse): """Whitespace characters which should be ignored at the end of the body.""" def simple_verify(self, chall: 'HTTP01', domain: str, account_public_key: jose.JWK, - port: Optional[int] = None) -> bool: + port: Optional[int] = None, timeout: int = 30) -> bool: """Simple verify. :param challenges.SimpleHTTP chall: Corresponding challenge. @@ -313,6 +309,7 @@ class HTTP01Response(KeyAuthorizationChallengeResponse): :param JWK account_public_key: Public key for the key pair being authorized. :param int port: Port used in the validation. + :param int timeout: Timeout in seconds. :returns: ``True`` iff validation with the files currently served by the HTTP server is successful. @@ -334,7 +331,7 @@ class HTTP01Response(KeyAuthorizationChallengeResponse): uri = chall.uri(domain) logger.debug("Verifying %s at %s...", chall.typ, uri) try: - http_response = requests.get(uri, verify=False) + http_response = requests.get(uri, verify=False, timeout=timeout) except requests.exceptions.RequestException as error: logger.error("Unable to reach %s: %s", uri, error) return False diff --git a/acme/acme/client.py b/acme/acme/client.py index 7d21b0fad..ee31f58a7 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -1,23 +1,13 @@ """ACME client API.""" -# pylint: disable=too-many-lines -# This pylint disable can be deleted once the deprecated ACMEv1 code is -# removed. import base64 -import collections import datetime from email.utils import parsedate_tz -import heapq import http.client as http_client import logging import re -import sys import time -from types import ModuleType from typing import Any -from typing import Callable from typing import cast -from typing import Dict -from typing import Iterable from typing import List from typing import Mapping from typing import Optional @@ -25,597 +15,25 @@ from typing import Set from typing import Text from typing import Tuple from typing import Union -import warnings import josepy as jose import OpenSSL import requests from requests.adapters import HTTPAdapter from requests.utils import parse_header_links -# We're capturing the warnings described at -# https://github.com/requests/toolbelt/issues/331 until we can remove this -# dependency in Certbot 2.0. -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", "'urllib3.contrib.pyopenssl", - DeprecationWarning) - from requests_toolbelt.adapters.source import SourceAddressAdapter from acme import challenges from acme import crypto_util from acme import errors from acme import jws from acme import messages -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - from acme.mixins import VersionedLEACMEMixin logger = logging.getLogger(__name__) DEFAULT_NETWORK_TIMEOUT = 45 -DER_CONTENT_TYPE = 'application/pkix-cert' - -class ClientBase: - """ACME client base object. - - .. deprecated:: 1.30.0 - Use `ClientV2` instead. - - :ivar messages.Directory directory: - :ivar .ClientNetwork net: Client network. - :ivar int acme_version: ACME protocol version. 1 or 2. - """ - def __init__(self, directory: messages.Directory, net: 'ClientNetwork', - acme_version: int) -> None: - """Initialize. - - :param .messages.Directory directory: Directory Resource - :param .ClientNetwork net: Client network. - :param int acme_version: ACME protocol version. 1 or 2. - """ - self.directory = directory - self.net = net - self.acme_version = acme_version - - @classmethod - def _regr_from_response(cls, response: requests.Response, uri: Optional[str] = None, - terms_of_service: Optional[str] = None - ) -> messages.RegistrationResource: - if 'terms-of-service' in response.links: - terms_of_service = response.links['terms-of-service']['url'] - - return messages.RegistrationResource( - body=messages.Registration.from_json(response.json()), - uri=response.headers.get('Location', uri), - terms_of_service=terms_of_service) - - def _send_recv_regr(self, regr: messages.RegistrationResource, - body: messages.Registration) -> messages.RegistrationResource: - response = self._post(regr.uri, body) - - # TODO: Boulder returns httplib.ACCEPTED - #assert response.status_code == httplib.OK - - # TODO: Boulder does not set Location or Link on update - # (c.f. acme-spec #94) - - return self._regr_from_response( - response, uri=regr.uri, - terms_of_service=regr.terms_of_service) - - def _post(self, *args: Any, **kwargs: Any) -> requests.Response: - """Wrapper around self.net.post that adds the acme_version. - - """ - kwargs.setdefault('acme_version', self.acme_version) - if hasattr(self.directory, 'newNonce'): - kwargs.setdefault('new_nonce_url', getattr(self.directory, 'newNonce')) - return self.net.post(*args, **kwargs) - - def update_registration(self, regr: messages.RegistrationResource, - update: Optional[messages.Registration] = None - ) -> messages.RegistrationResource: - """Update registration. - - :param messages.RegistrationResource regr: Registration Resource. - :param messages.Registration update: Updated body of the - resource. If not provided, body will be taken from `regr`. - - :returns: Updated Registration Resource. - :rtype: `.RegistrationResource` - - """ - update = regr.body if update is None else update - body = messages.UpdateRegistration(**dict(update)) - updated_regr = self._send_recv_regr(regr, body=body) - self.net.account = updated_regr - return updated_regr - - def deactivate_registration(self, regr: messages.RegistrationResource - ) -> messages.RegistrationResource: - """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, messages.Registration.from_json( - {"status": "deactivated", "contact": None})) - - def deactivate_authorization(self, - authzr: messages.AuthorizationResource - ) -> messages.AuthorizationResource: - """Deactivate authorization. - - :param messages.AuthorizationResource authzr: The Authorization resource - to be deactivated. - - :returns: The Authorization resource that was deactivated. - :rtype: `.AuthorizationResource` - - """ - body = messages.UpdateAuthorization(status='deactivated') - response = self._post(authzr.uri, body) - return self._authzr_from_response(response, - authzr.body.identifier, authzr.uri) - - def _authzr_from_response(self, response: requests.Response, - identifier: Optional[messages.Identifier] = None, - uri: Optional[str] = None) -> messages.AuthorizationResource: - authzr = messages.AuthorizationResource( - body=messages.Authorization.from_json(response.json()), - uri=response.headers.get('Location', uri)) - if identifier is not None and authzr.body.identifier != identifier: # pylint: disable=no-member - raise errors.UnexpectedUpdate(authzr) - return authzr - - def answer_challenge(self, challb: messages.ChallengeBody, - response: challenges.ChallengeResponse) -> messages.ChallengeResource: - """Answer challenge. - - :param challb: Challenge Resource body. - :type challb: `.ChallengeBody` - - :param response: Corresponding Challenge response - :type response: `.challenges.ChallengeResponse` - - :returns: Challenge Resource with updated body. - :rtype: `.ChallengeResource` - - :raises .UnexpectedUpdate: - - """ - resp = self._post(challb.uri, response) - try: - authzr_uri = resp.links['up']['url'] - except KeyError: - raise errors.ClientError('"up" Link header missing') - challr = messages.ChallengeResource( - authzr_uri=authzr_uri, - body=messages.ChallengeBody.from_json(resp.json())) - # TODO: check that challr.uri == resp.headers['Location']? - if challr.uri != challb.uri: - raise errors.UnexpectedUpdate(challr.uri) - return challr - - @classmethod - def retry_after(cls, response: requests.Response, default: int) -> datetime.datetime: - """Compute next `poll` time based on response ``Retry-After`` header. - - Handles integers and various datestring formats per - https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.37 - - :param requests.Response response: Response from `poll`. - :param int default: Default value (in seconds), used when - ``Retry-After`` header is not present or invalid. - - :returns: Time point when next `poll` should be performed. - :rtype: `datetime.datetime` - - """ - retry_after = response.headers.get('Retry-After', str(default)) - try: - seconds = int(retry_after) - except ValueError: - # The RFC 2822 parser handles all of RFC 2616's cases in modern - # environments (primarily HTTP 1.1+ but also py27+) - when = parsedate_tz(retry_after) - if when is not None: - try: - tz_secs = datetime.timedelta(when[-1] if when[-1] is not None else 0) - return datetime.datetime(*when[:7]) - tz_secs - except (ValueError, OverflowError): - pass - seconds = default - - return datetime.datetime.now() + datetime.timedelta(seconds=seconds) - - def _revoke(self, cert: jose.ComparableX509, rsn: int, url: str) -> None: - """Revoke certificate. - - :param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in - `.ComparableX509` - - :param int rsn: Reason code for certificate revocation. - - :param str url: ACME URL to post to - - :raises .ClientError: If revocation is unsuccessful. - - """ - response = self._post(url, - messages.Revocation( - certificate=cert, - reason=rsn)) - if response.status_code != http_client.OK: - raise errors.ClientError( - 'Successful revocation must return HTTP OK status') - - -class Client(ClientBase): - """ACME client for a v1 API. - - .. deprecated:: 1.18.0 - Use :class:`ClientV2` instead. - - .. todo:: - Clean up raised error types hierarchy, document, and handle (wrap) - instances of `.DeserializationError` raised in `from_json()`. - - :ivar messages.Directory directory: - :ivar key: `josepy.JWK` (private) - :ivar alg: `josepy.JWASignature` - :ivar bool verify_ssl: Verify SSL certificates? - :ivar .ClientNetwork net: Client network. Useful for testing. If not - supplied, it will be initialized using `key`, `alg` and - `verify_ssl`. - - """ - - def __init__(self, directory: messages.Directory, key: jose.JWK, - alg: jose.JWASignature=jose.RS256, verify_ssl: bool = True, - net: Optional['ClientNetwork'] = None) -> None: - """Initialize. - - :param directory: Directory Resource (`.messages.Directory`) or - URI from which the resource will be downloaded. - - """ - self.key = key - if net is None: - net = ClientNetwork(key, alg=alg, verify_ssl=verify_ssl) - - if isinstance(directory, str): - directory = messages.Directory.from_json( - net.get(directory).json()) - super().__init__(directory=directory, - net=net, acme_version=1) - - def register(self, new_reg: Optional[messages.NewRegistration] = None - ) -> messages.RegistrationResource: - """Register. - - :param .NewRegistration new_reg: - - :returns: Registration Resource. - :rtype: `.RegistrationResource` - - """ - new_reg = messages.NewRegistration() if new_reg is None else new_reg - response = self._post(self.directory['new-reg'], new_reg) - # TODO: handle errors - assert response.status_code == http_client.CREATED - - # "Instance of 'Field' has no key/contact member" bug: - return self._regr_from_response(response) - - def query_registration(self, regr: messages.RegistrationResource - ) -> messages.RegistrationResource: - """Query server about registration. - - :param messages.RegistrationResource regr: Existing Registration - Resource. - - """ - return self._send_recv_regr(regr, messages.UpdateRegistration()) - - def agree_to_tos(self, regr: messages.RegistrationResource - ) -> messages.RegistrationResource: - """Agree to the terms-of-service. - - Agree to the terms-of-service in a Registration Resource. - - :param regr: Registration Resource. - :type regr: `.RegistrationResource` - - :returns: Updated Registration Resource. - :rtype: `.RegistrationResource` - - """ - return self.update_registration( - regr.update(body=regr.body.update(agreement=regr.terms_of_service))) - - def request_challenges(self, identifier: messages.Identifier, - new_authzr_uri: Optional[str] = None) -> messages.AuthorizationResource: - """Request challenges. - - :param .messages.Identifier identifier: Identifier to be challenged. - :param str new_authzr_uri: Deprecated. Do not use. - - :returns: Authorization Resource. - :rtype: `.AuthorizationResource` - - :raises errors.WildcardUnsupportedError: if a wildcard is requested - - """ - if new_authzr_uri is not None: - logger.debug("request_challenges with new_authzr_uri deprecated.") - - if identifier.value.startswith("*"): - raise errors.WildcardUnsupportedError( - "Requesting an authorization for a wildcard name is" - " forbidden by this version of the ACME protocol.") - - new_authz = messages.NewAuthorization(identifier=identifier) - response = self._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: str,new_authzr_uri: Optional[str] = None - ) -> messages.AuthorizationResource: - """Request challenges for domain names. - - This is simply a convenience function that wraps around - `request_challenges`, but works with domain names instead of - generic identifiers. See ``request_challenges`` for more - documentation. - - :param str domain: Domain name to be challenged. - :param str new_authzr_uri: Deprecated. Do not use. - - :returns: Authorization Resource. - :rtype: `.AuthorizationResource` - - :raises errors.WildcardUnsupportedError: if a wildcard is requested - - """ - return self.request_challenges(messages.Identifier( - typ=messages.IDENTIFIER_FQDN, value=domain), new_authzr_uri) - - def request_issuance(self, csr: jose.ComparableX509, - authzrs: Iterable[messages.AuthorizationResource] - ) -> messages.CertificateResource: - """Request issuance. - - :param csr: CSR - :type csr: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` - - :param authzrs: `list` of `.AuthorizationResource` - - :returns: Issued certificate - :rtype: `.messages.CertificateResource` - - """ - assert authzrs, "Authorizations list is empty" - logger.debug("Requesting issuance...") - - # TODO: assert len(authzrs) == number of SANs - req = messages.CertificateRequest(csr=csr) - - content_type = DER_CONTENT_TYPE # TODO: add 'cert_type 'argument - response = self._post( - self.directory.new_cert, - req, - content_type=content_type, - headers={'Accept': content_type}) - - cert_chain_uri = response.links.get('up', {}).get('url') - - try: - uri = response.headers['Location'] - except KeyError: - raise errors.ClientError('"Location" Header missing') - - return messages.CertificateResource( - uri=uri, authzrs=authzrs, cert_chain_uri=cert_chain_uri, - body=jose.ComparableX509(OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_ASN1, response.content))) - - def poll(self, authzr: messages.AuthorizationResource - ) -> Tuple[messages.AuthorizationResource, requests.Response]: - """Poll Authorization Resource for status. - - :param authzr: Authorization Resource - :type authzr: `.AuthorizationResource` - - :returns: Updated Authorization Resource and HTTP response. - - :rtype: (`.AuthorizationResource`, `requests.Response`) - - """ - response = self.net.get(authzr.uri) - updated_authzr = self._authzr_from_response( - response, authzr.body.identifier, authzr.uri) - return updated_authzr, response - - def poll_and_request_issuance(self, csr: jose.ComparableX509, - authzrs: Iterable[messages.AuthorizationResource], - mintime: int = 5, max_attempts: int = 10 - ) -> Tuple[messages.CertificateResource, - Tuple[messages.AuthorizationResource, ...]]: - """Poll and request issuance. - - This function polls all provided Authorization Resource URIs - until all challenges are valid, respecting ``Retry-After`` HTTP - headers, and then calls `request_issuance`. - - :param .ComparableX509 csr: CSR (`OpenSSL.crypto.X509Req` - wrapped in `.ComparableX509`) - :param authzrs: `list` of `.AuthorizationResource` - :param int mintime: Minimum time before next attempt, used if - ``Retry-After`` is not present in the response. - :param int max_attempts: Maximum number of attempts (per - authorization) before `PollError` with non-empty ``waiting`` - is raised. - - :returns: ``(cert, updated_authzrs)`` `tuple` where ``cert`` is - the issued certificate (`.messages.CertificateResource`), - and ``updated_authzrs`` is a `tuple` consisting of updated - Authorization Resources (`.AuthorizationResource`) as - present in the responses from server, and in the same order - as the input ``authzrs``. - :rtype: `tuple` - - :raises PollError: in case of timeout or if some authorization - was marked by the CA as invalid - - """ - assert max_attempts > 0 - attempts: Dict[messages.AuthorizationResource, int] = collections.defaultdict(int) - exhausted = set() - - # priority queue with datetime.datetime (based on Retry-After) as key, - # and original Authorization Resource as value - waiting = [ - (datetime.datetime.now(), index, authzr) - for index, authzr in enumerate(authzrs) - ] - heapq.heapify(waiting) - # mapping between original Authorization Resource and the most - # recently updated one - updated = {authzr: authzr for authzr in authzrs} - - while waiting: - # find the smallest Retry-After, and sleep if necessary - when, index, authzr = heapq.heappop(waiting) - now = datetime.datetime.now() - if when > now: - seconds = (when - now).seconds - logger.debug('Sleeping for %d seconds', seconds) - time.sleep(seconds) - - # Note that we poll with the latest updated Authorization - # URI, which might have a different URI than initial one - updated_authzr, response = self.poll(updated[authzr]) - updated[authzr] = updated_authzr - - attempts[authzr] += 1 - if updated_authzr.body.status not in ( # pylint: disable=no-member - messages.STATUS_VALID, messages.STATUS_INVALID): - if attempts[authzr] < max_attempts: - # push back to the priority queue, with updated retry_after - heapq.heappush(waiting, (self.retry_after( - response, default=mintime), index, authzr)) - else: - exhausted.add(authzr) - - if exhausted or any(authzr.body.status == messages.STATUS_INVALID - for authzr in updated.values()): - raise errors.PollError(exhausted, updated) - - updated_authzrs = tuple(updated[authzr] for authzr in authzrs) - return self.request_issuance(csr, updated_authzrs), updated_authzrs - - def _get_cert(self, uri: str) -> Tuple[requests.Response, jose.ComparableX509]: - """Returns certificate from URI. - - :param str uri: URI of certificate - - :returns: tuple of the form - (response, :class:`josepy.util.ComparableX509`) - :rtype: tuple - - """ - content_type = DER_CONTENT_TYPE # TODO: make it a param - response = self.net.get(uri, headers={'Accept': content_type}, - content_type=content_type) - return response, jose.ComparableX509(OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_ASN1, response.content)) - - def check_cert(self, certr: messages.CertificateResource) -> messages.CertificateResource: - """Check for new cert. - - :param certr: Certificate Resource - :type certr: `.CertificateResource` - - :returns: Updated Certificate Resource. - :rtype: `.CertificateResource` - - """ - # TODO: acme-spec 5.1 table action should be renamed to - # "refresh cert", and this method integrated with self.refresh - response, cert = self._get_cert(certr.uri) - if 'Location' not in response.headers: - raise errors.ClientError('Location header missing') - if response.headers['Location'] != certr.uri: - raise errors.UnexpectedUpdate(response.text) - return certr.update(body=cert) - - def refresh(self, certr: messages.CertificateResource) -> messages.CertificateResource: - """Refresh certificate. - - :param certr: Certificate Resource - :type certr: `.CertificateResource` - - :returns: Updated Certificate Resource. - :rtype: `.CertificateResource` - - """ - # TODO: If a client sends a refresh request and the server is - # not willing to refresh the certificate, the server MUST - # respond with status code 403 (Forbidden) - return self.check_cert(certr) - - def fetch_chain(self, certr: messages.CertificateResource, - max_length: int = 10) -> List[jose.ComparableX509]: - """Fetch chain for certificate. - - :param .CertificateResource certr: Certificate Resource - :param int max_length: Maximum allowed length of the chain. - Note that each element in the certificate requires new - ``HTTP GET`` request, and the length of the chain is - controlled by the ACME CA. - - :raises errors.Error: if recursion exceeds `max_length` - - :returns: Certificate chain for the Certificate Resource. It is - a list ordered so that the first element is a signer of the - certificate from Certificate Resource. Will be empty if - ``cert_chain_uri`` is ``None``. - :rtype: `list` of `OpenSSL.crypto.X509` wrapped in `.ComparableX509` - - """ - chain: List[jose.ComparableX509] = [] - uri = certr.cert_chain_uri - while uri is not None and len(chain) < max_length: - response, cert = self._get_cert(uri) - uri = response.links.get('up', {}).get('url') - chain.append(cert) - if uri is not None: - raise errors.Error( - "Recursion limit reached. Didn't get {0}".format(uri)) - return chain - - def revoke(self, cert: jose.ComparableX509, rsn: int) -> None: - """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. - - """ - self._revoke(cert, rsn, self.directory['revoke-cert']) - - -class ClientV2(ClientBase): +class ClientV2: """ACME client for a v2 API. :ivar messages.Directory directory: @@ -628,7 +46,8 @@ class ClientV2(ClientBase): :param .messages.Directory directory: Directory Resource :param .ClientNetwork net: Client network. """ - super().__init__(directory=directory, net=net, acme_version=2) + self.directory = directory + self.net = net def new_account(self, new_account: messages.NewRegistration) -> messages.RegistrationResource: """Register. @@ -675,8 +94,13 @@ class ClientV2(ClientBase): """ # https://github.com/certbot/certbot/issues/6155 - new_regr = self._get_v2_account(regr) - return super().update_registration(new_regr, update) + regr = self._get_v2_account(regr) + + update = regr.body if update is None else update + body = messages.UpdateRegistration(**dict(update)) + updated_regr = self._send_recv_regr(regr, body=body) + self.net.account = updated_regr + return updated_regr def _get_v2_account(self, regr: messages.RegistrationResource, update_body: bool = False ) -> messages.RegistrationResource: @@ -838,7 +262,9 @@ class ClientV2(ClientBase): def external_account_required(self) -> bool: """Checks if ACME server requires External Account Binding authentication.""" - return hasattr(self.directory, 'meta') and self.directory.meta.external_account_required + return hasattr(self.directory, 'meta') and \ + hasattr(self.directory.meta, 'external_account_required') and \ + self.directory.meta.external_account_required def _post_as_get(self, *args: Any, **kwargs: Any) -> requests.Response: """ @@ -864,138 +290,156 @@ class ClientV2(ClientBase): return [l['url'] for l in links if 'rel' in l and 'url' in l and l['rel'] == relation_type] + @classmethod + def get_directory(cls, url: str, net: 'ClientNetwork') -> messages.Directory: + """ + Retrieves the ACME directory (RFC 8555 section 7.1.1) from the ACME server. + :param str url: the URL where the ACME directory is available + :param ClientNetwork net: the ClientNetwork to use to make the request -class BackwardsCompatibleClientV2: - """ACME client wrapper that tends towards V2-style calls, but - supports V1 servers. + :returns: the ACME directory object + :rtype: messages.Directory + """ + return messages.Directory.from_json(net.get(url).json()) - .. deprecated:: 1.18.0 - Use :class:`ClientV2` instead. - - .. note:: While this class handles the majority of the differences - between versions of the ACME protocol, if you need to support an - ACME server based on version 3 or older of the IETF ACME draft - that uses combinations in authorizations (or lack thereof) to - signal that the client needs to complete something other than - any single challenge in the authorization to make it valid, the - user of this class needs to understand and handle these - differences themselves. This does not apply to either of Let's - Encrypt's endpoints where successfully completing any challenge - in an authorization will make it valid. - - :ivar int acme_version: 1 or 2, corresponding to the Let's Encrypt endpoint - :ivar .ClientBase client: either Client or ClientV2 - """ - - def __init__(self, net: 'ClientNetwork', key: jose.JWK, server: str) -> None: - directory = messages.Directory.from_json(net.get(server).json()) - self.acme_version = self._acme_version_from_directory(directory) - self.client: Union[Client, ClientV2] - if self.acme_version == 1: - self.client = Client(directory, key=key, net=net) - else: - self.client = ClientV2(directory, net=net) - - def __getattr__(self, name: str) -> Any: - return getattr(self.client, name) - - def new_account_and_tos(self, regr: messages.NewRegistration, - check_tos_cb: Optional[Callable[[str], None]] = None + @classmethod + def _regr_from_response(cls, response: requests.Response, uri: Optional[str] = None, + terms_of_service: Optional[str] = None ) -> messages.RegistrationResource: - """Combined register and agree_tos for V1, new_account for V2 + if 'terms-of-service' in response.links: + terms_of_service = response.links['terms-of-service']['url'] - :param .NewRegistration regr: - :param callable check_tos_cb: callback that raises an error if - the check does not work - """ - def _assess_tos(tos: str) -> None: - if check_tos_cb is not None: - check_tos_cb(tos) - if self.acme_version == 1: - client_v1 = cast(Client, self.client) - regr_res = client_v1.register(regr) - if regr_res.terms_of_service is not None: - _assess_tos(regr_res.terms_of_service) - return client_v1.agree_to_tos(regr_res) - return regr_res - else: - client_v2 = cast(ClientV2, self.client) - if ("terms_of_service" in client_v2.directory.meta and - client_v2.directory.meta.terms_of_service is not None): - _assess_tos(client_v2.directory.meta.terms_of_service) - regr = regr.update(terms_of_service_agreed=True) - return client_v2.new_account(regr) + return messages.RegistrationResource( + body=messages.Registration.from_json(response.json()), + uri=response.headers.get('Location', uri), + terms_of_service=terms_of_service) - def new_order(self, csr_pem: bytes) -> messages.OrderResource: - """Request a new Order object from the server. + def _send_recv_regr(self, regr: messages.RegistrationResource, + body: messages.Registration) -> messages.RegistrationResource: + response = self._post(regr.uri, body) - If using ACMEv1, returns a dummy OrderResource with only - the authorizations field filled in. + # TODO: Boulder returns httplib.ACCEPTED + #assert response.status_code == httplib.OK - :param bytes csr_pem: A CSR in PEM format. + # TODO: Boulder does not set Location or Link on update + # (c.f. acme-spec #94) - :returns: The newly created order. - :rtype: OrderResource + return self._regr_from_response( + response, uri=regr.uri, + terms_of_service=regr.terms_of_service) - :raises errors.WildcardUnsupportedError: if a wildcard domain is - requested but unsupported by the ACME version + def _post(self, *args: Any, **kwargs: Any) -> requests.Response: + """Wrapper around self.net.post that adds the newNonce URL. + + This is used to retry the request in case of a badNonce error. """ - if self.acme_version == 1: - client_v1 = cast(Client, self.client) - csr = OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr_pem) - # pylint: disable=protected-access - dnsNames = crypto_util._pyopenssl_cert_or_req_all_names(csr) - authorizations = [] - for domain in dnsNames: - authorizations.append(client_v1.request_domain_challenges(domain)) - return messages.OrderResource(authorizations=authorizations, csr_pem=csr_pem) - return cast(ClientV2, self.client).new_order(csr_pem) + kwargs.setdefault('new_nonce_url', getattr(self.directory, 'newNonce')) + return self.net.post(*args, **kwargs) - def finalize_order(self, orderr: messages.OrderResource, deadline: datetime.datetime, - fetch_alternative_chains: bool = False) -> messages.OrderResource: - """Finalize an order and obtain a certificate. + def deactivate_registration(self, regr: messages.RegistrationResource + ) -> messages.RegistrationResource: + """Deactivate registration. - :param messages.OrderResource orderr: order to finalize - :param datetime.datetime deadline: when to stop polling and timeout - :param bool fetch_alternative_chains: whether to also fetch alternative - certificate chains + :param messages.RegistrationResource regr: The Registration Resource + to be deactivated. - :returns: finalized order - :rtype: messages.OrderResource + :returns: The Registration resource that was deactivated. + :rtype: `.RegistrationResource` """ - if self.acme_version == 1: - client_v1 = cast(Client, self.client) - csr_pem = orderr.csr_pem - certr = client_v1.request_issuance( - jose.ComparableX509( - OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr_pem)), - orderr.authorizations) + return self.update_registration(regr, messages.Registration.from_json( + {"status": "deactivated", "contact": None})) - chain = None - while datetime.datetime.now() < deadline: + def deactivate_authorization(self, + authzr: messages.AuthorizationResource + ) -> messages.AuthorizationResource: + """Deactivate authorization. + + :param messages.AuthorizationResource authzr: The Authorization resource + to be deactivated. + + :returns: The Authorization resource that was deactivated. + :rtype: `.AuthorizationResource` + + """ + body = messages.UpdateAuthorization(status='deactivated') + response = self._post(authzr.uri, body) + return self._authzr_from_response(response, + authzr.body.identifier, authzr.uri) + + def _authzr_from_response(self, response: requests.Response, + identifier: Optional[messages.Identifier] = None, + uri: Optional[str] = None) -> messages.AuthorizationResource: + authzr = messages.AuthorizationResource( + body=messages.Authorization.from_json(response.json()), + uri=response.headers.get('Location', uri)) + if identifier is not None and authzr.body.identifier != identifier: # pylint: disable=no-member + raise errors.UnexpectedUpdate(authzr) + return authzr + + def answer_challenge(self, challb: messages.ChallengeBody, + response: challenges.ChallengeResponse) -> messages.ChallengeResource: + """Answer challenge. + + :param challb: Challenge Resource body. + :type challb: `.ChallengeBody` + + :param response: Corresponding Challenge response + :type response: `.challenges.ChallengeResponse` + + :returns: Challenge Resource with updated body. + :rtype: `.ChallengeResource` + + :raises .UnexpectedUpdate: + + """ + resp = self._post(challb.uri, response) + try: + authzr_uri = resp.links['up']['url'] + except KeyError: + raise errors.ClientError('"up" Link header missing') + challr = messages.ChallengeResource( + authzr_uri=authzr_uri, + body=messages.ChallengeBody.from_json(resp.json())) + # TODO: check that challr.uri == resp.headers['Location']? + if challr.uri != challb.uri: + raise errors.UnexpectedUpdate(challr.uri) + return challr + + @classmethod + def retry_after(cls, response: requests.Response, default: int) -> datetime.datetime: + """Compute next `poll` time based on response ``Retry-After`` header. + + Handles integers and various datestring formats per + https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.37 + + :param requests.Response response: Response from `poll`. + :param int default: Default value (in seconds), used when + ``Retry-After`` header is not present or invalid. + + :returns: Time point when next `poll` should be performed. + :rtype: `datetime.datetime` + + """ + retry_after = response.headers.get('Retry-After', str(default)) + try: + seconds = int(retry_after) + except ValueError: + # The RFC 2822 parser handles all of RFC 2616's cases in modern + # environments (primarily HTTP 1.1+ but also py27+) + when = parsedate_tz(retry_after) + if when is not None: try: - chain = client_v1.fetch_chain(certr) - break - except errors.Error: - time.sleep(1) + tz_secs = datetime.timedelta(when[-1] if when[-1] is not None else 0) + return datetime.datetime(*when[:7]) - tz_secs + except (ValueError, OverflowError): + pass + seconds = default - if chain is None: - raise errors.TimeoutError( - 'Failed to fetch chain. You should not deploy the generated ' - 'certificate, please rerun the command for a new one.') + return datetime.datetime.now() + datetime.timedelta(seconds=seconds) - cert = OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_PEM, - cast(OpenSSL.crypto.X509, cast(jose.ComparableX509, certr.body).wrapped)).decode() - chain_str = crypto_util.dump_pyopenssl_chain(chain).decode() - - return orderr.update(fullchain_pem=(cert + chain_str)) - return cast(ClientV2, self.client).finalize_order( - orderr, deadline, fetch_alternative_chains) - - def revoke(self, cert: jose.ComparableX509, rsn: int) -> None: + def _revoke(self, cert: jose.ComparableX509, rsn: int, url: str) -> None: """Revoke certificate. :param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in @@ -1003,23 +447,18 @@ class BackwardsCompatibleClientV2: :param int rsn: Reason code for certificate revocation. + :param str url: ACME URL to post to + :raises .ClientError: If revocation is unsuccessful. """ - self.client.revoke(cert, rsn) - - def _acme_version_from_directory(self, directory: messages.Directory) -> int: - if hasattr(directory, 'newNonce'): - return 2 - return 1 - - def external_account_required(self) -> bool: - """Checks if the server requires an external account for ACMEv2 servers. - - Always return False for ACMEv1 servers, as it doesn't use External Account Binding.""" - if self.acme_version == 1: - return False - return cast(ClientV2, self.client).external_account_required() + response = self._post(url, + messages.Revocation( + certificate=cert, + reason=rsn)) + if response.status_code != http_client.OK: + raise errors.ClientError( + 'Successful revocation must return HTTP OK status') class ClientNetwork: @@ -1036,20 +475,16 @@ class ClientNetwork: :param josepy.JWK key: Account private key :param messages.RegistrationResource account: Account object. Required if you are - planning to use .post() with acme_version=2 for anything other than - creating a new account; may be set later after registering. + planning to use .post() for anything other than creating a new account; + may be set later after registering. :param josepy.JWASignature alg: Algorithm to use in signing JWS. :param bool verify_ssl: Whether to verify certificates on SSL connections. :param str user_agent: String to send as User-Agent header. - :param float timeout: Timeout for requests. - :param source_address: Optional source address to bind to when making - requests. (deprecated since 1.30.0) - :type source_address: str or tuple(str, int) + :param int timeout: Timeout for requests. """ def __init__(self, key: jose.JWK, account: Optional[messages.RegistrationResource] = None, alg: jose.JWASignature = jose.RS256, verify_ssl: bool = True, - user_agent: str = 'acme-python', timeout: int = DEFAULT_NETWORK_TIMEOUT, - source_address: Optional[Union[str, Tuple[str, int]]] = None) -> None: + user_agent: str = 'acme-python', timeout: int = DEFAULT_NETWORK_TIMEOUT) -> None: self.key = key self.account = account self.alg = alg @@ -1060,11 +495,6 @@ class ClientNetwork: self._default_timeout = timeout adapter = HTTPAdapter() - if source_address is not None: - warnings.warn("Support for source_address is deprecated and will be " - "removed soon.", DeprecationWarning, stacklevel=2) - adapter = SourceAddressAdapter(source_address) - self.session.mount("http://", adapter) self.session.mount("https://", adapter) @@ -1076,8 +506,7 @@ class ClientNetwork: except Exception: # pylint: disable=broad-except pass - def _wrap_in_jws(self, obj: jose.JSONDeSerializable, nonce: str, url: str, - acme_version: int) -> str: + def _wrap_in_jws(self, obj: jose.JSONDeSerializable, nonce: str, url: str) -> str: """Wrap `JSONDeSerializable` object in JWS. .. todo:: Implement ``acmePath``. @@ -1088,20 +517,17 @@ class ClientNetwork: :rtype: str """ - if isinstance(obj, VersionedLEACMEMixin): - obj.le_acme_version = acme_version jobj = obj.json_dumps(indent=2).encode() if obj else b'' logger.debug('JWS payload:\n%s', jobj) kwargs = { "alg": self.alg, "nonce": nonce, + "url": url } - if acme_version == 2: - kwargs["url"] = url - # newAccount and revokeCert work without the kid - # newAccount must not have kid - if self.account is not None: - kwargs["kid"] = self.account["uri"] + # newAccount and revokeCert work without the kid + # newAccount must not have kid + if self.account is not None: + kwargs["kid"] = self.account["uri"] kwargs["key"] = self.key return jws.JWS.sign(jobj, **cast(Mapping[str, Any], kwargs)).json_dumps(indent=2) @@ -1215,15 +641,11 @@ class ClientNetwork: host, path, _err_no, err_msg = m.groups() raise ValueError(f"Requesting {host}{path}:{err_msg}") - # If the Content-Type is DER or an Accept header was sent in the - # request, the response may not be UTF-8 encoded. In this case, we - # don't set response.encoding and log the base64 response instead of - # raw bytes to keep binary data out of the logs. This code can be - # simplified to only check for an Accept header in the request when - # ACMEv1 support is dropped. + # If an Accept header was sent in the request, the response may not be + # UTF-8 encoded. In this case, we don't set response.encoding and log + # the base64 response instead of raw bytes to keep binary data out of the logs. debug_content: Union[bytes, str] - if (response.headers.get("Content-Type") == DER_CONTENT_TYPE or - "Accept" in kwargs["headers"]): + if "Accept" in kwargs["headers"]: debug_content = base64.b64encode(response.content) else: # We set response.encoding so response.text knows the response is @@ -1294,44 +716,11 @@ class ClientNetwork: raise def _post_once(self, url: str, obj: jose.JSONDeSerializable, - content_type: str = JOSE_CONTENT_TYPE, acme_version: int = 1, - **kwargs: Any) -> requests.Response: + content_type: str = JOSE_CONTENT_TYPE, **kwargs: Any) -> requests.Response: new_nonce_url = kwargs.pop('new_nonce_url', None) - data = self._wrap_in_jws(obj, self._get_nonce(url, new_nonce_url), url, acme_version) + data = self._wrap_in_jws(obj, self._get_nonce(url, new_nonce_url), url) kwargs.setdefault('headers', {'Content-Type': content_type}) response = self._send_request('POST', url, data=data, **kwargs) response = self._check_response(response, content_type=content_type) self._add_nonce(response) return response - - -# This class takes a similar approach to the cryptography project to deprecate attributes -# in public modules. See the _ModuleWithDeprecation class here: -# https://github.com/pyca/cryptography/blob/91105952739442a74582d3e62b3d2111365b0dc7/src/cryptography/utils.py#L129 -class _ClientDeprecationModule: - """ - Internal class delegating to a module, and displaying warnings when attributes - related to deprecated attributes in the acme.client module. - """ - def __init__(self, module: ModuleType) -> None: - self.__dict__['_module'] = module - - def __getattr__(self, attr: str) -> Any: - if attr in ('Client', 'ClientBase', 'BackwardsCompatibleClientV2'): - warnings.warn('The {0} attribute in acme.client is deprecated ' - 'and will be removed soon.'.format(attr), - DeprecationWarning, stacklevel=2) - return getattr(self._module, attr) - - def __setattr__(self, attr: str, value: Any) -> None: # pragma: no cover - setattr(self._module, attr, value) - - def __delattr__(self, attr: str) -> None: # pragma: no cover - delattr(self._module, attr) - - def __dir__(self) -> List[str]: # pragma: no cover - return ['_module'] + dir(self._module) - - -# Patching ourselves to warn about deprecation and planned removal of some elements in the module. -sys.modules[__name__] = cast(ModuleType, _ClientDeprecationModule(sys.modules[__name__])) diff --git a/acme/acme/fields.py b/acme/acme/fields.py index 8a1dc8462..d642d10c5 100644 --- a/acme/acme/fields.py +++ b/acme/acme/fields.py @@ -1,12 +1,8 @@ """ACME JSON fields.""" import datetime -import logging -import sys -from types import ModuleType from typing import Any -from typing import cast -from typing import List -import warnings + +import logging import josepy as jose import pyrfc3339 @@ -55,26 +51,6 @@ class RFC3339Field(jose.Field): raise jose.DeserializationError(error) -class Resource(jose.Field): - """Resource MITM field. - - .. deprecated: 1.30.0 - - """ - - def __init__(self, resource_type: str, *args: Any, **kwargs: Any) -> None: - self.resource_type = resource_type - kwargs['default'] = resource_type - super().__init__('resource', *args, **kwargs) - - def decode(self, value: Any) -> Any: - if value != self.resource_type: - raise jose.DeserializationError( - 'Wrong resource type: {0} instead of {1}'.format( - value, self.resource_type)) - return value - - def fixed(json_name: str, value: Any) -> Any: """Generates a type-friendly Fixed field.""" return Fixed(json_name, value) @@ -83,43 +59,3 @@ def fixed(json_name: str, value: Any) -> Any: def rfc3339(json_name: str, omitempty: bool = False) -> Any: """Generates a type-friendly RFC3339 field.""" return RFC3339Field(json_name, omitempty=omitempty) - - -def resource(resource_type: str) -> Any: - """Generates a type-friendly Resource field. - - .. deprecated: 1.30.0 - - """ - return Resource(resource_type) - - -# This class takes a similar approach to the cryptography project to deprecate attributes -# in public modules. See the _ModuleWithDeprecation class here: -# https://github.com/pyca/cryptography/blob/91105952739442a74582d3e62b3d2111365b0dc7/src/cryptography/utils.py#L129 -class _FieldsDeprecationModule: # pragma: no cover - """ - Internal class delegating to a module, and displaying warnings when - module attributes deprecated in acme.fields are accessed. - """ - def __init__(self, module: ModuleType) -> None: - self.__dict__['_module'] = module - - def __getattr__(self, attr: str) -> None: - if attr in ('Resource', 'resource'): - warnings.warn('{0} attribute in acme.fields module is deprecated ' - 'and will be removed soon.'.format(attr), - DeprecationWarning, stacklevel=2) - return getattr(self._module, attr) - - def __setattr__(self, attr: str, value: Any) -> None: - setattr(self._module, attr, value) - - def __delattr__(self, attr: str) -> None: - delattr(self._module, attr) - - def __dir__(self) -> List[str]: - return ['_module'] + dir(self._module) - - -sys.modules[__name__] = cast(ModuleType, _FieldsDeprecationModule(sys.modules[__name__])) diff --git a/acme/acme/magic_typing.py b/acme/acme/magic_typing.py deleted file mode 100644 index b05d2c4bc..000000000 --- a/acme/acme/magic_typing.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Simple shim around the typing module. - -This was useful when this code supported Python 2 and typing wasn't always -available. This code is being kept for now for backwards compatibility. - -""" -import warnings -from typing import * # pylint: disable=wildcard-import, unused-wildcard-import -from typing import Any - -warnings.warn("acme.magic_typing is deprecated and will be removed in a future release.", - DeprecationWarning) - - -class TypingClass: - """Ignore import errors by getting anything""" - def __getattr__(self, name: str) -> Any: - return None # pragma: no cover diff --git a/acme/acme/messages.py b/acme/acme/messages.py index a6eab3467..0e02a054e 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -2,9 +2,7 @@ import datetime from collections.abc import Hashable import json -from types import ModuleType from typing import Any -from typing import cast from typing import Dict from typing import Iterator from typing import List @@ -13,11 +11,7 @@ from typing import MutableMapping from typing import Optional from typing import Tuple from typing import Type -from typing import TYPE_CHECKING from typing import TypeVar -from typing import Union -import sys -import warnings import josepy as jose @@ -26,16 +20,8 @@ from acme import errors from acme import fields from acme import jws from acme import util -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", ".*acme.mixins", category=DeprecationWarning) - from acme.mixins import ResourceMixin -if TYPE_CHECKING: - from typing_extensions import Protocol # pragma: no cover -else: - Protocol = object -OLD_ERROR_PREFIX = "urn:acme:error:" ERROR_PREFIX = "urn:ietf:params:acme:error:" ERROR_CODES = { @@ -73,15 +59,13 @@ ERROR_CODES = { ERROR_TYPE_DESCRIPTIONS = {**{ ERROR_PREFIX + name: desc for name, desc in ERROR_CODES.items() -}, **{ # add errors with old prefix, deprecate me - OLD_ERROR_PREFIX + name: desc for name, desc in ERROR_CODES.items() }} def is_acme_error(err: BaseException) -> bool: """Check if argument is an ACME error.""" if isinstance(err, Error) and (err.typ is not None): - return (ERROR_PREFIX in err.typ) or (OLD_ERROR_PREFIX in err.typ) + return ERROR_PREFIX in err.typ return False @@ -229,25 +213,15 @@ STATUS_READY = Status('ready') STATUS_DEACTIVATED = Status('deactivated') -class HasResourceType(Protocol): - """ - Represents a class with a resource_type class parameter of type string. - """ - resource_type: str = NotImplemented - - -GenericHasResourceType = TypeVar("GenericHasResourceType", bound=HasResourceType) - - class Directory(jose.JSONDeSerializable): - """Directory.""" + """Directory. - _REGISTERED_TYPES: Dict[str, Type[HasResourceType]] = {} + Directory resources must be accessed by the exact field name in RFC8555 (section 9.7.5). + """ class Meta(jose.JSONObjectWithFields): """Directory Meta.""" - _terms_of_service: str = jose.field('terms-of-service', omitempty=True) - _terms_of_service_v2: str = jose.field('termsOfService', omitempty=True) + _terms_of_service: str = jose.field('termsOfService', omitempty=True) website: str = jose.field('website', omitempty=True) caa_identities: List[str] = jose.field('caaIdentities', omitempty=True) external_account_required: bool = jose.field('externalAccountRequired', omitempty=True) @@ -259,7 +233,7 @@ class Directory(jose.JSONDeSerializable): @property def terms_of_service(self) -> str: """URL for the CA TOS""" - return self._terms_of_service or self._terms_of_service_v2 + return self._terms_of_service def __iter__(self) -> Iterator[str]: # When iterating over fields, use the external name 'terms_of_service' instead of @@ -270,51 +244,23 @@ class Directory(jose.JSONDeSerializable): def _internal_name(self, name: str) -> str: return '_' + name if name == 'terms_of_service' else name - @classmethod - def _canon_key(cls, key: Union[str, HasResourceType, Type[HasResourceType]]) -> str: - if isinstance(key, str): - return key - return key.resource_type - - @classmethod - def register(cls, - resource_body_cls: Type[GenericHasResourceType]) -> Type[GenericHasResourceType]: - """Register resource.""" - warnings.warn( - "acme.messages.Directory.register is deprecated and will be removed in the next " - "major release of Certbot", DeprecationWarning, stacklevel=2 - ) - resource_type = resource_body_cls.resource_type - assert resource_type not in cls._REGISTERED_TYPES - cls._REGISTERED_TYPES[resource_type] = resource_body_cls - return resource_body_cls - def __init__(self, jobj: Mapping[str, Any]) -> None: - canon_jobj = util.map_keys(jobj, self._canon_key) - # TODO: check that everything is an absolute URL; acme-spec is - # not clear on that - self._jobj = canon_jobj + self._jobj = jobj def __getattr__(self, name: str) -> Any: try: - return self[name.replace('_', '-')] + return self[name] except KeyError as error: raise AttributeError(str(error)) - def __getitem__(self, name: Union[str, HasResourceType, Type[HasResourceType]]) -> Any: - if not isinstance(name, str): - warnings.warn( - "Looking up acme.messages.Directory resources by non-string keys is deprecated " - "and will be removed in the next major release of Certbot", - DeprecationWarning, stacklevel=2 - ) + def __getitem__(self, name: str) -> Any: try: - return self._jobj[self._canon_key(name)] + return self._jobj[name] except KeyError: - raise KeyError('Directory field "' + self._canon_key(name) + '" not found') + raise KeyError(f'Directory field "{name}" not found') def to_partial_json(self) -> Dict[str, Any]: - return self._jobj + return util.map_keys(self._jobj, lambda k: k) @classmethod def from_json(cls, jobj: MutableMapping[str, Any]) -> 'Directory': @@ -475,6 +421,14 @@ class Registration(ResourceBody): return self._filter_contact(self.email_prefix) +class NewRegistration(Registration): + """New registration.""" + + +class UpdateRegistration(Registration): + """Update registration.""" + + class RegistrationResource(ResourceWithURI): """Registration Resource. @@ -510,7 +464,6 @@ class ChallengeBody(ResourceBody): # challenge object supports either one, but should be accessed through the # name "uri". In Client.answer_challenge, whichever one is set will be # used. - _uri: str = jose.field('uri', omitempty=True, default=None) _url: str = jose.field('url', omitempty=True, default=None) status: Status = jose.field('status', decoder=Status.from_json, omitempty=True, default=STATUS_PENDING) @@ -539,7 +492,7 @@ class ChallengeBody(ResourceBody): @property def uri(self) -> str: """The URL of this challenge.""" - return self._url or self._uri + return self._url def __getattr__(self, name: str) -> Any: return getattr(self.chall, name) @@ -548,10 +501,10 @@ class ChallengeBody(ResourceBody): # When iterating over fields, use the external name 'uri' instead of # the internal '_uri'. for name in super().__iter__(): - yield name[1:] if name == '_uri' else name + yield 'uri' if name == '_url' else name def _internal_name(self, name: str) -> str: - return '_' + name if name == 'uri' else name + return '_url' if name == 'uri' else name class ChallengeResource(Resource): @@ -575,15 +528,12 @@ class Authorization(ResourceBody): :ivar acme.messages.Identifier identifier: :ivar list challenges: `list` of `.ChallengeBody` - :ivar tuple combinations: Challenge combinations (`tuple` of `tuple` - of `int`, as opposed to `list` of `list` from the spec). (deprecated since 1.30.0) :ivar acme.messages.Status status: :ivar datetime.datetime expires: """ identifier: Identifier = jose.field('identifier', decoder=Identifier.from_json, omitempty=True) challenges: List[ChallengeBody] = jose.field('challenges', omitempty=True) - _combinations: Tuple[Tuple[int, ...], ...] = jose.field('combinations', omitempty=True) status: Status = jose.field('status', omitempty=True, decoder=Status.from_json) # TODO: 'expires' is allowed for Authorization Resources in @@ -593,53 +543,19 @@ class Authorization(ResourceBody): expires: datetime.datetime = fields.rfc3339('expires', omitempty=True) wildcard: bool = jose.field('wildcard', omitempty=True) - # combinations is temporarily renamed to _combinations during its deprecation - # period. See https://github.com/certbot/certbot/pull/9369#issuecomment-1199849262. - def __init__(self, **kwargs: Any) -> None: - if 'combinations' in kwargs: - kwargs['_combinations'] = kwargs.pop('combinations') - super().__init__(**kwargs) - # Mypy does not understand the josepy magic happening here, and falsely claims # that challenge is redefined. Let's ignore the type check here. @challenges.decoder # type: ignore def challenges(value: List[Dict[str, Any]]) -> Tuple[ChallengeBody, ...]: # pylint: disable=no-self-argument,missing-function-docstring return tuple(ChallengeBody.from_json(chall) for chall in value) - @property - def combinations(self) -> Tuple[Tuple[int, ...], ...]: - """Challenge combinations. - (`tuple` of `tuple` of `int`, as opposed to `list` of `list` from the spec). - .. deprecated: 1.30.0 +class NewAuthorization(Authorization): + """New authorization.""" - """ - warnings.warn( - "acme.messages.Authorization.combinations is deprecated and will be " - "removed in a future release.", DeprecationWarning, stacklevel=2) - return self._combinations - @combinations.setter - def combinations(self, combos: Tuple[Tuple[int, ...], ...]) -> None: # pragma: no cover - warnings.warn( - "acme.messages.Authorization.combinations is deprecated and will be " - "removed in a future release.", DeprecationWarning, stacklevel=2) - self._combinations = combos - - @property - def resolved_combinations(self) -> Tuple[Tuple[ChallengeBody, ...], ...]: - """Combinations with challenges instead of indices. - - .. deprecated: 1.30.0 - - """ - warnings.warn( - "acme.messages.Authorization.resolved_combinations is deprecated and will be " - "removed in a future release.", DeprecationWarning, stacklevel=2) - with warnings.catch_warnings(): - warnings.filterwarnings('ignore', '.*combinations', DeprecationWarning) - return tuple(tuple(self.challenges[idx] for idx in combo) - for combo in self.combinations) # pylint: disable=not-an-iterable +class UpdateAuthorization(Authorization): + """Update authorization.""" class AuthorizationResource(ResourceWithURI): @@ -653,6 +569,16 @@ class AuthorizationResource(ResourceWithURI): new_cert_uri: str = jose.field('new_cert_uri', omitempty=True) +class CertificateRequest(jose.JSONObjectWithFields): + """ACME newOrder request. + + :ivar jose.ComparableX509 csr: + `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` + + """ + csr: jose.ComparableX509 = jose.field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr) + + class CertificateResource(ResourceWithURI): """Certificate Resource. @@ -666,6 +592,18 @@ class CertificateResource(ResourceWithURI): authzrs: Tuple[AuthorizationResource, ...] = jose.field('authzrs') +class Revocation(jose.JSONObjectWithFields): + """Revocation message. + + :ivar jose.ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in + `jose.ComparableX509` + + """ + certificate: jose.ComparableX509 = jose.field( + 'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert) + reason: int = jose.field('reason') + + class Order(ResourceBody): """Order Resource Body. @@ -717,98 +655,5 @@ class OrderResource(ResourceWithURI): omitempty=True) -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", "acme.messages.Directory.register", DeprecationWarning) - warnings.filterwarnings("ignore", "resource attribute in acme.fields", DeprecationWarning) - - @Directory.register - class NewOrder(Order): - """New order.""" - resource_type = 'new-order' - - - @Directory.register - class Revocation(ResourceMixin, jose.JSONObjectWithFields): - """Revocation message. - - :ivar jose.ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in - `jose.ComparableX509` - - """ - resource_type = 'revoke-cert' - resource: str = fields.resource(resource_type) - certificate: jose.ComparableX509 = jose.field( - 'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert) - reason: int = jose.field('reason') - - - @Directory.register - class CertificateRequest(ResourceMixin, jose.JSONObjectWithFields): - """ACME new-cert request. - - :ivar jose.ComparableX509 csr: - `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` - - """ - resource_type = 'new-cert' - resource: str = fields.resource(resource_type) - csr: jose.ComparableX509 = jose.field('csr', decoder=jose.decode_csr, - encoder=jose.encode_csr) - - - @Directory.register - class NewAuthorization(ResourceMixin, Authorization): - """New authorization.""" - resource_type = 'new-authz' - resource: str = fields.resource(resource_type) - - - class UpdateAuthorization(ResourceMixin, Authorization): - """Update authorization.""" - resource_type = 'authz' - resource: str = fields.resource(resource_type) - - - @Directory.register - class NewRegistration(ResourceMixin, Registration): - """New registration.""" - resource_type = 'new-reg' - resource: str = fields.resource(resource_type) - - - class UpdateRegistration(ResourceMixin, Registration): - """Update registration.""" - resource_type = 'reg' - resource: str = fields.resource(resource_type) - - -# This class takes a similar approach to the cryptography project to deprecate attributes -# in public modules. See the _ModuleWithDeprecation class here: -# https://github.com/pyca/cryptography/blob/91105952739442a74582d3e62b3d2111365b0dc7/src/cryptography/utils.py#L129 -class _MessagesDeprecationModule: # pragma: no cover - """ - Internal class delegating to a module, and displaying warnings when - module attributes deprecated in acme.messages are accessed. - """ - def __init__(self, module: ModuleType) -> None: - self.__dict__['_module'] = module - - def __getattr__(self, attr: str) -> None: - if attr == 'OLD_ERROR_PREFIX': - warnings.warn('{0} attribute in acme.messages module is deprecated ' - 'and will be removed soon.'.format(attr), - DeprecationWarning, stacklevel=2) - return getattr(self._module, attr) - - def __setattr__(self, attr: str, value: Any) -> None: - setattr(self._module, attr, value) - - def __delattr__(self, attr: str) -> None: - delattr(self._module, attr) - - def __dir__(self) -> List[str]: - return ['_module'] + dir(self._module) - - -# Patching ourselves to warn about acme.messages.OLD_ERROR_PREFIX deprecation and planned removal. -sys.modules[__name__] = cast(ModuleType, _MessagesDeprecationModule(sys.modules[__name__])) +class NewOrder(Order): + """New order.""" diff --git a/acme/acme/mixins.py b/acme/acme/mixins.py deleted file mode 100644 index 4c52957a5..000000000 --- a/acme/acme/mixins.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Useful mixins for Challenge and Resource objects""" -from typing import Any -from typing import Dict -import warnings - -warnings.warn(f'The module {__name__} is deprecated and will be removed in a future release', - DeprecationWarning, stacklevel=2) - - -class VersionedLEACMEMixin: - """This mixin stores the version of Let's Encrypt's endpoint being used.""" - @property - def le_acme_version(self) -> int: - """Define the version of ACME protocol to use""" - return getattr(self, '_le_acme_version', 1) - - @le_acme_version.setter - def le_acme_version(self, version: int) -> None: - # We need to use object.__setattr__ to not depend on the specific implementation of - # __setattr__ in current class (eg. jose.TypedJSONObjectWithFields raises AttributeError - # for any attempt to set an attribute to make objects immutable). - object.__setattr__(self, '_le_acme_version', version) - - def __setattr__(self, key: str, value: Any) -> None: - if key == 'le_acme_version': - # Required for @property to operate properly. See comment above. - object.__setattr__(self, key, value) - else: - super().__setattr__(key, value) # pragma: no cover - - -class ResourceMixin(VersionedLEACMEMixin): - """ - This mixin generates a RFC8555 compliant JWS payload - by removing the `resource` field if needed (eg. ACME v2 protocol). - """ - def to_partial_json(self) -> Dict[str, Any]: - """See josepy.JSONDeserializable.to_partial_json()""" - return _safe_jobj_compliance(super(), - 'to_partial_json', 'resource') - - def fields_to_partial_json(self) -> Dict[str, Any]: - """See josepy.JSONObjectWithFields.fields_to_partial_json()""" - return _safe_jobj_compliance(super(), - 'fields_to_partial_json', 'resource') - - -class TypeMixin(VersionedLEACMEMixin): - """ - This mixin allows generation of a RFC8555 compliant JWS payload - by removing the `type` field if needed (eg. ACME v2 protocol). - """ - def to_partial_json(self) -> Dict[str, Any]: - """See josepy.JSONDeserializable.to_partial_json()""" - return _safe_jobj_compliance(super(), - 'to_partial_json', 'type') - - def fields_to_partial_json(self) -> Dict[str, Any]: - """See josepy.JSONObjectWithFields.fields_to_partial_json()""" - return _safe_jobj_compliance(super(), - 'fields_to_partial_json', 'type') - - -def _safe_jobj_compliance(instance: Any, jobj_method: str, - uncompliant_field: str) -> Dict[str, Any]: - if hasattr(instance, jobj_method): - jobj: Dict[str, Any] = getattr(instance, jobj_method)() - if instance.le_acme_version == 2: - jobj.pop(uncompliant_field, None) - return jobj - - raise AttributeError(f'Method {jobj_method}() is not implemented.') # pragma: no cover diff --git a/acme/examples/http01_example.py b/acme/examples/http01_example.py index 2dc197d09..ab62ecbcc 100644 --- a/acme/examples/http01_example.py +++ b/acme/examples/http01_example.py @@ -163,7 +163,7 @@ def example_http(): # Register account and accept TOS net = client.ClientNetwork(acc_key, user_agent=USER_AGENT) - directory = messages.Directory.from_json(net.get(DIRECTORY_URL).json()) + directory = client.ClientV2.get_directory(DIRECTORY_URL, net) client_acme = client.ClientV2(directory, net=net) # Terms of Service URL is in client_acme.directory.meta.terms_of_service @@ -215,8 +215,7 @@ def example_http(): try: regr = client_acme.query_registration(regr) except errors.Error as err: - if err.typ == messages.OLD_ERROR_PREFIX + 'unauthorized' \ - or err.typ == messages.ERROR_PREFIX + 'unauthorized': + if err.typ == messages.ERROR_PREFIX + 'unauthorized': # Status is deactivated. pass raise diff --git a/acme/setup.py b/acme/setup.py index 0617b0446..135ba05fc 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -3,7 +3,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '2.0.0.dev0' +version = '2.1.0.dev0' install_requires = [ 'cryptography>=2.5.0', @@ -12,7 +12,6 @@ install_requires = [ 'pyrfc3339', 'pytz>=2019.3', 'requests>=2.20.0', - 'requests-toolbelt>=0.3.0', 'setuptools>=41.6.0', ] @@ -46,6 +45,7 @@ setup( 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', ], diff --git a/acme/tests/challenges_test.py b/acme/tests/challenges_test.py index 36cd1e376..9e31b36c1 100644 --- a/acme/tests/challenges_test.py +++ b/acme/tests/challenges_test.py @@ -92,8 +92,7 @@ class DNS01ResponseTest(unittest.TestCase): self.response = self.chall.response(KEY) def test_to_partial_json(self): - self.assertEqual({k: v for k, v in self.jmsg.items() if k != 'keyAuthorization'}, - self.msg.to_partial_json()) + self.assertEqual({}, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import DNS01Response @@ -163,8 +162,7 @@ class HTTP01ResponseTest(unittest.TestCase): self.response = self.chall.response(KEY) def test_to_partial_json(self): - self.assertEqual({k: v for k, v in self.jmsg.items() if k != 'keyAuthorization'}, - self.msg.to_partial_json()) + self.assertEqual({}, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import HTTP01Response @@ -185,7 +183,8 @@ class HTTP01ResponseTest(unittest.TestCase): mock_get.return_value = mock.MagicMock(text=validation) self.assertTrue(self.response.simple_verify( self.chall, "local", KEY.public_key())) - mock_get.assert_called_once_with(self.chall.uri("local"), verify=False) + mock_get.assert_called_once_with(self.chall.uri("local"), verify=False, + timeout=mock.ANY) @mock.patch("acme.challenges.requests.get") def test_simple_verify_bad_validation(self, mock_get): @@ -201,7 +200,8 @@ class HTTP01ResponseTest(unittest.TestCase): HTTP01Response.WHITESPACE_CUTSET)) self.assertTrue(self.response.simple_verify( self.chall, "local", KEY.public_key())) - mock_get.assert_called_once_with(self.chall.uri("local"), verify=False) + mock_get.assert_called_once_with(self.chall.uri("local"), verify=False, + timeout=mock.ANY) @mock.patch("acme.challenges.requests.get") def test_simple_verify_connection_error(self, mock_get): @@ -217,6 +217,16 @@ class HTTP01ResponseTest(unittest.TestCase): self.assertEqual("local:8080", urllib_parse.urlparse( mock_get.mock_calls[0][1][0]).netloc) + @mock.patch("acme.challenges.requests.get") + def test_simple_verify_timeout(self, mock_get): + self.response.simple_verify(self.chall, "local", KEY.public_key()) + mock_get.assert_called_once_with(self.chall.uri("local"), verify=False, + timeout=30) + mock_get.reset_mock() + self.response.simple_verify(self.chall, "local", KEY.public_key(), timeout=1234) + mock_get.assert_called_once_with(self.chall.uri("local"), verify=False, + timeout=1234) + class HTTP01Test(unittest.TestCase): @@ -274,8 +284,7 @@ class TLSALPN01ResponseTest(unittest.TestCase): } def test_to_partial_json(self): - self.assertEqual({k: v for k, v in self.jmsg.items() if k != 'keyAuthorization'}, - self.response.to_partial_json()) + self.assertEqual({}, self.response.to_partial_json()) def test_from_json(self): from acme.challenges import TLSALPN01Response @@ -461,8 +470,6 @@ class DNSResponseTest(unittest.TestCase): from acme.challenges import DNSResponse self.msg = DNSResponse(validation=self.validation) self.jmsg_to = { - 'resource': 'challenge', - 'type': 'dns', 'validation': self.validation, } self.jmsg_from = { @@ -492,7 +499,6 @@ class JWSPayloadRFC8555Compliant(unittest.TestCase): from acme.challenges import HTTP01Response challenge_body = HTTP01Response() - challenge_body.le_acme_version = 2 jobj = challenge_body.json_dumps(indent=2).encode() # RFC8555 states that challenge responses must have an empty payload. diff --git a/acme/tests/client_test.py b/acme/tests/client_test.py index e717f734a..093ac519a 100644 --- a/acme/tests/client_test.py +++ b/acme/tests/client_test.py @@ -7,10 +7,8 @@ import json import unittest from typing import Dict from unittest import mock -import warnings import josepy as jose -import OpenSSL import requests from acme import challenges @@ -19,44 +17,24 @@ from acme import jws as acme_jws from acme import messages from acme.client import ClientNetwork from acme.client import ClientV2 -from acme.mixins import VersionedLEACMEMixin import messages_test import test_util -# Remove the following in Certbot 2.0: -with warnings.catch_warnings(): - warnings.filterwarnings('ignore', '.* in acme.client', DeprecationWarning) - from acme.client import BackwardsCompatibleClientV2 - from acme.client import Client - -CERT_DER = test_util.load_vector('cert.der') CERT_SAN_PEM = test_util.load_vector('cert-san.pem') -CSR_SAN_PEM = test_util.load_vector('csr-san.pem') CSR_MIXED_PEM = test_util.load_vector('csr-mixed.pem') KEY = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) -KEY2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) - -DIRECTORY_V1 = messages.Directory({ - messages.NewRegistration: - 'https://www.letsencrypt-demo.org/acme/new-reg', - messages.Revocation: - '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', -}) DIRECTORY_V2 = messages.Directory({ 'newAccount': 'https://www.letsencrypt-demo.org/acme/new-account', 'newNonce': 'https://www.letsencrypt-demo.org/acme/new-nonce', 'newOrder': 'https://www.letsencrypt-demo.org/acme/new-order', 'revokeCert': 'https://www.letsencrypt-demo.org/acme/revoke-cert', + 'meta': messages.Directory.Meta(), }) -class ClientTestBase(unittest.TestCase): - """Base for tests in acme.client.""" +class ClientV2Test(unittest.TestCase): + """Tests for acme.client.ClientV2.""" def setUp(self): self.response = mock.MagicMock( @@ -88,651 +66,13 @@ class ClientTestBase(unittest.TestCase): self.authz = messages.Authorization( identifier=messages.Identifier( typ=messages.IDENTIFIER_FQDN, value='example.com'), - challenges=(challb,), combinations=None) + challenges=(challb,)) self.authzr = messages.AuthorizationResource( body=self.authz, uri=authzr_uri) # Reason code for revocation self.rsn = 1 -class BackwardsCompatibleClientV2Test(ClientTestBase): - """Tests for acme.client.BackwardsCompatibleClientV2.""" - - def setUp(self): - super().setUp() - - # For some reason, required to suppress warnings on mock.patch('acme.client.Client') - self.warning_cap = warnings.catch_warnings() - self.warning_cap.__enter__() - warnings.filterwarnings('ignore', '.*acme.client', DeprecationWarning) - - # contains a loaded cert - self.certr = messages.CertificateResource( - body=messages_test.CERT) - - loaded = OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_PEM, CERT_SAN_PEM) - wrapped = jose.ComparableX509(loaded) - self.chain = [wrapped, wrapped] - - self.cert_pem = OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_PEM, messages_test.CERT.wrapped).decode() - - single_chain = OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_PEM, loaded).decode() - self.chain_pem = single_chain + single_chain - - self.fullchain_pem = self.cert_pem + self.chain_pem - - self.orderr = messages.OrderResource( - csr_pem=CSR_SAN_PEM) - - def tearDown(self) -> None: - self.warning_cap.__exit__() - return super().tearDown() - - def _init(self): - uri = 'http://www.letsencrypt-demo.org/directory' - return BackwardsCompatibleClientV2(net=self.net, - key=KEY, server=uri) - - def test_init_downloads_directory(self): - uri = 'http://www.letsencrypt-demo.org/directory' - BackwardsCompatibleClientV2(net=self.net, - key=KEY, server=uri) - self.net.get.assert_called_once_with(uri) - - def test_init_acme_version(self): - self.response.json.return_value = DIRECTORY_V1.to_json() - client = self._init() - self.assertEqual(client.acme_version, 1) - - self.response.json.return_value = DIRECTORY_V2.to_json() - client = self._init() - self.assertEqual(client.acme_version, 2) - - def test_query_registration_client_v2(self): - self.response.json.return_value = DIRECTORY_V2.to_json() - client = self._init() - self.response.json.return_value = self.regr.body.to_json() - self.response.headers = {'Location': 'https://www.letsencrypt-demo.org/acme/reg/1'} - self.assertEqual(self.regr, client.query_registration(self.regr)) - - def test_forwarding(self): - self.response.json.return_value = DIRECTORY_V1.to_json() - client = self._init() - self.assertEqual(client.directory, client.client.directory) - self.assertEqual(client.key, KEY) - self.assertEqual(client.deactivate_registration, client.client.deactivate_registration) - self.assertRaises(AttributeError, client.__getattr__, 'nonexistent') - self.assertRaises(AttributeError, client.__getattr__, 'new_account_and_tos') - self.assertRaises(AttributeError, client.__getattr__, 'new_account') - - def test_new_account_and_tos(self): - # v2 no tos - self.response.json.return_value = DIRECTORY_V2.to_json() - with mock.patch('acme.client.ClientV2') as mock_client: - client = self._init() - client.new_account_and_tos(self.new_reg) - mock_client().new_account.assert_called_with(self.new_reg) - - # v2 tos good - with mock.patch('acme.client.ClientV2') as mock_client: - mock_client().directory.meta.__contains__.return_value = True - client = self._init() - client.new_account_and_tos(self.new_reg, lambda x: True) - mock_client().new_account.assert_called_with( - self.new_reg.update(terms_of_service_agreed=True)) - - # v2 tos bad - with mock.patch('acme.client.ClientV2') as mock_client: - mock_client().directory.meta.__contains__.return_value = True - client = self._init() - def _tos_cb(tos): - raise errors.Error - self.assertRaises(errors.Error, client.new_account_and_tos, - self.new_reg, _tos_cb) - mock_client().new_account.assert_not_called() - - # v1 yes tos - self.response.json.return_value = DIRECTORY_V1.to_json() - with mock.patch('acme.client.Client') as mock_client: - regr = mock.MagicMock(terms_of_service="TOS") - mock_client().register.return_value = regr - client = self._init() - client.new_account_and_tos(self.new_reg) - mock_client().register.assert_called_once_with(self.new_reg) - mock_client().agree_to_tos.assert_called_once_with(regr) - - # v1 no tos - with mock.patch('acme.client.Client') as mock_client: - regr = mock.MagicMock(terms_of_service=None) - mock_client().register.return_value = regr - client = self._init() - client.new_account_and_tos(self.new_reg) - mock_client().register.assert_called_once_with(self.new_reg) - mock_client().agree_to_tos.assert_not_called() - - @mock.patch('OpenSSL.crypto.load_certificate_request') - @mock.patch('acme.crypto_util._pyopenssl_cert_or_req_all_names') - def test_new_order_v1(self, mock__pyopenssl_cert_or_req_all_names, - unused_mock_load_certificate_request): - self.response.json.return_value = DIRECTORY_V1.to_json() - mock__pyopenssl_cert_or_req_all_names.return_value = ['example.com', 'www.example.com'] - mock_csr_pem = mock.MagicMock() - with mock.patch('acme.client.Client') as mock_client: - mock_client().request_domain_challenges.return_value = mock.sentinel.auth - client = self._init() - orderr = client.new_order(mock_csr_pem) - self.assertEqual(orderr.authorizations, [mock.sentinel.auth, mock.sentinel.auth]) - - def test_new_order_v2(self): - self.response.json.return_value = DIRECTORY_V2.to_json() - mock_csr_pem = mock.MagicMock() - with mock.patch('acme.client.ClientV2') as mock_client: - client = self._init() - client.new_order(mock_csr_pem) - mock_client().new_order.assert_called_once_with(mock_csr_pem) - - @mock.patch('acme.client.Client') - def test_finalize_order_v1_success(self, mock_client): - self.response.json.return_value = DIRECTORY_V1.to_json() - - mock_client().request_issuance.return_value = self.certr - mock_client().fetch_chain.return_value = self.chain - - deadline = datetime.datetime(9999, 9, 9) - client = self._init() - result = client.finalize_order(self.orderr, deadline) - self.assertEqual(result.fullchain_pem, self.fullchain_pem) - mock_client().fetch_chain.assert_called_once_with(self.certr) - - @mock.patch('acme.client.Client') - def test_finalize_order_v1_fetch_chain_error(self, mock_client): - self.response.json.return_value = DIRECTORY_V1.to_json() - - mock_client().request_issuance.return_value = self.certr - mock_client().fetch_chain.return_value = self.chain - mock_client().fetch_chain.side_effect = [errors.Error, self.chain] - - deadline = datetime.datetime(9999, 9, 9) - client = self._init() - result = client.finalize_order(self.orderr, deadline) - self.assertEqual(result.fullchain_pem, self.fullchain_pem) - self.assertEqual(mock_client().fetch_chain.call_count, 2) - - @mock.patch('acme.client.Client') - def test_finalize_order_v1_timeout(self, mock_client): - self.response.json.return_value = DIRECTORY_V1.to_json() - - mock_client().request_issuance.return_value = self.certr - - deadline = deadline = datetime.datetime.now() - datetime.timedelta(seconds=60) - client = self._init() - self.assertRaises(errors.TimeoutError, client.finalize_order, - self.orderr, deadline) - - def test_finalize_order_v2(self): - self.response.json.return_value = DIRECTORY_V2.to_json() - mock_orderr = mock.MagicMock() - mock_deadline = mock.MagicMock() - with mock.patch('acme.client.ClientV2') as mock_client: - client = self._init() - client.finalize_order(mock_orderr, mock_deadline) - mock_client().finalize_order.assert_called_once_with(mock_orderr, mock_deadline, False) - - def test_revoke(self): - self.response.json.return_value = DIRECTORY_V1.to_json() - with mock.patch('acme.client.Client') as mock_client: - client = self._init() - client.revoke(messages_test.CERT, self.rsn) - mock_client().revoke.assert_called_once_with(messages_test.CERT, self.rsn) - - self.response.json.return_value = DIRECTORY_V2.to_json() - with mock.patch('acme.client.ClientV2') as mock_client: - client = self._init() - client.revoke(messages_test.CERT, self.rsn) - mock_client().revoke.assert_called_once_with(messages_test.CERT, self.rsn) - - def test_update_registration(self): - self.response.json.return_value = DIRECTORY_V1.to_json() - with mock.patch('acme.client.Client') as mock_client: - client = self._init() - client.update_registration(mock.sentinel.regr, None) - mock_client().update_registration.assert_called_once_with(mock.sentinel.regr, None) - - # newNonce present means it will pick acme_version 2 - def test_external_account_required_true(self): - self.response.json.return_value = messages.Directory({ - 'newNonce': 'http://letsencrypt-test.com/acme/new-nonce', - 'meta': messages.Directory.Meta(external_account_required=True), - }).to_json() - - client = self._init() - - self.assertTrue(client.external_account_required()) - - # newNonce present means it will pick acme_version 2 - def test_external_account_required_false(self): - self.response.json.return_value = messages.Directory({ - 'newNonce': 'http://letsencrypt-test.com/acme/new-nonce', - 'meta': messages.Directory.Meta(external_account_required=False), - }).to_json() - - client = self._init() - - self.assertFalse(client.external_account_required()) - - def test_external_account_required_false_v1(self): - self.response.json.return_value = messages.Directory({ - 'meta': messages.Directory.Meta(external_account_required=False), - }).to_json() - - client = self._init() - - self.assertFalse(client.external_account_required()) - - -class ClientTest(ClientTestBase): - """Tests for acme.client.Client.""" - - def setUp(self): - super().setUp() - - self.directory = DIRECTORY_V1 - - # Registration - self.regr = self.regr.update( - terms_of_service='https://www.letsencrypt-demo.org/tos') - - # Request issuance - self.certr = messages.CertificateResource( - body=messages_test.CERT, authzrs=(self.authzr,), - uri='https://www.letsencrypt-demo.org/acme/cert/1', - cert_chain_uri='https://www.letsencrypt-demo.org/ca') - - self.client = Client( - directory=self.directory, key=KEY, alg=jose.RS256, net=self.net) - - def test_init_downloads_directory(self): - uri = 'http://www.letsencrypt-demo.org/directory' - self.client = Client( - directory=uri, key=KEY, alg=jose.RS256, net=self.net) - self.net.get.assert_called_once_with(uri) - - @mock.patch('acme.client.ClientNetwork') - def test_init_without_net(self, mock_net): - mock_net.return_value = mock.sentinel.net - alg = jose.RS256 - self.client = Client( - directory=self.directory, key=KEY, alg=alg) - mock_net.called_once_with(KEY, alg=alg, verify_ssl=True) - self.assertEqual(self.client.net, mock.sentinel.net) - - def test_register(self): - # "Instance of 'Field' has no to_json/update member" bug: - self.response.status_code = http_client.CREATED - self.response.json.return_value = self.regr.body.to_json() - self.response.headers['Location'] = self.regr.uri - self.response.links.update({ - '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_update_registration(self): - # "Instance of 'Field' has no to_json/update member" bug: - self.response.headers['Location'] = self.regr.uri - self.response.json.return_value = self.regr.body.to_json() - self.assertEqual(self.regr, self.client.update_registration(self.regr)) - # TODO: test POST call arguments - - # TODO: split here and separate test - self.response.json.return_value = self.regr.body.update( - contact=()).to_json() - - 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_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_agree_to_tos(self): - self.client.update_registration = mock.Mock() - self.client.agree_to_tos(self.regr) - regr = self.client.update_registration.call_args[0][0] - self.assertEqual(self.regr.terms_of_service, regr.body.agreement) - - def _prepare_response_for_request_challenges(self): - self.response.status_code = http_client.CREATED - self.response.headers['Location'] = self.authzr.uri - self.response.json.return_value = self.authz.to_json() - - def test_request_challenges(self): - self._prepare_response_for_request_challenges() - self.client.request_challenges(self.identifier) - self.net.post.assert_called_once_with( - self.directory.new_authz, - messages.NewAuthorization(identifier=self.identifier), - acme_version=1) - - def test_request_challenges_deprecated_arg(self): - self._prepare_response_for_request_challenges() - self.client.request_challenges(self.identifier, new_authzr_uri="hi") - self.net.post.assert_called_once_with( - self.directory.new_authz, - messages.NewAuthorization(identifier=self.identifier), - acme_version=1) - - def test_request_challenges_custom_uri(self): - self._prepare_response_for_request_challenges() - self.client.request_challenges(self.identifier) - self.net.post.assert_called_once_with( - 'https://www.letsencrypt-demo.org/acme/new-authz', mock.ANY, - acme_version=1) - - def test_request_challenges_unexpected_update(self): - self._prepare_response_for_request_challenges() - self.response.json.return_value = self.authz.update( - identifier=self.identifier.update(value='foo')).to_json() - self.assertRaises( - errors.UnexpectedUpdate, self.client.request_challenges, - self.identifier) - - def test_request_challenges_wildcard(self): - wildcard_identifier = messages.Identifier( - typ=messages.IDENTIFIER_FQDN, value='*.example.org') - self.assertRaises( - errors.WildcardUnsupportedError, self.client.request_challenges, - wildcard_identifier) - - def test_request_domain_challenges(self): - self.client.request_challenges = mock.MagicMock() - self.assertEqual( - self.client.request_challenges(self.identifier), - self.client.request_domain_challenges('example.com')) - - def test_answer_challenge(self): - self.response.links['up'] = {'url': self.challr.authzr_uri} - self.response.json.return_value = self.challr.body.to_json() - - chall_response = challenges.DNSResponse(validation=None) - - self.client.answer_challenge(self.challr.body, chall_response) - - # TODO: split here and separate test - self.assertRaises(errors.UnexpectedUpdate, self.client.answer_challenge, - self.challr.body.update(uri='foo'), chall_response) - - def test_answer_challenge_missing_next(self): - self.assertRaises( - errors.ClientError, self.client.answer_challenge, - self.challr.body, challenges.DNSResponse(validation=None)) - - def test_retry_after_date(self): - self.response.headers['Retry-After'] = 'Fri, 31 Dec 1999 23:59:59 GMT' - self.assertEqual( - datetime.datetime(1999, 12, 31, 23, 59, 59), - self.client.retry_after(response=self.response, default=10)) - - @mock.patch('acme.client.datetime') - def test_retry_after_invalid(self, dt_mock): - dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) - dt_mock.timedelta = datetime.timedelta - - self.response.headers['Retry-After'] = 'foooo' - self.assertEqual( - datetime.datetime(2015, 3, 27, 0, 0, 10), - self.client.retry_after(response=self.response, default=10)) - - @mock.patch('acme.client.datetime') - def test_retry_after_overflow(self, dt_mock): - dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) - dt_mock.timedelta = datetime.timedelta - dt_mock.datetime.side_effect = datetime.datetime - - self.response.headers['Retry-After'] = "Tue, 116 Feb 2016 11:50:00 MST" - self.assertEqual( - datetime.datetime(2015, 3, 27, 0, 0, 10), - self.client.retry_after(response=self.response, default=10)) - - @mock.patch('acme.client.datetime') - def test_retry_after_seconds(self, dt_mock): - dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) - dt_mock.timedelta = datetime.timedelta - - self.response.headers['Retry-After'] = '50' - self.assertEqual( - datetime.datetime(2015, 3, 27, 0, 0, 50), - self.client.retry_after(response=self.response, default=10)) - - @mock.patch('acme.client.datetime') - def test_retry_after_missing(self, dt_mock): - dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) - dt_mock.timedelta = datetime.timedelta - - self.assertEqual( - datetime.datetime(2015, 3, 27, 0, 0, 10), - self.client.retry_after(response=self.response, default=10)) - - def test_poll(self): - self.response.json.return_value = self.authzr.body.to_json() - self.assertEqual((self.authzr, self.response), - self.client.poll(self.authzr)) - - # TODO: split here and separate test - self.response.json.return_value = self.authz.update( - identifier=self.identifier.update(value='foo')).to_json() - self.assertRaises( - errors.UnexpectedUpdate, self.client.poll, self.authzr) - - def test_request_issuance(self): - self.response.content = CERT_DER - self.response.headers['Location'] = self.certr.uri - self.response.links['up'] = {'url': self.certr.cert_chain_uri} - self.assertEqual(self.certr, self.client.request_issuance( - messages_test.CSR, (self.authzr,))) - # TODO: check POST args - - def test_request_issuance_missing_up(self): - self.response.content = CERT_DER - self.response.headers['Location'] = self.certr.uri - self.assertEqual( - self.certr.update(cert_chain_uri=None), - self.client.request_issuance(messages_test.CSR, (self.authzr,))) - - def test_request_issuance_missing_location(self): - self.assertRaises( - errors.ClientError, self.client.request_issuance, - messages_test.CSR, (self.authzr,)) - - @mock.patch('acme.client.datetime') - @mock.patch('acme.client.time') - def test_poll_and_request_issuance(self, time_mock, dt_mock): - # clock.dt | pylint: disable=no-member - clock = mock.MagicMock(dt=datetime.datetime(2015, 3, 27)) - - def sleep(seconds): - """increment clock""" - clock.dt += datetime.timedelta(seconds=seconds) - time_mock.sleep.side_effect = sleep - - def now(): - """return current clock value""" - return clock.dt - dt_mock.datetime.now.side_effect = now - dt_mock.timedelta = datetime.timedelta - - def poll(authzr): # pylint: disable=missing-docstring - # record poll start time based on the current clock value - authzr.times.append(clock.dt) - - # suppose it takes 2 seconds for server to produce the - # result, increment clock - clock.dt += datetime.timedelta(seconds=2) - - if len(authzr.retries) == 1: # no more retries - done = mock.MagicMock(uri=authzr.uri, times=authzr.times) - done.body.status = authzr.retries[0] - return done, [] - - # response (2nd result tuple element) is reduced to only - # Retry-After header contents represented as integer - # seconds; authzr.retries is a list of Retry-After - # headers, head(retries) is peeled of as a current - # Retry-After header, and tail(retries) is persisted for - # later poll() calls - return (mock.MagicMock(retries=authzr.retries[1:], - uri=authzr.uri + '.', times=authzr.times), - authzr.retries[0]) - self.client.poll = mock.MagicMock(side_effect=poll) - - mintime = 7 - - def retry_after(response, default): - # pylint: disable=missing-docstring - # check that poll_and_request_issuance correctly passes mintime - self.assertEqual(default, mintime) - return clock.dt + datetime.timedelta(seconds=response) - self.client.retry_after = mock.MagicMock(side_effect=retry_after) - - def request_issuance(csr, authzrs): # pylint: disable=missing-docstring - return csr, authzrs - self.client.request_issuance = mock.MagicMock( - side_effect=request_issuance) - - csr = mock.MagicMock() - authzrs = ( - mock.MagicMock(uri='a', times=[], retries=( - 8, 20, 30, messages.STATUS_VALID)), - mock.MagicMock(uri='b', times=[], retries=( - 5, messages.STATUS_VALID)), - ) - - cert, updated_authzrs = self.client.poll_and_request_issuance( - csr, authzrs, mintime=mintime, - # make sure that max_attempts is per-authorization, rather - # than global - max_attempts=max(len(authzrs[0].retries), len(authzrs[1].retries))) - self.assertIs(cert[0], csr) - self.assertIs(cert[1], updated_authzrs) - self.assertEqual(updated_authzrs[0].uri, 'a...') - self.assertEqual(updated_authzrs[1].uri, 'b.') - self.assertEqual(updated_authzrs[0].times, [ - datetime.datetime(2015, 3, 27), - # a is scheduled for 10, but b is polling [9..11), so it - # will be picked up as soon as b is finished, without - # additional sleeping - datetime.datetime(2015, 3, 27, 0, 0, 11), - datetime.datetime(2015, 3, 27, 0, 0, 33), - datetime.datetime(2015, 3, 27, 0, 1, 5), - ]) - self.assertEqual(updated_authzrs[1].times, [ - datetime.datetime(2015, 3, 27, 0, 0, 2), - datetime.datetime(2015, 3, 27, 0, 0, 9), - ]) - self.assertEqual(clock.dt, datetime.datetime(2015, 3, 27, 0, 1, 7)) - - # CA sets invalid | TODO: move to a separate test - invalid_authzr = mock.MagicMock( - times=[], retries=[messages.STATUS_INVALID]) - self.assertRaises( - errors.PollError, self.client.poll_and_request_issuance, - csr, authzrs=(invalid_authzr,), mintime=mintime) - - # 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) - - def test_deactivate_authorization(self): - authzb = self.authzr.body.update(status=messages.STATUS_DEACTIVATED) - self.response.json.return_value = authzb.to_json() - authzr = self.client.deactivate_authorization(self.authzr) - self.assertEqual(authzb, authzr.body) - self.assertEqual(self.client.net.post.call_count, 1) - self.assertIn(self.authzr.uri, self.net.post.call_args_list[0][0]) - - def test_check_cert(self): - self.response.headers['Location'] = self.certr.uri - self.response.content = CERT_DER - self.assertEqual(self.certr.update(body=messages_test.CERT), - self.client.check_cert(self.certr)) - - # TODO: split here and separate test - self.response.headers['Location'] = 'foo' - self.assertRaises( - errors.UnexpectedUpdate, self.client.check_cert, self.certr) - - def test_check_cert_missing_location(self): - self.response.content = CERT_DER - self.assertRaises( - errors.ClientError, self.client.check_cert, self.certr) - - def test_refresh(self): - self.client.check_cert = mock.MagicMock() - self.assertEqual( - self.client.check_cert(self.certr), self.client.refresh(self.certr)) - - def test_fetch_chain_no_up_link(self): - self.assertEqual([], self.client.fetch_chain(self.certr.update( - cert_chain_uri=None))) - - def test_fetch_chain_single(self): - # pylint: disable=protected-access - self.client._get_cert = mock.MagicMock() - self.client._get_cert.return_value = ( - mock.MagicMock(links={}), "certificate") - self.assertEqual([self.client._get_cert(self.certr.cert_chain_uri)[1]], - self.client.fetch_chain(self.certr)) - - def test_fetch_chain_max(self): - # pylint: disable=protected-access - up_response = mock.MagicMock(links={'up': {'url': 'http://cert'}}) - noup_response = mock.MagicMock(links={}) - self.client._get_cert = mock.MagicMock() - self.client._get_cert.side_effect = [ - (up_response, "cert")] * 9 + [(noup_response, "last_cert")] - chain = self.client.fetch_chain(self.certr, max_length=10) - self.assertEqual(chain, ["cert"] * 9 + ["last_cert"]) - - def test_fetch_chain_too_many(self): # recursive - # pylint: disable=protected-access - response = mock.MagicMock(links={'up': {'url': 'http://cert'}}) - self.client._get_cert = mock.MagicMock() - self.client._get_cert.return_value = (response, "certificate") - self.assertRaises(errors.Error, self.client.fetch_chain, self.certr) - - def test_revoke(self): - self.client.revoke(self.certr.body, self.rsn) - self.net.post.assert_called_once_with( - self.directory[messages.Revocation], mock.ANY, acme_version=1) - - def test_revocation_payload(self): - obj = messages.Revocation(certificate=self.certr.body, reason=self.rsn) - self.assertIn('reason', obj.to_partial_json().keys()) - self.assertEqual(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.rsn) - - -class ClientV2Test(ClientTestBase): - """Tests for acme.client.ClientV2.""" - - def setUp(self): - super().setUp() - self.directory = DIRECTORY_V2 self.client = ClientV2(self.directory, self.net) @@ -763,11 +103,40 @@ class ClientV2Test(ClientTestBase): self.assertEqual(self.regr, self.client.new_account(self.new_reg)) + def test_new_account_tos_link(self): + self.response.status_code = http_client.CREATED + self.response.json.return_value = self.regr.body.to_json() + self.response.headers['Location'] = self.regr.uri + self.response.links.update({ + 'terms-of-service': {'url': 'https://www.letsencrypt-demo.org/tos'}, + }) + + self.assertEqual(self.client.new_account(self.new_reg).terms_of_service, + 'https://www.letsencrypt-demo.org/tos') + + def test_new_account_conflict(self): self.response.status_code = http_client.OK self.response.headers['Location'] = self.regr.uri self.assertRaises(errors.ConflictError, self.client.new_account, self.new_reg) + def test_deactivate_account(self): + deactivated_regr = self.regr.update( + body=self.regr.body.update(status='deactivated')) + self.response.json.return_value = deactivated_regr.body.to_json() + self.response.status_code = http_client.OK + self.response.headers['Location'] = self.regr.uri + self.assertEqual(self.client.deactivate_registration(self.regr), deactivated_regr) + + def test_deactivate_authorization(self): + deactivated_authz = self.authzr.update( + body=self.authzr.body.update(status=messages.STATUS_DEACTIVATED)) + self.response.json.return_value = deactivated_authz.body.to_json() + authzr = self.client.deactivate_authorization(self.authzr) + self.assertEqual(deactivated_authz.body, authzr.body) + self.assertEqual(self.client.net.post.call_count, 1) + self.assertIn(self.authzr.uri, self.net.post.call_args_list[0][0]) + def test_new_order(self): order_response = copy.deepcopy(self.response) order_response.status_code = http_client.CREATED @@ -786,6 +155,20 @@ class ClientV2Test(ClientTestBase): mock_post_as_get.side_effect = (authz_response, authz_response2) self.assertEqual(self.client.new_order(CSR_MIXED_PEM), self.orderr) + def test_answer_challege(self): + self.response.links['up'] = {'url': self.challr.authzr_uri} + self.response.json.return_value = self.challr.body.to_json() + chall_response = challenges.DNSResponse(validation=None) + self.client.answer_challenge(self.challr.body, chall_response) + + self.assertRaises(errors.UnexpectedUpdate, self.client.answer_challenge, + self.challr.body.update(uri='foo'), chall_response) + + def test_answer_challenge_missing_next(self): + self.assertRaises( + errors.ClientError, self.client.answer_challenge, + self.challr.body, challenges.DNSResponse(validation=None)) + @mock.patch('acme.client.datetime') def test_poll_and_finalize(self, mock_datetime): mock_datetime.datetime.now.return_value = datetime.datetime(2018, 2, 15) @@ -832,6 +215,11 @@ class ClientV2Test(ClientTestBase): self.authz.to_json(), self.authz2.to_json(), updated_authz2.to_json()) self.assertEqual(self.client.poll_authorizations(self.orderr, deadline), updated_orderr) + def test_poll_unexpected_update(self): + updated_authz = self.authz.update(identifier=self.identifier.update(value='foo')) + self.response.json.return_value = updated_authz.to_json() + self.assertRaises(errors.UnexpectedUpdate, self.client.poll, self.authzr) + def test_finalize_order_success(self): updated_order = self.order.update( certificate='https://www.letsencrypt-demo.org/acme/cert/', @@ -883,9 +271,9 @@ class ClientV2Test(ClientTestBase): deadline = datetime.datetime(9999, 9, 9) resp = self.client.finalize_order(self.orderr, deadline, fetch_alternative_chains=True) self.net.post.assert_any_call('https://example.com/acme/cert/1', - mock.ANY, acme_version=2, new_nonce_url=mock.ANY) + mock.ANY, new_nonce_url=mock.ANY) self.net.post.assert_any_call('https://example.com/acme/cert/2', - mock.ANY, acme_version=2, new_nonce_url=mock.ANY) + mock.ANY, new_nonce_url=mock.ANY) self.assertEqual(resp, updated_orderr) del self.response.headers['Link'] @@ -895,8 +283,15 @@ class ClientV2Test(ClientTestBase): def test_revoke(self): self.client.revoke(messages_test.CERT, self.rsn) self.net.post.assert_called_once_with( - self.directory["revokeCert"], mock.ANY, acme_version=2, - new_nonce_url=DIRECTORY_V2['newNonce']) + self.directory["revokeCert"], mock.ANY, new_nonce_url=DIRECTORY_V2['newNonce']) + + def test_revoke_bad_status_raises_error(self): + self.response.status_code = http_client.METHOD_NOT_ALLOWED + self.assertRaises( + errors.ClientError, + self.client.revoke, + messages_test.CERT, + self.rsn) def test_update_registration(self): # "Instance of 'Field' has no to_json/update member" bug: @@ -927,6 +322,11 @@ class ClientV2Test(ClientTestBase): def test_external_account_required_default(self): self.assertFalse(self.client.external_account_required()) + def test_query_registration_client(self): + self.response.json.return_value = self.regr.body.to_json() + self.response.headers['Location'] = 'https://www.letsencrypt-demo.org/acme/reg/1' + self.assertEqual(self.regr, self.client.query_registration(self.regr)) + def test_post_as_get(self): with mock.patch('acme.client.ClientV2._authzr_from_response') as mock_client: mock_client.return_value = self.authzr2 @@ -934,12 +334,64 @@ class ClientV2Test(ClientTestBase): self.client.poll(self.authzr2) # pylint: disable=protected-access self.client.net.post.assert_called_once_with( - self.authzr2.uri, None, acme_version=2, + self.authzr2.uri, None, new_nonce_url='https://www.letsencrypt-demo.org/acme/new-nonce') self.client.net.get.assert_not_called() + def test_retry_after_date(self): + self.response.headers['Retry-After'] = 'Fri, 31 Dec 1999 23:59:59 GMT' + self.assertEqual( + datetime.datetime(1999, 12, 31, 23, 59, 59), + self.client.retry_after(response=self.response, default=10)) -class MockJSONDeSerializable(VersionedLEACMEMixin, jose.JSONDeSerializable): + @mock.patch('acme.client.datetime') + def test_retry_after_invalid(self, dt_mock): + dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) + dt_mock.timedelta = datetime.timedelta + + self.response.headers['Retry-After'] = 'foooo' + self.assertEqual( + datetime.datetime(2015, 3, 27, 0, 0, 10), + self.client.retry_after(response=self.response, default=10)) + + @mock.patch('acme.client.datetime') + def test_retry_after_overflow(self, dt_mock): + dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) + dt_mock.timedelta = datetime.timedelta + dt_mock.datetime.side_effect = datetime.datetime + + self.response.headers['Retry-After'] = "Tue, 116 Feb 2016 11:50:00 MST" + self.assertEqual( + datetime.datetime(2015, 3, 27, 0, 0, 10), + self.client.retry_after(response=self.response, default=10)) + + @mock.patch('acme.client.datetime') + def test_retry_after_seconds(self, dt_mock): + dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) + dt_mock.timedelta = datetime.timedelta + + self.response.headers['Retry-After'] = '50' + self.assertEqual( + datetime.datetime(2015, 3, 27, 0, 0, 50), + self.client.retry_after(response=self.response, default=10)) + + @mock.patch('acme.client.datetime') + def test_retry_after_missing(self, dt_mock): + dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) + dt_mock.timedelta = datetime.timedelta + + self.assertEqual( + datetime.datetime(2015, 3, 27, 0, 0, 10), + self.client.retry_after(response=self.response, default=10)) + + def test_get_directory(self): + self.response.json.return_value = DIRECTORY_V2.to_json() + self.assertEqual( + DIRECTORY_V2.to_partial_json(), + ClientV2.get_directory('https://example.com/dir', self.net).to_partial_json()) + + +class MockJSONDeSerializable(jose.JSONDeSerializable): # pylint: disable=missing-docstring def __init__(self, value): self.value = value @@ -973,8 +425,7 @@ class ClientNetworkTest(unittest.TestCase): def test_wrap_in_jws(self): # pylint: disable=protected-access jws_dump = self.net._wrap_in_jws( - MockJSONDeSerializable('foo'), nonce=b'Tg', url="url", - acme_version=1) + MockJSONDeSerializable('foo'), nonce=b'Tg', url="url") jws = acme_jws.JWS.json_loads(jws_dump) self.assertEqual(json.loads(jws.payload.decode()), {'foo': 'foo'}) self.assertEqual(jws.signature.combined.nonce, b'Tg') @@ -983,8 +434,7 @@ class ClientNetworkTest(unittest.TestCase): self.net.account = {'uri': 'acct-uri'} # pylint: disable=protected-access jws_dump = self.net._wrap_in_jws( - MockJSONDeSerializable('foo'), nonce=b'Tg', url="url", - acme_version=2) + MockJSONDeSerializable('foo'), nonce=b'Tg', url="url") jws = acme_jws.JWS.json_loads(jws_dump) self.assertEqual(json.loads(jws.payload.decode()), {'foo': 'foo'}) self.assertEqual(jws.signature.combined.nonce, b'Tg') @@ -1090,14 +540,13 @@ class ClientNetworkTest(unittest.TestCase): self.net.session = mock.MagicMock() self.net.session.request.return_value = mock.MagicMock( ok=True, status_code=http_client.OK, - headers={"Content-Type": "application/pkix-cert"}, content=b"hi") # pylint: disable=protected-access self.net._send_request('HEAD', 'http://example.com/', 'foo', - timeout=mock.ANY, bar='baz') + timeout=mock.ANY, bar='baz', headers={'Accept': 'application/pkix-cert'}) mock_logger.debug.assert_called_with( 'Received response:\nHTTP %d\n%s\n\n%s', 200, - 'Content-Type: application/pkix-cert', b'aGk=') + '', b'aGk=') def test_send_request_post(self): self.net.session = mock.MagicMock() @@ -1269,13 +718,13 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase): 'uri', self.obj, content_type=self.content_type)) self.assertTrue(self.response.checked) self.net._wrap_in_jws.assert_called_once_with( - self.obj, jose.b64decode(self.all_nonces.pop()), "uri", 1) + self.obj, jose.b64decode(self.all_nonces.pop()), "uri") 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( - self.obj, jose.b64decode(self.all_nonces.pop()), "uri", 1) + self.obj, jose.b64decode(self.all_nonces.pop()), "uri") def test_post_wrong_initial_nonce(self): # HEAD self.available_nonces = [b'f', jose.b64encode(b'good')] @@ -1333,42 +782,14 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase): check_response = mock.MagicMock() self.net._check_response = check_response self.assertRaises(errors.ClientError, self.net.post, 'uri', - self.obj, content_type=self.content_type, acme_version=2, + self.obj, content_type=self.content_type, new_nonce_url='new_nonce_uri') self.assertEqual(check_response.call_count, 1) def test_new_nonce_uri_removed(self): self.content_type = None - self.net.post('uri', self.obj, content_type=None, - acme_version=2, new_nonce_url='new_nonce_uri') + self.net.post('uri', self.obj, content_type=None, new_nonce_url='new_nonce_uri') -class ClientNetworkSourceAddressBindingTest(unittest.TestCase): - """Tests that if ClientNetwork has a source IP set manually, the underlying library has - used the provided source address.""" - - def setUp(self): - self.source_address = "8.8.8.8" - - def test_source_address_set(self): - with mock.patch('warnings.warn') as mock_warn: - net = ClientNetwork(key=None, alg=None, source_address=self.source_address) - mock_warn.assert_called_once() - self.assertIn('source_address', mock_warn.call_args[0][0]) - for adapter in net.session.adapters.values(): - self.assertIn(self.source_address, adapter.source_address) - - def test_behavior_assumption(self): - """This is a test that guardrails the HTTPAdapter behavior so that if the default for - a Session() changes, the assumptions here aren't violated silently.""" - # Source address not specified, so the default adapter type should be bound -- this - # test should fail if the default adapter type is changed by requests - net = ClientNetwork(key=None, alg=None) - session = requests.Session() - for scheme in session.adapters: - client_network_adapter = net.session.adapters.get(scheme) - default_adapter = session.adapters.get(scheme) - self.assertEqual(client_network_adapter.__class__, default_adapter.__class__) - if __name__ == '__main__': unittest.main() # pragma: no cover diff --git a/acme/tests/fields_test.py b/acme/tests/fields_test.py index 76b215342..68e5c9a7f 100644 --- a/acme/tests/fields_test.py +++ b/acme/tests/fields_test.py @@ -55,21 +55,5 @@ class RFC3339FieldTest(unittest.TestCase): jose.DeserializationError, RFC3339Field.default_decoder, '') -class ResourceTest(unittest.TestCase): - """Tests for acme.fields.Resource.""" - - def setUp(self): - with warnings.catch_warnings(): - warnings.filterwarnings('ignore', '.*Resource', DeprecationWarning) - from acme.fields import Resource - self.field = Resource('x') - - def test_decode_good(self): - self.assertEqual('x', self.field.decode('x')) - - def test_decode_wrong(self): - self.assertRaises(jose.DeserializationError, self.field.decode, 'y') - - if __name__ == '__main__': unittest.main() # pragma: no cover diff --git a/acme/tests/magic_typing_test.py b/acme/tests/magic_typing_test.py deleted file mode 100644 index d470337bd..000000000 --- a/acme/tests/magic_typing_test.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Tests for acme.magic_typing.""" -import sys -import unittest -import warnings -from unittest import mock - - -class MagicTypingTest(unittest.TestCase): - """Tests for acme.magic_typing.""" - def test_import_success(self): - try: - import typing as temp_typing - except ImportError: # pragma: no cover - temp_typing = None # pragma: no cover - typing_class_mock = mock.MagicMock() - text_mock = mock.MagicMock() - typing_class_mock.Text = text_mock - sys.modules['typing'] = typing_class_mock - if 'acme.magic_typing' in sys.modules: - del sys.modules['acme.magic_typing'] # pragma: no cover - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - from acme.magic_typing import Text - self.assertEqual(Text, text_mock) - del sys.modules['acme.magic_typing'] - sys.modules['typing'] = temp_typing - - -if __name__ == '__main__': - unittest.main() # pragma: no cover diff --git a/acme/tests/messages_test.py b/acme/tests/messages_test.py index 782955fb4..cbff65771 100644 --- a/acme/tests/messages_test.py +++ b/acme/tests/messages_test.py @@ -135,8 +135,8 @@ class DirectoryTest(unittest.TestCase): def setUp(self): from acme.messages import Directory self.dir = Directory({ - 'new-reg': 'reg', - mock.MagicMock(resource_type='new-cert'): 'cert', + 'newReg': 'reg', + 'newCert': 'cert', 'meta': Directory.Meta( terms_of_service='https://example.com/acme/terms', website='https://www.example.com/', @@ -149,28 +149,23 @@ class DirectoryTest(unittest.TestCase): Directory({'foo': 'bar'}) def test_getitem(self): - self.assertEqual('reg', self.dir['new-reg']) - from acme.messages import NewRegistration - with warnings.catch_warnings(): - warnings.filterwarnings('ignore', '.* non-string keys', DeprecationWarning) - self.assertEqual('reg', self.dir[NewRegistration]) - self.assertEqual('reg', self.dir[NewRegistration()]) + self.assertEqual('reg', self.dir['newReg']) def test_getitem_fails_with_key_error(self): self.assertRaises(KeyError, self.dir.__getitem__, 'foo') def test_getattr(self): - self.assertEqual('reg', self.dir.new_reg) + self.assertEqual('reg', self.dir.newReg) def test_getattr_fails_with_attribute_error(self): self.assertRaises(AttributeError, self.dir.__getattr__, 'foo') def test_to_json(self): self.assertEqual(self.dir.to_json(), { - 'new-reg': 'reg', - 'new-cert': 'cert', + 'newReg': 'reg', + 'newCert': 'cert', 'meta': { - 'terms-of-service': 'https://example.com/acme/terms', + 'termsOfService': 'https://example.com/acme/terms', 'website': 'https://www.example.com/', 'caaIdentities': ['example.com'], }, @@ -290,7 +285,7 @@ class UpdateRegistrationTest(unittest.TestCase): def test_empty(self): from acme.messages import UpdateRegistration jstring = '{"resource": "reg"}' - self.assertEqual(jstring, UpdateRegistration().json_dumps()) + self.assertEqual('{}', UpdateRegistration().json_dumps()) self.assertEqual( UpdateRegistration(), UpdateRegistration.json_loads(jstring)) @@ -338,7 +333,7 @@ class ChallengeBodyTest(unittest.TestCase): error=error) self.jobj_to = { - 'uri': 'http://challb', + 'url': 'http://challb', 'status': self.status, 'type': 'dns', 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', @@ -385,20 +380,17 @@ class AuthorizationTest(unittest.TestCase): chall=challenges.DNS( token=b'DGyRejmCefe7v4NfDGDKfA')), ) - combinations = ((0,), (1,)) from acme.messages import Authorization from acme.messages import Identifier from acme.messages import IDENTIFIER_FQDN identifier = Identifier(typ=IDENTIFIER_FQDN, value='example.com') self.authz = Authorization( - identifier=identifier, combinations=combinations, - challenges=self.challbs) + identifier=identifier, challenges=self.challbs) self.jobj_from = { 'identifier': identifier.to_json(), 'challenges': [challb.to_json() for challb in self.challbs], - 'combinations': combinations, } def test_from_json(self): @@ -409,14 +401,6 @@ class AuthorizationTest(unittest.TestCase): from acme.messages import Authorization hash(Authorization.from_json(self.jobj_from)) - def test_resolved_combinations(self): - with warnings.catch_warnings(): - warnings.filterwarnings('ignore', '.*resolved_combinations', DeprecationWarning) - self.assertEqual(self.authz.resolved_combinations, ( - (self.challbs[0],), - (self.challbs[1],), - )) - class AuthorizationResourceTest(unittest.TestCase): """Tests for acme.messages.AuthorizationResource.""" @@ -507,7 +491,6 @@ class JWSPayloadRFC8555Compliant(unittest.TestCase): from acme.messages import NewAuthorization new_order = NewAuthorization() - new_order.le_acme_version = 2 jobj = new_order.json_dumps(indent=2).encode() # RFC8555 states that JWS bodies must not have a resource field. diff --git a/certbot-apache/certbot_apache/_internal/apacheparser.py b/certbot-apache/certbot_apache/_internal/apacheparser.py index 0aba0cb3c..8d2a79754 100644 --- a/certbot-apache/certbot_apache/_internal/apacheparser.py +++ b/certbot-apache/certbot_apache/_internal/apacheparser.py @@ -118,7 +118,8 @@ class ApacheBlockNode(ApacheDirectiveNode): # pylint: disable=unused-argument def add_child_directive(self, name: str, parameters: Optional[List[str]] = None, - position: int = None) -> ApacheDirectiveNode: # pragma: no cover + position: Optional[int] = None + ) -> ApacheDirectiveNode: # pragma: no cover """Adds a new DirectiveNode to the sequence of children""" new_dir = ApacheDirectiveNode(name=assertions.PASS, parameters=assertions.PASS, diff --git a/certbot-apache/certbot_apache/_internal/configurator.py b/certbot-apache/certbot_apache/_internal/configurator.py index db9eea444..41209deb4 100644 --- a/certbot-apache/certbot_apache/_internal/configurator.py +++ b/certbot-apache/certbot_apache/_internal/configurator.py @@ -366,12 +366,9 @@ class ApacheConfigurator(common.Configurator): self.version = self.get_version() logger.debug('Apache version is %s', '.'.join(str(i) for i in self.version)) - if self.version < (2, 2): + if self.version < (2, 4): raise errors.NotSupportedError( "Apache Version {0} not supported.".format(str(self.version))) - elif self.version < (2, 4): - logger.warning('Support for Apache 2.2 is deprecated and will be removed in a ' - 'future release.') # Recover from previous crash before Augeas initialization to have the # correct parse tree from the get go. @@ -815,7 +812,7 @@ class ApacheConfigurator(common.Configurator): return self._find_best_vhost(target, filtered_vhosts, filter_defaults) def _find_best_vhost( - self, target_name: str, vhosts: List[obj.VirtualHost] = None, + self, target_name: str, vhosts: Optional[List[obj.VirtualHost]] = None, filter_defaults: bool = True ) -> Optional[obj.VirtualHost]: """Finds the best vhost for a target_name. @@ -1188,46 +1185,6 @@ class ApacheConfigurator(common.Configurator): vhost.aliases.add(serveralias) vhost.name = servername - def is_name_vhost(self, target_addr: obj.Addr) -> bool: - """Returns if vhost is a name based vhost - - NameVirtualHost was deprecated in Apache 2.4 as all VirtualHosts are - now NameVirtualHosts. If version is earlier than 2.4, check if addr - has a NameVirtualHost directive in the Apache config - - :param certbot_apache._internal.obj.Addr target_addr: vhost address - - :returns: Success - :rtype: bool - - """ - # Mixed and matched wildcard NameVirtualHost with VirtualHost - # behavior is undefined. Make sure that an exact match exists - - # search for NameVirtualHost directive for ip_addr - # note ip_addr can be FQDN although Apache does not recommend it - return (self.version >= (2, 4) or - bool(self.parser.find_dir("NameVirtualHost", str(target_addr)))) - - def add_name_vhost(self, addr: obj.Addr) -> None: - """Adds NameVirtualHost directive for given address. - - :param addr: Address that will be added as NameVirtualHost directive - :type addr: :class:`~certbot_apache._internal.obj.Addr` - - """ - - loc = parser.get_aug_path(self.parser.loc["name"]) - if addr.get_port() == "443": - self.parser.add_dir_to_ifmodssl( - loc, "NameVirtualHost", [str(addr)]) - else: - self.parser.add_dir(loc, "NameVirtualHost", [str(addr)]) - - msg = "Setting {0} to be NameBasedVirtualHost\n".format(addr) - logger.debug(msg) - self.save_notes += msg - def prepare_server_https(self, port: str, temp: bool = False) -> None: """Prepare the server for HTTPS. @@ -1375,8 +1332,7 @@ class ApacheConfigurator(common.Configurator): """ if self.options.handle_modules: - if self.version >= (2, 4) and ("socache_shmcb_module" not in - self.parser.modules): + if "socache_shmcb_module" not in self.parser.modules: self.enable_mod("socache_shmcb", temp=temp) if "ssl_module" not in self.parser.modules: self.enable_mod("ssl", temp=temp) @@ -1463,10 +1419,6 @@ class ApacheConfigurator(common.Configurator): # for the new directives; For these reasons... this is tacked # on after fully creating the new vhost - # Now check if addresses need to be added as NameBasedVhost addrs - # This is for compliance with versions of Apache < 2.4 - self._add_name_vhost_if_necessary(ssl_vhost) - return ssl_vhost def _get_new_vh_path(self, orig_matches: List[str], new_matches: List[str]) -> Optional[str]: @@ -1765,40 +1717,6 @@ class ApacheConfigurator(common.Configurator): aliases = (self.parser.aug.get(match) for match in matches) return self.domain_in_names(aliases, target_name) - def _add_name_vhost_if_necessary(self, vhost: obj.VirtualHost) -> None: - """Add NameVirtualHost Directives if necessary for new vhost. - - NameVirtualHosts was a directive in Apache < 2.4 - https://httpd.apache.org/docs/2.2/mod/core.html#namevirtualhost - - :param vhost: New virtual host that was recently created. - :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost` - - """ - need_to_save: bool = False - - # See if the exact address appears in any other vhost - # Remember 1.1.1.1:* == 1.1.1.1 -> hence any() - for addr in vhost.addrs: - # In Apache 2.2, when a NameVirtualHost directive is not - # set, "*" and "_default_" will conflict when sharing a port - addrs = {addr,} - if addr.get_addr() in ("*", "_default_"): - addrs.update(obj.Addr((a, addr.get_port(),)) - for a in ("*", "_default_")) - - for test_vh in self.vhosts: - if (vhost.filep != test_vh.filep and - any(test_addr in addrs for - test_addr in test_vh.addrs) and not self.is_name_vhost(addr)): - self.add_name_vhost(addr) - logger.info("Enabling NameVirtualHosts on %s", addr) - need_to_save = True - break - - if need_to_save: - self.save() - def find_vhost_by_id(self, id_str: str) -> obj.VirtualHost: """ Searches through VirtualHosts and tries to match the id in a comment @@ -2014,12 +1932,6 @@ class ApacheConfigurator(common.Configurator): :param unused_options: Not currently used :type unused_options: Not Available """ - min_apache_ver = (2, 3, 3) - if self.get_version() < min_apache_ver: - raise errors.PluginError( - "Unable to set OCSP directives.\n" - "Apache version is below 2.3.3.") - if "socache_shmcb_module" not in self.parser.modules: self.enable_mod("socache_shmcb") @@ -2200,10 +2112,7 @@ class ApacheConfigurator(common.Configurator): general_vh.filep, ssl_vhost.filep) def _set_https_redirection_rewrite_rule(self, vhost: obj.VirtualHost) -> None: - if self.get_version() >= (2, 3, 9): - self.parser.add_dir(vhost.path, "RewriteRule", constants.REWRITE_HTTPS_ARGS_WITH_END) - else: - self.parser.add_dir(vhost.path, "RewriteRule", constants.REWRITE_HTTPS_ARGS) + self.parser.add_dir(vhost.path, "RewriteRule", constants.REWRITE_HTTPS_ARGS) def _verify_no_certbot_redirect(self, vhost: obj.VirtualHost) -> None: """Checks to see if a redirect was already installed by certbot. @@ -2235,9 +2144,6 @@ class ApacheConfigurator(common.Configurator): rewrite_args_dict[dir_path].append(match) if rewrite_args_dict: - redirect_args = [constants.REWRITE_HTTPS_ARGS, - constants.REWRITE_HTTPS_ARGS_WITH_END] - for dir_path, args_paths in rewrite_args_dict.items(): arg_vals = [self.parser.aug.get(x) for x in args_paths] @@ -2249,7 +2155,7 @@ class ApacheConfigurator(common.Configurator): raise errors.PluginEnhancementAlreadyPresent( "Certbot has already enabled redirection") - if arg_vals in redirect_args: + if arg_vals == constants.REWRITE_HTTPS_ARGS: raise errors.PluginEnhancementAlreadyPresent( "Certbot has already enabled redirection") @@ -2318,12 +2224,6 @@ class ApacheConfigurator(common.Configurator): if ssl_vhost.aliases: serveralias = "ServerAlias " + " ".join(ssl_vhost.aliases) - rewrite_rule_args: List[str] - if self.get_version() >= (2, 3, 9): - rewrite_rule_args = constants.REWRITE_HTTPS_ARGS_WITH_END - else: - rewrite_rule_args = constants.REWRITE_HTTPS_ARGS - return ( f"\n" f"{servername} \n" @@ -2331,7 +2231,7 @@ class ApacheConfigurator(common.Configurator): f"ServerSignature Off\n" f"\n" f"RewriteEngine On\n" - f"RewriteRule {' '.join(rewrite_rule_args)}\n" + f"RewriteRule {' '.join(constants.REWRITE_HTTPS_ARGS)}\n" "\n" f"ErrorLog {self.options.logs_root}/redirect.error.log\n" f"LogLevel warn\n" diff --git a/certbot-apache/certbot_apache/_internal/constants.py b/certbot-apache/certbot_apache/_internal/constants.py index 4e6fa1791..0861e2da9 100644 --- a/certbot-apache/certbot_apache/_internal/constants.py +++ b/certbot-apache/certbot_apache/_internal/constants.py @@ -42,18 +42,14 @@ AUGEAS_LENS_DIR = pkg_resources.resource_filename( """Path to the Augeas lens directory""" REWRITE_HTTPS_ARGS: List[str] = [ - "^", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,NE,R=permanent]"] -"""Apache version<2.3.9 rewrite rule arguments used for redirections to -https vhost""" - -REWRITE_HTTPS_ARGS_WITH_END: List[str] = [ "^", "https://%{SERVER_NAME}%{REQUEST_URI}", "[END,NE,R=permanent]"] """Apache version >= 2.3.9 rewrite rule arguments used for redirections to https vhost""" OLD_REWRITE_HTTPS_ARGS: List[List[str]] = [ ["^", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,QSA,R=permanent]"], - ["^", "https://%{SERVER_NAME}%{REQUEST_URI}", "[END,QSA,R=permanent]"]] + ["^", "https://%{SERVER_NAME}%{REQUEST_URI}", "[END,QSA,R=permanent]"], + ["^", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,NE,R=permanent]"]] HSTS_ARGS: List[str] = ["always", "set", "Strict-Transport-Security", "\"max-age=31536000\""] diff --git a/certbot-apache/certbot_apache/_internal/http_01.py b/certbot-apache/certbot_apache/_internal/http_01.py index ade2265ea..e7ca87608 100644 --- a/certbot-apache/certbot_apache/_internal/http_01.py +++ b/certbot-apache/certbot_apache/_internal/http_01.py @@ -24,22 +24,6 @@ logger = logging.getLogger(__name__) class ApacheHttp01(common.ChallengePerformer): """Class that performs HTTP-01 challenges within the Apache configurator.""" - CONFIG_TEMPLATE22_PRE = """\ - RewriteEngine on - RewriteRule ^/\\.well-known/acme-challenge/([A-Za-z0-9-_=]+)$ {0}/$1 [L] - - """ - CONFIG_TEMPLATE22_POST = """\ - - Order Allow,Deny - Allow from all - - - Order Allow,Deny - Allow from all - - """ - CONFIG_TEMPLATE24_PRE = """\ RewriteEngine on RewriteRule ^/\\.well-known/acme-challenge/([A-Za-z0-9-_=]+)$ {0}/$1 [END] @@ -90,11 +74,7 @@ class ApacheHttp01(common.ChallengePerformer): """Make sure that we have the needed modules available for http01""" if self.configurator.conf("handle-modules"): - needed_modules = ["rewrite"] - if self.configurator.version < (2, 4): - needed_modules.append("authz_host") - else: - needed_modules.append("authz_core") + needed_modules = ["rewrite", "authz_core"] for mod in needed_modules: if mod + "_module" not in self.configurator.parser.modules: self.configurator.enable_mod(mod, temp=True) @@ -131,15 +111,8 @@ class ApacheHttp01(common.ChallengePerformer): self.configurator.reverter.register_file_creation( True, self.challenge_conf_post) - if self.configurator.version < (2, 4): - config_template_pre = self.CONFIG_TEMPLATE22_PRE - config_template_post = self.CONFIG_TEMPLATE22_POST - else: - config_template_pre = self.CONFIG_TEMPLATE24_PRE - config_template_post = self.CONFIG_TEMPLATE24_POST - - config_text_pre = config_template_pre.format(self.challenge_dir) - config_text_post = config_template_post.format(self.challenge_dir) + config_text_pre = self.CONFIG_TEMPLATE24_PRE.format(self.challenge_dir) + config_text_post = self.CONFIG_TEMPLATE24_POST.format(self.challenge_dir) logger.debug("writing a pre config file with text:\n %s", config_text_pre) with open(self.challenge_conf_pre, "w") as new_conf: @@ -184,15 +157,13 @@ class ApacheHttp01(common.ChallengePerformer): def _set_up_challenges(self) -> List[KeyAuthorizationChallengeResponse]: if not os.path.isdir(self.challenge_dir): - old_umask = filesystem.umask(0o022) - try: - filesystem.makedirs(self.challenge_dir, 0o755) - except OSError as exception: - if exception.errno not in (errno.EEXIST, errno.EISDIR): - raise errors.PluginError( - "Couldn't create root for http-01 challenge") - finally: - filesystem.umask(old_umask) + with filesystem.temp_umask(0o022): + try: + filesystem.makedirs(self.challenge_dir, 0o755) + except OSError as exception: + if exception.errno not in (errno.EEXIST, errno.EISDIR): + raise errors.PluginError( + "Couldn't create root for http-01 challenge") responses = [] for achall in self.achalls: diff --git a/certbot-apache/certbot_apache/_internal/override_centos.py b/certbot-apache/certbot_apache/_internal/override_centos.py index 583d30b98..9883bb1f1 100644 --- a/certbot-apache/certbot_apache/_internal/override_centos.py +++ b/certbot-apache/certbot_apache/_internal/override_centos.py @@ -1,8 +1,6 @@ """ Distribution specific override class for CentOS family (RHEL, Fedora) """ import logging from typing import Any -from typing import cast -from typing import List from certbot_apache._internal import apache_util from certbot_apache._internal import configurator @@ -11,7 +9,6 @@ from certbot_apache._internal.configurator import OsOptions from certbot import errors from certbot import util -from certbot.errors import MisconfigurationError logger = logging.getLogger(__name__) @@ -61,8 +58,13 @@ class CentOSConfigurator(configurator.ApacheConfigurator): "rhel", "redhatenterpriseserver", "red hat enterprise linux server", "scientific", "scientific linux", ] + # It is important that the loose version comparison below is not made + # if the OS is not RHEL derived. See + # https://github.com/certbot/certbot/issues/9481. + if not rhel_derived: + return False at_least_v9 = util.parse_loose_version(os_version) >= util.parse_loose_version('9') - return rhel_derived and at_least_v9 + return at_least_v9 def _override_cmds(self) -> None: super()._override_cmds() @@ -101,82 +103,6 @@ class CentOSConfigurator(configurator.ApacheConfigurator): return CentOSParser( self.options.server_root, self, self.options.vhost_root, self.version) - def _deploy_cert(self, *args: Any, **kwargs: Any) -> None: # pylint: disable=arguments-differ - """ - Override _deploy_cert in order to ensure that the Apache configuration - has "LoadModule ssl_module..." before parsing the VirtualHost configuration - that was created by Certbot - """ - super()._deploy_cert(*args, **kwargs) - if self.version < (2, 4, 0): - self._deploy_loadmodule_ssl_if_needed() - - def _deploy_loadmodule_ssl_if_needed(self) -> None: - """ - Add "LoadModule ssl_module " to main httpd.conf if - it doesn't exist there already. - """ - - loadmods = self.parser.find_dir("LoadModule", "ssl_module", exclude=False) - - correct_ifmods: List[str] = [] - loadmod_args: List[str] = [] - loadmod_paths: List[str] = [] - for m in loadmods: - noarg_path = m.rpartition("/")[0] - path_args = self.parser.get_all_args(noarg_path) - if loadmod_args: - if loadmod_args != path_args: - msg = ("Certbot encountered multiple LoadModule directives " - "for LoadModule ssl_module with differing library paths. " - "Please remove or comment out the one(s) that are not in " - "use, and run Certbot again.") - raise MisconfigurationError(msg) - else: - loadmod_args = [arg for arg in path_args if arg] - - centos_parser: CentOSParser = cast(CentOSParser, self.parser) - if centos_parser.not_modssl_ifmodule(noarg_path): - if centos_parser.loc["default"] in noarg_path: - # LoadModule already in the main configuration file - if "ifmodule/" in noarg_path.lower() or "ifmodule[1]" in noarg_path.lower(): - # It's the first or only IfModule in the file - return - # Populate the list of known !mod_ssl.c IfModules - nodir_path = noarg_path.rpartition("/directive")[0] - correct_ifmods.append(nodir_path) - else: - loadmod_paths.append(noarg_path) - - if not loadmod_args: - # Do not try to enable mod_ssl - return - - # Force creation as the directive wasn't found from the beginning of - # httpd.conf - rootconf_ifmod = self.parser.create_ifmod( - parser.get_aug_path(self.parser.loc["default"]), - "!mod_ssl.c", beginning=True) - # parser.get_ifmod returns a path postfixed with "/", remove that - self.parser.add_dir(rootconf_ifmod[:-1], "LoadModule", loadmod_args) - correct_ifmods.append(rootconf_ifmod[:-1]) - self.save_notes += "Added LoadModule ssl_module to main configuration.\n" - - # Wrap LoadModule mod_ssl inside of if it's not - # configured like this already. - for loadmod_path in loadmod_paths: - nodir_path = loadmod_path.split("/directive")[0] - # Remove the old LoadModule directive - self.parser.aug.remove(loadmod_path) - - # Create a new IfModule !mod_ssl.c if not already found on path - ssl_ifmod = self.parser.get_ifmod(nodir_path, "!mod_ssl.c", beginning=True)[:-1] - if ssl_ifmod not in correct_ifmods: - self.parser.add_dir(ssl_ifmod, "LoadModule", loadmod_args) - correct_ifmods.append(ssl_ifmod) - self.save_notes += ("Wrapped pre-existing LoadModule ssl_module " - "inside of block.\n") - class CentOSParser(parser.ApacheParser): """CentOS specific ApacheParser override class""" @@ -196,33 +122,3 @@ class CentOSParser(parser.ApacheParser): defines = apache_util.parse_define_file(self.sysconfig_filep, "OPTIONS") for k, v in defines.items(): self.variables[k] = v - - def not_modssl_ifmodule(self, path: str) -> bool: - """Checks if the provided Augeas path has argument !mod_ssl""" - - if "ifmodule" not in path.lower(): - return False - - # Trim the path to the last ifmodule - workpath = path.lower() - while workpath: - # Get path to the last IfModule (ignore the tail) - parts = workpath.rpartition("ifmodule") - - if not parts[0]: - # IfModule not found - break - ifmod_path = parts[0] + parts[1] - # Check if ifmodule had an index - if parts[2].startswith("["): - # Append the index from tail - ifmod_path += parts[2].partition("/")[0] - # Get the original path trimmed to correct length - # This is required to preserve cases - ifmod_real_path = path[0:len(ifmod_path)] - if "!mod_ssl.c" in self.get_all_args(ifmod_real_path): - return True - # Set the workpath to the heading part - workpath = parts[0] - - return False diff --git a/certbot-apache/certbot_apache/_internal/parser.py b/certbot-apache/certbot_apache/_internal/parser.py index 019172d37..9805ad781 100644 --- a/certbot-apache/certbot_apache/_internal/parser.py +++ b/certbot-apache/certbot_apache/_internal/parser.py @@ -47,6 +47,7 @@ class ApacheParser: arg_var_interpreter: Pattern = re.compile(r"\$\{[^ \}]*}") fnmatch_chars: Set[str] = {"*", "?", "\\", "[", "]"} + # pylint: disable=unused-argument def __init__(self, root: str, configurator: "ApacheConfigurator", vhostroot: str, version: Tuple[int, ...] = (2, 4)) -> None: # Note: Order is important here. @@ -74,9 +75,8 @@ class ApacheParser: self.loc: Dict[str, str] = {"root": self._find_config_root()} self.parse_file(self.loc["root"]) - if version >= (2, 4): - # Look up variables from httpd and add to DOM if not already parsed - self.update_runtime_variables() + # Look up variables from httpd and add to DOM if not already parsed + self.update_runtime_variables() # This problem has been fixed in Augeas 1.0 self.standardize_excl() @@ -95,11 +95,6 @@ class ApacheParser: self.parse_file(os.path.abspath(vhostroot) + "/" + self.configurator.options.vhost_files) - # check to see if there were unparsed define statements - if version < (2, 4): - if self.find_dir("Define", exclude=False): - raise errors.PluginError("Error parsing runtime variables") - def check_parsing_errors(self, lens: str) -> None: """Verify Augeas can parse all of the lens files. @@ -382,7 +377,7 @@ class ApacheParser: for i, arg in enumerate(args): self.aug.set("%s/arg[%d]" % (nvh_path, i + 1), arg) - def get_ifmod(self, aug_conf_path: str, mod: str, beginning: bool = False) -> str: + def get_ifmod(self, aug_conf_path: str, mod: str) -> str: """Returns the path to and creates one if it doesn't exist. :param str aug_conf_path: Augeas configuration path @@ -399,35 +394,26 @@ class ApacheParser: if_mods = self.aug.match(("%s/IfModule/*[self::arg='%s']" % (aug_conf_path, mod))) if not if_mods: - return self.create_ifmod(aug_conf_path, mod, beginning) + return self.create_ifmod(aug_conf_path, mod) # Strip off "arg" at end of first ifmod path return if_mods[0].rpartition("arg")[0] - def create_ifmod(self, aug_conf_path: str, mod: str, beginning: bool = False) -> str: + def create_ifmod(self, aug_conf_path: str, mod: str) -> str: """Creates a new and returns its path. :param str aug_conf_path: Augeas configuration path :param str mod: module ie. mod_ssl.c - :param bool beginning: If the IfModule should be created to the beginning - of augeas path DOM tree. :returns: Augeas path of the newly created IfModule directive. The path may be dynamic, i.e. .../IfModule[last()] :rtype: str """ - if beginning: - c_path_arg = "{}/IfModule[1]/arg".format(aug_conf_path) - # Insert IfModule before the first directive - self.aug.insert("{}/directive[1]".format(aug_conf_path), - "IfModule", True) - retpath = "{}/IfModule[1]/".format(aug_conf_path) - else: - c_path = "{}/IfModule[last() + 1]".format(aug_conf_path) - c_path_arg = "{}/IfModule[last()]/arg".format(aug_conf_path) - self.aug.set(c_path, "") - retpath = "{}/IfModule[last()]/".format(aug_conf_path) + c_path = "{}/IfModule[last() + 1]".format(aug_conf_path) + c_path_arg = "{}/IfModule[last()]/arg".format(aug_conf_path) + self.aug.set(c_path, "") + retpath = "{}/IfModule[last()]/".format(aug_conf_path) self.aug.set(c_path_arg, mod) return retpath @@ -587,20 +573,6 @@ class ApacheParser: return ordered_matches - def get_all_args(self, match: str) -> List[Optional[str]]: - """ - Tries to fetch all arguments for a directive. See get_arg. - - Note that if match is an ancestor node, it returns all names of - child directives as well as the list of arguments. - - """ - - if match[-1] != "/": - match = match + "/" - allargs = self.aug.match(match + '*') - return [self.get_arg(arg) for arg in allargs] - def get_arg(self, match: str) -> Optional[str]: """Uses augeas.get to get argument value and interprets result. diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py index eab74f5e3..b3fa9b84f 100644 --- a/certbot-apache/setup.py +++ b/certbot-apache/setup.py @@ -1,7 +1,7 @@ from setuptools import find_packages from setuptools import setup -version = '2.0.0.dev0' +version = '2.1.0.dev0' install_requires = [ # We specify the minimum acme and certbot version as the current plugin @@ -38,6 +38,7 @@ setup( 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-apache/tests/augeasnode_test.py b/certbot-apache/tests/augeasnode_test.py index 1e11b5eb3..591634d35 100644 --- a/certbot-apache/tests/augeasnode_test.py +++ b/certbot-apache/tests/augeasnode_test.py @@ -1,13 +1,9 @@ """Tests for AugeasParserNode classes""" from typing import List -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore - import os import util +from unittest import mock from certbot import errors diff --git a/certbot-apache/tests/autohsts_test.py b/certbot-apache/tests/autohsts_test.py index 664d791bd..70ed2ca1a 100644 --- a/certbot-apache/tests/autohsts_test.py +++ b/certbot-apache/tests/autohsts_test.py @@ -2,11 +2,7 @@ """Test for certbot_apache._internal.configurator AutoHSTS functionality""" import re import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore +from unittest import mock from certbot import errors from certbot_apache._internal import constants diff --git a/certbot-apache/tests/centos6_test.py b/certbot-apache/tests/centos6_test.py deleted file mode 100644 index 85f1333e9..000000000 --- a/certbot-apache/tests/centos6_test.py +++ /dev/null @@ -1,228 +0,0 @@ -"""Test for certbot_apache._internal.configurator for CentOS 6 overrides""" -import unittest -from unittest import mock - -from certbot.compat import os -from certbot.errors import MisconfigurationError -from certbot_apache._internal import obj -from certbot_apache._internal import override_centos -from certbot_apache._internal import parser -import util - - -def get_vh_truth(temp_dir, config_name): - """Return the ground truth for the specified directory.""" - prefix = os.path.join( - temp_dir, config_name, "httpd/conf.d") - - aug_pre = "/files" + prefix - vh_truth = [ - obj.VirtualHost( - os.path.join(prefix, "test.example.com.conf"), - os.path.join(aug_pre, "test.example.com.conf/VirtualHost"), - {obj.Addr.fromstring("*:80")}, - False, True, "test.example.com"), - obj.VirtualHost( - os.path.join(prefix, "ssl.conf"), - os.path.join(aug_pre, "ssl.conf/VirtualHost"), - {obj.Addr.fromstring("_default_:443")}, - True, True, None) - ] - return vh_truth - -class CentOS6Tests(util.ApacheTest): - """Tests for CentOS 6""" - - def setUp(self): # pylint: disable=arguments-differ - test_dir = "centos6_apache/apache" - config_root = "centos6_apache/apache/httpd" - vhost_root = "centos6_apache/apache/httpd/conf.d" - super().setUp(test_dir=test_dir, - config_root=config_root, - vhost_root=vhost_root) - - self.config = util.get_apache_configurator( - self.config_path, self.vhost_path, self.config_dir, self.work_dir, - version=(2, 2, 15), os_info="centos") - self.vh_truth = get_vh_truth( - self.temp_dir, "centos6_apache/apache") - - def test_get_parser(self): - self.assertIsInstance(self.config.parser, override_centos.CentOSParser) - - def test_get_virtual_hosts(self): - """Make sure all vhosts are being properly found.""" - vhs = self.config.get_virtual_hosts() - self.assertEqual(len(vhs), 2) - found = 0 - - for vhost in vhs: - for centos_truth in self.vh_truth: - if vhost == centos_truth: - found += 1 - break - else: - raise Exception("Missed: %s" % vhost) # pragma: no cover - self.assertEqual(found, 2) - - @mock.patch("certbot_apache._internal.configurator.display_util.notify") - def test_loadmod_default(self, unused_mock_notify): - ssl_loadmods = self.config.parser.find_dir( - "LoadModule", "ssl_module", exclude=False) - self.assertEqual(len(ssl_loadmods), 1) - # Make sure the LoadModule ssl_module is in ssl.conf (default) - self.assertIn("ssl.conf", ssl_loadmods[0]) - # ...and that it's not inside of - self.assertNotIn("IfModule", ssl_loadmods[0]) - - # Get the example vhost - self.config.assoc["test.example.com"] = self.vh_truth[0] - self.config.deploy_cert( - "random.demo", "example/cert.pem", "example/key.pem", - "example/cert_chain.pem", "example/fullchain.pem") - self.config.save() - - post_loadmods = self.config.parser.find_dir( - "LoadModule", "ssl_module", exclude=False) - - # We should now have LoadModule ssl_module in root conf and ssl.conf - self.assertEqual(len(post_loadmods), 2) - for lm in post_loadmods: - # lm[:-7] removes "/arg[#]" from the path - arguments = self.config.parser.get_all_args(lm[:-7]) - self.assertEqual(arguments, ["ssl_module", "modules/mod_ssl.so"]) - # ...and both of them should be wrapped in - # lm[:-17] strips off /directive/arg[1] from the path. - ifmod_args = self.config.parser.get_all_args(lm[:-17]) - self.assertIn("!mod_ssl.c", ifmod_args) - - @mock.patch("certbot_apache._internal.configurator.display_util.notify") - def test_loadmod_multiple(self, unused_mock_notify): - sslmod_args = ["ssl_module", "modules/mod_ssl.so"] - # Adds another LoadModule to main httpd.conf in addtition to ssl.conf - self.config.parser.add_dir(self.config.parser.loc["default"], "LoadModule", - sslmod_args) - self.config.save() - pre_loadmods = self.config.parser.find_dir( - "LoadModule", "ssl_module", exclude=False) - # LoadModules are not within IfModule blocks - self.assertIs(any("ifmodule" in m.lower() for m in pre_loadmods), False) - self.config.assoc["test.example.com"] = self.vh_truth[0] - self.config.deploy_cert( - "random.demo", "example/cert.pem", "example/key.pem", - "example/cert_chain.pem", "example/fullchain.pem") - post_loadmods = self.config.parser.find_dir( - "LoadModule", "ssl_module", exclude=False) - - for mod in post_loadmods: - with self.subTest(mod=mod): - # pylint: disable=no-member - self.assertIs(self.config.parser.not_modssl_ifmodule(mod), True) - - @mock.patch("certbot_apache._internal.configurator.display_util.notify") - def test_loadmod_rootconf_exists(self, unused_mock_notify): - sslmod_args = ["ssl_module", "modules/mod_ssl.so"] - rootconf_ifmod = self.config.parser.get_ifmod( - parser.get_aug_path(self.config.parser.loc["default"]), - "!mod_ssl.c", beginning=True) - self.config.parser.add_dir(rootconf_ifmod[:-1], "LoadModule", sslmod_args) - self.config.save() - # Get the example vhost - self.config.assoc["test.example.com"] = self.vh_truth[0] - self.config.deploy_cert( - "random.demo", "example/cert.pem", "example/key.pem", - "example/cert_chain.pem", "example/fullchain.pem") - self.config.save() - - root_loadmods = self.config.parser.find_dir( - "LoadModule", "ssl_module", - start=parser.get_aug_path(self.config.parser.loc["default"]), - exclude=False) - - mods = [lm for lm in root_loadmods if self.config.parser.loc["default"] in lm] - - self.assertEqual(len(mods), 1) - # [:-7] removes "/arg[#]" from the path - self.assertEqual( - self.config.parser.get_all_args(mods[0][:-7]), - sslmod_args) - - @mock.patch("certbot_apache._internal.configurator.display_util.notify") - def test_neg_loadmod_already_on_path(self, unused_mock_notify): - loadmod_args = ["ssl_module", "modules/mod_ssl.so"] - ifmod = self.config.parser.get_ifmod( - self.vh_truth[1].path, "!mod_ssl.c", beginning=True) - self.config.parser.add_dir(ifmod[:-1], "LoadModule", loadmod_args) - self.config.parser.add_dir(self.vh_truth[1].path, "LoadModule", loadmod_args) - self.config.save() - pre_loadmods = self.config.parser.find_dir( - "LoadModule", "ssl_module", start=self.vh_truth[1].path, exclude=False) - self.assertEqual(len(pre_loadmods), 2) - # The ssl.conf now has two LoadModule directives, one inside of - # !mod_ssl.c IfModule - self.config.assoc["test.example.com"] = self.vh_truth[0] - self.config.deploy_cert( - "random.demo", "example/cert.pem", "example/key.pem", - "example/cert_chain.pem", "example/fullchain.pem") - self.config.save() - # Ensure that the additional LoadModule wasn't written into the IfModule - post_loadmods = self.config.parser.find_dir( - "LoadModule", "ssl_module", start=self.vh_truth[1].path, exclude=False) - self.assertEqual(len(post_loadmods), 1) - - def test_loadmod_non_duplicate(self): - # the modules/mod_ssl.so exists in ssl.conf - sslmod_args = ["ssl_module", "modules/mod_somethingelse.so"] - rootconf_ifmod = self.config.parser.get_ifmod( - parser.get_aug_path(self.config.parser.loc["default"]), - "!mod_ssl.c", beginning=True) - self.config.parser.add_dir(rootconf_ifmod[:-1], "LoadModule", sslmod_args) - self.config.save() - self.config.assoc["test.example.com"] = self.vh_truth[0] - pre_matches = self.config.parser.find_dir("LoadModule", - "ssl_module", exclude=False) - - self.assertRaises(MisconfigurationError, self.config.deploy_cert, - "random.demo", "example/cert.pem", "example/key.pem", - "example/cert_chain.pem", "example/fullchain.pem") - - post_matches = self.config.parser.find_dir("LoadModule", - "ssl_module", exclude=False) - # Make sure that none was changed - self.assertEqual(pre_matches, post_matches) - - @mock.patch("certbot_apache._internal.configurator.display_util.notify") - def test_loadmod_not_found(self, unused_mock_notify): - # Remove all existing LoadModule ssl_module... directives - orig_loadmods = self.config.parser.find_dir("LoadModule", - "ssl_module", - exclude=False) - for mod in orig_loadmods: - noarg_path = mod.rpartition("/")[0] - self.config.parser.aug.remove(noarg_path) - self.config.save() - self.config.deploy_cert( - "random.demo", "example/cert.pem", "example/key.pem", - "example/cert_chain.pem", "example/fullchain.pem") - - post_loadmods = self.config.parser.find_dir("LoadModule", - "ssl_module", - exclude=False) - self.assertEqual(post_loadmods, []) - - def test_no_ifmod_search_false(self): - #pylint: disable=no-member - - self.assertIs(self.config.parser.not_modssl_ifmodule( - "/path/does/not/include/ifmod" - ), False) - self.assertIs(self.config.parser.not_modssl_ifmodule( - "" - ), False) - self.assertIs(self.config.parser.not_modssl_ifmodule( - "/path/includes/IfModule/but/no/arguments" - ), False) - - -if __name__ == "__main__": - unittest.main() # pragma: no cover diff --git a/certbot-apache/tests/centos_test.py b/certbot-apache/tests/centos_test.py index a9a7d8dcc..3f8e88467 100644 --- a/certbot-apache/tests/centos_test.py +++ b/certbot-apache/tests/centos_test.py @@ -1,10 +1,6 @@ """Test for certbot_apache._internal.configurator for Centos overrides""" import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore +from unittest import mock from certbot import errors from certbot.compat import filesystem diff --git a/certbot-apache/tests/configurator_reverter_test.py b/certbot-apache/tests/configurator_reverter_test.py index 72b8fe2bd..fe0dfb39d 100644 --- a/certbot-apache/tests/configurator_reverter_test.py +++ b/certbot-apache/tests/configurator_reverter_test.py @@ -1,11 +1,7 @@ """Test for certbot_apache._internal.configurator implementations of reverter""" import shutil import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore +from unittest import mock from certbot import errors import util diff --git a/certbot-apache/tests/configurator_test.py b/certbot-apache/tests/configurator_test.py index 3a557bb71..0978b302e 100644 --- a/certbot-apache/tests/configurator_test.py +++ b/certbot-apache/tests/configurator_test.py @@ -5,11 +5,7 @@ import shutil import socket import tempfile import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore +from unittest import mock from acme import challenges from certbot import achallenges @@ -443,18 +439,6 @@ class MultipleVhostsTest(util.ApacheTest): "SSLCertificateChainFile", "two/cert_chain.pem", self.vh_truth[1].path)) - def test_is_name_vhost(self): - addr = obj.Addr.fromstring("*:80") - self.assertIs(self.config.is_name_vhost(addr), True) - self.config.version = (2, 2) - self.assertIs(self.config.is_name_vhost(addr), False) - - def test_add_name_vhost(self): - self.config.add_name_vhost(obj.Addr.fromstring("*:443")) - self.config.add_name_vhost(obj.Addr.fromstring("*:80")) - self.assertTrue(self.config.parser.find_dir("NameVirtualHost", "*:443", exclude=False)) - self.assertTrue(self.config.parser.find_dir("NameVirtualHost", "*:80")) - def test_add_listen_80(self): mock_find = mock.Mock() mock_add_dir = mock.Mock() @@ -642,9 +626,6 @@ class MultipleVhostsTest(util.ApacheTest): self.assertIs(ssl_vhost.ssl, True) self.assertIs(ssl_vhost.enabled, False) - self.assertEqual(self.config.is_name_vhost(self.vh_truth[0]), - self.config.is_name_vhost(ssl_vhost)) - self.assertEqual(len(self.config.vhosts), 13) def test_clean_vhost_ssl(self): @@ -721,21 +702,6 @@ class MultipleVhostsTest(util.ApacheTest): # pylint: disable=protected-access self.assertIs(self.config._get_ssl_vhost_path("example_path").endswith(".conf"), True) - def test_add_name_vhost_if_necessary(self): - # pylint: disable=protected-access - self.config.add_name_vhost = mock.Mock() - self.config.version = (2, 2) - self.config._add_name_vhost_if_necessary(self.vh_truth[0]) - self.assertIs(self.config.add_name_vhost.called, True) - - new_addrs = set() - for addr in self.vh_truth[0].addrs: - new_addrs.add(obj.Addr(("_default_", addr.get_port(),))) - - self.vh_truth[0].addrs = new_addrs - self.config._add_name_vhost_if_necessary(self.vh_truth[0]) - self.assertEqual(self.config.add_name_vhost.call_count, 2) - @mock.patch("certbot_apache._internal.configurator.http_01.ApacheHttp01.perform") @mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.restart") def test_perform(self, mock_restart, mock_http_perform): @@ -946,20 +912,6 @@ class MultipleVhostsTest(util.ApacheTest): self.assertEqual(len(stapling_cache_aug_path), 1) - - @mock.patch("certbot.util.exe_exists") - def test_ocsp_unsupported_apache_version(self, mock_exe): - mock_exe.return_value = True - self.config.parser.update_runtime_variables = mock.Mock() - self.config.parser.modules["mod_ssl.c"] = None - self.config.parser.modules["socache_shmcb_module"] = None - self.config.get_version = mock.Mock(return_value=(2, 2, 0)) - self.config.choose_vhost("certbot.demo") - - self.assertRaises(errors.PluginError, - self.config.enhance, "certbot.demo", "staple-ocsp") - - def test_get_http_vhost_third_filter(self): ssl_vh = obj.VirtualHost( "fp", "ap", {obj.Addr(("*", "443"))}, @@ -1137,7 +1089,7 @@ class MultipleVhostsTest(util.ApacheTest): self.config.parser.modules["rewrite_module"] = None self.config.parser.update_runtime_variables = mock.Mock() mock_exe.return_value = True - self.config.get_version = mock.Mock(return_value=(2, 2, 0)) + self.config.get_version = mock.Mock(return_value=(2, 4, 0)) ssl_vhost = self.config.choose_vhost("certbot.demo") @@ -1567,9 +1519,6 @@ class MultiVhostsTest(util.ApacheTest): self.assertIs(ssl_vhost.ssl, True) self.assertIs(ssl_vhost.enabled, False) - self.assertEqual(self.config.is_name_vhost(self.vh_truth[1]), - self.config.is_name_vhost(ssl_vhost)) - mock_path = "certbot_apache._internal.configurator.ApacheConfigurator._get_new_vh_path" with mock.patch(mock_path) as mock_getpath: mock_getpath.return_value = None diff --git a/certbot-apache/tests/debian_test.py b/certbot-apache/tests/debian_test.py index 2bbf40312..facc65107 100644 --- a/certbot-apache/tests/debian_test.py +++ b/certbot-apache/tests/debian_test.py @@ -1,11 +1,7 @@ """Test for certbot_apache._internal.configurator for Debian overrides""" import shutil import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore +from unittest import mock from certbot import errors from certbot.compat import os diff --git a/certbot-apache/tests/display_ops_test.py b/certbot-apache/tests/display_ops_test.py index 50ab6bfc7..26927ffad 100644 --- a/certbot-apache/tests/display_ops_test.py +++ b/certbot-apache/tests/display_ops_test.py @@ -1,10 +1,6 @@ """Test certbot_apache._internal.display_ops.""" import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore +from unittest import mock from certbot import errors from certbot.display import util as display_util diff --git a/certbot-apache/tests/dualnode_test.py b/certbot-apache/tests/dualnode_test.py index 83a5729a5..a3e28d09e 100644 --- a/certbot-apache/tests/dualnode_test.py +++ b/certbot-apache/tests/dualnode_test.py @@ -1,10 +1,6 @@ """Tests for DualParserNode implementation""" import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore +from unittest import mock from certbot_apache._internal import assertions from certbot_apache._internal import augeasparser diff --git a/certbot-apache/tests/entrypoint_test.py b/certbot-apache/tests/entrypoint_test.py index 2a2694415..0b9644f09 100644 --- a/certbot-apache/tests/entrypoint_test.py +++ b/certbot-apache/tests/entrypoint_test.py @@ -1,10 +1,6 @@ """Test for certbot_apache._internal.entrypoint for override class resolution""" import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore +from unittest import mock from certbot_apache._internal import configurator from certbot_apache._internal import entrypoint diff --git a/certbot-apache/tests/fedora_test.py b/certbot-apache/tests/fedora_test.py index fca3c4ba4..4ff704aaf 100644 --- a/certbot-apache/tests/fedora_test.py +++ b/certbot-apache/tests/fedora_test.py @@ -1,10 +1,6 @@ """Test for certbot_apache._internal.configurator for Fedora 29+ overrides""" import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore +from unittest import mock from certbot import errors from certbot.compat import filesystem diff --git a/certbot-apache/tests/gentoo_test.py b/certbot-apache/tests/gentoo_test.py index 25f9e929b..4df46e70f 100644 --- a/certbot-apache/tests/gentoo_test.py +++ b/certbot-apache/tests/gentoo_test.py @@ -1,10 +1,6 @@ """Test for certbot_apache._internal.configurator for Gentoo overrides""" import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore +from unittest import mock from certbot import errors from certbot.compat import filesystem diff --git a/certbot-apache/tests/http_01_test.py b/certbot-apache/tests/http_01_test.py index 9085f68dc..fe5b69b33 100644 --- a/certbot-apache/tests/http_01_test.py +++ b/certbot-apache/tests/http_01_test.py @@ -2,11 +2,7 @@ import unittest import errno from typing import List - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore +from unittest import mock from acme import challenges from certbot import achallenges @@ -53,15 +49,6 @@ class ApacheHttp01Test(util.ApacheTest): def test_empty_perform(self): self.assertEqual(len(self.http.perform()), 0) - @mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.enable_mod") - def test_enable_modules_apache_2_2(self, mock_enmod): - self.config.version = (2, 2) - del self.config.parser.modules["authz_host_module"] - del self.config.parser.modules["mod_authz_host.c"] - - enmod_calls = self.common_enable_modules_test(mock_enmod) - self.assertEqual(enmod_calls[0][0][0], "authz_host") - @mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.enable_mod") def test_enable_modules_apache_2_4(self, mock_enmod): del self.config.parser.modules["authz_core_module"] @@ -143,21 +130,12 @@ class ApacheHttp01Test(util.ApacheTest): self.config.config.http01_port = 12345 self.assertRaises(errors.PluginError, self.http.perform) - def test_perform_1_achall_apache_2_2(self): - self.combinations_perform_test(num_achalls=1, minor_version=2) - def test_perform_1_achall_apache_2_4(self): self.combinations_perform_test(num_achalls=1, minor_version=4) - def test_perform_2_achall_apache_2_2(self): - self.combinations_perform_test(num_achalls=2, minor_version=2) - def test_perform_2_achall_apache_2_4(self): self.combinations_perform_test(num_achalls=2, minor_version=4) - def test_perform_3_achall_apache_2_2(self): - self.combinations_perform_test(num_achalls=3, minor_version=2) - def test_perform_3_achall_apache_2_4(self): self.combinations_perform_test(num_achalls=3, minor_version=4) @@ -230,10 +208,7 @@ class ApacheHttp01Test(util.ApacheTest): self.assertIn("RewriteRule", pre_conf_contents) self.assertIn(self.http.challenge_dir, post_conf_contents) - if self.config.version < (2, 4): - self.assertIn("Allow from all", post_conf_contents) - else: - self.assertIn("Require all granted", post_conf_contents) + self.assertIn("Require all granted", post_conf_contents) def _test_challenge_file(self, achall): name = os.path.join(self.http.challenge_dir, achall.chall.encode("token")) diff --git a/certbot-apache/tests/parser_test.py b/certbot-apache/tests/parser_test.py index 1062156e3..89633ae47 100644 --- a/certbot-apache/tests/parser_test.py +++ b/certbot-apache/tests/parser_test.py @@ -1,11 +1,7 @@ """Tests for certbot_apache._internal.parser.""" import shutil import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore +from unittest import mock from certbot import errors from certbot.compat import os @@ -370,15 +366,6 @@ class ParserInitTest(util.ApacheTest): ApacheParser, os.path.relpath(self.config_path), self.config, "/dummy/vhostpath", version=(2, 4, 22)) - @mock.patch("certbot_apache._internal.apache_util._get_runtime_cfg") - def test_unparseable(self, mock_cfg): - from certbot_apache._internal.parser import ApacheParser - mock_cfg.return_value = ('Define: TEST') - self.assertRaises( - errors.PluginError, - ApacheParser, os.path.relpath(self.config_path), self.config, - "/dummy/vhostpath", version=(2, 2, 22)) - def test_root_normalized(self): from certbot_apache._internal.parser import ApacheParser diff --git a/certbot-apache/tests/parsernode_configurator_test.py b/certbot-apache/tests/parsernode_configurator_test.py index ebeda3c37..6c153acc4 100644 --- a/certbot-apache/tests/parsernode_configurator_test.py +++ b/certbot-apache/tests/parsernode_configurator_test.py @@ -1,10 +1,6 @@ """Tests for ApacheConfigurator for AugeasParserNode classes""" import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore +from unittest import mock import util diff --git a/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/README b/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/README deleted file mode 100644 index c12e149f2..000000000 --- a/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/README +++ /dev/null @@ -1,9 +0,0 @@ - -This directory holds Apache 2.0 module-specific configuration files; -any files in this directory which have the ".conf" extension will be -processed as Apache configuration files. - -Files are processed in alphabetical order, so if using configuration -directives which depend on, say, mod_perl being loaded, ensure that -these are placed in a filename later in the sort order than "perl.conf". - diff --git a/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/ssl.conf b/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/ssl.conf deleted file mode 100644 index abe07dd0c..000000000 --- a/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/ssl.conf +++ /dev/null @@ -1,222 +0,0 @@ -# -# This is the Apache server configuration file providing SSL support. -# It contains the configuration directives to instruct the server how to -# serve pages over an https connection. For detailing information about these -# directives see -# -# Do NOT simply read the instructions in here without understanding -# what they do. They're here only as hints or reminders. If you are unsure -# consult the online docs. You have been warned. -# - -LoadModule ssl_module modules/mod_ssl.so - -# -# When we also provide SSL we have to listen to the -# the HTTPS port in addition. -# -Listen 443 - -## -## SSL Global Context -## -## All SSL configuration in this context applies both to -## the main server and all SSL-enabled virtual hosts. -## - -# Pass Phrase Dialog: -# Configure the pass phrase gathering process. -# The filtering dialog program (`builtin' is an internal -# terminal dialog) has to provide the pass phrase on stdout. -SSLPassPhraseDialog builtin - -# Inter-Process Session Cache: -# Configure the SSL Session Cache: First the mechanism -# to use and second the expiring timeout (in seconds). -SSLSessionCache shmcb:/var/cache/mod_ssl/scache(512000) -SSLSessionCacheTimeout 300 - -# Semaphore: -# Configure the path to the mutual exclusion semaphore the -# SSL engine uses internally for inter-process synchronization. -SSLMutex default - -# Pseudo Random Number Generator (PRNG): -# Configure one or more sources to seed the PRNG of the -# SSL library. The seed data should be of good random quality. -# WARNING! On some platforms /dev/random blocks if not enough entropy -# is available. This means you then cannot use the /dev/random device -# because it would lead to very long connection times (as long as -# it requires to make more entropy available). But usually those -# platforms additionally provide a /dev/urandom device which doesn't -# block. So, if available, use this one instead. Read the mod_ssl User -# Manual for more details. -SSLRandomSeed startup file:/dev/urandom 256 -SSLRandomSeed connect builtin -#SSLRandomSeed startup file:/dev/random 512 -#SSLRandomSeed connect file:/dev/random 512 -#SSLRandomSeed connect file:/dev/urandom 512 - -# -# Use "SSLCryptoDevice" to enable any supported hardware -# accelerators. Use "openssl engine -v" to list supported -# engine names. NOTE: If you enable an accelerator and the -# server does not start, consult the error logs and ensure -# your accelerator is functioning properly. -# -SSLCryptoDevice builtin -#SSLCryptoDevice ubsec - -## -## SSL Virtual Host Context -## - - - -# General setup for the virtual host, inherited from global configuration -#DocumentRoot "/var/www/html" -#ServerName www.example.com:443 - -# Use separate log files for the SSL virtual host; note that LogLevel -# is not inherited from httpd.conf. -ErrorLog logs/ssl_error_log -TransferLog logs/ssl_access_log -LogLevel warn - -# SSL Engine Switch: -# Enable/Disable SSL for this virtual host. -SSLEngine on - -# SSL Protocol support: -# List the enable protocol levels with which clients will be able to -# connect. Disable SSLv2 access by default: -SSLProtocol all -SSLv2 - -# SSL Cipher Suite: -# List the ciphers that the client is permitted to negotiate. -# See the mod_ssl documentation for a complete list. -SSLCipherSuite DEFAULT:!EXP:!SSLv2:!DES:!IDEA:!SEED:+3DES - -# Server Certificate: -# Point SSLCertificateFile at a PEM encoded certificate. If -# the certificate is encrypted, then you will be prompted for a -# pass phrase. Note that a kill -HUP will prompt again. A new -# certificate can be generated using the genkey(1) command. -SSLCertificateFile /etc/pki/tls/certs/localhost.crt - -# Server Private Key: -# If the key is not combined with the certificate, use this -# directive to point at the key file. Keep in mind that if -# you've both a RSA and a DSA private key you can configure -# both in parallel (to also allow the use of DSA ciphers, etc.) -SSLCertificateKeyFile /etc/pki/tls/private/localhost.key - -# Server Certificate Chain: -# Point SSLCertificateChainFile at a file containing the -# concatenation of PEM encoded CA certificates which form the -# certificate chain for the server certificate. Alternatively -# the referenced file can be the same as SSLCertificateFile -# when the CA certificates are directly appended to the server -# certificate for convinience. -#SSLCertificateChainFile /etc/pki/tls/certs/server-chain.crt - -# Certificate Authority (CA): -# Set the CA certificate verification path where to find CA -# certificates for client authentication or alternatively one -# huge file containing all of them (file must be PEM encoded) -#SSLCACertificateFile /etc/pki/tls/certs/ca-bundle.crt - -# Client Authentication (Type): -# Client certificate verification type and depth. Types are -# none, optional, require and optional_no_ca. Depth is a -# number which specifies how deeply to verify the certificate -# issuer chain before deciding the certificate is not valid. -#SSLVerifyClient require -#SSLVerifyDepth 10 - -# Access Control: -# With SSLRequire you can do per-directory access control based -# on arbitrary complex boolean expressions containing server -# variable checks and other lookup directives. The syntax is a -# mixture between C and Perl. See the mod_ssl documentation -# for more details. -# -#SSLRequire ( %{SSL_CIPHER} !~ m/^(EXP|NULL)/ \ -# and %{SSL_CLIENT_S_DN_O} eq "Snake Oil, Ltd." \ -# and %{SSL_CLIENT_S_DN_OU} in {"Staff", "CA", "Dev"} \ -# and %{TIME_WDAY} >= 1 and %{TIME_WDAY} <= 5 \ -# and %{TIME_HOUR} >= 8 and %{TIME_HOUR} <= 20 ) \ -# or %{REMOTE_ADDR} =~ m/^192\.76\.162\.[0-9]+$/ -# - -# SSL Engine Options: -# Set various options for the SSL engine. -# o FakeBasicAuth: -# Translate the client X.509 into a Basic Authorisation. This means that -# the standard Auth/DBMAuth methods can be used for access control. The -# user name is the `one line' version of the client's X.509 certificate. -# Note that no password is obtained from the user. Every entry in the user -# file needs this password: `xxj31ZMTZzkVA'. -# o ExportCertData: -# This exports two additional environment variables: SSL_CLIENT_CERT and -# SSL_SERVER_CERT. These contain the PEM-encoded certificates of the -# server (always existing) and the client (only existing when client -# authentication is used). This can be used to import the certificates -# into CGI scripts. -# o StdEnvVars: -# This exports the standard SSL/TLS related `SSL_*' environment variables. -# Per default this exportation is switched off for performance reasons, -# because the extraction step is an expensive operation and is usually -# useless for serving static content. So one usually enables the -# exportation for CGI and SSI requests only. -# o StrictRequire: -# This denies access when "SSLRequireSSL" or "SSLRequire" applied even -# under a "Satisfy any" situation, i.e. when it applies access is denied -# and no other module can change it. -# o OptRenegotiate: -# This enables optimized SSL connection renegotiation handling when SSL -# directives are used in per-directory context. -#SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire - - SSLOptions +StdEnvVars - - - SSLOptions +StdEnvVars - - -# SSL Protocol Adjustments: -# The safe and default but still SSL/TLS standard compliant shutdown -# approach is that mod_ssl sends the close notify alert but doesn't wait for -# the close notify alert from client. When you need a different shutdown -# approach you can use one of the following variables: -# o ssl-unclean-shutdown: -# This forces an unclean shutdown when the connection is closed, i.e. no -# SSL close notify alert is send or allowed to received. This violates -# the SSL/TLS standard but is needed for some brain-dead browsers. Use -# this when you receive I/O errors because of the standard approach where -# mod_ssl sends the close notify alert. -# o ssl-accurate-shutdown: -# This forces an accurate shutdown when the connection is closed, i.e. a -# SSL close notify alert is send and mod_ssl waits for the close notify -# alert of the client. This is 100% SSL/TLS standard compliant, but in -# practice often causes hanging connections with brain-dead browsers. Use -# this only for browsers where you know that their SSL implementation -# works correctly. -# Notice: Most problems of broken clients are also related to the HTTP -# keep-alive facility, so you usually additionally want to disable -# keep-alive for those clients, too. Use variable "nokeepalive" for this. -# Similarly, one has to force some clients to use HTTP/1.0 to workaround -# their broken HTTP/1.1 implementation. Use variables "downgrade-1.0" and -# "force-response-1.0" for this. -SetEnvIf User-Agent ".*MSIE.*" \ - nokeepalive ssl-unclean-shutdown \ - downgrade-1.0 force-response-1.0 - -# Per-Server Logging: -# The home of a custom SSL log file. Use this when you want a -# compact non-error SSL logfile on a virtual host basis. -CustomLog logs/ssl_request_log \ - "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b" - - - diff --git a/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/test.example.com.conf b/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/test.example.com.conf deleted file mode 100644 index 3dd7b18f1..000000000 --- a/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/test.example.com.conf +++ /dev/null @@ -1,7 +0,0 @@ - - ServerName test.example.com - ServerAdmin webmaster@dummy-host.example.com - DocumentRoot /var/www/htdocs - ErrorLog logs/dummy-host.example.com-error_log - CustomLog logs/dummy-host.example.com-access_log common - diff --git a/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/welcome.conf b/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/welcome.conf deleted file mode 100644 index c1d23c512..000000000 --- a/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf.d/welcome.conf +++ /dev/null @@ -1,11 +0,0 @@ -# -# This configuration file enables the default "Welcome" -# page if there is no default index page present for -# the root URL. To disable the Welcome page, comment -# out all the lines below. -# - - Options -Indexes - ErrorDocument 403 /error/noindex.html - - diff --git a/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf/httpd.conf b/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf/httpd.conf deleted file mode 100644 index eac6143da..000000000 --- a/certbot-apache/tests/testdata/centos6_apache/apache/httpd/conf/httpd.conf +++ /dev/null @@ -1,1009 +0,0 @@ -# -# This is the main Apache server configuration file. It contains the -# configuration directives that give the server its instructions. -# See for detailed information. -# In particular, see -# -# for a discussion of each configuration directive. -# -# -# Do NOT simply read the instructions in here without understanding -# what they do. They're here only as hints or reminders. If you are unsure -# consult the online docs. You have been warned. -# -# The configuration directives are grouped into three basic sections: -# 1. Directives that control the operation of the Apache server process as a -# whole (the 'global environment'). -# 2. Directives that define the parameters of the 'main' or 'default' server, -# which responds to requests that aren't handled by a virtual host. -# These directives also provide default values for the settings -# of all virtual hosts. -# 3. Settings for virtual hosts, which allow Web requests to be sent to -# different IP addresses or hostnames and have them handled by the -# same Apache server process. -# -# Configuration and logfile names: If the filenames you specify for many -# of the server's control files begin with "/" (or "drive:/" for Win32), the -# server will use that explicit path. If the filenames do *not* begin -# with "/", the value of ServerRoot is prepended -- so "logs/foo.log" -# with ServerRoot set to "/etc/httpd" will be interpreted by the -# server as "/etc/httpd/logs/foo.log". -# - -### Section 1: Global Environment -# -# The directives in this section affect the overall operation of Apache, -# such as the number of concurrent requests it can handle or where it -# can find its configuration files. -# - -# -# Don't give away too much information about all the subcomponents -# we are running. Comment out this line if you don't mind remote sites -# finding out what major optional modules you are running -ServerTokens OS - -# -# ServerRoot: The top of the directory tree under which the server's -# configuration, error, and log files are kept. -# -# NOTE! If you intend to place this on an NFS (or otherwise network) -# mounted filesystem then please read the LockFile documentation -# (available at ); -# you will save yourself a lot of trouble. -# -# Do NOT add a slash at the end of the directory path. -# -ServerRoot "/etc/httpd" - -# -# PidFile: The file in which the server should record its process -# identification number when it starts. Note the PIDFILE variable in -# /etc/sysconfig/httpd must be set appropriately if this location is -# changed. -# -PidFile run/httpd.pid - -# -# Timeout: The number of seconds before receives and sends time out. -# -Timeout 60 - -# -# KeepAlive: Whether or not to allow persistent connections (more than -# one request per connection). Set to "Off" to deactivate. -# -KeepAlive Off - -# -# MaxKeepAliveRequests: The maximum number of requests to allow -# during a persistent connection. Set to 0 to allow an unlimited amount. -# We recommend you leave this number high, for maximum performance. -# -MaxKeepAliveRequests 100 - -# -# KeepAliveTimeout: Number of seconds to wait for the next request from the -# same client on the same connection. -# -KeepAliveTimeout 15 - -## -## Server-Pool Size Regulation (MPM specific) -## - -# prefork MPM -# StartServers: number of server processes to start -# MinSpareServers: minimum number of server processes which are kept spare -# MaxSpareServers: maximum number of server processes which are kept spare -# ServerLimit: maximum value for MaxClients for the lifetime of the server -# MaxClients: maximum number of server processes allowed to start -# MaxRequestsPerChild: maximum number of requests a server process serves - -StartServers 8 -MinSpareServers 5 -MaxSpareServers 20 -ServerLimit 256 -MaxClients 256 -MaxRequestsPerChild 4000 - - -# worker MPM -# StartServers: initial number of server processes to start -# MaxClients: maximum number of simultaneous client connections -# MinSpareThreads: minimum number of worker threads which are kept spare -# MaxSpareThreads: maximum number of worker threads which are kept spare -# ThreadsPerChild: constant number of worker threads in each server process -# MaxRequestsPerChild: maximum number of requests a server process serves - -StartServers 4 -MaxClients 300 -MinSpareThreads 25 -MaxSpareThreads 75 -ThreadsPerChild 25 -MaxRequestsPerChild 0 - - -# -# Listen: Allows you to bind Apache to specific IP addresses and/or -# ports, in addition to the default. See also the -# directive. -# -# Change this to Listen on specific IP addresses as shown below to -# prevent Apache from glomming onto all bound IP addresses (0.0.0.0) -# -#Listen 12.34.56.78:80 -Listen 80 - -# -# Dynamic Shared Object (DSO) Support -# -# To be able to use the functionality of a module which was built as a DSO you -# have to place corresponding `LoadModule' lines at this location so the -# directives contained in it are actually available _before_ they are used. -# Statically compiled modules (those listed by `httpd -l') do not need -# to be loaded here. -# -# Example: -# LoadModule foo_module modules/mod_foo.so -# -LoadModule auth_basic_module modules/mod_auth_basic.so -LoadModule auth_digest_module modules/mod_auth_digest.so -LoadModule authn_file_module modules/mod_authn_file.so -LoadModule authn_alias_module modules/mod_authn_alias.so -LoadModule authn_anon_module modules/mod_authn_anon.so -LoadModule authn_dbm_module modules/mod_authn_dbm.so -LoadModule authn_default_module modules/mod_authn_default.so -LoadModule authz_host_module modules/mod_authz_host.so -LoadModule authz_user_module modules/mod_authz_user.so -LoadModule authz_owner_module modules/mod_authz_owner.so -LoadModule authz_groupfile_module modules/mod_authz_groupfile.so -LoadModule authz_dbm_module modules/mod_authz_dbm.so -LoadModule authz_default_module modules/mod_authz_default.so -LoadModule ldap_module modules/mod_ldap.so -LoadModule authnz_ldap_module modules/mod_authnz_ldap.so -LoadModule include_module modules/mod_include.so -LoadModule log_config_module modules/mod_log_config.so -LoadModule logio_module modules/mod_logio.so -LoadModule env_module modules/mod_env.so -LoadModule ext_filter_module modules/mod_ext_filter.so -LoadModule mime_magic_module modules/mod_mime_magic.so -LoadModule expires_module modules/mod_expires.so -LoadModule deflate_module modules/mod_deflate.so -LoadModule headers_module modules/mod_headers.so -LoadModule usertrack_module modules/mod_usertrack.so -LoadModule setenvif_module modules/mod_setenvif.so -LoadModule mime_module modules/mod_mime.so -LoadModule dav_module modules/mod_dav.so -LoadModule status_module modules/mod_status.so -LoadModule autoindex_module modules/mod_autoindex.so -LoadModule info_module modules/mod_info.so -LoadModule dav_fs_module modules/mod_dav_fs.so -LoadModule vhost_alias_module modules/mod_vhost_alias.so -LoadModule negotiation_module modules/mod_negotiation.so -LoadModule dir_module modules/mod_dir.so -LoadModule actions_module modules/mod_actions.so -LoadModule speling_module modules/mod_speling.so -LoadModule userdir_module modules/mod_userdir.so -LoadModule alias_module modules/mod_alias.so -LoadModule substitute_module modules/mod_substitute.so -LoadModule rewrite_module modules/mod_rewrite.so -LoadModule proxy_module modules/mod_proxy.so -LoadModule proxy_balancer_module modules/mod_proxy_balancer.so -LoadModule proxy_ftp_module modules/mod_proxy_ftp.so -LoadModule proxy_http_module modules/mod_proxy_http.so -LoadModule proxy_ajp_module modules/mod_proxy_ajp.so -LoadModule proxy_connect_module modules/mod_proxy_connect.so -LoadModule cache_module modules/mod_cache.so -LoadModule suexec_module modules/mod_suexec.so -LoadModule disk_cache_module modules/mod_disk_cache.so -LoadModule cgi_module modules/mod_cgi.so -LoadModule version_module modules/mod_version.so - -# -# The following modules are not loaded by default: -# -#LoadModule asis_module modules/mod_asis.so -#LoadModule authn_dbd_module modules/mod_authn_dbd.so -#LoadModule cern_meta_module modules/mod_cern_meta.so -#LoadModule cgid_module modules/mod_cgid.so -#LoadModule dbd_module modules/mod_dbd.so -#LoadModule dumpio_module modules/mod_dumpio.so -#LoadModule filter_module modules/mod_filter.so -#LoadModule ident_module modules/mod_ident.so -#LoadModule log_forensic_module modules/mod_log_forensic.so -#LoadModule unique_id_module modules/mod_unique_id.so -# - -# -# Load config files from the config directory "/etc/httpd/conf.d". -# -Include conf.d/*.conf - -# -# ExtendedStatus controls whether Apache will generate "full" status -# information (ExtendedStatus On) or just basic information (ExtendedStatus -# Off) when the "server-status" handler is called. The default is Off. -# -#ExtendedStatus On - -# -# If you wish httpd to run as a different user or group, you must run -# httpd as root initially and it will switch. -# -# User/Group: The name (or #number) of the user/group to run httpd as. -# . On SCO (ODT 3) use "User nouser" and "Group nogroup". -# . On HPUX you may not be able to use shared memory as nobody, and the -# suggested workaround is to create a user www and use that user. -# NOTE that some kernels refuse to setgid(Group) or semctl(IPC_SET) -# when the value of (unsigned)Group is above 60000; -# don't use Group #-1 on these systems! -# -User apache -Group apache - -### Section 2: 'Main' server configuration -# -# The directives in this section set up the values used by the 'main' -# server, which responds to any requests that aren't handled by a -# definition. These values also provide defaults for -# any containers you may define later in the file. -# -# All of these directives may appear inside containers, -# in which case these default settings will be overridden for the -# virtual host being defined. -# - -# -# ServerAdmin: Your address, where problems with the server should be -# e-mailed. This address appears on some server-generated pages, such -# as error documents. e.g. admin@your-domain.com -# -ServerAdmin root@localhost - -# -# ServerName gives the name and port that the server uses to identify itself. -# This can often be determined automatically, but we recommend you specify -# it explicitly to prevent problems during startup. -# -# If this is not set to valid DNS name for your host, server-generated -# redirections will not work. See also the UseCanonicalName directive. -# -# If your host doesn't have a registered DNS name, enter its IP address here. -# You will have to access it by its address anyway, and this will make -# redirections work in a sensible way. -# -#ServerName www.example.com:80 - -# -# UseCanonicalName: Determines how Apache constructs self-referencing -# URLs and the SERVER_NAME and SERVER_PORT variables. -# When set "Off", Apache will use the Hostname and Port supplied -# by the client. When set "On", Apache will use the value of the -# ServerName directive. -# -UseCanonicalName Off - -# -# DocumentRoot: The directory out of which you will serve your -# documents. By default, all requests are taken from this directory, but -# symbolic links and aliases may be used to point to other locations. -# -DocumentRoot "/var/www/html" - -# -# Each directory to which Apache has access can be configured with respect -# to which services and features are allowed and/or disabled in that -# directory (and its subdirectories). -# -# First, we configure the "default" to be a very restrictive set of -# features. -# - - Options FollowSymLinks - AllowOverride None - - -# -# Note that from this point forward you must specifically allow -# particular features to be enabled - so if something's not working as -# you might expect, make sure that you have specifically enabled it -# below. -# - -# -# This should be changed to whatever you set DocumentRoot to. -# - - -# -# Possible values for the Options directive are "None", "All", -# or any combination of: -# Indexes Includes FollowSymLinks SymLinksifOwnerMatch ExecCGI MultiViews -# -# Note that "MultiViews" must be named *explicitly* --- "Options All" -# doesn't give it to you. -# -# The Options directive is both complicated and important. Please see -# http://httpd.apache.org/docs/2.2/mod/core.html#options -# for more information. -# - Options Indexes FollowSymLinks - -# -# AllowOverride controls what directives may be placed in .htaccess files. -# It can be "All", "None", or any combination of the keywords: -# Options FileInfo AuthConfig Limit -# - AllowOverride None - -# -# Controls who can get stuff from this server. -# - Order allow,deny - Allow from all - - - -# -# UserDir: The name of the directory that is appended onto a user's home -# directory if a ~user request is received. -# -# The path to the end user account 'public_html' directory must be -# accessible to the webserver userid. This usually means that ~userid -# must have permissions of 711, ~userid/public_html must have permissions -# of 755, and documents contained therein must be world-readable. -# Otherwise, the client will only receive a "403 Forbidden" message. -# -# See also: http://httpd.apache.org/docs/misc/FAQ.html#forbidden -# - - # - # UserDir is disabled by default since it can confirm the presence - # of a username on the system (depending on home directory - # permissions). - # - UserDir disabled - - # - # To enable requests to /~user/ to serve the user's public_html - # directory, remove the "UserDir disabled" line above, and uncomment - # the following line instead: - # - #UserDir public_html - - - -# -# Control access to UserDir directories. The following is an example -# for a site where these directories are restricted to read-only. -# -# -# AllowOverride FileInfo AuthConfig Limit -# Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec -# -# Order allow,deny -# Allow from all -# -# -# Order deny,allow -# Deny from all -# -# - -# -# DirectoryIndex: sets the file that Apache will serve if a directory -# is requested. -# -# The index.html.var file (a type-map) is used to deliver content- -# negotiated documents. The MultiViews Option can be used for the -# same purpose, but it is much slower. -# -DirectoryIndex index.html index.html.var - -# -# AccessFileName: The name of the file to look for in each directory -# for additional configuration directives. See also the AllowOverride -# directive. -# -AccessFileName .htaccess - -# -# The following lines prevent .htaccess and .htpasswd files from being -# viewed by Web clients. -# - - Order allow,deny - Deny from all - Satisfy All - - -# -# TypesConfig describes where the mime.types file (or equivalent) is -# to be found. -# -TypesConfig /etc/mime.types - -# -# DefaultType is the default MIME type the server will use for a document -# if it cannot otherwise determine one, such as from filename extensions. -# If your server contains mostly text or HTML documents, "text/plain" is -# a good value. If most of your content is binary, such as applications -# or images, you may want to use "application/octet-stream" instead to -# keep browsers from trying to display binary files as though they are -# text. -# -DefaultType text/plain - -# -# The mod_mime_magic module allows the server to use various hints from the -# contents of the file itself to determine its type. The MIMEMagicFile -# directive tells the module where the hint definitions are located. -# - -# MIMEMagicFile /usr/share/magic.mime - MIMEMagicFile conf/magic - - -# -# HostnameLookups: Log the names of clients or just their IP addresses -# e.g., www.apache.org (on) or 204.62.129.132 (off). -# The default is off because it'd be overall better for the net if people -# had to knowingly turn this feature on, since enabling it means that -# each client request will result in AT LEAST one lookup request to the -# nameserver. -# -HostnameLookups Off - -# -# EnableMMAP: Control whether memory-mapping is used to deliver -# files (assuming that the underlying OS supports it). -# The default is on; turn this off if you serve from NFS-mounted -# filesystems. On some systems, turning it off (regardless of -# filesystem) can improve performance; for details, please see -# http://httpd.apache.org/docs/2.2/mod/core.html#enablemmap -# -#EnableMMAP off - -# -# EnableSendfile: Control whether the sendfile kernel support is -# used to deliver files (assuming that the OS supports it). -# The default is on; turn this off if you serve from NFS-mounted -# filesystems. Please see -# http://httpd.apache.org/docs/2.2/mod/core.html#enablesendfile -# -#EnableSendfile off - -# -# ErrorLog: The location of the error log file. -# If you do not specify an ErrorLog directive within a -# container, error messages relating to that virtual host will be -# logged here. If you *do* define an error logfile for a -# container, that host's errors will be logged there and not here. -# -ErrorLog logs/error_log - -# -# LogLevel: Control the number of messages logged to the error_log. -# Possible values include: debug, info, notice, warn, error, crit, -# alert, emerg. -# -LogLevel warn - -# -# The following directives define some format nicknames for use with -# a CustomLog directive (see below). -# -LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined -LogFormat "%h %l %u %t \"%r\" %>s %b" common -LogFormat "%{Referer}i -> %U" referer -LogFormat "%{User-agent}i" agent - -# "combinedio" includes actual counts of actual bytes received (%I) and sent (%O); this -# requires the mod_logio module to be loaded. -#LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio - -# -# The location and format of the access logfile (Common Logfile Format). -# If you do not define any access logfiles within a -# container, they will be logged here. Contrariwise, if you *do* -# define per- access logfiles, transactions will be -# logged therein and *not* in this file. -# -#CustomLog logs/access_log common - -# -# If you would like to have separate agent and referer logfiles, uncomment -# the following directives. -# -#CustomLog logs/referer_log referer -#CustomLog logs/agent_log agent - -# -# For a single logfile with access, agent, and referer information -# (Combined Logfile Format), use the following directive: -# -CustomLog logs/access_log combined - -# -# Optionally add a line containing the server version and virtual host -# name to server-generated pages (internal error documents, FTP directory -# listings, mod_status and mod_info output etc., but not CGI generated -# documents or custom error documents). -# Set to "EMail" to also include a mailto: link to the ServerAdmin. -# Set to one of: On | Off | EMail -# -ServerSignature On - -# -# Aliases: Add here as many aliases as you need (with no limit). The format is -# Alias fakename realname -# -# Note that if you include a trailing / on fakename then the server will -# require it to be present in the URL. So "/icons" isn't aliased in this -# example, only "/icons/". If the fakename is slash-terminated, then the -# realname must also be slash terminated, and if the fakename omits the -# trailing slash, the realname must also omit it. -# -# We include the /icons/ alias for FancyIndexed directory listings. If you -# do not use FancyIndexing, you may comment this out. -# -Alias /icons/ "/var/www/icons/" - - - Options Indexes MultiViews FollowSymLinks - AllowOverride None - Order allow,deny - Allow from all - - -# -# WebDAV module configuration section. -# - - # Location of the WebDAV lock database. - DAVLockDB /var/lib/dav/lockdb - - -# -# ScriptAlias: This controls which directories contain server scripts. -# ScriptAliases are essentially the same as Aliases, except that -# documents in the realname directory are treated as applications and -# run by the server when requested rather than as documents sent to the client. -# The same rules about trailing "/" apply to ScriptAlias directives as to -# Alias. -# -ScriptAlias /cgi-bin/ "/var/www/cgi-bin/" - -# -# "/var/www/cgi-bin" should be changed to whatever your ScriptAliased -# CGI directory exists, if you have that configured. -# - - AllowOverride None - Options None - Order allow,deny - Allow from all - - -# -# Redirect allows you to tell clients about documents which used to exist in -# your server's namespace, but do not anymore. This allows you to tell the -# clients where to look for the relocated document. -# Example: -# Redirect permanent /foo http://www.example.com/bar - -# -# Directives controlling the display of server-generated directory listings. -# - -# -# IndexOptions: Controls the appearance of server-generated directory -# listings. -# -IndexOptions FancyIndexing VersionSort NameWidth=* HTMLTable Charset=UTF-8 - -# -# AddIcon* directives tell the server which icon to show for different -# files or filename extensions. These are only displayed for -# FancyIndexed directories. -# -AddIconByEncoding (CMP,/icons/compressed.gif) x-compress x-gzip - -AddIconByType (TXT,/icons/text.gif) text/* -AddIconByType (IMG,/icons/image2.gif) image/* -AddIconByType (SND,/icons/sound2.gif) audio/* -AddIconByType (VID,/icons/movie.gif) video/* - -AddIcon /icons/binary.gif .bin .exe -AddIcon /icons/binhex.gif .hqx -AddIcon /icons/tar.gif .tar -AddIcon /icons/world2.gif .wrl .wrl.gz .vrml .vrm .iv -AddIcon /icons/compressed.gif .Z .z .tgz .gz .zip -AddIcon /icons/a.gif .ps .ai .eps -AddIcon /icons/layout.gif .html .shtml .htm .pdf -AddIcon /icons/text.gif .txt -AddIcon /icons/c.gif .c -AddIcon /icons/p.gif .pl .py -AddIcon /icons/f.gif .for -AddIcon /icons/dvi.gif .dvi -AddIcon /icons/uuencoded.gif .uu -AddIcon /icons/script.gif .conf .sh .shar .csh .ksh .tcl -AddIcon /icons/tex.gif .tex -AddIcon /icons/bomb.gif /core - -AddIcon /icons/back.gif .. -AddIcon /icons/hand.right.gif README -AddIcon /icons/folder.gif ^^DIRECTORY^^ -AddIcon /icons/blank.gif ^^BLANKICON^^ - -# -# DefaultIcon is which icon to show for files which do not have an icon -# explicitly set. -# -DefaultIcon /icons/unknown.gif - -# -# AddDescription allows you to place a short description after a file in -# server-generated indexes. These are only displayed for FancyIndexed -# directories. -# Format: AddDescription "description" filename -# -#AddDescription "GZIP compressed document" .gz -#AddDescription "tar archive" .tar -#AddDescription "GZIP compressed tar archive" .tgz - -# -# ReadmeName is the name of the README file the server will look for by -# default, and append to directory listings. -# -# HeaderName is the name of a file which should be prepended to -# directory indexes. -ReadmeName README.html -HeaderName HEADER.html - -# -# IndexIgnore is a set of filenames which directory indexing should ignore -# and not include in the listing. Shell-style wildcarding is permitted. -# -IndexIgnore .??* *~ *# HEADER* README* RCS CVS *,v *,t - -# -# DefaultLanguage and AddLanguage allows you to specify the language of -# a document. You can then use content negotiation to give a browser a -# file in a language the user can understand. -# -# Specify a default language. This means that all data -# going out without a specific language tag (see below) will -# be marked with this one. You probably do NOT want to set -# this unless you are sure it is correct for all cases. -# -# * It is generally better to not mark a page as -# * being a certain language than marking it with the wrong -# * language! -# -# DefaultLanguage nl -# -# Note 1: The suffix does not have to be the same as the language -# keyword --- those with documents in Polish (whose net-standard -# language code is pl) may wish to use "AddLanguage pl .po" to -# avoid the ambiguity with the common suffix for perl scripts. -# -# Note 2: The example entries below illustrate that in some cases -# the two character 'Language' abbreviation is not identical to -# the two character 'Country' code for its country, -# E.g. 'Danmark/dk' versus 'Danish/da'. -# -# Note 3: In the case of 'ltz' we violate the RFC by using a three char -# specifier. There is 'work in progress' to fix this and get -# the reference data for rfc1766 cleaned up. -# -# Catalan (ca) - Croatian (hr) - Czech (cs) - Danish (da) - Dutch (nl) -# English (en) - Esperanto (eo) - Estonian (et) - French (fr) - German (de) -# Greek-Modern (el) - Hebrew (he) - Italian (it) - Japanese (ja) -# Korean (ko) - Luxembourgeois* (ltz) - Norwegian Nynorsk (nn) -# Norwegian (no) - Polish (pl) - Portuguese (pt) -# Brazilian Portuguese (pt-BR) - Russian (ru) - Swedish (sv) -# Simplified Chinese (zh-CN) - Spanish (es) - Traditional Chinese (zh-TW) -# -AddLanguage ca .ca -AddLanguage cs .cz .cs -AddLanguage da .dk -AddLanguage de .de -AddLanguage el .el -AddLanguage en .en -AddLanguage eo .eo -AddLanguage es .es -AddLanguage et .et -AddLanguage fr .fr -AddLanguage he .he -AddLanguage hr .hr -AddLanguage it .it -AddLanguage ja .ja -AddLanguage ko .ko -AddLanguage ltz .ltz -AddLanguage nl .nl -AddLanguage nn .nn -AddLanguage no .no -AddLanguage pl .po -AddLanguage pt .pt -AddLanguage pt-BR .pt-br -AddLanguage ru .ru -AddLanguage sv .sv -AddLanguage zh-CN .zh-cn -AddLanguage zh-TW .zh-tw - -# -# LanguagePriority allows you to give precedence to some languages -# in case of a tie during content negotiation. -# -# Just list the languages in decreasing order of preference. We have -# more or less alphabetized them here. You probably want to change this. -# -LanguagePriority en ca cs da de el eo es et fr he hr it ja ko ltz nl nn no pl pt pt-BR ru sv zh-CN zh-TW - -# -# ForceLanguagePriority allows you to serve a result page rather than -# MULTIPLE CHOICES (Prefer) [in case of a tie] or NOT ACCEPTABLE (Fallback) -# [in case no accepted languages matched the available variants] -# -ForceLanguagePriority Prefer Fallback - -# -# Specify a default charset for all content served; this enables -# interpretation of all content as UTF-8 by default. To use the -# default browser choice (ISO-8859-1), or to allow the META tags -# in HTML content to override this choice, comment out this -# directive: -# -AddDefaultCharset UTF-8 - -# -# AddType allows you to add to or override the MIME configuration -# file mime.types for specific file types. -# -#AddType application/x-tar .tgz - -# -# AddEncoding allows you to have certain browsers uncompress -# information on the fly. Note: Not all browsers support this. -# Despite the name similarity, the following Add* directives have nothing -# to do with the FancyIndexing customization directives above. -# -#AddEncoding x-compress .Z -#AddEncoding x-gzip .gz .tgz - -# If the AddEncoding directives above are commented-out, then you -# probably should define those extensions to indicate media types: -# -AddType application/x-compress .Z -AddType application/x-gzip .gz .tgz - -# -# MIME-types for downloading Certificates and CRLs -# -AddType application/x-x509-ca-cert .crt -AddType application/x-pkcs7-crl .crl - -# -# AddHandler allows you to map certain file extensions to "handlers": -# actions unrelated to filetype. These can be either built into the server -# or added with the Action directive (see below) -# -# To use CGI scripts outside of ScriptAliased directories: -# (You will also need to add "ExecCGI" to the "Options" directive.) -# -#AddHandler cgi-script .cgi - -# -# For files that include their own HTTP headers: -# -#AddHandler send-as-is asis - -# -# For type maps (negotiated resources): -# (This is enabled by default to allow the Apache "It Worked" page -# to be distributed in multiple languages.) -# -AddHandler type-map var - -# -# Filters allow you to process content before it is sent to the client. -# -# To parse .shtml files for server-side includes (SSI): -# (You will also need to add "Includes" to the "Options" directive.) -# -AddType text/html .shtml -AddOutputFilter INCLUDES .shtml - -# -# Action lets you define media types that will execute a script whenever -# a matching file is called. This eliminates the need for repeated URL -# pathnames for oft-used CGI file processors. -# Format: Action media/type /cgi-script/location -# Format: Action handler-name /cgi-script/location -# - -# -# Customizable error responses come in three flavors: -# 1) plain text 2) local redirects 3) external redirects -# -# Some examples: -#ErrorDocument 500 "The server made a boo boo." -#ErrorDocument 404 /missing.html -#ErrorDocument 404 "/cgi-bin/missing_handler.pl" -#ErrorDocument 402 http://www.example.com/subscription_info.html -# - -# -# Putting this all together, we can internationalize error responses. -# -# We use Alias to redirect any /error/HTTP_.html.var response to -# our collection of by-error message multi-language collections. We use -# includes to substitute the appropriate text. -# -# You can modify the messages' appearance without changing any of the -# default HTTP_.html.var files by adding the line: -# -# Alias /error/include/ "/your/include/path/" -# -# which allows you to create your own set of files by starting with the -# /var/www/error/include/ files and -# copying them to /your/include/path/, even on a per-VirtualHost basis. -# - -Alias /error/ "/var/www/error/" - - - - - AllowOverride None - Options IncludesNoExec - AddOutputFilter Includes html - AddHandler type-map var - Order allow,deny - Allow from all - LanguagePriority en es de fr - ForceLanguagePriority Prefer Fallback - - -# ErrorDocument 400 /error/HTTP_BAD_REQUEST.html.var -# ErrorDocument 401 /error/HTTP_UNAUTHORIZED.html.var -# ErrorDocument 403 /error/HTTP_FORBIDDEN.html.var -# ErrorDocument 404 /error/HTTP_NOT_FOUND.html.var -# ErrorDocument 405 /error/HTTP_METHOD_NOT_ALLOWED.html.var -# ErrorDocument 408 /error/HTTP_REQUEST_TIME_OUT.html.var -# ErrorDocument 410 /error/HTTP_GONE.html.var -# ErrorDocument 411 /error/HTTP_LENGTH_REQUIRED.html.var -# ErrorDocument 412 /error/HTTP_PRECONDITION_FAILED.html.var -# ErrorDocument 413 /error/HTTP_REQUEST_ENTITY_TOO_LARGE.html.var -# ErrorDocument 414 /error/HTTP_REQUEST_URI_TOO_LARGE.html.var -# ErrorDocument 415 /error/HTTP_UNSUPPORTED_MEDIA_TYPE.html.var -# ErrorDocument 500 /error/HTTP_INTERNAL_SERVER_ERROR.html.var -# ErrorDocument 501 /error/HTTP_NOT_IMPLEMENTED.html.var -# ErrorDocument 502 /error/HTTP_BAD_GATEWAY.html.var -# ErrorDocument 503 /error/HTTP_SERVICE_UNAVAILABLE.html.var -# ErrorDocument 506 /error/HTTP_VARIANT_ALSO_VARIES.html.var - - - - -# -# The following directives modify normal HTTP response behavior to -# handle known problems with browser implementations. -# -BrowserMatch "Mozilla/2" nokeepalive -BrowserMatch "MSIE 4\.0b2;" nokeepalive downgrade-1.0 force-response-1.0 -BrowserMatch "RealPlayer 4\.0" force-response-1.0 -BrowserMatch "Java/1\.0" force-response-1.0 -BrowserMatch "JDK/1\.0" force-response-1.0 - -# -# The following directive disables redirects on non-GET requests for -# a directory that does not include the trailing slash. This fixes a -# problem with Microsoft WebFolders which does not appropriately handle -# redirects for folders with DAV methods. -# Same deal with Apple's DAV filesystem and Gnome VFS support for DAV. -# -BrowserMatch "Microsoft Data Access Internet Publishing Provider" redirect-carefully -BrowserMatch "MS FrontPage" redirect-carefully -BrowserMatch "^WebDrive" redirect-carefully -BrowserMatch "^WebDAVFS/1.[0123]" redirect-carefully -BrowserMatch "^gnome-vfs/1.0" redirect-carefully -BrowserMatch "^XML Spy" redirect-carefully -BrowserMatch "^Dreamweaver-WebDAV-SCM1" redirect-carefully - -# -# Allow server status reports generated by mod_status, -# with the URL of http://servername/server-status -# Change the ".example.com" to match your domain to enable. -# -# -# SetHandler server-status -# Order deny,allow -# Deny from all -# Allow from .example.com -# - -# -# Allow remote server configuration reports, with the URL of -# http://servername/server-info (requires that mod_info.c be loaded). -# Change the ".example.com" to match your domain to enable. -# -# -# SetHandler server-info -# Order deny,allow -# Deny from all -# Allow from .example.com -# - -# -# Proxy Server directives. Uncomment the following lines to -# enable the proxy server: -# -# -#ProxyRequests On -# -# -# Order deny,allow -# Deny from all -# Allow from .example.com -# - -# -# Enable/disable the handling of HTTP/1.1 "Via:" headers. -# ("Full" adds the server version; "Block" removes all outgoing Via: headers) -# Set to one of: Off | On | Full | Block -# -#ProxyVia On - -# -# To enable a cache of proxied content, uncomment the following lines. -# See http://httpd.apache.org/docs/2.2/mod/mod_cache.html for more details. -# -# -# CacheEnable disk / -# CacheRoot "/var/cache/mod_proxy" -# -# - -# -# End of proxy directives. - -### Section 3: Virtual Hosts -# -# VirtualHost: If you want to maintain multiple domains/hostnames on your -# machine you can setup VirtualHost containers for them. Most configurations -# use only name-based virtual hosts so the server doesn't need to worry about -# IP addresses. This is indicated by the asterisks in the directives below. -# -# Please see the documentation at -# -# for further details before you try to setup virtual hosts. -# -# You may use the command line option '-S' to verify your virtual host -# configuration. - -# -# Use name-based virtual hosting. -# -#NameVirtualHost *:80 -# -# NOTE: NameVirtualHost cannot be used without a port specifier -# (e.g. :80) if mod_ssl is being used, due to the nature of the -# SSL protocol. -# - -# -# VirtualHost example: -# Almost any Apache directive may go into a VirtualHost container. -# The first VirtualHost section is used for requests without a known -# server name. -# -# -# ServerAdmin webmaster@dummy-host.example.com -# DocumentRoot /www/docs/dummy-host.example.com -# ServerName dummy-host.example.com -# ErrorLog logs/dummy-host.example.com-error_log -# CustomLog logs/dummy-host.example.com-access_log common -# diff --git a/certbot-apache/tests/util.py b/certbot-apache/tests/util.py index 2f119938b..7cea90f25 100644 --- a/certbot-apache/tests/util.py +++ b/certbot-apache/tests/util.py @@ -4,11 +4,7 @@ import unittest import augeas import josepy as jose - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore +from unittest import mock from certbot.compat import os from certbot.plugins import common diff --git a/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py b/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py index 3563b30af..a1e814405 100644 --- a/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py +++ b/certbot-ci/certbot_integration_tests/certbot_tests/assertions.py @@ -33,8 +33,8 @@ def assert_elliptic_key(key: str, curve: Type[EllipticCurve]) -> None: key = load_pem_private_key(data=privkey1, password=None, backend=default_backend()) - assert isinstance(key, EllipticCurvePrivateKey) - assert isinstance(key.curve, curve) + assert isinstance(key, EllipticCurvePrivateKey), f"should be an EC key but was {type(key)}" + assert isinstance(key.curve, curve), f"should have curve {curve} but was {key.curve}" def assert_rsa_key(key: str, key_size: Optional[int] = None) -> None: diff --git a/certbot-ci/certbot_integration_tests/certbot_tests/context.py b/certbot-ci/certbot_integration_tests/certbot_tests/context.py index 0dc732880..6b7407e50 100644 --- a/certbot-ci/certbot_integration_tests/certbot_tests/context.py +++ b/certbot-ci/certbot_integration_tests/certbot_tests/context.py @@ -17,8 +17,8 @@ class IntegrationTestsContext: self.request = request if hasattr(request.config, 'workerinput'): # Worker node - self.worker_id = request.config.workerinput['workerid'] # type: ignore[attr-defined] - acme_xdist = request.config.workerinput['acme_xdist'] # type: ignore[attr-defined] + self.worker_id = request.config.workerinput['workerid'] + acme_xdist = request.config.workerinput['acme_xdist'] else: # Primary node self.worker_id = 'primary' acme_xdist = request.config.acme_xdist # type: ignore[attr-defined] diff --git a/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py b/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py index 7bef646be..65eca976d 100644 --- a/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py +++ b/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py @@ -112,7 +112,7 @@ def test_http_01(context: IntegrationTestsContext) -> None: assert_hook_execution(context.hook_probe, 'deploy') assert_saved_renew_hook(context.config_dir, certname) - assert_saved_lineage_option(context.config_dir, certname, 'key_type', 'rsa') + assert_saved_lineage_option(context.config_dir, certname, 'key_type', 'ecdsa') def test_manual_http_auth(context: IntegrationTestsContext) -> None: @@ -315,23 +315,23 @@ def test_graceful_renew_it_is_time(context: IntegrationTestsContext) -> None: def test_renew_with_changed_private_key_complexity(context: IntegrationTestsContext) -> None: """Test proper renew with updated private key complexity.""" certname = context.get_domain('renew') - context.certbot(['-d', certname, '--rsa-key-size', '4096']) + context.certbot(['-d', certname, '--key-type', 'rsa', '--rsa-key-size', '4096']) key1 = join(context.config_dir, 'archive', certname, 'privkey1.pem') - assert os.stat(key1).st_size > 3000 # 4096 bits keys takes more than 3000 bytes + assert_rsa_key(key1, 4096) assert_cert_count_for_lineage(context.config_dir, certname, 1) context.certbot(['renew']) assert_cert_count_for_lineage(context.config_dir, certname, 2) key2 = join(context.config_dir, 'archive', certname, 'privkey2.pem') - assert os.stat(key2).st_size > 3000 + assert_rsa_key(key2, 4096) context.certbot(['renew', '--rsa-key-size', '2048']) assert_cert_count_for_lineage(context.config_dir, certname, 3) key3 = join(context.config_dir, 'archive', certname, 'privkey3.pem') - assert os.stat(key3).st_size < 1800 # 2048 bits keys takes less than 1800 bytes + assert_rsa_key(key3, 2048) def test_renew_ignoring_directory_hooks(context: IntegrationTestsContext) -> None: @@ -482,7 +482,7 @@ def test_new_key(context: IntegrationTestsContext) -> None: certname = context.get_domain('newkey') context.certbot(['--domains', certname, '--reuse-key', - '--key-type', 'rsa', '--rsa-key-size', '4096']) + '--key-type', 'ecdsa', '--elliptic-curve', 'secp384r1']) privkey1, _ = private_key(1) # renew: --new-key should replace the key, but keep reuse_key and the key type + params @@ -490,22 +490,34 @@ def test_new_key(context: IntegrationTestsContext) -> None: privkey2, privkey2_path = private_key(2) assert privkey1 != privkey2 assert_saved_lineage_option(context.config_dir, certname, 'reuse_key', 'True') - assert_rsa_key(privkey2_path, 4096) + assert_elliptic_key(privkey2_path, SECP384R1) - # certonly: it should replace the key but the key size will change + # certonly: it should replace the key but the elliptic curve will change context.certbot(['certonly', '-d', certname, '--reuse-key', '--new-key']) privkey3, privkey3_path = private_key(3) assert privkey2 != privkey3 assert_saved_lineage_option(context.config_dir, certname, 'reuse_key', 'True') - assert_rsa_key(privkey3_path, 2048) + assert_elliptic_key(privkey3_path, SECP256R1) # certonly: it should be possible to change the key type and keep reuse_key - context.certbot(['certonly', '-d', certname, '--reuse-key', '--new-key', '--key-type', 'ecdsa', - '--cert-name', certname]) + context.certbot(['certonly', '-d', certname, '--reuse-key', '--new-key', '--key-type', 'rsa', + '--rsa-key-size', '4096', '--cert-name', certname]) privkey4, privkey4_path = private_key(4) assert privkey3 != privkey4 assert_saved_lineage_option(context.config_dir, certname, 'reuse_key', 'True') - assert_elliptic_key(privkey4_path, SECP256R1) + assert_rsa_key(privkey4_path, 4096) + + # certonly: it should not be possible to change a key parameter without --new-key + with pytest.raises(subprocess.CalledProcessError) as error: + context.certbot(['certonly', '-d', certname, '--key-type', 'rsa', '--reuse-key', + '--rsa-key-size', '2048']) + assert 'Unable to change the --rsa-key-size' in error.value.stderr + + # certonly: not specifying --key-type should keep the existing key type (non-interactively). + context.certbot(['certonly', '-d', certname, '--no-reuse-key']) + privkey5, privkey5_path = private_key(5) + assert_rsa_key(privkey5_path, 2048) + assert privkey4 != privkey5 def test_incorrect_key_type(context: IntegrationTestsContext) -> None: @@ -535,24 +547,24 @@ def test_ecdsa(context: IntegrationTestsContext) -> None: def test_default_key_type(context: IntegrationTestsContext) -> None: - """Test default key type is RSA""" + """Test default key type is ECDSA""" certname = context.get_domain('renew') context.certbot([ 'certonly', '--cert-name', certname, '-d', certname ]) filename = join(context.config_dir, 'archive/{0}/privkey1.pem').format(certname) - assert_rsa_key(filename) + assert_elliptic_key(filename, SECP256R1) -def test_default_curve_type(context: IntegrationTestsContext) -> None: - """test that the curve used when not specifying any is secp256r1""" +def test_default_rsa_size(context: IntegrationTestsContext) -> None: + """test that the RSA key size used when not specifying any is 2048""" certname = context.get_domain('renew') context.certbot([ - '--key-type', 'ecdsa', '--cert-name', certname, '-d', certname + '--key-type', 'rsa', '--cert-name', certname, '-d', certname ]) key1 = join(context.config_dir, 'archive/{0}/privkey1.pem'.format(certname)) - assert_elliptic_key(key1, SECP256R1) + assert_rsa_key(key1, 2048) @pytest.mark.parametrize('curve,curve_cls,skip_servers', [ diff --git a/certbot-ci/certbot_integration_tests/rfc2136_tests/context.py b/certbot-ci/certbot_integration_tests/rfc2136_tests/context.py index dde41f367..4a383fe56 100644 --- a/certbot-ci/certbot_integration_tests/rfc2136_tests/context.py +++ b/certbot-ci/certbot_integration_tests/rfc2136_tests/context.py @@ -21,7 +21,7 @@ class IntegrationTestsContext(certbot_context.IntegrationTestsContext): self.request = request if hasattr(request.config, 'workerinput'): # Worker node - self._dns_xdist = request.config.workerinput['dns_xdist'] # type: ignore[attr-defined] + self._dns_xdist = request.config.workerinput['dns_xdist'] else: # Primary node self._dns_xdist = request.config.dns_xdist # type: ignore[attr-defined] diff --git a/certbot-ci/certbot_integration_tests/utils/acme_server.py b/certbot-ci/certbot_integration_tests/utils/acme_server.py index c12057cd4..ecd7fe778 100755 --- a/certbot-ci/certbot_integration_tests/utils/acme_server.py +++ b/certbot-ci/certbot_integration_tests/utils/acme_server.py @@ -219,7 +219,8 @@ class ACMEServer: # Configure challtestsrv to answer any A record request with ip of the docker host. response = requests.post( f'{BOULDER_V2_CHALLTESTSRV_URL}/set-default-ipv4', - json={'ip': '10.77.77.1'} + json={'ip': '10.77.77.1'}, + timeout=10 ) response.raise_for_status() except BaseException: diff --git a/certbot-ci/certbot_integration_tests/utils/dns_server.py b/certbot-ci/certbot_integration_tests/utils/dns_server.py index ed5b6ccae..8ec024bb3 100644 --- a/certbot-ci/certbot_integration_tests/utils/dns_server.py +++ b/certbot-ci/certbot_integration_tests/utils/dns_server.py @@ -70,7 +70,7 @@ class DNSServer: try: self.process.terminate() self.process.wait(constants.MAX_SUBPROCESS_WAIT) - except BaseException as e: + except BaseException as e: # pylint: disable=broad-except print("BIND9 did not stop cleanly: {}".format(e), file=sys.stderr) shutil.rmtree(self.bind_root, ignore_errors=True) diff --git a/certbot-ci/certbot_integration_tests/utils/misc.py b/certbot-ci/certbot_integration_tests/utils/misc.py index a491ce9ba..f21f7492a 100644 --- a/certbot-ci/certbot_integration_tests/utils/misc.py +++ b/certbot-ci/certbot_integration_tests/utils/misc.py @@ -65,9 +65,9 @@ def check_until_timeout(url: str, attempts: int = 30) -> None: for _ in range(attempts): time.sleep(1) try: - if requests.get(url, verify=False).status_code == 200: + if requests.get(url, verify=False, timeout=10).status_code == 200: return - except requests.exceptions.ConnectionError: + except requests.exceptions.RequestException: pass raise ValueError('Error, url did not respond after {0} attempts: {1}'.format(attempts, url)) @@ -331,7 +331,9 @@ def get_acme_issuers(context: IntegrationTestsContext) -> List[Certificate]: issuers = [] for i in range(PEBBLE_ALTERNATE_ROOTS + 1): - request = requests.get(PEBBLE_MANAGEMENT_URL + '/intermediates/{}'.format(i), verify=False) + request = requests.get(PEBBLE_MANAGEMENT_URL + '/intermediates/{}'.format(i), + verify=False, + timeout=10) issuers.append(load_pem_x509_certificate(request.content, default_backend())) return issuers diff --git a/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py b/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py index 8600be5e6..9cd8ddbbe 100644 --- a/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py +++ b/certbot-ci/certbot_integration_tests/utils/pebble_artifacts.py @@ -31,7 +31,7 @@ def _fetch_asset(asset: str, suffix: str) -> str: if not os.path.exists(asset_path): asset_url = ('https://github.com/letsencrypt/pebble/releases/download/{0}/{1}_{2}' .format(PEBBLE_VERSION, asset, suffix)) - response = requests.get(asset_url) + response = requests.get(asset_url, timeout=30) response.raise_for_status() with open(asset_path, 'wb') as file_h: file_h.write(response.content) diff --git a/certbot-ci/certbot_integration_tests/utils/pebble_ocsp_server.py b/certbot-ci/certbot_integration_tests/utils/pebble_ocsp_server.py index 6fe966ac6..e9c8acd49 100755 --- a/certbot-ci/certbot_integration_tests/utils/pebble_ocsp_server.py +++ b/certbot-ci/certbot_integration_tests/utils/pebble_ocsp_server.py @@ -23,10 +23,12 @@ from certbot_integration_tests.utils.misc import GracefulTCPServer class _ProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler): # pylint: disable=missing-function-docstring def do_POST(self) -> None: - request = requests.get(PEBBLE_MANAGEMENT_URL + '/intermediate-keys/0', verify=False) + request = requests.get(PEBBLE_MANAGEMENT_URL + '/intermediate-keys/0', + verify=False, timeout=10) issuer_key = serialization.load_pem_private_key(request.content, None, default_backend()) - request = requests.get(PEBBLE_MANAGEMENT_URL + '/intermediates/0', verify=False) + request = requests.get(PEBBLE_MANAGEMENT_URL + '/intermediates/0', + verify=False, timeout=10) issuer_cert = x509.load_pem_x509_certificate(request.content, default_backend()) content_len = int(self.headers.get('Content-Length')) @@ -34,7 +36,7 @@ class _ProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler): ocsp_request = ocsp.load_der_ocsp_request(self.rfile.read(content_len)) response = requests.get('{0}/cert-status-by-serial/{1}'.format( PEBBLE_MANAGEMENT_URL, str(hex(ocsp_request.serial_number)).replace('0x', '')), - verify=False + verify=False, timeout=10 ) if not response.ok: diff --git a/certbot-ci/certbot_integration_tests/utils/proxy.py b/certbot-ci/certbot_integration_tests/utils/proxy.py index 3f8e099cf..ba53381e6 100644 --- a/certbot-ci/certbot_integration_tests/utils/proxy.py +++ b/certbot-ci/certbot_integration_tests/utils/proxy.py @@ -21,7 +21,7 @@ def _create_proxy(mapping: Mapping[str, str]) -> Type[BaseHTTPServer.BaseHTTPReq headers = {key.lower(): value for key, value in self.headers.items()} backend = [backend for pattern, backend in mapping.items() if re.match(pattern, headers['host'])][0] - response = requests.get(backend + self.path, headers=headers) + response = requests.get(backend + self.path, headers=headers, timeout=10) self.send_response(response.status_code) for key, value in response.headers.items(): diff --git a/certbot-ci/setup.py b/certbot-ci/setup.py index 728c6c707..ec632a471 100644 --- a/certbot-ci/setup.py +++ b/certbot-ci/setup.py @@ -50,6 +50,7 @@ setup( 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', ], diff --git a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py index 26b7660ab..6a29e74f5 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/test_driver.py +++ b/certbot-compatibility-test/certbot_compatibility_test/test_driver.py @@ -33,7 +33,6 @@ from acme import messages from certbot import achallenges from certbot import errors as le_errors from certbot._internal.display import obj as display_obj -from certbot.display import util as display_util from certbot.tests import acme_util DESCRIPTION = """ @@ -339,7 +338,7 @@ def setup_logging(args: argparse.Namespace) -> None: def setup_display() -> None: """"Prepares a display utility instance for the Certbot plugins """ - displayer = display_util.NoninteractiveDisplay(sys.stdout) + displayer = display_obj.NoninteractiveDisplay(sys.stdout) display_obj.set_display(displayer) diff --git a/certbot-compatibility-test/certbot_compatibility_test/validator.py b/certbot-compatibility-test/certbot_compatibility_test/validator.py index 1da38ef41..da333e8c5 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/validator.py +++ b/certbot-compatibility-test/certbot_compatibility_test/validator.py @@ -15,6 +15,9 @@ from acme import errors as acme_errors logger = logging.getLogger(__name__) +_VALIDATION_TIMEOUT = 10 + + class Validator: """Collection of functions to test a live webserver's configuration""" @@ -43,9 +46,12 @@ class Validator: """Test whether webserver redirects to secure connection.""" url = "http://{0}:{1}".format(name, port) if headers: - response = requests.get(url, headers=headers, allow_redirects=False) + response = requests.get(url, headers=headers, + allow_redirects=False, + timeout=_VALIDATION_TIMEOUT) else: - response = requests.get(url, allow_redirects=False) + response = requests.get(url, allow_redirects=False, + timeout=_VALIDATION_TIMEOUT) redirect_location = response.headers.get("location", "") # We're checking that the redirect we added behaves correctly. @@ -65,15 +71,19 @@ class Validator: """Test whether webserver redirects.""" url = "http://{0}:{1}".format(name, port) if headers: - response = requests.get(url, headers=headers, allow_redirects=False) + response = requests.get(url, headers=headers, + allow_redirects=False, + timeout=_VALIDATION_TIMEOUT) else: - response = requests.get(url, allow_redirects=False) + response = requests.get(url, allow_redirects=False, + timeout=_VALIDATION_TIMEOUT) return response.status_code in range(300, 309) def hsts(self, name: str) -> bool: """Test for HTTP Strict Transport Security header""" - headers = requests.get("https://" + name).headers + headers = requests.get("https://" + name, + timeout=_VALIDATION_TIMEOUT).headers hsts_header = headers.get("strict-transport-security") if not hsts_header: diff --git a/certbot-compatibility-test/setup.py b/certbot-compatibility-test/setup.py index 238906efb..c30edd54a 100644 --- a/certbot-compatibility-test/setup.py +++ b/certbot-compatibility-test/setup.py @@ -1,7 +1,7 @@ from setuptools import find_packages from setuptools import setup -version = '2.0.0.dev0' +version = '2.1.0.dev0' install_requires = [ 'certbot', @@ -29,6 +29,7 @@ setup( 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', ], diff --git a/certbot-dns-cloudflare/setup.py b/certbot-dns-cloudflare/setup.py index 65eb11399..ea2f28adc 100644 --- a/certbot-dns-cloudflare/setup.py +++ b/certbot-dns-cloudflare/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '2.0.0.dev0' +version = '2.1.0.dev0' install_requires = [ 'cloudflare>=1.5.1', @@ -51,6 +51,7 @@ setup( 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-cloudflare/tests/dns_cloudflare_test.py b/certbot-dns-cloudflare/tests/dns_cloudflare_test.py index 2b1827831..cd73adc8f 100644 --- a/certbot-dns-cloudflare/tests/dns_cloudflare_test.py +++ b/certbot-dns-cloudflare/tests/dns_cloudflare_test.py @@ -1,12 +1,9 @@ """Tests for certbot_dns_cloudflare._internal.dns_cloudflare.""" import unittest +from unittest import mock import CloudFlare -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore from certbot import errors from certbot.compat import os diff --git a/certbot-dns-cloudxns/LICENSE.txt b/certbot-dns-cloudxns/LICENSE.txt deleted file mode 100644 index 981c46c9f..000000000 --- a/certbot-dns-cloudxns/LICENSE.txt +++ /dev/null @@ -1,190 +0,0 @@ - Copyright 2015 Electronic Frontier Foundation and others - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS diff --git a/certbot-dns-cloudxns/MANIFEST.in b/certbot-dns-cloudxns/MANIFEST.in deleted file mode 100644 index 20810e7eb..000000000 --- a/certbot-dns-cloudxns/MANIFEST.in +++ /dev/null @@ -1,7 +0,0 @@ -include LICENSE.txt -include README.rst -recursive-include docs * -recursive-include tests * -include certbot_dns_cloudxns/py.typed -global-exclude __pycache__ -global-exclude *.py[cod] diff --git a/certbot-dns-cloudxns/README.rst b/certbot-dns-cloudxns/README.rst deleted file mode 100644 index b127770df..000000000 --- a/certbot-dns-cloudxns/README.rst +++ /dev/null @@ -1 +0,0 @@ -CloudXNS DNS Authenticator plugin for Certbot diff --git a/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py b/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py deleted file mode 100644 index 0ba512ee4..000000000 --- a/certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -.. danger:: - The certbot-dns-cloudxns plugin is deprecated and will be removed in the next major - release of Certbot. The CloudXNS DNS service is defunct and we recommend uninstalling - the plugin. - ----------- - -The `~certbot_dns_cloudxns.dns_cloudxns` plugin automates the process of -completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and -subsequently removing, TXT records using the CloudXNS API. - -.. note:: - The plugin is not installed by default. It can be installed by heading to - `certbot.eff.org `_, choosing your system and - selecting the Wildcard tab. - -Named Arguments ---------------- - -======================================== ===================================== -``--dns-cloudxns-credentials`` CloudXNS credentials_ INI file. - (Required) -``--dns-cloudxns-propagation-seconds`` The number of seconds to wait for DNS - to propagate before asking the ACME - server to verify the DNS record. - (Default: 30) -======================================== ===================================== - - -Credentials ------------ - -Use of this plugin requires a configuration file containing CloudXNS API -credentials, obtained from your CloudXNS -`API page `_. - -.. code-block:: ini - :name: credentials.ini - :caption: Example credentials file: - - # CloudXNS API credentials used by Certbot - dns_cloudxns_api_key = 1234567890abcdef1234567890abcdef - dns_cloudxns_secret_key = 1122334455667788 - -The path to this file can be provided interactively or using the -``--dns-cloudxns-credentials`` command-line argument. Certbot records the path -to this file for use during renewal, but does not store the file's contents. - -.. caution:: - You should protect these API credentials as you would the password to your - CloudXNS account. Users who can read this file can use these credentials to - issue arbitrary API calls on your behalf. Users who can cause Certbot to run - using these credentials can complete a ``dns-01`` challenge to acquire new - certificates or revoke existing certificates for associated domains, even if - those domains aren't being managed by this server. - -Certbot will emit a warning if it detects that the credentials file can be -accessed by other users on your system. The warning reads "Unsafe permissions -on credentials configuration file", followed by the path to the credentials -file. This warning will be emitted each time Certbot uses the credentials file, -including for renewal, and cannot be silenced except by addressing the issue -(e.g., by using a command like ``chmod 600`` to restrict access to the file). - - -Examples --------- - -.. code-block:: bash - :caption: To acquire a certificate for ``example.com`` - - certbot certonly \\ - --dns-cloudxns \\ - --dns-cloudxns-credentials ~/.secrets/certbot/cloudxns.ini \\ - -d example.com - -.. code-block:: bash - :caption: To acquire a single certificate for both ``example.com`` and - ``www.example.com`` - - certbot certonly \\ - --dns-cloudxns \\ - --dns-cloudxns-credentials ~/.secrets/certbot/cloudxns.ini \\ - -d example.com \\ - -d www.example.com - -.. code-block:: bash - :caption: To acquire a certificate for ``example.com``, waiting 60 seconds - for DNS propagation - - certbot certonly \\ - --dns-cloudxns \\ - --dns-cloudxns-credentials ~/.secrets/certbot/cloudxns.ini \\ - --dns-cloudxns-propagation-seconds 60 \\ - -d example.com - -""" diff --git a/certbot-dns-cloudxns/certbot_dns_cloudxns/_internal/__init__.py b/certbot-dns-cloudxns/certbot_dns_cloudxns/_internal/__init__.py deleted file mode 100644 index e2177417d..000000000 --- a/certbot-dns-cloudxns/certbot_dns_cloudxns/_internal/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Internal implementation of `~certbot_dns_cloudxns.dns_cloudxns` plugin.""" diff --git a/certbot-dns-cloudxns/certbot_dns_cloudxns/_internal/dns_cloudxns.py b/certbot-dns-cloudxns/certbot_dns_cloudxns/_internal/dns_cloudxns.py deleted file mode 100644 index 743e8567e..000000000 --- a/certbot-dns-cloudxns/certbot_dns_cloudxns/_internal/dns_cloudxns.py +++ /dev/null @@ -1,99 +0,0 @@ -"""DNS Authenticator for CloudXNS DNS.""" -import logging -from typing import Any -from typing import Callable -from typing import Optional -import warnings - -from lexicon.providers import cloudxns -from requests import HTTPError - -from certbot import errors -from certbot.plugins import dns_common -from certbot.plugins import dns_common_lexicon -from certbot.plugins.dns_common import CredentialsConfiguration - -logger = logging.getLogger(__name__) - -ACCOUNT_URL = 'https://www.cloudxns.net/en/AccountManage/apimanage.html' - - -class Authenticator(dns_common.DNSAuthenticator): - """DNS Authenticator for CloudXNS DNS - - This Authenticator uses the CloudXNS DNS API to fulfill a dns-01 challenge. - """ - - description = 'Obtain certificates using a DNS TXT record (if you are using CloudXNS for DNS).' - ttl = 60 - - def __init__(self, *args: Any, **kwargs: Any) -> None: - warnings.warn( - "The CloudXNS authenticator is deprecated and will be removed in the " - "next major release of Certbot. The CloudXNS DNS service is defunct and " - "we recommend removing the plugin." - ) - super().__init__(*args, **kwargs) - self.credentials: Optional[CredentialsConfiguration] = None - - @classmethod - def add_parser_arguments(cls, add: Callable[..., None], - default_propagation_seconds: int = 30) -> None: - super().add_parser_arguments(add, default_propagation_seconds) - add('credentials', help='CloudXNS credentials INI file.') - - def more_info(self) -> str: - return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \ - 'the CloudXNS API.' - - def _setup_credentials(self) -> None: - self.credentials = self._configure_credentials( - 'credentials', - 'CloudXNS credentials INI file', - { - 'api-key': 'API key for CloudXNS account, obtained from {0}'.format(ACCOUNT_URL), - 'secret-key': 'Secret key for CloudXNS account, obtained from {0}' - .format(ACCOUNT_URL) - } - ) - - def _perform(self, domain: str, validation_name: str, validation: str) -> None: - self._get_cloudxns_client().add_txt_record(domain, validation_name, validation) - - def _cleanup(self, domain: str, validation_name: str, validation: str) -> None: - self._get_cloudxns_client().del_txt_record(domain, validation_name, validation) - - def _get_cloudxns_client(self) -> "_CloudXNSLexiconClient": - if not self.credentials: # pragma: no cover - raise errors.Error("Plugin has not been prepared.") - return _CloudXNSLexiconClient(self.credentials.conf('api-key'), - self.credentials.conf('secret-key'), - self.ttl) - - -class _CloudXNSLexiconClient(dns_common_lexicon.LexiconClient): - """ - Encapsulates all communication with the CloudXNS via Lexicon. - """ - - def __init__(self, api_key: str, secret_key: str, ttl: int) -> None: - super().__init__() - - config = dns_common_lexicon.build_lexicon_config('cloudxns', { - 'ttl': ttl, - }, { - 'auth_username': api_key, - 'auth_token': secret_key, - }) - - self.provider = cloudxns.Provider(config) - - def _handle_http_error(self, e: HTTPError, domain_name: str) -> Optional[errors.PluginError]: - hint = None - if str(e).startswith('400 Client Error:'): - hint = 'Are your API key and Secret key values correct?' - - hint_disp = f' ({hint})' if hint else '' - - return errors.PluginError(f'Error determining zone identifier for {domain_name}: ' - f'{e}.{hint_disp}') diff --git a/certbot-dns-cloudxns/certbot_dns_cloudxns/py.typed b/certbot-dns-cloudxns/certbot_dns_cloudxns/py.typed deleted file mode 100644 index e69de29bb..000000000 diff --git a/certbot-dns-cloudxns/docs/.gitignore b/certbot-dns-cloudxns/docs/.gitignore deleted file mode 100644 index ba65b13af..000000000 --- a/certbot-dns-cloudxns/docs/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/_build/ diff --git a/certbot-dns-cloudxns/docs/Makefile b/certbot-dns-cloudxns/docs/Makefile deleted file mode 100644 index ecda13dfe..000000000 --- a/certbot-dns-cloudxns/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SPHINXPROJ = certbot-dns-cloudxns -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/certbot-dns-cloudxns/docs/api.rst b/certbot-dns-cloudxns/docs/api.rst deleted file mode 100644 index ac13c3df2..000000000 --- a/certbot-dns-cloudxns/docs/api.rst +++ /dev/null @@ -1,5 +0,0 @@ -================= -API Documentation -================= - -Certbot plugins implement the Certbot plugins API, and do not otherwise have an external API. diff --git a/certbot-dns-cloudxns/docs/conf.py b/certbot-dns-cloudxns/docs/conf.py deleted file mode 100644 index f516d9a1e..000000000 --- a/certbot-dns-cloudxns/docs/conf.py +++ /dev/null @@ -1,181 +0,0 @@ -# -*- coding: utf-8 -*- -# -# certbot-dns-cloudxns documentation build configuration file, created by -# sphinx-quickstart on Wed May 10 16:05:50 2017. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -import os - -# import sys -# sys.path.insert(0, os.path.abspath('.')) - - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -# -needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.viewcode'] - -autodoc_member_order = 'bysource' -autodoc_default_flags = ['show-inheritance'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'certbot-dns-cloudxns' -copyright = u'2017, Certbot Project' -author = u'Certbot Project' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = u'0' -# The full version, including alpha/beta/rc tags. -release = u'0' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = 'en' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -default_role = 'py:obj' - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# - -# https://docs.readthedocs.io/en/stable/faq.html#i-want-to-use-the-read-the-docs-theme-locally -# on_rtd is whether we are on readthedocs.org -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' -if not on_rtd: # only import and set the theme if we're building docs locally - import sphinx_rtd_theme - html_theme = 'sphinx_rtd_theme' - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] -# otherwise, readthedocs.org uses their theme by default, so no need to specify it - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -#html_static_path = ['_static'] - - -# -- Options for HTMLHelp output ------------------------------------------ - -# Output file base name for HTML help builder. -htmlhelp_basename = 'certbot-dns-cloudxnsdoc' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'certbot-dns-cloudxns.tex', u'certbot-dns-cloudxns Documentation', - u'Certbot Project', 'manual'), -] - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'certbot-dns-cloudxns', u'certbot-dns-cloudxns Documentation', - [author], 1) -] - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'certbot-dns-cloudxns', u'certbot-dns-cloudxns Documentation', - author, 'certbot-dns-cloudxns', 'One line description of project.', - 'Miscellaneous'), -] - - - - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = { - 'python': ('https://docs.python.org/', None), - 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), - 'certbot': ('https://eff-certbot.readthedocs.io/en/stable/', None), -} diff --git a/certbot-dns-cloudxns/docs/index.rst b/certbot-dns-cloudxns/docs/index.rst deleted file mode 100644 index 83c6ca18d..000000000 --- a/certbot-dns-cloudxns/docs/index.rst +++ /dev/null @@ -1,28 +0,0 @@ -.. certbot-dns-cloudxns documentation master file, created by - sphinx-quickstart on Wed May 10 16:05:50 2017. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to certbot-dns-cloudxns's documentation! -================================================ - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - -.. automodule:: certbot_dns_cloudxns - :members: - -.. toctree:: - :maxdepth: 1 - - api - - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/certbot-dns-cloudxns/docs/make.bat b/certbot-dns-cloudxns/docs/make.bat deleted file mode 100644 index dddd6db56..000000000 --- a/certbot-dns-cloudxns/docs/make.bat +++ /dev/null @@ -1,36 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build -set SPHINXPROJ=certbot-dns-cloudxns - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% - -:end -popd diff --git a/certbot-dns-cloudxns/readthedocs.org.requirements.txt b/certbot-dns-cloudxns/readthedocs.org.requirements.txt deleted file mode 100644 index c1754a936..000000000 --- a/certbot-dns-cloudxns/readthedocs.org.requirements.txt +++ /dev/null @@ -1,15 +0,0 @@ -# readthedocs.org gives no way to change the install command to "pip -# install -e certbot-dns-cloudxns[docs]" (that would in turn install documentation -# dependencies), but it allows to specify a requirements.txt file at -# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259) - -# Although ReadTheDocs certainly doesn't need to install the project -# in --editable mode (-e), just "pip install certbot-dns-cloudxns[docs]" does not work as -# expected and "pip install -e certbot-dns-cloudxns[docs]" must be used instead - -# We also pin our dependencies for increased stability. - --c ../tools/requirements.txt --e acme --e certbot --e certbot-dns-cloudxns[docs] diff --git a/certbot-dns-cloudxns/setup.py b/certbot-dns-cloudxns/setup.py deleted file mode 100644 index b56f3b40d..000000000 --- a/certbot-dns-cloudxns/setup.py +++ /dev/null @@ -1,73 +0,0 @@ -import os -import sys - -from setuptools import find_packages -from setuptools import setup - -version = '2.0.0.dev0' - -install_requires = [ - 'dns-lexicon>=3.2.1', - 'setuptools>=41.6.0', -] - -if not os.environ.get('SNAP_BUILD'): - install_requires.extend([ - # We specify the minimum acme and certbot version as the current plugin - # version for simplicity. See - # https://github.com/certbot/certbot/issues/8761 for more info. - f'acme>={version}', - f'certbot>={version}', - ]) -elif 'bdist_wheel' in sys.argv[1:]: - raise RuntimeError('Unset SNAP_BUILD when building wheels ' - 'to include certbot dependencies.') -if os.environ.get('SNAP_BUILD'): - install_requires.append('packaging') - -docs_extras = [ - 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags - 'sphinx_rtd_theme', -] - -setup( - name='certbot-dns-cloudxns', - version=version, - description="CloudXNS DNS Authenticator plugin for Certbot", - url='https://github.com/certbot/certbot', - author="Certbot Project", - author_email='certbot-dev@eff.org', - license='Apache License 2.0', - python_requires='>=3.7', - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Plugins', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: POSIX :: Linux', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Security', - 'Topic :: System :: Installation/Setup', - 'Topic :: System :: Networking', - 'Topic :: System :: Systems Administration', - 'Topic :: Utilities', - ], - - packages=find_packages(), - include_package_data=True, - install_requires=install_requires, - extras_require={ - 'docs': docs_extras, - }, - entry_points={ - 'certbot.plugins': [ - 'dns-cloudxns = certbot_dns_cloudxns._internal.dns_cloudxns:Authenticator', - ], - }, -) diff --git a/certbot-dns-cloudxns/tests/dns_cloudxns_test.py b/certbot-dns-cloudxns/tests/dns_cloudxns_test.py deleted file mode 100644 index 62cb2c28b..000000000 --- a/certbot-dns-cloudxns/tests/dns_cloudxns_test.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Tests for certbot_dns_cloudxns._internal.dns_cloudxns.""" - -import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore -from requests.exceptions import HTTPError -from requests.exceptions import RequestException -import warnings - -from certbot.compat import os -from certbot.plugins import dns_test_common -from certbot.plugins import dns_test_common_lexicon -from certbot.tests import util as test_util - -DOMAIN_NOT_FOUND = Exception('No domain found') -GENERIC_ERROR = RequestException -LOGIN_ERROR = HTTPError('400 Client Error: ...') - -API_KEY = 'foo' -SECRET = 'bar' - - -class AuthenticatorTest(test_util.TempDirTestCase, - dns_test_common_lexicon.BaseLexiconAuthenticatorTest): - - def setUp(self): - super().setUp() - - from certbot_dns_cloudxns._internal.dns_cloudxns import Authenticator - - path = os.path.join(self.tempdir, 'file.ini') - dns_test_common.write({"cloudxns_api_key": API_KEY, "cloudxns_secret_key": SECRET}, path) - - self.config = mock.MagicMock(cloudxns_credentials=path, - cloudxns_propagation_seconds=0) # don't wait during tests - - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=UserWarning) - self.auth = Authenticator(self.config, "cloudxns") - - self.mock_client = mock.MagicMock() - # _get_cloudxns_client | pylint: disable=protected-access - self.auth._get_cloudxns_client = mock.MagicMock(return_value=self.mock_client) - - -class CloudXNSLexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest): - - def setUp(self): - from certbot_dns_cloudxns._internal.dns_cloudxns import _CloudXNSLexiconClient - - self.client = _CloudXNSLexiconClient(API_KEY, SECRET, 0) - - self.provider_mock = mock.MagicMock() - self.client.provider = self.provider_mock - - -if __name__ == "__main__": - unittest.main() # pragma: no cover diff --git a/certbot-dns-digitalocean/setup.py b/certbot-dns-digitalocean/setup.py index a51077afe..400837616 100644 --- a/certbot-dns-digitalocean/setup.py +++ b/certbot-dns-digitalocean/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '2.0.0.dev0' +version = '2.1.0.dev0' install_requires = [ 'python-digitalocean>=1.11', # 1.15.0 or newer is recommended for TTL support @@ -51,6 +51,7 @@ setup( 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-digitalocean/tests/dns_digitalocean_test.py b/certbot-dns-digitalocean/tests/dns_digitalocean_test.py index 4683893e8..8fdee38f3 100644 --- a/certbot-dns-digitalocean/tests/dns_digitalocean_test.py +++ b/certbot-dns-digitalocean/tests/dns_digitalocean_test.py @@ -1,12 +1,9 @@ """Tests for certbot_dns_digitalocean._internal.dns_digitalocean.""" import unittest +from unittest import mock import digitalocean -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore from certbot import errors from certbot.compat import os diff --git a/certbot-dns-dnsimple/setup.py b/certbot-dns-dnsimple/setup.py index 6ad861554..2f466592a 100644 --- a/certbot-dns-dnsimple/setup.py +++ b/certbot-dns-dnsimple/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '2.0.0.dev0' +version = '2.1.0.dev0' install_requires = [ # This version of lexicon is required to address the problem described in @@ -53,6 +53,7 @@ setup( 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-dnsimple/tests/dns_dnsimple_test.py b/certbot-dns-dnsimple/tests/dns_dnsimple_test.py index fc3dc5b1f..0e28f43b2 100644 --- a/certbot-dns-dnsimple/tests/dns_dnsimple_test.py +++ b/certbot-dns-dnsimple/tests/dns_dnsimple_test.py @@ -1,11 +1,8 @@ """Tests for certbot_dns_dnsimple._internal.dns_dnsimple.""" import unittest +from unittest import mock -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore from requests.exceptions import HTTPError from certbot.compat import os diff --git a/certbot-dns-dnsmadeeasy/setup.py b/certbot-dns-dnsmadeeasy/setup.py index e1b4989c3..44a507eae 100644 --- a/certbot-dns-dnsmadeeasy/setup.py +++ b/certbot-dns-dnsmadeeasy/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '2.0.0.dev0' +version = '2.1.0.dev0' install_requires = [ 'dns-lexicon>=3.2.1', @@ -51,6 +51,7 @@ setup( 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-dnsmadeeasy/tests/dns_dnsmadeeasy_test.py b/certbot-dns-dnsmadeeasy/tests/dns_dnsmadeeasy_test.py index a04716d95..46f5895a8 100644 --- a/certbot-dns-dnsmadeeasy/tests/dns_dnsmadeeasy_test.py +++ b/certbot-dns-dnsmadeeasy/tests/dns_dnsmadeeasy_test.py @@ -1,11 +1,8 @@ """Tests for certbot_dns_dnsmadeeasy._internal.dns_dnsmadeeasy.""" import unittest +from unittest import mock -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore from requests.exceptions import HTTPError from certbot.compat import os diff --git a/certbot-dns-gehirn/setup.py b/certbot-dns-gehirn/setup.py index 0acad3dba..3557e5600 100644 --- a/certbot-dns-gehirn/setup.py +++ b/certbot-dns-gehirn/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '2.0.0.dev0' +version = '2.1.0.dev0' install_requires = [ 'dns-lexicon>=3.2.1', @@ -51,6 +51,7 @@ setup( 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-gehirn/tests/dns_gehirn_test.py b/certbot-dns-gehirn/tests/dns_gehirn_test.py index 1310f74ca..b982e3e1b 100644 --- a/certbot-dns-gehirn/tests/dns_gehirn_test.py +++ b/certbot-dns-gehirn/tests/dns_gehirn_test.py @@ -1,11 +1,8 @@ """Tests for certbot_dns_gehirn._internal.dns_gehirn.""" import unittest +from unittest import mock -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore from requests.exceptions import HTTPError from certbot.compat import os diff --git a/certbot-dns-google/setup.py b/certbot-dns-google/setup.py index 86b11fe23..dd1d29500 100644 --- a/certbot-dns-google/setup.py +++ b/certbot-dns-google/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '2.0.0.dev0' +version = '2.1.0.dev0' install_requires = [ 'google-api-python-client>=1.5.5', @@ -54,6 +54,7 @@ setup( 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-google/tests/dns_google_test.py b/certbot-dns-google/tests/dns_google_test.py index b6f63a937..27e8b1a65 100644 --- a/certbot-dns-google/tests/dns_google_test.py +++ b/certbot-dns-google/tests/dns_google_test.py @@ -6,10 +6,8 @@ from googleapiclient import discovery from googleapiclient.errors import Error from googleapiclient.http import HttpMock from httplib2 import ServerNotFoundError -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore + +from unittest import mock from certbot import errors from certbot.compat import os diff --git a/certbot-dns-linode/setup.py b/certbot-dns-linode/setup.py index 51cae0ab6..f81b3babc 100644 --- a/certbot-dns-linode/setup.py +++ b/certbot-dns-linode/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '2.0.0.dev0' +version = '2.1.0.dev0' install_requires = [ 'dns-lexicon>=3.2.1', @@ -51,6 +51,7 @@ setup( 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-linode/tests/dns_linode_test.py b/certbot-dns-linode/tests/dns_linode_test.py index d0d6ceb03..c227ef4b5 100644 --- a/certbot-dns-linode/tests/dns_linode_test.py +++ b/certbot-dns-linode/tests/dns_linode_test.py @@ -1,11 +1,7 @@ """Tests for certbot_dns_linode._internal.dns_linode.""" import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore +from unittest import mock from certbot import errors from certbot.compat import os diff --git a/certbot-dns-luadns/setup.py b/certbot-dns-luadns/setup.py index cc3cce7ae..58c178a98 100644 --- a/certbot-dns-luadns/setup.py +++ b/certbot-dns-luadns/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '2.0.0.dev0' +version = '2.1.0.dev0' install_requires = [ 'dns-lexicon>=3.2.1', @@ -51,6 +51,7 @@ setup( 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-luadns/tests/dns_luadns_test.py b/certbot-dns-luadns/tests/dns_luadns_test.py index 7592e2323..3c1ac6841 100644 --- a/certbot-dns-luadns/tests/dns_luadns_test.py +++ b/certbot-dns-luadns/tests/dns_luadns_test.py @@ -1,11 +1,8 @@ """Tests for certbot_dns_luadns._internal.dns_luadns.""" import unittest +from unittest import mock -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore from requests.exceptions import HTTPError from certbot.compat import os diff --git a/certbot-dns-nsone/setup.py b/certbot-dns-nsone/setup.py index aef590629..e5959b798 100644 --- a/certbot-dns-nsone/setup.py +++ b/certbot-dns-nsone/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '2.0.0.dev0' +version = '2.1.0.dev0' install_requires = [ 'dns-lexicon>=3.2.1', @@ -51,6 +51,7 @@ setup( 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-nsone/tests/dns_nsone_test.py b/certbot-dns-nsone/tests/dns_nsone_test.py index 3754f9811..13ea09b3d 100644 --- a/certbot-dns-nsone/tests/dns_nsone_test.py +++ b/certbot-dns-nsone/tests/dns_nsone_test.py @@ -1,11 +1,8 @@ """Tests for certbot_dns_nsone._internal.dns_nsone.""" import unittest +from unittest import mock -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore from requests.exceptions import HTTPError from certbot.compat import os diff --git a/certbot-dns-ovh/setup.py b/certbot-dns-ovh/setup.py index 1d19ceed4..1b9330bfd 100644 --- a/certbot-dns-ovh/setup.py +++ b/certbot-dns-ovh/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '2.0.0.dev0' +version = '2.1.0.dev0' install_requires = [ 'dns-lexicon>=3.2.1', @@ -51,6 +51,7 @@ setup( 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-ovh/tests/dns_ovh_test.py b/certbot-dns-ovh/tests/dns_ovh_test.py index 7f93967eb..7eb767b70 100644 --- a/certbot-dns-ovh/tests/dns_ovh_test.py +++ b/certbot-dns-ovh/tests/dns_ovh_test.py @@ -1,11 +1,8 @@ """Tests for certbot_dns_ovh._internal.dns_ovh.""" import unittest +from unittest import mock -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore from requests.exceptions import HTTPError from certbot.compat import os diff --git a/certbot-dns-rfc2136/setup.py b/certbot-dns-rfc2136/setup.py index 0c3283ed4..d68f79c84 100644 --- a/certbot-dns-rfc2136/setup.py +++ b/certbot-dns-rfc2136/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '2.0.0.dev0' +version = '2.1.0.dev0' install_requires = [ 'dnspython>=1.15.0', @@ -51,6 +51,7 @@ setup( 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-rfc2136/tests/dns_rfc2136_test.py b/certbot-dns-rfc2136/tests/dns_rfc2136_test.py index d0434aef5..1f91d3cb6 100644 --- a/certbot-dns-rfc2136/tests/dns_rfc2136_test.py +++ b/certbot-dns-rfc2136/tests/dns_rfc2136_test.py @@ -1,14 +1,11 @@ """Tests for certbot_dns_rfc2136._internal.dns_rfc2136.""" import unittest +from unittest import mock import dns.flags import dns.rcode import dns.tsig -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore from certbot import errors from certbot.compat import os diff --git a/certbot-dns-route53/setup.py b/certbot-dns-route53/setup.py index 51b1a5824..8930e9166 100644 --- a/certbot-dns-route53/setup.py +++ b/certbot-dns-route53/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '2.0.0.dev0' +version = '2.1.0.dev0' install_requires = [ 'boto3>=1.15.15', @@ -51,6 +51,7 @@ setup( 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-route53/tests/dns_route53_test.py b/certbot-dns-route53/tests/dns_route53_test.py index 69b6b115d..bdc70e048 100644 --- a/certbot-dns-route53/tests/dns_route53_test.py +++ b/certbot-dns-route53/tests/dns_route53_test.py @@ -1,13 +1,10 @@ """Tests for certbot_dns_route53._internal.dns_route53.Authenticator""" import unittest +from unittest import mock from botocore.exceptions import ClientError from botocore.exceptions import NoCredentialsError -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore from certbot import errors from certbot.compat import os diff --git a/certbot-dns-sakuracloud/setup.py b/certbot-dns-sakuracloud/setup.py index d7dda2406..e857bf7c8 100644 --- a/certbot-dns-sakuracloud/setup.py +++ b/certbot-dns-sakuracloud/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import find_packages from setuptools import setup -version = '2.0.0.dev0' +version = '2.1.0.dev0' install_requires = [ 'dns-lexicon>=3.2.1', @@ -51,6 +51,7 @@ setup( 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-dns-sakuracloud/tests/dns_sakuracloud_test.py b/certbot-dns-sakuracloud/tests/dns_sakuracloud_test.py index 1c64df372..a1abf7b78 100644 --- a/certbot-dns-sakuracloud/tests/dns_sakuracloud_test.py +++ b/certbot-dns-sakuracloud/tests/dns_sakuracloud_test.py @@ -1,11 +1,8 @@ """Tests for certbot_dns_sakuracloud._internal.dns_sakuracloud.""" import unittest +from unittest import mock -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore from requests.exceptions import HTTPError from certbot.compat import os diff --git a/certbot-nginx/certbot_nginx/_internal/configurator.py b/certbot-nginx/certbot_nginx/_internal/configurator.py index 39784110c..b91b11a53 100644 --- a/certbot-nginx/certbot_nginx/_internal/configurator.py +++ b/certbot-nginx/certbot_nginx/_internal/configurator.py @@ -268,10 +268,12 @@ class NginxConfigurator(common.Configurator): """Prompts user to choose vhosts to install a wildcard certificate for""" if prefer_ssl: vhosts_cache = self._wildcard_vhosts - preference_test = lambda x: x.ssl + def preference_test(x: obj.VirtualHost) -> bool: + return x.ssl else: vhosts_cache = self._wildcard_redirect_vhosts - preference_test = lambda x: not x.ssl + def preference_test(x: obj.VirtualHost) -> bool: + return not x.ssl # Caching! if domain in vhosts_cache: @@ -609,8 +611,9 @@ class NginxConfigurator(common.Configurator): # if we want ssl vhosts: either 'ssl on' or 'addr.ssl' should be enabled # if we want plaintext vhosts: neither 'ssl on' nor 'addr.ssl' should be enabled - _ssl_matches = lambda addr: addr.ssl or all_addrs_are_ssl if ssl else \ - not addr.ssl and not all_addrs_are_ssl + def _ssl_matches(addr: obj.Addr) -> bool: + return addr.ssl or all_addrs_are_ssl if ssl else \ + not addr.ssl and not all_addrs_are_ssl # if there are no listen directives at all, Nginx defaults to # listening on port 80. @@ -1057,8 +1060,8 @@ class NginxConfigurator(common.Configurator): product_name, product_version = version_matches[0] if product_name != 'nginx': - logger.warning("NGINX derivative %s is not officially supported by" - " certbot", product_name) + logger.warning("nginx derivative %s is not officially supported by " + "Certbot.", product_name) nginx_version = tuple(int(i) for i in product_version.split(".")) @@ -1112,7 +1115,7 @@ class NginxConfigurator(common.Configurator): ################################################### # Wrapper functions for Reverter class (Installer) ################################################### - def save(self, title: str = None, temporary: bool = False) -> None: + def save(self, title: Optional[str] = None, temporary: bool = False) -> None: """Saves all changes to the configuration files. :param str title: The title of the save. If a title is given, the diff --git a/certbot-nginx/certbot_nginx/_internal/nginxparser.py b/certbot-nginx/certbot_nginx/_internal/nginxparser.py index 874a544f7..99955447a 100644 --- a/certbot-nginx/certbot_nginx/_internal/nginxparser.py +++ b/certbot-nginx/certbot_nginx/_internal/nginxparser.py @@ -33,6 +33,18 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) +class UnsupportedDirectiveException(RuntimeError): + """Exception when encountering an nginx directive which is not supported + by this parser.""" + + directive_name: str + line_no: int + + def __init__(self, directive_name: str, line_no: int) -> None: + self.directive_name = directive_name + self.line_no = line_no + + class RawNginxParser: # pylint: disable=pointless-statement """A class that parses nginx configuration with pyparsing.""" @@ -74,6 +86,7 @@ class RawNginxParser: def __init__(self, source: str) -> None: self.source = source + self.whitespace_token_group.addParseAction(self._check_disallowed_directive) def parse(self) -> ParseResults: """Returns the parsed tree.""" @@ -83,6 +96,12 @@ class RawNginxParser: """Returns the parsed tree as a list.""" return self.parse().asList() + def _check_disallowed_directive(self, _source: str, line: int, results: ParseResults) -> None: + # *_by_lua_block might be first or second result, due to optional leading whitespace + toks = [t for t in results[0:2] if isinstance(t, str) and t.endswith("_by_lua_block")] + if toks: + raise UnsupportedDirectiveException(toks[0], line) + class RawNginxDumper: """A class that dumps nginx configuration from the provided tree.""" @@ -119,7 +138,9 @@ class RawNginxDumper: return ''.join(self) -spacey = lambda x: (isinstance(x, str) and x.isspace()) or x == '' +def spacey(x: Any) -> bool: + """Is x an empty string or whitespace?""" + return (isinstance(x, str) and x.isspace()) or x == '' class UnspacedList(List[Any]): diff --git a/certbot-nginx/certbot_nginx/_internal/parser.py b/certbot-nginx/certbot_nginx/_internal/parser.py index e60d66eb2..bc1643426 100644 --- a/certbot-nginx/certbot_nginx/_internal/parser.py +++ b/certbot-nginx/certbot_nginx/_internal/parser.py @@ -223,6 +223,12 @@ class NginxParser: "supported.", item) except pyparsing.ParseException as err: logger.warning("Could not parse file: %s due to %s", item, err) + except nginxparser.UnsupportedDirectiveException as e: + logger.warning( + "%s:%d contained the '%s' directive, which is not supported by Certbot. The " + "file has been ignored, which may prevent Certbot from functioning properly. " + "Consider using the --webroot plugin and manually installing the certificate.", + item, e.line_no, e.directive_name) return trees def _find_config_root(self) -> str: diff --git a/certbot-nginx/setup.py b/certbot-nginx/setup.py index d3d294367..34cb30bb9 100644 --- a/certbot-nginx/setup.py +++ b/certbot-nginx/setup.py @@ -1,7 +1,7 @@ from setuptools import find_packages from setuptools import setup -version = '2.0.0.dev0' +version = '2.1.0.dev0' install_requires = [ # We specify the minimum acme and certbot version as the current plugin @@ -35,6 +35,7 @@ setup( 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot-nginx/tests/configurator_test.py b/certbot-nginx/tests/configurator_test.py index a182f789a..916dfe3f5 100644 --- a/certbot-nginx/tests/configurator_test.py +++ b/certbot-nginx/tests/configurator_test.py @@ -1,10 +1,7 @@ """Test for certbot_nginx._internal.configurator.""" import unittest +from unittest import mock -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore import OpenSSL from acme import challenges diff --git a/certbot-nginx/tests/http_01_test.py b/certbot-nginx/tests/http_01_test.py index b9917af35..05be06202 100644 --- a/certbot-nginx/tests/http_01_test.py +++ b/certbot-nginx/tests/http_01_test.py @@ -1,11 +1,8 @@ """Tests for certbot_nginx._internal.http_01""" import unittest +from unittest import mock import josepy as jose -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore from acme import challenges from certbot import achallenges diff --git a/certbot-nginx/tests/nginxparser_test.py b/certbot-nginx/tests/nginxparser_test.py index d8f0a5909..3713f16e4 100644 --- a/certbot-nginx/tests/nginxparser_test.py +++ b/certbot-nginx/tests/nginxparser_test.py @@ -12,6 +12,7 @@ from certbot_nginx._internal.nginxparser import load from certbot_nginx._internal.nginxparser import loads from certbot_nginx._internal.nginxparser import RawNginxParser from certbot_nginx._internal.nginxparser import UnspacedList +from certbot_nginx._internal.nginxparser import UnsupportedDirectiveException import test_util as util FIRST = operator.itemgetter(0) @@ -354,6 +355,69 @@ class TestRawNginxParser(unittest.TestCase): parsed = loads("") self.assertEqual(parsed, []) + def test_lua(self): + # https://github.com/certbot/certbot/issues/9066 + self.assertRaises(UnsupportedDirectiveException, loads, """ + location /foo { + content_by_lua_block { + ngx.say('Hello World') + } + } + """) + + # Without leading whitespace + self.assertRaises(UnsupportedDirectiveException, loads, """ + location /foo {content_by_lua_block { + ngx.say('Hello World') + } + } + """) + + # Doesn't trigger if it's commented or not in the right position. + parsed = loads(""" + location /foo {server_name content_by_lua_block; + #content_by_lua_block { + # ngx.say('Hello World') + # } + } + """) + self.assertEqual( + parsed, + [ + [['location', '/foo'], + [['server_name', 'content_by_lua_block'], + ['#', 'content_by_lua_block {'], + ['#', " ngx.say('Hello World')"], + ['#', ' }'] + ]] + ]) + + # *_by_lua should parse successfully. + parsed = loads(""" + location / { + set $a 32; + set $b 56; + set_by_lua $sum + 'return tonumber(ngx.arg[1]) + tonumber(ngx.arg[2])' + $a $b; + content_by_lua ' + ngx.say("foo"); + '; + } + """) + self.assertEqual( + parsed, + [ + [['location', '/'], + [['set', '$a', '32'], + ['set', '$b', '56'], + ['set_by_lua', '$sum', + "'return tonumber(ngx.arg[1]) + tonumber(ngx.arg[2])'", '$a', '$b' + ], + ['content_by_lua', '\'\n ngx.say("foo");\n \''] + ]] + ] + ) class TestUnspacedList(unittest.TestCase): """Test the UnspacedList data structure""" diff --git a/certbot-nginx/tests/parser_obj_test.py b/certbot-nginx/tests/parser_obj_test.py index 4d1f25277..60ff1c975 100644 --- a/certbot-nginx/tests/parser_obj_test.py +++ b/certbot-nginx/tests/parser_obj_test.py @@ -1,11 +1,7 @@ """ Tests for functions and classes in parser_obj.py """ import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore +from unittest import mock from certbot_nginx._internal.parser_obj import COMMENT_BLOCK from certbot_nginx._internal.parser_obj import parse_raw diff --git a/certbot-nginx/tests/parser_test.py b/certbot-nginx/tests/parser_test.py index 7d5b5e669..93a8dcedc 100644 --- a/certbot-nginx/tests/parser_test.py +++ b/certbot-nginx/tests/parser_test.py @@ -4,6 +4,7 @@ import re import shutil import unittest from typing import List +from unittest import mock from certbot import errors from certbot.compat import os @@ -532,6 +533,14 @@ class NginxParserTest(util.NginxTest): for output in log.output )) + @mock.patch('certbot_nginx._internal.parser.logger.warning') + def test_load_unsupported_directive_logged(self, mock_warn): + nparser = parser.NginxParser(self.config_path) + nparser.config_root = nparser.abs_path('unsupported_directives.conf') + nparser.load() + self.assertEqual(mock_warn.call_count, 1) + self.assertIn("which is not supported by Certbot", mock_warn.call_args[0][0]) + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/certbot-nginx/tests/test_util.py b/certbot-nginx/tests/test_util.py index 6cc701f42..1ac649318 100644 --- a/certbot-nginx/tests/test_util.py +++ b/certbot-nginx/tests/test_util.py @@ -4,10 +4,7 @@ import shutil import tempfile import josepy as jose -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore +from unittest import mock import pkg_resources from certbot import util diff --git a/certbot-nginx/tests/testdata/etc_nginx/unsupported_directives.conf b/certbot-nginx/tests/testdata/etc_nginx/unsupported_directives.conf new file mode 100644 index 000000000..d071c68c1 --- /dev/null +++ b/certbot-nginx/tests/testdata/etc_nginx/unsupported_directives.conf @@ -0,0 +1,11 @@ +# This configuration file contains unsupported direcives. + +server { + listen 80; + + location / { + foobar_by_lua_block { + ngx.say("Hello World") + } + } +} diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md index f19ebb57f..2afddb300 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -2,7 +2,7 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). -## 2.0.0 - master +## 2.1.0 - master ### Added @@ -14,7 +14,14 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). ### Fixed -* +* Interfaces which plugins register themselves as implementing without inheriting from them now show up in `certbot plugins` output. +* `IPluginFactory`, `IPlugin`, `IAuthenticator` and `IInstaller` have been re-added to + `certbot.interfaces`. + - This is to fix compatibility with a number of third-party DNS plugins which may + have started erroring with `AttributeError` in Certbot v2.0.0. + - Plugin authors can find more information about Certbot 2.x compatibility + [here](https://github.com/certbot/certbot/wiki/Certbot-v2.x-Plugin-Compatibility). +* A bug causing our certbot-apache tests to crash on some systems has been resolved. More details about these changes can be found on our GitHub repo. @@ -28,6 +35,45 @@ This release was not pushed to PyPI since those packages were unaffected. More details about these changes can be found on our GitHub repo. +## 2.0.0 - 2022-11-21 + +### Added + +* Support for Python 3.11 was added to Certbot and all of its components. +* `acme.challenges.HTTP01Response.simple_verify` now accepts a timeout argument which defaults to 30 that causes the verification request to timeout after that many seconds. + +### Changed + +* The default key type for new certificates is now ECDSA `secp256r1` (P-256). It was previously RSA 2048-bit. Existing certificates are not affected. +* The Apache plugin no longer supports Apache 2.2. +* `acme` and Certbot no longer support versions of ACME from before the RFC 8555 standard. +* `acme` and Certbot no longer support the old `urn:acme:error:` ACME error prefix. +* Removed the deprecated `certbot-dns-cloudxns` plugin. +* Certbot will now error if a certificate has `--reuse-key` set and a conflicting `--key-type`, `--key-size` or `--elliptic-curve` is requested on the CLI. Use `--new-key` to change the key while preserving `--reuse-key`. +* 3rd party plugins no longer support the `dist_name:plugin_name` format on the CLI and in configuration files. Use the shorter `plugin_name` format. +* `acme.client.Client`, `acme.client.ClientBase`, `acme.client.BackwardsCompatibleClientV2`, `acme.mixins`, `acme.client.DER_CONTENT_TYPE`, `acme.fields.Resource`, `acme.fields.resource`, `acme.magic_typing`, `acme.messages.OLD_ERROR_PREFIX`, `acme.messages.Directory.register`, `acme.messages.Authorization.resolved_combinations`, `acme.messages.Authorization.combinations` have been removed. +* `acme.messages.Directory` now only supports lookups by the exact resource name string in the ACME directory (e.g. `directory['newOrder']`). +* Removed the deprecated `source_address` argument for `acme.client.ClientNetwork`. +* The `zope` based interfaces in `certbot.interfaces` have been removed in favor of the `abc` based interfaces found in the same module. +* Certbot no longer depends on `zope`. +* Removed deprecated function `certbot.util.get_strict_version`. +* Removed deprecated functions `certbot.crypto_util.init_save_csr`, `certbot.crypto_util.init_save_key`, + and `certbot.compat.misc.execute_command` +* The attributes `FileDisplay`, `NoninteractiveDisplay`, `SIDE_FRAME`, `input_with_timeout`, `separate_list_input`, `summarize_domain_list`, `HELP`, and `ESC` from `certbot.display.util` have been removed. +* Removed deprecated functions `certbot.tests.util.patch_get_utility*`. Plugins should now + patch `certbot.display.util` themselves in their tests or use + `certbot.tests.util.patch_display_util` as a temporary workaround. +* Certbot's test API under `certbot.tests` now uses `unittest.mock` instead of the 3rd party `mock` library. + +### Fixed + +* Fixes a bug where the certbot working directory has unusably restrictive permissions on systems with stricter default umasks. +* Requests to subscribe to the EFF mailing list now time out after 60 seconds. + +We plan to slowly roll out Certbot 2.0 to all of our snap users in the coming months. If you want to use the Certbot 2.0 snap now, please follow the instructions at https://community.letsencrypt.org/t/certbot-2-0-beta-call-for-testing/185945. + +More details about these changes can be found on our GitHub repo. + ## 1.32.0 - 2022-11-08 ### Added diff --git a/certbot/certbot/__init__.py b/certbot/certbot/__init__.py index 8e3878654..39b99ca90 100644 --- a/certbot/certbot/__init__.py +++ b/certbot/certbot/__init__.py @@ -1,3 +1,3 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '2.0.0.dev0' +__version__ = '2.1.0.dev0' diff --git a/certbot/certbot/_internal/account.py b/certbot/certbot/_internal/account.py index d8f002948..eb2466f32 100644 --- a/certbot/certbot/_internal/account.py +++ b/certbot/certbot/_internal/account.py @@ -20,7 +20,7 @@ import pytz from acme import fields as acme_fields from acme import messages -from acme.client import ClientBase +from acme.client import ClientV2 from certbot import configuration from certbot import errors from certbot import interfaces @@ -108,13 +108,13 @@ class Account: class AccountMemoryStorage(interfaces.AccountStorage): """In-memory account storage.""" - def __init__(self, initial_accounts: Dict[str, Account] = None) -> None: + def __init__(self, initial_accounts: Optional[Dict[str, Account]] = None) -> None: self.accounts = initial_accounts if initial_accounts is not None else {} def find_all(self) -> List[Account]: return list(self.accounts.values()) - def save(self, account: Account, client: ClientBase) -> None: + def save(self, account: Account, client: ClientV2) -> None: if account.id in self.accounts: logger.debug("Overwriting account: %s", account.id) self.accounts[account.id] = account @@ -243,11 +243,11 @@ class AccountFileStorage(interfaces.AccountStorage): def load(self, account_id: str) -> Account: return self._load_for_server_path(account_id, self.config.server_path) - def save(self, account: Account, client: ClientBase) -> None: + def save(self, account: Account, client: ClientV2) -> None: """Create a new account. :param Account account: account to create - :param ClientBase client: ACME client associated to the account + :param ClientV2 client: ACME client associated to the account """ try: @@ -258,11 +258,11 @@ class AccountFileStorage(interfaces.AccountStorage): except IOError as error: raise errors.AccountStorageError(error) - def update_regr(self, account: Account, client: ClientBase) -> None: + def update_regr(self, account: Account, client: ClientV2) -> None: """Update the registration resource. :param Account account: account to update - :param ClientBase client: ACME client associated to the account + :param ClientV2 client: ACME client associated to the account """ try: @@ -358,7 +358,7 @@ class AccountFileStorage(interfaces.AccountStorage): with util.safe_open(self._key_path(dir_path), "w", chmod=0o400) as key_file: key_file.write(account.key.json_dumps()) - def _update_regr(self, account: Account, acme: ClientBase, dir_path: str) -> None: + def _update_regr(self, account: Account, acme: ClientV2, dir_path: str) -> None: with open(self._regr_path(dir_path), "w") as regr_file: regr = account.regr # If we have a value for new-authz, save it for forwards diff --git a/certbot/certbot/_internal/auth_handler.py b/certbot/certbot/_internal/auth_handler.py index 979ef0220..05feaadc0 100644 --- a/certbot/certbot/_internal/auth_handler.py +++ b/certbot/certbot/_internal/auth_handler.py @@ -36,7 +36,7 @@ class AuthHandler: :class:`~acme.challenges.Challenge` types :type auth: certbot.interfaces.Authenticator - :ivar acme.client.BackwardsCompatibleClientV2 acme_client: ACME client API. + :ivar acme.client.ClientV2 acme_client: ACME client API. :ivar account: Client's Account :type account: :class:`certbot._internal.account.Account` @@ -226,15 +226,10 @@ class AuthHandler: logger.info("Performing the following challenges:") for authzr in pending_authzrs: authzr_challenges = authzr.body.challenges - if self.acme.acme_version == 1: - combinations = authzr.body.combinations - else: - combinations = tuple((i,) for i in range(len(authzr_challenges))) path = gen_challenge_path( authzr_challenges, - self._get_chall_pref(authzr.body.identifier.value), - combinations) + self._get_chall_pref(authzr.body.identifier.value)) achalls.extend(self._challenge_factory(authzr, path)) @@ -387,12 +382,9 @@ def challb_to_achall(challb: messages.ChallengeBody, account_key: josepy.JWK, def gen_challenge_path(challbs: List[messages.ChallengeBody], - preferences: List[Type[challenges.Challenge]], - combinations: Tuple[Tuple[int, ...], ...]) -> Tuple[int, ...]: + preferences: List[Type[challenges.Challenge]]) -> Tuple[int, ...]: """Generate a plan to get authority over the identity. - .. todo:: This can be possibly be rewritten to use resolved_combinations. - :param tuple challbs: A tuple of challenges (:class:`acme.messages.Challenge`) from :class:`acme.messages.AuthorizationResource` to be @@ -402,10 +394,6 @@ def gen_challenge_path(challbs: List[messages.ChallengeBody], :param list preferences: List of challenge preferences for domain (:class:`acme.challenges.Challenge` subclasses) - :param tuple combinations: A collection of sets of challenges from - :class:`acme.messages.Challenge`, each of which would - be sufficient to prove possession of the identifier. - :returns: list of indices from ``challenges``. :rtype: list @@ -413,21 +401,6 @@ def gen_challenge_path(challbs: List[messages.ChallengeBody], path cannot be created that satisfies the CA given the preferences and combinations. - """ - if combinations: - return _find_smart_path(challbs, preferences, combinations) - return _find_dumb_path(challbs, preferences) - - -def _find_smart_path(challbs: List[messages.ChallengeBody], - preferences: List[Type[challenges.Challenge]], - combinations: Tuple[Tuple[int, ...], ...] - ) -> Tuple[int, ...]: - """Find challenge path with server hints. - - Can be called if combinations is included. Function uses a simple - ranking system to choose the combo with the lowest cost. - """ chall_cost = {} max_cost = 1 @@ -441,6 +414,8 @@ def _find_smart_path(challbs: List[messages.ChallengeBody], # Set above completing all of the available challenges best_combo_cost = max_cost + combinations = tuple((i,) for i in range(len(challbs))) + combo_total = 0 for combo in combinations: for challenge_index in combo: @@ -459,28 +434,6 @@ def _find_smart_path(challbs: List[messages.ChallengeBody], return best_combo -def _find_dumb_path(challbs: List[messages.ChallengeBody], - preferences: List[Type[challenges.Challenge]]) -> Tuple[int, ...]: - """Find challenge path without server hints. - - Should be called if the combinations hint is not included by the - server. This function either returns a path containing all - challenges provided by the CA or raises an exception. - - """ - path = [] - for i, challb in enumerate(challbs): - # supported is set to True if the challenge type is supported - supported = next((True for pref_c in preferences - if isinstance(challb.chall, pref_c)), False) - if supported: - path.append(i) - else: - raise _report_no_chall_path(challbs) - - return tuple(path) - - def _report_no_chall_path(challbs: List[messages.ChallengeBody]) -> errors.AuthorizationError: """Logs and return a raisable error reporting that no satisfiable chall path exists. diff --git a/certbot/certbot/_internal/cli/plugins_parsing.py b/certbot/certbot/_internal/cli/plugins_parsing.py index f0a976bf4..d19825738 100644 --- a/certbot/certbot/_internal/cli/plugins_parsing.py +++ b/certbot/certbot/_internal/cli/plugins_parsing.py @@ -45,10 +45,6 @@ def _plugins_parsing(helpful: "helpful.HelpfulArgumentParser", default=flag_default("dns_cloudflare"), help=("Obtain certificates using a DNS TXT record (if you are " "using Cloudflare for DNS).")) - helpful.add(["plugins", "certonly"], "--dns-cloudxns", action="store_true", - default=flag_default("dns_cloudxns"), - help=("Obtain certificates using a DNS TXT record (if you are " - "using CloudXNS for DNS).")) helpful.add(["plugins", "certonly"], "--dns-digitalocean", action="store_true", default=flag_default("dns_digitalocean"), help=("Obtain certificates using a DNS TXT record (if you are " diff --git a/certbot/certbot/_internal/client.py b/certbot/certbot/_internal/client.py index bcd9713db..89c0e498a 100644 --- a/certbot/certbot/_internal/client.py +++ b/certbot/certbot/_internal/client.py @@ -10,7 +10,6 @@ from typing import IO from typing import List from typing import Optional from typing import Tuple -import warnings from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric.rsa import generate_private_key @@ -70,16 +69,8 @@ def acme_from_config_key(config: configuration.NamespaceConfig, key: jose.JWK, verify_ssl=(not config.no_verify_ssl), user_agent=determine_user_agent(config)) - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - - client = acme_client.BackwardsCompatibleClientV2(net, key, config.server) - if client.acme_version == 1: - logger.warning( - "Certbot is configured to use an ACMEv1 server (%s). ACMEv1 support is deprecated" - " and will soon be removed. See https://community.letsencrypt.org/t/143839 for " - "more information.", config.server) - return cast(acme_client.ClientV2, client) + directory = acme_client.ClientV2.get_directory(config.server, net) + return acme_client.ClientV2(directory, net) def determine_user_agent(config: configuration.NamespaceConfig) -> str: @@ -256,18 +247,13 @@ def perform_registration(acme: acme_client.ClientV2, config: configuration.Names " Please use --eab-kid and --eab-hmac-key.") raise errors.Error(msg) + tos = acme.directory.meta.terms_of_service + if tos_cb and tos: + tos_cb(tos) + try: - newreg = messages.NewRegistration.from_data( - email=config.email, external_account_binding=eab) - # Until ACME v1 support is removed from Certbot, we actually need the provided - # ACME client to be a wrapper of type BackwardsCompatibleClientV2. - # TODO: Remove this cast and rewrite the logic when the client is actually a ClientV2 - try: - return cast(acme_client.BackwardsCompatibleClientV2, - acme).new_account_and_tos(newreg, tos_cb) - except AttributeError: - raise errors.Error("The ACME client must be an instance of " - "acme.client.BackwardsCompatibleClientV2") + return acme.new_account(messages.NewRegistration.from_data( + email=config.email, terms_of_service_agreed=True, external_account_binding=eab)) except messages.Error as e: if e.code in ("invalidEmail", "invalidContact"): if config.noninteractive_mode: @@ -291,8 +277,8 @@ class Client: :ivar .Authenticator auth: Prepared (`.Authenticator.prepare`) authenticator that can solve ACME challenges. :ivar .Installer installer: Installer. - :ivar acme.client.BackwardsCompatibleClientV2 acme: Optional ACME - client API handle. You might already have one from `register`. + :ivar acme.client.ClientV2 acme: Optional ACME client API handle. You might + already have one from `register`. """ diff --git a/certbot/certbot/_internal/constants.py b/certbot/certbot/_internal/constants.py index 22bba0607..bd999bf6a 100644 --- a/certbot/certbot/_internal/constants.py +++ b/certbot/certbot/_internal/constants.py @@ -61,7 +61,7 @@ CLI_DEFAULTS: Dict[str, Any] = dict( # noqa break_my_certs=False, rsa_key_size=2048, elliptic_curve="secp256r1", - key_type="rsa", + key_type="ecdsa", must_staple=False, redirect=None, auto_hsts=False, @@ -112,7 +112,6 @@ CLI_DEFAULTS: Dict[str, Any] = dict( # noqa manual=False, webroot=False, dns_cloudflare=False, - dns_cloudxns=False, dns_digitalocean=False, dns_dnsimple=False, dns_dnsmadeeasy=False, diff --git a/certbot/certbot/_internal/display/dummy_readline.py b/certbot/certbot/_internal/display/dummy_readline.py index 2b6e4c310..62152b050 100644 --- a/certbot/certbot/_internal/display/dummy_readline.py +++ b/certbot/certbot/_internal/display/dummy_readline.py @@ -11,6 +11,7 @@ def get_completer() -> Optional[Callable[[], str]]: def get_completer_delims() -> List[str]: """An empty implementation of readline.get_completer_delims.""" + return [] def parse_and_bind(unused_command: str) -> None: diff --git a/certbot/certbot/_internal/display/obj.py b/certbot/certbot/_internal/display/obj.py index c5c2e388b..54c248b0c 100644 --- a/certbot/certbot/_internal/display/obj.py +++ b/certbot/certbot/_internal/display/obj.py @@ -10,11 +10,7 @@ from typing import Tuple from typing import TypeVar from typing import Union -import zope.component -import zope.interface - from certbot import errors -from certbot import interfaces from certbot._internal import constants from certbot._internal.display import completer from certbot._internal.display import util @@ -34,6 +30,7 @@ SIDE_FRAME = ("- " * 39) + "-" """Display boundary (alternates spaces, so when copy-pasted, markdown doesn't interpret it as a heading)""" + # This class holds the global state of the display service. Using this class # eliminates potential gotchas that exist if self.display was just a global # variable. In particular, in functions `_DISPLAY = ` would create a @@ -50,9 +47,6 @@ _SERVICE = _DisplayService() T = TypeVar("T") -# This use of IDisplay can be removed when this class is no longer accessible -# through the public API in certbot.display.util. -@zope.interface.implementer(interfaces.IDisplay) class FileDisplay: """File-based display.""" # see https://github.com/certbot/certbot/issues/3915 @@ -410,9 +404,6 @@ class FileDisplay: return OK, selection -# This use of IDisplay can be removed when this class is no longer accessible -# through the public API in certbot.display.util. -@zope.interface.implementer(interfaces.IDisplay) class NoninteractiveDisplay: """A display utility implementation that never asks for interactive user input""" @@ -573,8 +564,4 @@ def set_display(display: Union[FileDisplay, NoninteractiveDisplay]) -> None: :param Union[FileDisplay, NoninteractiveDisplay] display: the display service """ - # This call is done only for retro-compatibility purposes. - # TODO: Remove this call once zope dependencies are removed from Certbot. - zope.component.provideUtility(display, interfaces.IDisplay) - _SERVICE.display = display diff --git a/certbot/certbot/_internal/eff.py b/certbot/certbot/_internal/eff.py index 729991e0d..6b7ceeb48 100644 --- a/certbot/certbot/_internal/eff.py +++ b/certbot/certbot/_internal/eff.py @@ -88,7 +88,7 @@ def subscribe(email: str) -> None: 'form_id': 'eff_supporters_library_subscribe_form'} logger.info('Subscribe to the EFF mailing list (email: %s).', email) logger.debug('Sending POST request to %s:\n%s', url, data) - _check_response(requests.post(url, data=data)) + _check_response(requests.post(url, data=data, timeout=60)) def _check_response(response: requests.Response) -> None: diff --git a/certbot/certbot/_internal/log.py b/certbot/certbot/_internal/log.py index b6b7d4601..0aa33df6b 100644 --- a/certbot/certbot/_internal/log.py +++ b/certbot/certbot/_internal/log.py @@ -348,7 +348,11 @@ def post_arg_parse_except_hook(exc_type: Type[BaseException], exc_value: BaseExc """ exc_info = (exc_type, exc_value, trace) # Only print human advice if not running under --quiet - exit_func = lambda: sys.exit(1) if quiet else exit_with_advice(log_path) + def exit_func() -> None: + if quiet: + sys.exit(1) + else: + exit_with_advice(log_path) # constants.QUIET_LOGGING_LEVEL or higher should be used to # display message the user, otherwise, a lower level like # logger.DEBUG should be used diff --git a/certbot/certbot/_internal/main.py b/certbot/certbot/_internal/main.py index 098ce3243..0712f1962 100644 --- a/certbot/certbot/_internal/main.py +++ b/certbot/certbot/_internal/main.py @@ -17,8 +17,6 @@ from typing import Union import configobj import josepy as jose -import zope.component -import zope.interface from acme import client as acme_client from acme import errors as acme_errors @@ -38,7 +36,6 @@ from certbot._internal import eff from certbot._internal import hooks from certbot._internal import log from certbot._internal import renewal -from certbot._internal import reporter from certbot._internal import snap_config from certbot._internal import storage from certbot._internal import updater @@ -150,19 +147,21 @@ def _get_and_save_cert(le_client: client.Client, config: configuration.Namespace def _handle_unexpected_key_type_migration(config: configuration.NamespaceConfig, - cert: storage.RenewableCert) -> None: + cert: storage.RenewableCert) -> bool: """ This function ensures that the user will not implicitly migrate an existing key from one type to another in the situation where a certificate for that lineage already exist and they have not provided explicitly --key-type and --cert-name. :param config: Current configuration provided by the client :param cert: Matching certificate that could be renewed + :returns: Whether a key type migration is going ahead. + :rtype: `bool` """ new_key_type = config.key_type.upper() cur_key_type = cert.private_key_type.upper() if new_key_type == cur_key_type: - return + return False # If both --key-type and --cert-name are provided, we consider the user's intent to # be unambiguous: to change the key type of this lineage. @@ -175,7 +174,7 @@ def _handle_unexpected_key_type_migration(config: configuration.NamespaceConfig, yes_label='Update key type', no_label='Keep existing key type', default=False, force_interactive=False, ): - return + return True # If --key-type was set on the CLI but the user did not confirm the key type change using # one of the two above methods, their intent is ambiguous. Error out. @@ -191,6 +190,7 @@ def _handle_unexpected_key_type_migration(config: configuration.NamespaceConfig, # default value. The user is not asking for a key change: keep the key type of the existing # lineage. config.key_type = cur_key_type.lower() + return False def _handle_subset_cert_request(config: configuration.NamespaceConfig, @@ -257,11 +257,11 @@ def _handle_identical_cert_request(config: configuration.NamespaceConfig, :rtype: `tuple` of `str` """ - _handle_unexpected_key_type_migration(config, lineage) + is_key_type_changing = _handle_unexpected_key_type_migration(config, lineage) if not lineage.ensure_deployed(): return "reinstall", lineage - if renewal.should_renew(config, lineage): + if is_key_type_changing or renewal.should_renew(config, lineage): return "renew", lineage if config.reinstall: # Set with --reinstall, force an identical certificate to be @@ -1165,15 +1165,14 @@ def plugins_cmd(config: configuration.NamespaceConfig, return filtered.init(config) - verified = filtered.verify(ifaces) - logger.debug("Verified plugins: %r", verified) + logger.debug("Filtered plugins: %r", filtered) if not config.prepare: - notify(str(verified)) + notify(str(filtered)) return - verified.prepare() - available = verified.available() + filtered.prepare() + available = filtered.available() logger.debug("Prepared plugins: %s", available) notify(str(available)) @@ -1643,7 +1642,10 @@ def make_or_verify_needed_dirs(config: configuration.NamespaceConfig) -> None: """ util.set_up_core_dir(config.config_dir, constants.CONFIG_DIRS_MODE, config.strict_permissions) - util.set_up_core_dir(config.work_dir, constants.CONFIG_DIRS_MODE, config.strict_permissions) + + # Ensure the working directory has the expected mode, even under stricter umask settings + with filesystem.temp_umask(0o022): + util.set_up_core_dir(config.work_dir, constants.CONFIG_DIRS_MODE, config.strict_permissions) hook_dirs = (config.renewal_pre_hooks_dir, config.renewal_deploy_hooks_dir, @@ -1654,8 +1656,8 @@ def make_or_verify_needed_dirs(config: configuration.NamespaceConfig) -> None: @contextmanager def make_displayer(config: configuration.NamespaceConfig - ) -> Generator[Union[display_util.NoninteractiveDisplay, - display_util.FileDisplay], None, None]: + ) -> Generator[Union[display_obj.NoninteractiveDisplay, + display_obj.FileDisplay], None, None]: """Creates a display object appropriate to the flags in the supplied config. :param config: Configuration object @@ -1663,18 +1665,18 @@ def make_displayer(config: configuration.NamespaceConfig :returns: Display object """ - displayer: Union[None, display_util.NoninteractiveDisplay, - display_util.FileDisplay] = None + displayer: Union[None, display_obj.NoninteractiveDisplay, + display_obj.FileDisplay] = None devnull: Optional[IO] = None if config.quiet: config.noninteractive_mode = True devnull = open(os.devnull, "w") # pylint: disable=consider-using-with - displayer = display_util.NoninteractiveDisplay(devnull) + displayer = display_obj.NoninteractiveDisplay(devnull) elif config.noninteractive_mode: - displayer = display_util.NoninteractiveDisplay(sys.stdout) + displayer = display_obj.NoninteractiveDisplay(sys.stdout) else: - displayer = display_util.FileDisplay( + displayer = display_obj.FileDisplay( sys.stdout, config.force_interactive) try: @@ -1684,7 +1686,7 @@ def make_displayer(config: configuration.NamespaceConfig devnull.close() -def main(cli_args: List[str] = None) -> Optional[Union[str, int]]: +def main(cli_args: Optional[List[str]] = None) -> Optional[Union[str, int]]: """Run Certbot. :param cli_args: command line to Certbot, defaults to ``sys.argv[1:]`` @@ -1716,10 +1718,6 @@ def main(cli_args: List[str] = None) -> Optional[Union[str, int]]: args = cli.prepare_and_parse_args(plugins, cli_args) config = configuration.NamespaceConfig(args) - # This call is done only for retro-compatibility purposes. - # TODO: Remove this call once zope dependencies are removed from Certbot. - zope.component.provideUtility(config, interfaces.IConfig) - # On windows, shell without administrative right cannot create symlinks required by certbot. # So we check the rights before continuing. misc.raise_for_non_administrative_windows_rights() @@ -1732,12 +1730,6 @@ def main(cli_args: List[str] = None) -> Optional[Union[str, int]]: if config.func != plugins_cmd: # pylint: disable=comparison-with-callable raise - # These calls are done only for retro-compatibility purposes. - # TODO: Remove these calls once zope dependencies are removed from Certbot. - report = reporter.Reporter(config) - zope.component.provideUtility(report, interfaces.IReporter) - util.atexit_register(report.print_messages) - with make_displayer(config) as displayer: display_obj.set_display(displayer) diff --git a/certbot/certbot/_internal/plugins/disco.py b/certbot/certbot/_internal/plugins/disco.py index 30409aff0..5e767ae6d 100644 --- a/certbot/certbot/_internal/plugins/disco.py +++ b/certbot/certbot/_internal/plugins/disco.py @@ -12,11 +12,8 @@ from typing import Mapping from typing import Optional from typing import Type from typing import Union -import warnings import pkg_resources -import zope.interface -import zope.interface.verify from certbot import configuration from certbot import errors @@ -27,26 +24,9 @@ from certbot.errors import Error logger = logging.getLogger(__name__) -PREFIX_FREE_DISTRIBUTIONS = [ - "certbot", - "certbot-apache", - "certbot-dns-cloudflare", - "certbot-dns-cloudxns", - "certbot-dns-digitalocean", - "certbot-dns-dnsimple", - "certbot-dns-dnsmadeeasy", - "certbot-dns-gehirn", - "certbot-dns-google", - "certbot-dns-linode", - "certbot-dns-luadns", - "certbot-dns-nsone", - "certbot-dns-ovh", - "certbot-dns-rfc2136", - "certbot-dns-route53", - "certbot-dns-sakuracloud", - "certbot-nginx", -] -"""Distributions for which prefix will be omitted.""" + +PLUGIN_INTERFACES = [interfaces.Authenticator, interfaces.Installer, interfaces.Plugin] +"""Interfaces that should be listed in `certbot plugins` output""" class PluginEntryPoint: @@ -55,32 +35,23 @@ class PluginEntryPoint: # this object is mutable, don't allow it to be hashed! __hash__ = None # type: ignore - def __init__(self, entry_point: pkg_resources.EntryPoint, with_prefix: bool = False) -> None: - self.name = self.entry_point_to_plugin_name(entry_point, with_prefix) + def __init__(self, entry_point: pkg_resources.EntryPoint) -> None: + self.name = self.entry_point_to_plugin_name(entry_point) self.plugin_cls: Type[interfaces.Plugin] = entry_point.load() self.entry_point = entry_point self.warning_message: Optional[str] = None self._initialized: Optional[interfaces.Plugin] = None self._prepared: Optional[Union[bool, Error]] = None - self._hidden = False - self._long_description: Optional[str] = None def check_name(self, name: Optional[str]) -> bool: """Check if the name refers to this plugin.""" if name == self.name: - if self.warning_message: - logger.warning(self.warning_message) return True return False @classmethod - def entry_point_to_plugin_name(cls, entry_point: pkg_resources.EntryPoint, - with_prefix: bool) -> str: + def entry_point_to_plugin_name(cls, entry_point: pkg_resources.EntryPoint) -> str: """Unique plugin name for an ``entry_point``""" - if with_prefix: - if not entry_point.dist: - raise errors.Error(f"Entrypoint {entry_point.name} has no distribution!") - return entry_point.dist.key + ":" + entry_point.name return entry_point.name @property @@ -96,27 +67,17 @@ class PluginEntryPoint: @property def long_description(self) -> str: """Long description of the plugin.""" - if self._long_description: - return self._long_description return getattr(self.plugin_cls, "long_description", self.description) - @long_description.setter - def long_description(self, description: str) -> None: - self._long_description = description - @property def hidden(self) -> bool: """Should this plugin be hidden from UI?""" - return self._hidden or getattr(self.plugin_cls, "hidden", False) - - @hidden.setter - def hidden(self, hide: bool) -> None: - self._hidden = hide + return getattr(self.plugin_cls, "hidden", False) def ifaces(self, *ifaces_groups: Iterable[Type]) -> bool: """Does plugin implements specified interface groups?""" return not ifaces_groups or any( - all(_implements(self.plugin_cls, iface) + all(issubclass(self.plugin_cls, iface) for iface in ifaces) for ifaces in ifaces_groups) @@ -134,16 +95,6 @@ class PluginEntryPoint: self._initialized = self.plugin_cls(config, self.name) return self._initialized - def verify(self, ifaces: Iterable[Type]) -> bool: - """Verify that the plugin conforms to the specified interfaces.""" - if not self.initialized: - raise ValueError("Plugin is not initialized.") - for iface in ifaces: # zope.interface.providedBy(plugin) - if not _verify(self.init(), self.plugin_cls, iface): - return False - - return True - @property def prepared(self) -> bool: """Has the plugin been prepared already?""" @@ -198,8 +149,8 @@ class PluginEntryPoint: "* {0}".format(self.name), "Description: {0}".format(self.plugin_cls.description), "Interfaces: {0}".format(", ".join( - cls.__name__ for cls in self.plugin_cls.mro() - if cls.__module__ == 'certbot.interfaces' + iface.__name__ for iface in PLUGIN_INTERFACES + if issubclass(self.plugin_cls, iface) )), "Entry point: {0}".format(self.entry_point), ] @@ -238,41 +189,26 @@ class PluginsRegistry(Mapping): pkg_resources.iter_entry_points( constants.OLD_SETUPTOOLS_PLUGINS_ENTRY_POINT),) for entry_point in entry_points: - plugin_ep = cls._load_entry_point(entry_point, plugins, with_prefix=False) - # entry_point.dist cannot be None here, we would have blown up - # earlier, however, this assertion is needed for mypy. - assert entry_point.dist is not None - if entry_point.dist.key not in PREFIX_FREE_DISTRIBUTIONS: - prefixed_plugin_ep = cls._load_entry_point(entry_point, plugins, with_prefix=True) - prefixed_plugin_ep.hidden = True - message = ( - "Plugin legacy name {0} may be removed in a future version. " - "Please use {1} instead.").format(prefixed_plugin_ep.name, plugin_ep.name) - prefixed_plugin_ep.warning_message = message - prefixed_plugin_ep.long_description = "(WARNING: {0}) {1}".format( - message, prefixed_plugin_ep.long_description) + cls._load_entry_point(entry_point, plugins) return cls(plugins) @classmethod def _load_entry_point(cls, entry_point: pkg_resources.EntryPoint, - plugins: Dict[str, PluginEntryPoint], - with_prefix: bool) -> PluginEntryPoint: - plugin_ep = PluginEntryPoint(entry_point, with_prefix) + plugins: Dict[str, PluginEntryPoint]) -> None: + plugin_ep = PluginEntryPoint(entry_point) if plugin_ep.name in plugins: other_ep = plugins[plugin_ep.name] plugin1 = plugin_ep.entry_point.dist.key if plugin_ep.entry_point.dist else "unknown" plugin2 = other_ep.entry_point.dist.key if other_ep.entry_point.dist else "unknown" raise Exception("Duplicate plugin name {0} from {1} and {2}.".format( plugin_ep.name, plugin1, plugin2)) - if _provides(plugin_ep.plugin_cls, interfaces.Plugin): + if issubclass(plugin_ep.plugin_cls, interfaces.Plugin): plugins[plugin_ep.name] = plugin_ep else: # pragma: no cover logger.warning( "%r does not inherit from Plugin, skipping", plugin_ep) - return plugin_ep - def __getitem__(self, name: str) -> PluginEntryPoint: return self._plugins[name] @@ -300,10 +236,6 @@ class PluginsRegistry(Mapping): """Filter plugins based on interfaces.""" return self.filter(lambda p_ep: p_ep.ifaces(*ifaces_groups)) - def verify(self, ifaces: Iterable[Type]) -> "PluginsRegistry": - """Filter plugins based on verification.""" - return self.filter(lambda p_ep: p_ep.verify(ifaces)) - def prepare(self) -> List[Union[bool, Error]]: """Prepare all plugins in the registry.""" return [plugin_ep.prepare() for plugin_ep in self._plugins.values()] @@ -342,88 +274,3 @@ class PluginsRegistry(Mapping): if not self._plugins: return "No plugins" return "\n\n".join(str(p_ep) for p_ep in self._plugins.values()) - - -_DEPRECATION_PLUGIN = ("Zope interface certbot.interfaces.IPlugin is deprecated, " - "use ABC certbot.interface.Plugin instead.") - -_DEPRECATION_AUTHENTICATOR = ("Zope interface certbot.interfaces.IAuthenticator is deprecated, " - "use ABC certbot.interface.Authenticator instead.") - -_DEPRECATION_INSTALLER = ("Zope interface certbot.interfaces.IInstaller is deprecated, " - "use ABC certbot.interface.Installer instead.") - -_DEPRECATION_FACTORY = ("Zope interface certbot.interfaces.IPluginFactory is deprecated, " - "use ABC certbot.interface.Plugin instead.") - - -def _provides(target_class: Type[interfaces.Plugin], iface: Type) -> bool: - if issubclass(target_class, iface): - return True - - if iface == interfaces.Plugin and interfaces.IPluginFactory.providedBy(target_class): - logging.warning(_DEPRECATION_FACTORY) - warnings.warn(_DEPRECATION_FACTORY, DeprecationWarning) - return True - - return False - - -def _implements(target_class: Type[interfaces.Plugin], iface: Type) -> bool: - if issubclass(target_class, iface): - return True - - if iface == interfaces.Plugin and interfaces.IPlugin.implementedBy(target_class): - logging.warning(_DEPRECATION_PLUGIN) - warnings.warn(_DEPRECATION_PLUGIN, DeprecationWarning) - return True - - if iface == interfaces.Authenticator and interfaces.IAuthenticator.implementedBy(target_class): - logging.warning(_DEPRECATION_AUTHENTICATOR) - warnings.warn(_DEPRECATION_AUTHENTICATOR, DeprecationWarning) - return True - - if iface == interfaces.Installer and interfaces.IInstaller.implementedBy(target_class): - logging.warning(_DEPRECATION_INSTALLER) - warnings.warn(_DEPRECATION_INSTALLER, DeprecationWarning) - return True - - return False - - -def _verify(target_instance: interfaces.Plugin, target_class: Type[interfaces.Plugin], - iface: Type) -> bool: - if issubclass(target_class, iface): - # No need to trigger some verify logic for ABCs: when the object is instantiated, - # an error would be raised if implementation is not done properly. - # So the checks have been done effectively when the plugin has been initialized. - return True - - zope_iface: Optional[Type[zope.interface.Interface]] = None - message = "" - - if iface == interfaces.Plugin: - zope_iface = interfaces.IPlugin - message = _DEPRECATION_PLUGIN - if iface == interfaces.Authenticator: - zope_iface = interfaces.IAuthenticator - message = _DEPRECATION_AUTHENTICATOR - if iface == interfaces.Installer: - zope_iface = interfaces.IInstaller - message = _DEPRECATION_INSTALLER - - if not zope_iface: - raise ValueError(f"Unexpected type: {iface.__name__}") - - try: - zope.interface.verify.verifyObject(zope_iface, target_instance) - logging.warning(message) - warnings.warn(message, DeprecationWarning) - return True - except zope.interface.exceptions.BrokenImplementation as error: - if zope_iface.implementedBy(target_class): - logger.debug( - "%s implements %s but object does not verify: %s", - target_class, zope_iface.__name__, error, exc_info=True) - - return False diff --git a/certbot/certbot/_internal/plugins/manual.py b/certbot/certbot/_internal/plugins/manual.py index a5c9559e4..d22b91afb 100644 --- a/certbot/certbot/_internal/plugins/manual.py +++ b/certbot/certbot/_internal/plugins/manual.py @@ -133,7 +133,8 @@ permitted by DNS standards.) 'the user or by performing the setup manually.') def auth_hint(self, failed_achalls: Iterable[achallenges.AnnotatedChallenge]) -> str: - has_chall = lambda cls: any(isinstance(achall.chall, cls) for achall in failed_achalls) + def has_chall(cls: Type[challenges.Challenge]) -> bool: + return any(isinstance(achall.chall, cls) for achall in failed_achalls) has_dns = has_chall(challenges.DNS01) resource_names = { diff --git a/certbot/certbot/_internal/plugins/selection.py b/certbot/certbot/_internal/plugins/selection.py index cde6bd221..708877d3e 100644 --- a/certbot/certbot/_internal/plugins/selection.py +++ b/certbot/certbot/_internal/plugins/selection.py @@ -65,7 +65,6 @@ def get_unprepared_installer(config: configuration.NamespaceConfig, return None installers = plugins.filter(lambda p_ep: p_ep.check_name(req_inst)) installers.init(config) - installers = installers.verify((interfaces.Installer,)) if len(installers) > 1: raise errors.PluginSelectionError( "Found multiple installers with the name %s, Certbot is unable to " @@ -116,9 +115,8 @@ def pick_plugin(config: configuration.NamespaceConfig, default: Optional[str], filtered = plugins.visible().ifaces(ifaces) filtered.init(config) - verified = filtered.verify(ifaces) - verified.prepare() - prepared = verified.available() + filtered.prepare() + prepared = filtered.available() if len(prepared) > 1: logger.debug("Multiple candidate plugins: %s", prepared) @@ -168,7 +166,7 @@ def choose_plugin(prepared: List[disco.PluginEntryPoint], return None -noninstaller_plugins = ["webroot", "manual", "standalone", "dns-cloudflare", "dns-cloudxns", +noninstaller_plugins = ["webroot", "manual", "standalone", "dns-cloudflare", "dns-digitalocean", "dns-dnsimple", "dns-dnsmadeeasy", "dns-gehirn", "dns-google", "dns-linode", "dns-luadns", "dns-nsone", "dns-ovh", "dns-rfc2136", "dns-route53", "dns-sakuracloud"] @@ -316,8 +314,6 @@ def cli_plugin_requests(config: configuration.NamespaceConfig req_auth = set_configurator(req_auth, "manual") if config.dns_cloudflare: req_auth = set_configurator(req_auth, "dns-cloudflare") - if config.dns_cloudxns: - req_auth = set_configurator(req_auth, "dns-cloudxns") if config.dns_digitalocean: req_auth = set_configurator(req_auth, "dns-digitalocean") if config.dns_dnsimple: diff --git a/certbot/certbot/_internal/plugins/webroot.py b/certbot/certbot/_internal/plugins/webroot.py index 4a84197b0..e98cb77c8 100644 --- a/certbot/certbot/_internal/plugins/webroot.py +++ b/certbot/certbot/_internal/plugins/webroot.py @@ -193,8 +193,7 @@ to serve all files under specified web root ({0}).""" # Change the permissions to be writable (GH #1389) # Umask is used instead of chmod to ensure the client can also # run as non-root (GH #1795) - old_umask = filesystem.umask(0o022) - try: + with filesystem.temp_umask(0o022): # We ignore the last prefix in the next iteration, # as it does not correspond to a folder path ('/' or 'C:') for prefix in sorted(util.get_prefixes(self.full_roots[name])[:-1], key=len): @@ -219,8 +218,6 @@ to serve all files under specified web root ({0}).""" raise errors.PluginError( "Couldn't create root for {0} http-01 " "challenge responses: {1}".format(name, exception)) - finally: - filesystem.umask(old_umask) # On Windows, generate a local web.config file that allows IIS to serve expose # challenge files despite the fact they do not have a file extension. @@ -246,13 +243,9 @@ to serve all files under specified web root ({0}).""" logger.debug("Attempting to save validation to %s", validation_path) # Change permissions to be world-readable, owner-writable (GH #1795) - old_umask = filesystem.umask(0o022) - - try: + with filesystem.temp_umask(0o022): with safe_open(validation_path, mode="wb", chmod=0o644) as validation_file: validation_file.write(validation.encode()) - finally: - filesystem.umask(old_umask) self.performed[root_path].add(achall) return response diff --git a/certbot/certbot/_internal/renewal.py b/certbot/certbot/_internal/renewal.py index 0ba2e8108..df5168ea2 100644 --- a/certbot/certbot/_internal/renewal.py +++ b/certbot/certbot/_internal/renewal.py @@ -19,12 +19,10 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.serialization import load_pem_private_key -import zope.component from certbot import configuration from certbot import crypto_util from certbot import errors -from certbot import interfaces from certbot import util from certbot._internal import cli from certbot._internal import client @@ -326,12 +324,57 @@ def _avoid_invalidating_lineage(config: configuration.NamespaceConfig, "unless you use the --break-my-certs flag!") +def _avoid_reuse_key_conflicts(config: configuration.NamespaceConfig, + lineage: storage.RenewableCert) -> None: + """Don't allow combining --reuse-key with any flags that would conflict + with key reuse (--key-type, --rsa-key-size, --elliptic-curve), unless + --new-key is also set. + """ + # If --no-reuse-key is set, no conflict + if cli.set_by_cli("reuse_key") and not config.reuse_key: + return + + # If reuse_key is not set on the lineage and --reuse-key is not + # set on the CLI, no conflict. + if not lineage.reuse_key and not config.reuse_key: + return + + # If --new-key is set, no conflict + if config.new_key: + return + + kt = config.key_type.lower() + + # The remaining cases where conflicts are present: + # - --key-type is set on the CLI and doesn't match the stored private key + # - It's an RSA key and --rsa-key-size is set and doesn't match + # - It's an ECDSA key and --eliptic-curve is set and doesn't match + potential_conflicts = [ + ("--key-type", + lambda: kt != lineage.private_key_type.lower()), + ("--rsa-key-size", + lambda: kt == "rsa" and config.rsa_key_size != lineage.rsa_key_size), + ("--elliptic-curve", + lambda: kt == "ecdsa" and lineage.elliptic_curve and \ + config.elliptic_curve.lower() != lineage.elliptic_curve.lower()) + ] + + for conflict in potential_conflicts: + if conflict[1](): + raise errors.Error( + f"Unable to change the {conflict[0]} of this certificate because --reuse-key " + "is set. To stop reusing the private key, specify --no-reuse-key. " + "To change the private key this one time and then reuse it in future, " + "add --new-key.") + + def renew_cert(config: configuration.NamespaceConfig, domains: Optional[List[str]], le_client: client.Client, lineage: storage.RenewableCert) -> None: """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) + _avoid_reuse_key_conflicts(config, lineage) if not domains: domains = lineage.names() # The private key is the existing lineage private key if reuse_key is set. @@ -460,9 +503,6 @@ def handle_renewal_request(config: configuration.NamespaceConfig) -> None: if not renewal_candidate: parse_failures.append(renewal_file) else: - # This call is done only for retro-compatibility purposes. - # TODO: Remove this call once zope dependencies are removed from Certbot. - zope.component.provideUtility(lineage_config, interfaces.IConfig) renewal_candidate.ensure_deployed() from certbot._internal import main plugins = plugins_disco.PluginsRegistry.find_all() diff --git a/certbot/certbot/_internal/reporter.py b/certbot/certbot/_internal/reporter.py deleted file mode 100644 index fb09b1f27..000000000 --- a/certbot/certbot/_internal/reporter.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Collects and displays information to the user.""" -import collections -import logging -import queue -import sys -import textwrap - -from certbot import configuration -from certbot import util - -logger = logging.getLogger(__name__) - - -class Reporter: - """Collects and displays information to the user. - - :ivar `queue.PriorityQueue` messages: Messages to be displayed to - the user. - - """ - - HIGH_PRIORITY = 0 - """High priority constant. See `add_message`.""" - MEDIUM_PRIORITY = 1 - """Medium priority constant. See `add_message`.""" - LOW_PRIORITY = 2 - """Low priority constant. See `add_message`.""" - - _msg_type = collections.namedtuple('_msg_type', 'priority text on_crash') - - def __init__(self, config: configuration.NamespaceConfig) -> None: - self.messages: "queue.PriorityQueue[Reporter._msg_type]" = queue.PriorityQueue() - self.config = config - - def add_message(self, msg: str, priority: int, on_crash: bool = True) -> None: - """Adds msg to the list of messages to be printed. - - :param str msg: Message to be displayed to the user. - - :param int priority: One of `HIGH_PRIORITY`, `MEDIUM_PRIORITY`, - or `LOW_PRIORITY`. - - :param bool on_crash: Whether or not the message should be - printed if the program exits abnormally. - - """ - assert self.HIGH_PRIORITY <= priority <= self.LOW_PRIORITY - self.messages.put(self._msg_type(priority, msg, on_crash)) - logger.debug("Reporting to user: %s", msg) - - def print_messages(self) -> None: - """Prints messages to the user and clears the message queue. - - If there is an unhandled exception, only messages for which - ``on_crash`` is ``True`` are printed. - - """ - bold_on = False - if not self.messages.empty(): - no_exception = sys.exc_info()[0] is None - bold_on = sys.stdout.isatty() - if not self.config.quiet: - if bold_on: - print(util.ANSI_SGR_BOLD) - print('IMPORTANT NOTES:') - first_wrapper = textwrap.TextWrapper( - initial_indent=' - ', - subsequent_indent=(' ' * 3), - break_long_words=False, - break_on_hyphens=False) - next_wrapper = textwrap.TextWrapper( - initial_indent=first_wrapper.subsequent_indent, - subsequent_indent=first_wrapper.subsequent_indent, - break_long_words=False, - break_on_hyphens=False) - while not self.messages.empty(): - msg = self.messages.get() - if self.config.quiet: - # In --quiet mode, we only print high priority messages that - # are flagged for crash cases - if not (msg.priority == self.HIGH_PRIORITY and msg.on_crash): - continue - if no_exception or msg.on_crash: - if bold_on and msg.priority > self.HIGH_PRIORITY: - if not self.config.quiet: - sys.stdout.write(util.ANSI_SGR_RESET) - bold_on = False - lines = msg.text.splitlines() - print(first_wrapper.fill(lines[0])) - if len(lines) > 1: - print("\n".join( - next_wrapper.fill(line) for line in lines[1:])) - if bold_on and not self.config.quiet: - sys.stdout.write(util.ANSI_SGR_RESET) diff --git a/certbot/certbot/_internal/storage.py b/certbot/certbot/_internal/storage.py index 567073acf..978295cce 100644 --- a/certbot/certbot/_internal/storage.py +++ b/certbot/certbot/_internal/storage.py @@ -12,10 +12,12 @@ from typing import List from typing import Mapping from typing import Optional from typing import Tuple +from typing import Union import configobj from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey +from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey from cryptography.hazmat.primitives.serialization import load_pem_private_key import parsedatetime import pkg_resources @@ -569,6 +571,12 @@ class RenewableCert(interfaces.RenewableCert): return util.is_staging(self.server) return False + @property + def reuse_key(self) -> bool: + """Returns whether this certificate is configured to reuse its private key""" + return "reuse_key" in self.configuration["renewalparams"] and \ + self.configuration["renewalparams"].as_bool("reuse_key") + def _check_symlinks(self) -> None: """Raises an exception if a symlink doesn't exist""" for kind in ALL_FOUR: @@ -1115,22 +1123,47 @@ class RenewableCert(interfaces.RenewableCert): target, values) return cls(new_config.filename, cli_config) - @property - def private_key_type(self) -> str: - """ - :returns: The type of algorithm for the private, RSA or ECDSA - :rtype: str - """ + def _private_key(self) -> Union[RSAPrivateKey, EllipticCurvePrivateKey]: with open(self.configuration["privkey"], "rb") as priv_key_file: key = load_pem_private_key( data=priv_key_file.read(), password=None, backend=default_backend() ) + return key + + @property + def private_key_type(self) -> str: + """ + :returns: The type of algorithm for the private, RSA or ECDSA + :rtype: str + """ + key = self._private_key() if isinstance(key, RSAPrivateKey): return "RSA" - else: - return "ECDSA" + return "ECDSA" + + @property + def rsa_key_size(self) -> Optional[int]: + """ + :returns: If the private key is an RSA key, its size. + :rtype: int + """ + key = self._private_key() + if isinstance(key, RSAPrivateKey): + return key.key_size + return None + + @property + def elliptic_curve(self) -> Optional[str]: + """ + :returns: If the private key is an elliptic key, the name of its curve. + :rtype: str + """ + key = self._private_key() + if isinstance(key, EllipticCurvePrivateKey): + return key.curve.name + return None def save_successor(self, prior_version: int, new_cert: bytes, new_privkey: bytes, new_chain: bytes, cli_config: configuration.NamespaceConfig) -> int: diff --git a/certbot/certbot/compat/filesystem.py b/certbot/certbot/compat/filesystem.py index bd832f7a2..1bf89a733 100644 --- a/certbot/certbot/compat/filesystem.py +++ b/certbot/certbot/compat/filesystem.py @@ -1,6 +1,7 @@ """Compat module to handle files security on Windows and Linux""" from __future__ import absolute_import +from contextlib import contextmanager import errno import os # pylint: disable=os-module-forbidden import stat @@ -8,6 +9,7 @@ import sys from typing import Any from typing import Dict from typing import List +from typing import Generator from typing import Optional try: @@ -76,6 +78,23 @@ def umask(mask: int) -> int: return previous_umask +@contextmanager +def temp_umask(mask: int) -> Generator[None, None, None]: + """ + Apply a umask temporarily, meant to be used in a `with` block. Uses the Certbot + implementation of umask. + + :param int mask: The user file-creation mode mask to apply temporarily + """ + old_umask: Optional[int] = None + try: + old_umask = umask(mask) + yield None + finally: + if old_umask is not None: + umask(old_umask) + + # One could ask why there is no copy_ownership() function, or even a reimplementation # of os.chown() that would modify the ownership of file without touching the mode itself. # This is because on Windows, it would require recalculating the existing DACL against diff --git a/certbot/certbot/compat/misc.py b/certbot/certbot/compat/misc.py index 8ca876962..d40f3fa65 100644 --- a/certbot/certbot/compat/misc.py +++ b/certbot/certbot/compat/misc.py @@ -10,7 +10,6 @@ import subprocess import sys from typing import Optional from typing import Tuple -import warnings from certbot import errors from certbot.compat import os @@ -144,8 +143,8 @@ def execute_command_status(cmd_name: str, shell_cmd: str, subprocess.run(shell=True) - on Windows command will be run in a Powershell shell - This differs from execute_command: it returns the exit code, and does not log the result - and output of the command. + This function returns the exit code, and does not log the result and output + of the command. :param str cmd_name: the user facing name of the hook being run :param str shell_cmd: shell command to execute @@ -168,36 +167,3 @@ def execute_command_status(cmd_name: str, shell_cmd: str, # bytes in Python 3 out, err = proc.stdout, proc.stderr return proc.returncode, err, out - - -def execute_command(cmd_name: str, shell_cmd: str, env: Optional[dict] = None) -> Tuple[str, str]: - """ - Run a command: - - on Linux command will be run by the standard shell selected with - subprocess.run(shell=True) - - on Windows command will be run in a Powershell shell - - This differs from execute_command: it returns the exit code, and does not log the result - and output of the command. - - :param str cmd_name: the user facing name of the hook being run - :param str shell_cmd: shell command to execute - :param dict env: environ to pass into subprocess.run - - :returns: `tuple` (`str` stderr, `str` stdout) - """ - # Deprecation per https://github.com/certbot/certbot/issues/8854 - warnings.warn( - "execute_command will be deprecated in the future, use execute_command_status instead", - PendingDeprecationWarning - ) - returncode, err, out = execute_command_status(cmd_name, shell_cmd, env) - base_cmd = os.path.basename(shell_cmd.split(None, 1)[0]) - if out: - logger.info('Output from %s command %s:\n%s', cmd_name, base_cmd, out) - if returncode != 0: - logger.error('%s command "%s" returned error code %d', - cmd_name, shell_cmd, returncode) - if err: - logger.error('Error output from %s command %s:\n%s', cmd_name, base_cmd, err) - return err, out diff --git a/certbot/certbot/crypto_util.py b/certbot/certbot/crypto_util.py index f45bf3505..a9a8269fe 100644 --- a/certbot/certbot/crypto_util.py +++ b/certbot/certbot/crypto_util.py @@ -15,7 +15,6 @@ from typing import Set from typing import Tuple from typing import TYPE_CHECKING from typing import Union -import warnings from cryptography import x509 from cryptography.exceptions import InvalidSignature @@ -35,7 +34,6 @@ import josepy from OpenSSL import crypto from OpenSSL import SSL import pyrfc3339 -import zope.component from acme import crypto_util as acme_crypto_util from certbot import errors @@ -100,41 +98,6 @@ def generate_key(key_size: int, key_dir: str, key_type: str = "rsa", return util.Key(key_path, key_pem) -# TODO: Remove this call once zope dependencies are removed from Certbot. -def init_save_key(key_size: int, key_dir: str, key_type: str = "rsa", - elliptic_curve: str = "secp256r1", - keyname: str = "key-certbot.pem") -> util.Key: - """Initializes and saves a privkey. - - Inits key and saves it in PEM format on the filesystem. - - .. note:: keyname is the attempted filename, it may be different if a file - already exists at the path. - - .. deprecated:: 1.16.0 - Use :func:`generate_key` instead. - - :param int key_size: key size in bits if key size is rsa. - :param str key_dir: Key save directory. - :param str key_type: Key Type [rsa, ecdsa] - :param str elliptic_curve: Name of the elliptic curve if key type is ecdsa. - :param str keyname: Filename of key - - :returns: Key - :rtype: :class:`certbot.util.Key` - - :raises ValueError: If unable to generate the key given key_size. - - """ - warnings.warn("certbot.crypto_util.init_save_key is deprecated, please use " - "certbot.crypto_util.generate_key instead.", DeprecationWarning) - - config = zope.component.getUtility(interfaces.IConfig) - - return generate_key(key_size, key_dir, key_type=key_type, elliptic_curve=elliptic_curve, - keyname=keyname, strict_permissions=config.strict_permissions) - - def generate_csr(privkey: util.Key, names: Union[List[str], Set[str]], path: str, must_staple: bool = False, strict_permissions: bool = True) -> util.CSR: """Initialize a CSR with the given private key. @@ -165,33 +128,6 @@ def generate_csr(privkey: util.Key, names: Union[List[str], Set[str]], path: str return util.CSR(csr_filename, csr_pem, "pem") -# TODO: Remove this call once zope dependencies are removed from Certbot. -def init_save_csr(privkey: util.Key, names: Set[str], path: str) -> util.CSR: - """Initialize a CSR with the given private key. - - .. deprecated:: 1.16.0 - Use :func:`generate_csr` instead. - - :param privkey: Key to include in the CSR - :type privkey: :class:`certbot.util.Key` - - :param set names: `str` names to include in the CSR - - :param str path: Certificate save directory. - - :returns: CSR - :rtype: :class:`certbot.util.CSR` - - """ - warnings.warn("certbot.crypto_util.init_save_csr is deprecated, please use " - "certbot.crypto_util.generate_csr instead.", DeprecationWarning) - - config = zope.component.getUtility(interfaces.IConfig) - - return generate_csr(privkey, names, path, must_staple=config.must_staple, - strict_permissions=config.strict_permissions) - - # WARNING: the csr and private key file are possible attack vectors for TOCTOU # We should either... # A. Do more checks to verify that the CSR is trusted/valid diff --git a/certbot/certbot/display/util.py b/certbot/certbot/display/util.py index 06c64b56e..defa0a9db 100644 --- a/certbot/certbot/display/util.py +++ b/certbot/certbot/display/util.py @@ -9,26 +9,12 @@ should be used whenever: Other messages can use the `logging` module. See `log.py`. """ -import sys -from types import ModuleType -from typing import Any -from typing import cast from typing import List from typing import Optional from typing import Tuple from typing import Union -import warnings from certbot._internal.display import obj -# These specific imports from certbot._internal.display.obj and -# certbot._internal.display.util are done to not break the public API of this -# module. -from certbot._internal.display.obj import FileDisplay # pylint: disable=unused-import -from certbot._internal.display.obj import NoninteractiveDisplay # pylint: disable=unused-import -from certbot._internal.display.obj import SIDE_FRAME # pylint: disable=unused-import -from certbot._internal.display.util import input_with_timeout # pylint: disable=unused-import -from certbot._internal.display.util import separate_list_input # pylint: disable=unused-import -from certbot._internal.display.util import summarize_domain_list # pylint: disable=unused-import # These constants are defined this way to make them easier to document with # Sphinx and to not couple our public docstrings to our internal ones. @@ -38,17 +24,8 @@ OK = obj.OK CANCEL = obj.CANCEL """Display exit code for a user canceling the display.""" -# These constants are unused and should be removed in a major release of -# Certbot. WIDTH = 72 -HELP = "help" -"""Display exit code when for when the user requests more help. (UNUSED)""" - -ESC = "esc" -"""Display exit code when the user hits Escape (UNUSED)""" - - def notify(msg: str) -> None: """Display a basic status message. @@ -204,36 +181,3 @@ def assert_valid_call(prompt: str, default: str, cli_flag: str, force_interactiv 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 - - -# This class takes a similar approach to the cryptography project to deprecate attributes -# in public modules. See the _ModuleWithDeprecation class here: -# https://github.com/pyca/cryptography/blob/91105952739442a74582d3e62b3d2111365b0dc7/src/cryptography/utils.py#L129 -class _DisplayUtilDeprecationModule: - """ - Internal class delegating to a module, and displaying warnings when attributes - related to deprecated attributes in the certbot.display.util module. - """ - def __init__(self, module: ModuleType) -> None: - self.__dict__['_module'] = module - - def __getattr__(self, attr: str) -> Any: - if attr in ('FileDisplay', 'NoninteractiveDisplay', 'SIDE_FRAME', 'input_with_timeout', - 'separate_list_input', 'summarize_domain_list', 'WIDTH', 'HELP', 'ESC'): - warnings.warn('{0} attribute in certbot.display.util module is deprecated ' - 'and will be removed soon.'.format(attr), - DeprecationWarning, stacklevel=2) - return getattr(self._module, attr) - - def __setattr__(self, attr: str, value: Any) -> None: # pragma: no cover - setattr(self._module, attr, value) - - def __delattr__(self, attr: str) -> None: # pragma: no cover - delattr(self._module, attr) - - def __dir__(self) -> List[str]: # pragma: no cover - return ['_module'] + dir(self._module) - - -# Patching ourselves to warn about deprecation and planned removal of some elements in the module. -sys.modules[__name__] = cast(ModuleType, _DisplayUtilDeprecationModule(sys.modules[__name__])) diff --git a/certbot/certbot/interfaces.py b/certbot/certbot/interfaces.py index 0d12ffd2e..176f1a21c 100644 --- a/certbot/certbot/interfaces.py +++ b/certbot/certbot/interfaces.py @@ -2,26 +2,25 @@ from abc import ABCMeta from abc import abstractmethod from argparse import ArgumentParser -import sys -from types import ModuleType from typing import Any -from typing import Union -from typing import cast from typing import Iterable from typing import List from typing import Optional from typing import Type from typing import TYPE_CHECKING -import warnings - -import zope.interface +from typing import Union from acme.challenges import Challenge from acme.challenges import ChallengeResponse -from acme.client import ClientBase +from acme.client import ClientV2 from certbot import configuration from certbot.achallenges import AnnotatedChallenge +try: + from zope.interface import Interface as ZopeInterface +except ImportError: + ZopeInterface = object + if TYPE_CHECKING: from certbot._internal.account import Account @@ -53,7 +52,7 @@ class AccountStorage(metaclass=ABCMeta): raise NotImplementedError() @abstractmethod - def save(self, account: 'Account', client: ClientBase) -> None: # pragma: no cover + def save(self, account: 'Account', client: ClientV2) -> None: # pragma: no cover """Save account. :raises .AccountStorageError: if account could not be saved @@ -62,18 +61,6 @@ class AccountStorage(metaclass=ABCMeta): raise NotImplementedError() -class IConfig(zope.interface.Interface): # pylint: disable=inherit-non-class - """Deprecated, use certbot.configuration.NamespaceConfig instead.""" - - -class IPluginFactory(zope.interface.Interface): # pylint: disable=inherit-non-class - """Deprecated, use certbot.interfaces.Plugin as ABC instead.""" - - -class IPlugin(zope.interface.Interface): # pylint: disable=inherit-non-class - """Deprecated, use certbot.interfaces.Plugin as ABC instead.""" - - class Plugin(metaclass=ABCMeta): """Certbot plugin. @@ -168,10 +155,6 @@ class Plugin(metaclass=ABCMeta): """ -class IAuthenticator(IPlugin): # pylint: disable=inherit-non-class - """Deprecated, use certbot.interfaces.Authenticator as ABC instead.""" - - class Authenticator(Plugin): """Generic Certbot Authenticator. @@ -231,10 +214,6 @@ class Authenticator(Plugin): """ -class IInstaller(IPlugin): # pylint: disable=inherit-non-class - """Deprecated, use certbot.interfaces.Installer as ABC instead.""" - - class Installer(Plugin): """Generic Certbot Installer Interface. @@ -362,14 +341,6 @@ class Installer(Plugin): """ -class IDisplay(zope.interface.Interface): # pylint: disable=inherit-non-class - """Deprecated, use your own Display implementation instead.""" - - -class IReporter(zope.interface.Interface): # pylint: disable=inherit-non-class - """Deprecated, use your own Reporter implementation instead.""" - - class RenewableCert(metaclass=ABCMeta): """Interface to a certificate lineage.""" @@ -501,34 +472,14 @@ class RenewDeployer(metaclass=ABCMeta): """ -# This class takes a similar approach to the cryptography project to deprecate attributes -# in public modules. See the _ModuleWithDeprecation class here: -# https://github.com/pyca/cryptography/blob/91105952739442a74582d3e62b3d2111365b0dc7/src/cryptography/utils.py#L129 -class _ZopeInterfacesDeprecationModule: - """ - Internal class delegating to a module, and displaying warnings when - attributes related to Zope interfaces are accessed. - """ - def __init__(self, module: ModuleType) -> None: - self.__dict__['_module'] = module +class IPluginFactory(ZopeInterface): + """Compatibility shim for plugins that still use Certbot's old zope.interface classes.""" - def __getattr__(self, attr: str) -> None: - if attr in ('IConfig', 'IPlugin', 'IPluginFactory', 'IAuthenticator', - 'IInstaller', 'IDisplay', 'IReporter'): - warnings.warn('{0} attribute in certbot.interfaces module is deprecated ' - 'and will be removed soon.'.format(attr), - DeprecationWarning, stacklevel=2) - return getattr(self._module, attr) +class IPlugin(ZopeInterface): + """Compatibility shim for plugins that still use Certbot's old zope.interface classes.""" - def __setattr__(self, attr: str, value: Any) -> None: # pragma: no cover - setattr(self._module, attr, value) +class IAuthenticator(IPlugin): + """Compatibility shim for plugins that still use Certbot's old zope.interface classes.""" - def __delattr__(self, attr: str) -> None: # pragma: no cover - delattr(self._module, attr) - - def __dir__(self) -> List[str]: # pragma: no cover - return ['_module'] + dir(self._module) - - -# Patching ourselves to warn about Zope interfaces deprecation and planned removal. -sys.modules[__name__] = cast(ModuleType, _ZopeInterfacesDeprecationModule(sys.modules[__name__])) +class IInstaller(IPlugin): + """Compatibility shim for plugins that still use Certbot's old zope.interface classes.""" diff --git a/certbot/certbot/plugins/dns_test_common.py b/certbot/certbot/plugins/dns_test_common.py index a2ab84dcb..65c9cc2c8 100644 --- a/certbot/certbot/plugins/dns_test_common.py +++ b/certbot/certbot/plugins/dns_test_common.py @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING import configobj import josepy as jose +from unittest import mock from acme import challenges from certbot import achallenges @@ -19,12 +20,6 @@ else: Protocol = object -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore - - DOMAIN = 'example.com' KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) diff --git a/certbot/certbot/plugins/dns_test_common_lexicon.py b/certbot/certbot/plugins/dns_test_common_lexicon.py index 01f4c6d61..371040404 100644 --- a/certbot/certbot/plugins/dns_test_common_lexicon.py +++ b/certbot/certbot/plugins/dns_test_common_lexicon.py @@ -1,6 +1,7 @@ """Base test class for DNS authenticators built on Lexicon.""" from typing import Any from typing import TYPE_CHECKING +from unittest import mock from unittest.mock import MagicMock import josepy as jose @@ -14,10 +15,6 @@ from certbot.plugins.dns_common_lexicon import LexiconClient from certbot.plugins.dns_test_common import _AuthenticatorCallableTestCase from certbot.tests import util as test_util -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore if TYPE_CHECKING: from typing_extensions import Protocol else: diff --git a/certbot/certbot/tests/acme_util.py b/certbot/certbot/tests/acme_util.py index d8ee7f9a8..6412f5dc7 100644 --- a/certbot/certbot/tests/acme_util.py +++ b/certbot/certbot/tests/acme_util.py @@ -3,7 +3,6 @@ import datetime from typing import Any from typing import Dict from typing import Iterable -from typing import Tuple import josepy as jose @@ -24,12 +23,6 @@ DNS01_2 = challenges.DNS01(token=b"cafecafecafecafecafecafe0feedbac") CHALLENGES = [HTTP01, DNS01] -def gen_combos(challbs: Iterable[messages.ChallengeBody]) -> Tuple[Tuple[int], ...]: - """Generate natural combinations for challbs.""" - # completing a single DV challenge satisfies the CA - return tuple((i,) for i, _ in enumerate(challbs)) - - def chall_to_challb(chall: challenges.Challenge, status: messages.Status) -> messages.ChallengeBody: """Return ChallengeBody from Challenge.""" kwargs = { @@ -61,15 +54,13 @@ ACHALLENGES = [HTTP01_A, DNS01_A] def gen_authzr(authz_status: messages.Status, domain: str, challs: Iterable[challenges.Challenge], - statuses: Iterable[messages.Status], - combos: bool = True) -> messages.AuthorizationResource: + statuses: Iterable[messages.Status]) -> messages.AuthorizationResource: """Generate an authorization resource. :param authz_status: Status object :type authz_status: :class:`acme.messages.Status` :param list challs: Challenge objects :param list statuses: status of each challenge object - :param bool combos: Whether or not to add combinations """ challbs = tuple( @@ -81,8 +72,6 @@ def gen_authzr(authz_status: messages.Status, domain: str, challs: Iterable[chal typ=messages.IDENTIFIER_FQDN, value=domain), "challenges": challbs, } - if combos: - authz_kwargs.update({"combinations": gen_combos(challbs)}) if authz_status == messages.STATUS_VALID: authz_kwargs.update({ "status": authz_status, diff --git a/certbot/certbot/tests/util.py b/certbot/certbot/tests/util.py index 532220f4d..772fcd5a5 100644 --- a/certbot/certbot/tests/util.py +++ b/certbot/certbot/tests/util.py @@ -16,7 +16,7 @@ from typing import Iterable from typing import List from typing import Optional import unittest -import warnings +from unittest import mock from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization @@ -35,25 +35,11 @@ from certbot.compat import os from certbot.display import util as display_util from certbot.plugins import common -try: - # When we remove this deprecated import, we should also remove the - # "external-mock" test environment and the mock dependency listed in - # tools/pinning/pyproject.toml. - import mock - warnings.warn( - "The external mock module is being used for backwards compatibility " - "since it is available, however, future versions of Certbot's tests will " - "use unittest.mock. Be sure to update your code accordingly.", - PendingDeprecationWarning - ) -except ImportError: # pragma: no cover - from unittest import mock # type: ignore - class DummyInstaller(common.Installer): """Dummy installer plugin for test purpose.""" def get_all_names(self) -> Iterable[str]: - pass + return [] def deploy_cert(self, domain: str, cert_path: str, key_path: str, chain_path: str, fullchain_path: str) -> None: @@ -64,7 +50,7 @@ class DummyInstaller(common.Installer): pass def supported_enhancements(self) -> List[str]: - pass + return [] def save(self, title: Optional[str] = None, temporary: bool = False) -> None: pass @@ -83,7 +69,7 @@ class DummyInstaller(common.Installer): pass def more_info(self) -> str: - pass + return "" def vector_path(*names: str) -> str: @@ -153,7 +139,7 @@ def load_pyopenssl_private_key(*names: str) -> crypto.PKey: return crypto.load_privatekey(loader, load_vector(*names)) -def make_lineage(config_dir: str, testfile: str, ec: bool = False) -> str: +def make_lineage(config_dir: str, testfile: str, ec: bool = True) -> str: """Creates a lineage defined by testfile. This creates the archive, live, and renewal directories if @@ -198,56 +184,18 @@ def make_lineage(config_dir: str, testfile: str, ec: bool = False) -> str: return conf_path -def patch_get_utility(target: str = 'zope.component.getUtility') -> mock.MagicMock: - """Deprecated, patch certbot.display.util directly or use patch_display_util instead. - - :param str target: path to patch - - :returns: mock zope.component.getUtility - :rtype: mock.MagicMock - - """ - warnings.warn('Decorator certbot.tests.util.patch_get_utility is deprecated. You should now ' - 'patch certbot.display.util yourself directly or use ' - 'certbot.tests.util.patch_display_util as a temporary workaround.') - return cast(mock.MagicMock, mock.patch(target, new_callable=_create_display_util_mock)) - - -def patch_get_utility_with_stdout(target: str = 'zope.component.getUtility', - stdout: Optional[IO] = None) -> mock.MagicMock: - """Deprecated, patch certbot.display.util directly - or use patch_display_util_with_stdout instead. - - :param str target: path to patch - :param object stdout: object to write standard output to; it is - expected to have a `write` method - - :returns: mock zope.component.getUtility - :rtype: mock.MagicMock - - """ - warnings.warn('Decorator certbot.tests.util.patch_get_utility_with_stdout is deprecated. You ' - 'should now patch certbot.display.util yourself directly or use ' - 'use certbot.tests.util.patch_display_util_with_stdout as a temporary ' - 'workaround.') - stdout = stdout if stdout else io.StringIO() - freezable_mock = _create_display_util_mock_with_stdout(stdout) - return cast(mock.MagicMock, mock.patch(target, new=freezable_mock)) - - def patch_display_util() -> mock.MagicMock: """Patch certbot.display.util to use a special mock display utility. The mock display utility works like a regular mock object, except it also also asserts that methods are called with valid arguments. - The mock created by this patch mocks out Certbot internals so this can be - used like the old patch_get_utility function. That is, the mock object will - be called by the certbot.display.util functions and the mock returned by - that call will be used as the display utility. This was done to simplify - the transition from zope.component and mocking certbot.display.util - functions directly in test code should be preferred over using this - function in the future. + The mock created by this patch mocks out Certbot internals. That is, the + mock object will be called by the certbot.display.util functions and the + mock returned by that call will be used as the display utility. This was + done to simplify the transition from zope.component and mocking + certbot.display.util functions directly in test code should be preferred + over using this function in the future. See https://github.com/certbot/certbot/issues/8948 @@ -267,13 +215,12 @@ def patch_display_util_with_stdout( The mock display utility works like a regular mock object, except it also asserts that methods are called with valid arguments. - The mock created by this patch mocks out Certbot internals so this can be - used like the old patch_get_utility function. That is, the mock object will - be called by the certbot.display.util functions and the mock returned by - that call will be used as the display utility. This was done to simplify - the transition from zope.component and mocking certbot.display.util - functions directly in test code should be preferred over using this - function in the future. + The mock created by this patch mocks out Certbot internals. That is, the + mock object will be called by the certbot.display.util functions and the + mock returned by that call will be used as the display utility. This was + done to simplify the transition from zope.component and mocking + certbot.display.util functions directly in test code should be preferred + over using this function in the future. See https://github.com/certbot/certbot/issues/8948 @@ -306,7 +253,7 @@ class FreezableMock: value of func is ignored. """ - def __init__(self, frozen: bool = False, func: Callable[..., Any] = None, + def __init__(self, frozen: bool = False, func: Optional[Callable[..., Any]] = None, return_value: Any = mock.sentinel.DEFAULT) -> None: self._frozen_set = set() if frozen else {'freeze', } self._func = func diff --git a/certbot/certbot/util.py b/certbot/certbot/util.py index 9a6c5de78..12507ef36 100644 --- a/certbot/certbot/util.py +++ b/certbot/certbot/util.py @@ -17,7 +17,6 @@ from typing import List from typing import Optional from typing import Set from typing import Tuple -from typing import TYPE_CHECKING from typing import Union import warnings @@ -33,9 +32,6 @@ _USE_DISTRO = sys.platform.startswith('linux') if _USE_DISTRO: import distro -if TYPE_CHECKING: - import distutils.version # pylint: disable=deprecated-module - logger = logging.getLogger(__name__) @@ -611,24 +607,6 @@ def is_wildcard_domain(domain: Union[str, bytes]) -> bool: return domain.startswith(b"*.") -def get_strict_version(normalized: str) -> "distutils.version.StrictVersion": - """Converts a normalized version to a strict version. - - :param str normalized: normalized version string - - :returns: An equivalent strict version - :rtype: distutils.version.StrictVersion - - """ - warnings.warn("certbot.util.get_strict_version is deprecated and will be " - "removed in a future release.", DeprecationWarning) - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - import distutils.version # pylint: disable=deprecated-module - # strict version ending with "a" and a number designates a pre-release - return distutils.version.StrictVersion(normalized.replace(".dev", "a")) - - def is_staging(srv: str) -> bool: """ Determine whether a given ACME server is a known test / staging server. diff --git a/certbot/docs/cli-help.txt b/certbot/docs/cli-help.txt index 7daf8641e..bf3004961 100644 --- a/certbot/docs/cli-help.txt +++ b/certbot/docs/cli-help.txt @@ -35,7 +35,7 @@ manage your account: --agree-tos Agree to the ACME server's Subscriber Agreement -m EMAIL Email address for important account notifications -optional arguments: +options: -h, --help show this help message and exit -c CONFIG_FILE, --config CONFIG_FILE path to config file (default: /etc/letsencrypt/cli.ini @@ -532,17 +532,6 @@ dns-cloudflare: --dns-cloudflare-credentials DNS_CLOUDFLARE_CREDENTIALS Cloudflare credentials INI file. (default: None) -dns-cloudxns: - Obtain certificates using a DNS TXT record (if you are using CloudXNS for - DNS). - - --dns-cloudxns-propagation-seconds DNS_CLOUDXNS_PROPAGATION_SECONDS - The number of seconds to wait for DNS to propagate - before asking the ACME server to verify the DNS - record. (default: 30) - --dns-cloudxns-credentials DNS_CLOUDXNS_CREDENTIALS - CloudXNS credentials INI file. (default: None) - dns-digitalocean: Obtain certificates using a DNS TXT record (if you are using DigitalOcean for DNS). diff --git a/certbot/docs/packaging.rst b/certbot/docs/packaging.rst index a1fb23100..75349ad14 100644 --- a/certbot/docs/packaging.rst +++ b/certbot/docs/packaging.rst @@ -12,7 +12,6 @@ We release packages and upload them to PyPI (wheels and source tarballs). - https://pypi.python.org/pypi/certbot-apache - https://pypi.python.org/pypi/certbot-nginx - https://pypi.python.org/pypi/certbot-dns-cloudflare -- https://pypi.python.org/pypi/certbot-dns-cloudxns - https://pypi.python.org/pypi/certbot-dns-digitalocean - https://pypi.python.org/pypi/certbot-dns-dnsimple - https://pypi.python.org/pypi/certbot-dns-dnsmadeeasy diff --git a/certbot/docs/using.rst b/certbot/docs/using.rst index ebb2348cf..daa38bfa0 100644 --- a/certbot/docs/using.rst +++ b/certbot/docs/using.rst @@ -206,7 +206,6 @@ use the DNS plugins on your system. Once installed, you can find documentation on how to use each plugin at: * `certbot-dns-cloudflare `_ -* `certbot-dns-cloudxns `_ * `certbot-dns-digitalocean `_ * `certbot-dns-dnsimple `_ * `certbot-dns-dnsmadeeasy `_ @@ -456,68 +455,49 @@ replace that set entirely:: certbot certonly --cert-name example.com -d example.org,www.example.org +.. _using-ecdsa-keys: -Using ECDSA keys ----------------- +RSA and ECDSA keys +------------------------ -As of version 1.10, Certbot supports two types of private key algorithms: -``rsa`` and ``ecdsa``. The type of key used by Certbot can be controlled -through the ``--key-type`` option. You can also use the ``--elliptic-curve`` -option to control the curve used in ECDSA certificates. +Certbot supports two certificate private key algorithms: ``rsa`` and ``ecdsa``. + +As of version 2.0.0, Certbot defaults to ECDSA ``secp256r1`` (P-256) certificate private keys +for all new certificates. Existing certificates will continue to renew using their existing key +type, unless a key type change is requested. + +The type of key used by Certbot can be controlled through the ``--key-type`` option. +You can use the ``--elliptic-curve`` option to control the curve used in ECDSA +certificates and the ``--rsa-key-size`` option to control the size of RSA keys. .. warning:: If you obtain certificates using ECDSA keys, you should be careful - not to downgrade your Certbot installation since ECDSA keys are not - supported by older versions of Certbot. Downgrades like this are possible if - you switch from something like the snaps or pip to packages - provided by your operating system which often lag behind. + not to downgrade to a Certbot version earlier than 1.10.0 where ECDSA keys were + not supported. Downgrades like this are possible if you switch from something like + the snaps or pip to packages provided by your operating system which often lag behind. -Changing existing certificates from RSA to ECDSA -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Changing a certificate's key type +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Unless you are aware that you need to support very old HTTPS clients that are -not supported by most sites, you can safely just transition your site to use -ECDSA keys instead of RSA keys. To accomplish this if you have existing -certificates managed by Certbot, you may freely change the certificate to a new -private key. - -If you want to use ECDSA keys for all certificates in the future, you can -simply add the following line to Certbot's :ref:`configuration file ` - -.. code-block:: ini - - key-type = ecdsa - -After this option is set, newly obtained certificates will use ECDSA keys. This -includes certificates managed by Certbot that previously used RSA keys. +not supported by most sites, you can safely transition your site to use +ECDSA keys instead of RSA keys. If you want to change a single certificate to use ECDSA keys, you'll need to -issue a new Certbot command setting ``--key-type ecdsa`` on the command line -like +create or renew a certificate while setting ``--key-type ecdsa`` on the command line: .. code-block:: shell certbot renew --key-type ecdsa --cert-name example.com --force-renewal -Obtaining ECDSA certificates in addition to RSA certificates -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +If you want to use ECDSA keys for all certificates in the future (including renewals +of existing certificates), you can add the following line to Certbot's +:ref:`configuration file `: -When Certbot configures the certificates it obtains with Apache or Nginx, all -HTTPS clients that we try to support can use certificates with ECDSA keys. If, -however, you are aware of having a specific need to support very old TLS -clients, you may want to obtain both ECDSA and RSA certificates for the same -domains. Certbot can only configure Apache or Nginx to use a single -certificate, however, you could manually configure your software to use the -different certificates depending on your needs. +.. code-block:: ini -When obtaining both ECDSA and RSA certificates for the same domains with -Certbot, we recommend using the ``--cert-name`` option to give your -certificates names so that you can easily identify them. For instance, you may -want to append "ecdsa" to the name of your ECDSA certificate by using a command -like + key-type = ecdsa -.. code-block:: shell - - certbot certonly --key-type ecdsa --cert-name example.com-ecdsa +which will take effect upon the next renewal of each certificate. Revoking certificates --------------------- diff --git a/certbot/setup.py b/certbot/setup.py index 144dca36a..fddc7c026 100644 --- a/certbot/setup.py +++ b/certbot/setup.py @@ -60,8 +60,6 @@ install_requires = [ # installation on Linux. 'pywin32>=300 ; sys_platform == "win32"', f'setuptools>={min_setuptools_version}', - 'zope.component', - 'zope.interface', ] dev_extras = [ @@ -90,13 +88,13 @@ test_extras = [ 'coverage', 'mypy', 'pip', - 'pylint', + # Our pinned version of pylint requires Python >= 3.7.2. + 'pylint ; python_full_version >= "3.7.2"', 'pytest', 'pytest-cov', 'pytest-xdist', 'setuptools', 'tox', - 'types-mock', 'types-pyOpenSSL', 'types-pyRFC3339', 'types-pytz', @@ -135,6 +133,7 @@ setup( 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/certbot/tests/account_test.py b/certbot/tests/account_test.py index e034c5f32..0037de31e 100644 --- a/certbot/tests/account_test.py +++ b/certbot/tests/account_test.py @@ -4,10 +4,7 @@ import json import unittest import josepy as jose -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock +from unittest import mock import pytz from acme import messages diff --git a/certbot/tests/auth_handler_test.py b/certbot/tests/auth_handler_test.py index e13dfbfe5..23d5b2ae2 100644 --- a/certbot/tests/auth_handler_test.py +++ b/certbot/tests/auth_handler_test.py @@ -4,10 +4,7 @@ import logging import unittest from josepy import b64encode -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock +from unittest import mock from acme import challenges from acme import client as acme_client @@ -33,7 +30,7 @@ class ChallengeFactoryTest(unittest.TestCase): self.authzr = acme_util.gen_authzr( messages.STATUS_PENDING, "test", acme_util.CHALLENGES, - [messages.STATUS_PENDING] * 6, False) + [messages.STATUS_PENDING] * 6) def test_all(self): achalls = self.handler._challenge_factory( @@ -70,8 +67,7 @@ class HandleAuthorizationsTest(unittest.TestCase): self.mock_display = mock.Mock() self.mock_config = mock.Mock(debug_challenges=False) - with mock.patch("zope.component.provideUtility"): - display_obj.set_display(self.mock_display) + display_obj.set_display(self.mock_display) self.mock_auth = mock.MagicMock(name="Authenticator") @@ -81,7 +77,6 @@ class HandleAuthorizationsTest(unittest.TestCase): self.mock_account = mock.MagicMock() self.mock_net = mock.MagicMock(spec=acme_client.ClientV2) - self.mock_net.acme_version = 1 self.mock_net.retry_after.side_effect = acme_client.ClientV2.retry_after self.handler = AuthHandler( @@ -92,8 +87,8 @@ class HandleAuthorizationsTest(unittest.TestCase): def tearDown(self): logging.disable(logging.NOTSET) - def _test_name1_http_01_1_common(self, combos): - authzr = gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES, combos=combos) + def _test_name1_http_01_1_common(self): + authzr = gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES) mock_order = mock.MagicMock(authorizations=[authzr]) self.mock_net.poll.side_effect = _gen_mock_on_poll(retry=1, wait_value=30) @@ -117,39 +112,14 @@ class HandleAuthorizationsTest(unittest.TestCase): self.assertEqual(len(authzr), 1) - def test_name1_http_01_1_acme_1(self): - self._test_name1_http_01_1_common(combos=True) - def test_name1_http_01_1_acme_2(self): - self.mock_net.acme_version = 2 - self._test_name1_http_01_1_common(combos=False) - - def test_name1_http_01_1_dns_1_acme_1(self): - self.mock_net.poll.side_effect = _gen_mock_on_poll() - self.mock_auth.get_chall_pref.return_value.append(challenges.DNS01) - - authzr = gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES, combos=False) - mock_order = mock.MagicMock(authorizations=[authzr]) - authzr = self.handler.handle_authorizations(mock_order, self.mock_config) - - self.assertEqual(self.mock_net.answer_challenge.call_count, 2) - - self.assertEqual(self.mock_net.poll.call_count, 1) - - self.assertEqual(self.mock_auth.cleanup.call_count, 1) - # Test if list first element is http-01, use typ because it is an achall - for achall in self.mock_auth.cleanup.call_args[0][0]: - self.assertIn(achall.typ, ["http-01", "dns-01"]) - - # Length of authorizations list - self.assertEqual(len(authzr), 1) + self._test_name1_http_01_1_common() def test_name1_http_01_1_dns_1_acme_2(self): - self.mock_net.acme_version = 2 self.mock_net.poll.side_effect = _gen_mock_on_poll() self.mock_auth.get_chall_pref.return_value.append(challenges.DNS01) - authzr = gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES, combos=False) + authzr = gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES) mock_order = mock.MagicMock(authorizations=[authzr]) authzr = self.handler.handle_authorizations(mock_order, self.mock_config) @@ -165,7 +135,7 @@ class HandleAuthorizationsTest(unittest.TestCase): # Length of authorizations list self.assertEqual(len(authzr), 1) - def _test_name3_http_01_3_common(self, combos): + def test_name3_http_01_3_common_acme_2(self): authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES), gen_dom_authzr(domain="1", challs=acme_util.CHALLENGES), gen_dom_authzr(domain="2", challs=acme_util.CHALLENGES)] @@ -183,13 +153,6 @@ class HandleAuthorizationsTest(unittest.TestCase): self.assertEqual(len(authzr), 3) - def test_name3_http_01_3_common_acme_1(self): - self._test_name3_http_01_3_common(combos=True) - - def test_name3_http_01_3_common_acme_2(self): - self.mock_net.acme_version = 2 - self._test_name3_http_01_3_common(combos=False) - def test_debug_challenges(self): config = mock.Mock(debug_challenges=True, verbose_count=0) authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES)] @@ -269,8 +232,8 @@ class HandleAuthorizationsTest(unittest.TestCase): self.assertRaises(errors.AuthorizationError, self.handler.handle_authorizations, mock_order, self.mock_config) - def _test_preferred_challenge_choice_common(self, combos): - authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES, combos=combos)] + def test_preferred_challenge_choice_common_acme_2(self): + authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES)] mock_order = mock.MagicMock(authorizations=authzrs) self.mock_auth.get_chall_pref.return_value.append(challenges.HTTP01) @@ -285,28 +248,14 @@ class HandleAuthorizationsTest(unittest.TestCase): self.assertEqual( self.mock_auth.cleanup.call_args[0][0][0].typ, "http-01") - def test_preferred_challenge_choice_common_acme_1(self): - self._test_preferred_challenge_choice_common(combos=True) - - def test_preferred_challenge_choice_common_acme_2(self): - self.mock_net.acme_version = 2 - self._test_preferred_challenge_choice_common(combos=False) - - def _test_preferred_challenges_not_supported_common(self, combos): - authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES, combos=combos)] + def test_preferred_challenges_not_supported_acme_2(self): + authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES)] mock_order = mock.MagicMock(authorizations=authzrs) self.handler.pref_challs.append(challenges.DNS01.typ) self.assertRaises( errors.AuthorizationError, self.handler.handle_authorizations, mock_order, self.mock_config) - def test_preferred_challenges_not_supported_acme_1(self): - self._test_preferred_challenges_not_supported_common(combos=True) - - def test_preferred_challenges_not_supported_acme_2(self): - self.mock_net.acme_version = 2 - self._test_preferred_challenges_not_supported_common(combos=False) - def test_dns_only_challenge_not_supported(self): authzrs = [gen_dom_authzr(domain="0", challs=[acme_util.DNS01])] mock_order = mock.MagicMock(authorizations=authzrs) @@ -317,7 +266,7 @@ class HandleAuthorizationsTest(unittest.TestCase): def test_perform_error(self): self.mock_auth.perform.side_effect = errors.AuthorizationError - authzr = gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES, combos=True) + authzr = gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES) mock_order = mock.MagicMock(authorizations=[authzr]) self.assertRaises(errors.AuthorizationError, self.handler.handle_authorizations, mock_order, self.mock_config) @@ -392,7 +341,7 @@ class HandleAuthorizationsTest(unittest.TestCase): authzr = acme_util.gen_authzr( messages.STATUS_PENDING, "0", [acme_util.DNS01], - [messages.STATUS_PENDING], False) + [messages.STATUS_PENDING]) mock_order = mock.MagicMock(authorizations=[authzr]) self.assertRaises( errors.AuthorizationError, self.handler.handle_authorizations, @@ -404,7 +353,7 @@ class HandleAuthorizationsTest(unittest.TestCase): authzr = acme_util.gen_authzr( messages.STATUS_VALID, "0", [acme_util.DNS01], - [messages.STATUS_VALID], False) + [messages.STATUS_VALID]) mock_order = mock.MagicMock(authorizations=[authzr]) self.handler.handle_authorizations(mock_order, self.mock_config) @@ -426,7 +375,7 @@ class HandleAuthorizationsTest(unittest.TestCase): ("is_valid_but_will_fail", messages.STATUS_VALID)] to_deactivate = [acme_util.gen_authzr(a[1], a[0], [acme_util.HTTP01], - [a[1], False]) for a in to_deactivate] + [a[1]]) for a in to_deactivate] orderr = mock.MagicMock(authorizations=to_deactivate) self.mock_net.deactivate_authorization.side_effect = _mock_deactivate @@ -452,8 +401,7 @@ def _gen_mock_on_poll(status=messages.STATUS_VALID, retry=0, wait_value=1): effective_status, authzr.body.identifier.value, [challb.chall for challb in authzr.body.challenges], - [effective_status] * len(authzr.body.challenges), - authzr.body.combinations) + [effective_status] * len(authzr.body.challenges)) return updated_azr, mock.MagicMock(headers={'Retry-After': str(wait_value)}) return _mock @@ -477,8 +425,6 @@ class ChallbToAchallTest(unittest.TestCase): class GenChallengePathTest(unittest.TestCase): """Tests for certbot._internal.auth_handler.gen_challenge_path. - .. todo:: Add more tests for dumb_path... depending on what we want to do. - """ def setUp(self): logging.disable(logging.FATAL) @@ -487,34 +433,25 @@ class GenChallengePathTest(unittest.TestCase): logging.disable(logging.NOTSET) @classmethod - def _call(cls, challbs, preferences, combinations): + def _call(cls, challbs, preferences): from certbot._internal.auth_handler import gen_challenge_path - return gen_challenge_path(challbs, preferences, combinations) + return gen_challenge_path(challbs, preferences) def test_common_case(self): """Given DNS01 and HTTP01 with appropriate combos.""" challbs = (acme_util.DNS01_P, acme_util.HTTP01_P) prefs = [challenges.DNS01, challenges.HTTP01] - combos = ((0,), (1,)) - # Smart then trivial dumb path test - self.assertEqual(self._call(challbs, prefs, combos), (0,)) - self.assertTrue(self._call(challbs, prefs, None)) - # Rearrange order... - self.assertEqual(self._call(challbs[::-1], prefs, combos), (1,)) - self.assertTrue(self._call(challbs[::-1], prefs, None)) + self.assertEqual(self._call(challbs, prefs), (0,)) + self.assertEqual(self._call(challbs[::-1], prefs), (1,)) def test_not_supported(self): - challbs = (acme_util.DNS01_P, acme_util.HTTP01_P) + challbs = (acme_util.DNS01_P,) prefs = [challenges.HTTP01] - combos = ((0, 1),) - # smart path fails because no challs in perfs satisfies combos + # smart path fails because no challs in prefs satisfies combos self.assertRaises( - errors.AuthorizationError, self._call, challbs, prefs, combos) - # dumb path fails because all challbs are not supported - self.assertRaises( - errors.AuthorizationError, self._call, challbs, prefs, None) + errors.AuthorizationError, self._call, challbs, prefs) class ReportFailedAuthzrsTest(unittest.TestCase): @@ -615,11 +552,11 @@ def gen_auth_resp(chall_list): for chall in chall_list] -def gen_dom_authzr(domain, challs, combos=True): +def gen_dom_authzr(domain, challs): """Generates new authzr for domains.""" return acme_util.gen_authzr( messages.STATUS_PENDING, domain, challs, - [messages.STATUS_PENDING] * len(challs), combos) + [messages.STATUS_PENDING] * len(challs)) if __name__ == "__main__": diff --git a/certbot/tests/cert_manager_test.py b/certbot/tests/cert_manager_test.py index 0ed09eccd..157d45b55 100644 --- a/certbot/tests/cert_manager_test.py +++ b/certbot/tests/cert_manager_test.py @@ -7,10 +7,7 @@ import tempfile import unittest import configobj -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock +from unittest import mock from certbot import errors, configuration from certbot._internal.storage import ALL_FOUR diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py index 514351f32..54abe2594 100644 --- a/certbot/tests/cli_test.py +++ b/certbot/tests/cli_test.py @@ -5,6 +5,7 @@ from importlib import reload as reload_module import io import tempfile import unittest +from unittest import mock from acme import challenges from certbot import errors @@ -16,11 +17,6 @@ from certbot.compat import os import certbot.tests.util as test_util from certbot.tests.util import TempDirTestCase -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock - PLUGINS = disco.PluginsRegistry.find_all() @@ -85,7 +81,7 @@ class ParseTest(unittest.TestCase): @staticmethod def parse(*args, **kwargs): - """Mocks zope.component.getUtility and calls _unmocked_parse.""" + """Mocks certbot._internal.display.obj.get_display and calls _unmocked_parse.""" with test_util.patch_display_util(): return ParseTest._unmocked_parse(*args, **kwargs) diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index 28daefea8..6b430831f 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -5,6 +5,7 @@ import platform import shutil import tempfile import unittest +from unittest import mock from unittest.mock import MagicMock from josepy import interfaces @@ -17,11 +18,6 @@ from certbot._internal import constants from certbot.compat import os import certbot.tests.util as test_util -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock - KEY = test_util.load_vector("rsa512_key.pem") CSR_SAN = test_util.load_vector("csr-san_512.pem") @@ -69,13 +65,12 @@ class RegisterTest(test_util.ConfigTestCase): self.config.register_unsafely_without_email = False self.config.email = "alias@example.com" self.account_storage = account.AccountMemoryStorage() - with mock.patch("zope.component.provideUtility"): - display_obj.set_display(MagicMock()) + self.tos_cb = mock.MagicMock() + display_obj.set_display(MagicMock()) def _call(self): from certbot._internal.client import register - tos_cb = mock.MagicMock() - return register(self.config, self.account_storage, tos_cb) + return register(self.config, self.account_storage, self.tos_cb) @staticmethod def _public_key_mock(): @@ -98,31 +93,42 @@ class RegisterTest(test_util.ConfigTestCase): @staticmethod @contextlib.contextmanager def _patched_acme_client(): - # This function is written this way to avoid deprecation warnings that - # are raised when BackwardsCompatibleClientV2 is accessed on the real - # acme.client module. with mock.patch('certbot._internal.client.acme_client') as mock_acme_client: - yield mock_acme_client.BackwardsCompatibleClientV2 + yield mock_acme_client.ClientV2 def test_no_tos(self): with self._patched_acme_client() as mock_client: - mock_client.new_account_and_tos().terms_of_service = "http://tos" + mock_client.new_account().terms_of_service = "http://tos" mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot._internal.eff.prepare_subscription") as mock_prepare: - mock_client().new_account_and_tos.side_effect = errors.Error + mock_client().new_account.side_effect = errors.Error self.assertRaises(errors.Error, self._call) self.assertIs(mock_prepare.called, False) - mock_client().new_account_and_tos.side_effect = None + mock_client().new_account.side_effect = None self._call() self.assertIs(mock_prepare.called, True) + @mock.patch('certbot._internal.eff.prepare_subscription') + def test_empty_meta(self, unused_mock_prepare): + # Test that we can handle an ACME server which does not implement the 'meta' + # directory object (for terms-of-service handling). + with self._patched_acme_client() as mock_client: + from acme.messages import Directory + mock_client().directory = Directory.from_json({}) + + mock_client().external_account_required.side_effect = self._false_mock + + self._call() + self.assertIs(self.tos_cb.called, False) + @test_util.patch_display_util() def test_it(self, unused_mock_get_utility): with self._patched_acme_client() as mock_client: mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot._internal.eff.handle_subscription"): self._call() + self.assertIs(self.tos_cb.called, True) @mock.patch("certbot._internal.client.display_ops.get_email") def test_email_retry(self, mock_get_email): @@ -133,7 +139,7 @@ class RegisterTest(test_util.ConfigTestCase): with self._patched_acme_client() as mock_client: mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot._internal.eff.prepare_subscription") as mock_prepare: - mock_client().new_account_and_tos.side_effect = [mx_err, mock.MagicMock()] + mock_client().new_account.side_effect = [mx_err, mock.MagicMock()] self._call() self.assertEqual(mock_get_email.call_count, 1) self.assertIs(mock_prepare.called, True) @@ -146,7 +152,7 @@ class RegisterTest(test_util.ConfigTestCase): with self._patched_acme_client() as mock_client: mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot._internal.eff.handle_subscription"): - mock_client().new_account_and_tos.side_effect = [mx_err, mock.MagicMock()] + mock_client().new_account.side_effect = [mx_err, mock.MagicMock()] self.assertRaises(errors.Error, self._call) def test_needs_email(self): @@ -176,7 +182,7 @@ class RegisterTest(test_util.ConfigTestCase): # check Certbot did not ask the user to provide an email self.assertIs(mock_get_email.called, False) # check Certbot created an account with no email. Contact should return empty - self.assertFalse(mock_client().new_account_and_tos.call_args[0][0].contact) + self.assertFalse(mock_client().new_account.call_args[0][0].contact) @test_util.patch_display_util() def test_with_eab_arguments(self, unused_mock_get_utility): @@ -228,7 +234,7 @@ class RegisterTest(test_util.ConfigTestCase): ) mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot._internal.eff.handle_subscription") as mock_handle: - mock_client().new_account_and_tos.side_effect = [mx_err, mock.MagicMock()] + mock_client().new_account.side_effect = [mx_err, mock.MagicMock()] self.assertRaises(messages.Error, self._call) self.assertIs(mock_handle.called, False) @@ -245,7 +251,7 @@ class ClientTestCommon(test_util.ConfigTestCase): from certbot._internal.client import Client with mock.patch("certbot._internal.client.acme_client") as acme: - self.acme_client = acme.BackwardsCompatibleClientV2 + self.acme_client = acme.ClientV2 self.acme = self.acme_client.return_value = mock.MagicMock() self.client_network = acme.ClientNetwork self.client = Client( diff --git a/certbot/tests/compat/filesystem_test.py b/certbot/tests/compat/filesystem_test.py index 9aab49c34..e94068d4e 100644 --- a/certbot/tests/compat/filesystem_test.py +++ b/certbot/tests/compat/filesystem_test.py @@ -2,11 +2,7 @@ import contextlib import errno import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock +from unittest import mock from certbot import util from certbot._internal import lock @@ -290,6 +286,31 @@ class WindowsOpenTest(TempDirTestCase): os.close(handler) +class TempUmaskTests(test_util.TempDirTestCase): + """Tests for using the TempUmask class in `with` statements""" + def _check_umask(self): + old_umask = filesystem.umask(0) + filesystem.umask(old_umask) + return old_umask + + def test_works_normally(self): + filesystem.umask(0o0022) + self.assertEqual(self._check_umask(), 0o0022) + with filesystem.temp_umask(0o0077): + self.assertEqual(self._check_umask(), 0o0077) + self.assertEqual(self._check_umask(), 0o0022) + + def test_resets_umask_after_exception(self): + filesystem.umask(0o0022) + self.assertEqual(self._check_umask(), 0o0022) + try: + with filesystem.temp_umask(0o0077): + self.assertEqual(self._check_umask(), 0o0077) + raise Exception() + except: + self.assertEqual(self._check_umask(), 0o0022) + + @unittest.skipIf(POSIX_MODE, reason='Test specific to Windows security') class WindowsMkdirTests(test_util.TempDirTestCase): """Unit tests for Windows mkdir + makedirs functions in filesystem module""" diff --git a/certbot/tests/compat/misc_test.py b/certbot/tests/compat/misc_test.py index f64d8891f..5cb8167b6 100644 --- a/certbot/tests/compat/misc_test.py +++ b/certbot/tests/compat/misc_test.py @@ -1,60 +1,11 @@ """Tests for certbot.compat.misc""" -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore import unittest -import warnings +from unittest import mock from certbot.compat import os - -class ExecuteTest(unittest.TestCase): - """Tests for certbot.compat.misc.execute_command.""" - - @classmethod - def _call(cls, *args, **kwargs): - from certbot.compat.misc import execute_command - # execute_command is superseded by execute_command_status - with warnings.catch_warnings(): - warnings.simplefilter('ignore', category=PendingDeprecationWarning) - return execute_command(*args, **kwargs) - - def test_it(self): - for returncode in range(0, 2): - for stdout in ("", "Hello World!",): - for stderr in ("", "Goodbye Cruel World!"): - self._test_common(returncode, stdout, stderr) - - def _test_common(self, returncode, stdout, stderr): - given_command = "foo" - given_name = "foo-hook" - with mock.patch("certbot.compat.misc.subprocess.run") as mock_run: - mock_run.return_value.stdout = stdout - mock_run.return_value.stderr = stderr - mock_run.return_value.returncode = returncode - with mock.patch("certbot.compat.misc.logger") as mock_logger: - self.assertEqual(self._call(given_name, given_command), (stderr, stdout)) - - executed_command = mock_run.call_args[1].get( - "args", mock_run.call_args[0][0]) - if os.name == 'nt': - expected_command = ['powershell.exe', '-Command', given_command] - else: - expected_command = given_command - self.assertEqual(executed_command, expected_command) - - mock_logger.info.assert_any_call("Running %s command: %s", - given_name, given_command) - if stdout: - mock_logger.info.assert_any_call(mock.ANY, mock.ANY, - mock.ANY, stdout) - if stderr or returncode: - self.assertIs(mock_logger.error.called, True) - - -class ExecuteStatusTest(ExecuteTest): +class ExecuteStatusTest(unittest.TestCase): """Tests for certbot.compat.misc.execute_command_status.""" @classmethod @@ -84,6 +35,12 @@ class ExecuteStatusTest(ExecuteTest): mock_logger.info.assert_any_call("Running %s command: %s", given_name, given_command) + def test_it(self): + for returncode in range(0, 2): + for stdout in ("", "Hello World!",): + for stderr in ("", "Goodbye Cruel World!"): + self._test_common(returncode, stdout, stderr) + if __name__ == "__main__": unittest.main() # pragma: no cover diff --git a/certbot/tests/configuration_test.py b/certbot/tests/configuration_test.py index 1c122615b..61c902bc9 100644 --- a/certbot/tests/configuration_test.py +++ b/certbot/tests/configuration_test.py @@ -1,10 +1,6 @@ """Tests for certbot.configuration.""" import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock +from unittest import mock from certbot import errors from certbot._internal import constants diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index 858db079c..3031cf531 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -1,13 +1,8 @@ """Tests for certbot.crypto_util.""" import logging import unittest +from unittest import mock -import certbot.util - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock import OpenSSL from certbot import errors @@ -67,23 +62,6 @@ class GenerateKeyTest(test_util.TempDirTestCase): self.assertRaises(ValueError, self._call, 431, self.workdir) -class InitSaveKey(unittest.TestCase): - """Test for certbot.crypto_util.init_save_key.""" - @mock.patch("certbot.crypto_util.generate_key") - @mock.patch("certbot.crypto_util.zope.component") - def test_it(self, mock_zope, mock_generate): - from certbot.crypto_util import init_save_key - - mock_zope.getUtility.return_value = mock.MagicMock(strict_permissions=True) - - with self.assertWarns(DeprecationWarning): - init_save_key(4096, "/some/path") - - mock_generate.assert_called_with(4096, "/some/path", elliptic_curve="secp256r1", - key_type="rsa", keyname="key-certbot.pem", - strict_permissions=True) - - class GenerateCSRTest(test_util.TempDirTestCase): """Tests for certbot.crypto_util.generate_csr.""" @mock.patch('acme.crypto_util.make_csr') @@ -100,24 +78,6 @@ class GenerateCSRTest(test_util.TempDirTestCase): self.assertIn('csr-certbot.pem', csr.file) -class InitSaveCsr(unittest.TestCase): - """Tests for certbot.crypto_util.init_save_csr.""" - @mock.patch("certbot.crypto_util.generate_csr") - @mock.patch("certbot.crypto_util.zope.component") - def test_it(self, mock_zope, mock_generate): - from certbot.crypto_util import init_save_csr - - mock_zope.getUtility.return_value = mock.MagicMock(must_staple=True, - strict_permissions=True) - key = certbot.util.Key(file=None, pem=None) - - with self.assertWarns(DeprecationWarning): - init_save_csr(key, {"dummy"}, "/some/path") - - mock_generate.assert_called_with(key, {"dummy"}, "/some/path", - must_staple=True, strict_permissions=True) - - class ValidCSRTest(unittest.TestCase): """Tests for certbot.crypto_util.valid_csr.""" diff --git a/certbot/tests/display/completer_test.py b/certbot/tests/display/completer_test.py index a6ada8b9a..73722151a 100644 --- a/certbot/tests/display/completer_test.py +++ b/certbot/tests/display/completer_test.py @@ -9,17 +9,12 @@ from importlib import reload as reload_module import string import sys import unittest +from unittest import mock from certbot.compat import filesystem from certbot.compat import os import certbot.tests.util as test_util -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock - - class CompleterTest(test_util.TempDirTestCase): """Test certbot._internal.display.completer.Completer.""" diff --git a/certbot/tests/display/internal_util_test.py b/certbot/tests/display/internal_util_test.py index 86489b6a5..b29396c41 100644 --- a/certbot/tests/display/internal_util_test.py +++ b/certbot/tests/display/internal_util_test.py @@ -3,15 +3,11 @@ import io import socket import tempfile import unittest +from unittest import mock from acme import messages as acme_messages from certbot import errors -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock - class WrapLinesTest(unittest.TestCase): def test_wrap_lines(self): diff --git a/certbot/tests/display/obj_test.py b/certbot/tests/display/obj_test.py index f6fe41a68..4da2c3b3b 100644 --- a/certbot/tests/display/obj_test.py +++ b/certbot/tests/display/obj_test.py @@ -275,7 +275,7 @@ class NoninteractiveDisplayTest(unittest.TestCase): """Test non-interactive display. These tests are pretty easy!""" def setUp(self): self.mock_stdout = mock.MagicMock() - self.displayer = display_util.NoninteractiveDisplay(self.mock_stdout) + self.displayer = display_obj.NoninteractiveDisplay(self.mock_stdout) @mock.patch("certbot._internal.display.obj.logger") def test_notification_no_pause(self, mock_logger): diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index e00eeb086..1235190a7 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -2,6 +2,7 @@ """Test certbot.display.ops.""" import sys import unittest +from unittest import mock import josepy as jose @@ -15,11 +16,6 @@ from certbot.display import ops from certbot.display import util as display_util import certbot.tests.util as test_util -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock - KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) diff --git a/certbot/tests/display/util_test.py b/certbot/tests/display/util_test.py index 7985de753..7eb45653c 100644 --- a/certbot/tests/display/util_test.py +++ b/certbot/tests/display/util_test.py @@ -3,15 +3,11 @@ import io import socket import tempfile import unittest +from unittest import mock from certbot import errors import certbot.tests.util as test_util -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock - class NotifyTest(unittest.TestCase): """Tests for certbot.display.util.notify""" diff --git a/certbot/tests/eff_test.py b/certbot/tests/eff_test.py index c61f183cb..6a8ac2c61 100644 --- a/certbot/tests/eff_test.py +++ b/certbot/tests/eff_test.py @@ -1,11 +1,8 @@ """Tests for certbot._internal.eff.""" import datetime import unittest +from unittest import mock -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock import josepy import pytz import requests diff --git a/certbot/tests/error_handler_test.py b/certbot/tests/error_handler_test.py index 010a756c1..d6d506956 100644 --- a/certbot/tests/error_handler_test.py +++ b/certbot/tests/error_handler_test.py @@ -6,15 +6,10 @@ from typing import Callable from typing import Dict from typing import Union import unittest +from unittest import mock from certbot.compat import os -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock - - def get_signals(signums): """Get the handlers for an iterable of signums.""" diff --git a/certbot/tests/errors_test.py b/certbot/tests/errors_test.py index 792868df0..d05f2b43e 100644 --- a/certbot/tests/errors_test.py +++ b/certbot/tests/errors_test.py @@ -1,10 +1,6 @@ """Tests for certbot.errors.""" import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock +from unittest import mock from acme import messages from certbot import achallenges diff --git a/certbot/tests/helpful_test.py b/certbot/tests/helpful_test.py index 0abe277bf..c67211a43 100644 --- a/certbot/tests/helpful_test.py +++ b/certbot/tests/helpful_test.py @@ -1,10 +1,6 @@ """Tests for certbot.helpful_parser""" import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock +from unittest import mock from certbot import errors from certbot._internal.cli import HelpfulArgumentParser diff --git a/certbot/tests/hook_test.py b/certbot/tests/hook_test.py index fad18dc9f..8cd8e6631 100644 --- a/certbot/tests/hook_test.py +++ b/certbot/tests/hook_test.py @@ -1,10 +1,6 @@ """Tests for certbot._internal.hooks.""" import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock +from unittest import mock from certbot import errors from certbot import util diff --git a/certbot/tests/lock_test.py b/certbot/tests/lock_test.py index b45eb8f7a..1e7525782 100644 --- a/certbot/tests/lock_test.py +++ b/certbot/tests/lock_test.py @@ -2,11 +2,7 @@ import functools import multiprocessing import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock +from unittest import mock from certbot import errors from certbot.compat import os diff --git a/certbot/tests/log_test.py b/certbot/tests/log_test.py index aec3ac65a..855582591 100644 --- a/certbot/tests/log_test.py +++ b/certbot/tests/log_test.py @@ -6,6 +6,7 @@ import sys import time from typing import Optional import unittest +from unittest import mock from acme import messages from certbot import errors @@ -15,11 +16,6 @@ from certbot.compat import filesystem from certbot.compat import os from certbot.tests import util as test_util -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock - class PreArgParseSetupTest(unittest.TestCase): diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 61632fc8f..e857f6c33 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -12,6 +12,7 @@ import tempfile import traceback from typing import List import unittest +from unittest import mock import josepy as jose import pytz @@ -34,11 +35,6 @@ from certbot.compat import os from certbot.plugins import enhancements import certbot.tests.util as test_util -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock - CERT_PATH = test_util.vector_path('cert_512.pem') @@ -61,6 +57,21 @@ class TestHandleCerts(unittest.TestCase): self.assertEqual(ret, ("reinstall", mock_lineage)) self.assertTrue(mock_handle_migration.called) + @mock.patch('certbot._internal.renewal.should_renew') + @mock.patch("certbot.display.util.menu") + @mock.patch("certbot._internal.main._handle_unexpected_key_type_migration") + def test_handle_identical_cert_key_type_change(self, mock_handle_migration, mock_menu, + mock_should_renew): + mock_handle_migration.return_value = True + mock_lineage = mock.Mock() + mock_lineage.ensure_deployed.return_value = True + mock_should_renew.return_value = False + ret = main._handle_identical_cert_request(mock.MagicMock(verb="run", reinstall=False), + mock_lineage) + self.assertTrue(mock_handle_migration.called) + self.assertFalse(mock_menu.called) + self.assertEqual(ret, ("renew", mock_lineage)) + @mock.patch("certbot._internal.main._handle_unexpected_key_type_migration") def test_handle_subset_cert_request(self, mock_handle_migration): mock_config = mock.Mock() @@ -388,7 +399,7 @@ class RevokeTest(test_util.TempDirTestCase): mock.patch('certbot._internal.main._determine_account'), mock.patch('certbot._internal.main.display_ops.success_revocation') ] - self.mock_acme_client = patches[0].start().BackwardsCompatibleClientV2 + self.mock_acme_client = patches[0].start().ClientV2 patches[1].start() self.mock_determine_account = patches[2].start() self.mock_success_revoke = patches[3].start() @@ -418,12 +429,19 @@ class RevokeTest(test_util.TempDirTestCase): from certbot._internal.main import revoke revoke(config, plugins) + def _mock_set_by_cli(self, mocked: mock.MagicMock, key: str, value: bool) -> None: + def set_by_cli(k: str) -> bool: + if key == k: + return value + return mock.DEFAULT + mocked.side_effect = set_by_cli + @mock.patch('certbot._internal.main._delete_if_appropriate') @mock.patch('certbot._internal.main.client.acme_client') def test_revoke_with_reason(self, mock_acme_client, mock_delete_if_appropriate): mock_delete_if_appropriate.return_value = False - mock_revoke = mock_acme_client.BackwardsCompatibleClientV2().revoke + mock_revoke = mock_acme_client.ClientV2().revoke expected = [] for reason, code in constants.REVOCATION_REASONS.items(): args = 'revoke --cert-path={0} --reason {1}'.format(self.tmp_cert_path, reason).split() @@ -438,42 +456,56 @@ class RevokeTest(test_util.TempDirTestCase): @mock.patch('certbot._internal.main._delete_if_appropriate') @mock.patch('certbot._internal.storage.RenewableCert') @mock.patch('certbot._internal.storage.renewal_file_for_certname') - def test_revoke_by_certname(self, unused_mock_renewal_file_for_certname, - mock_cert, mock_delete_if_appropriate): + @mock.patch('certbot._internal.client.acme_from_config_key') + @mock.patch('certbot._internal.cli.set_by_cli') + def test_revoke_by_certname(self, mock_set_by_cli, mock_acme_from_config, + unused_mock_renewal_file_for_certname, mock_cert, + mock_delete_if_appropriate): + self._mock_set_by_cli(mock_set_by_cli, "server", False) + mock_acme_from_config.return_value = self.mock_acme_client mock_cert.return_value = mock.MagicMock(cert_path=self.tmp_cert_path, server="https://acme.example") args = 'revoke --cert-name=example.com'.split() mock_delete_if_appropriate.return_value = False self._call(args) - self.mock_acme_client.assert_called_once_with(mock.ANY, mock.ANY, 'https://acme.example') + self.assertEqual(mock_acme_from_config.call_args_list[0][0][0].server, + 'https://acme.example') self.mock_success_revoke.assert_called_once_with(self.tmp_cert_path) @mock.patch('certbot._internal.main._delete_if_appropriate') @mock.patch('certbot._internal.storage.RenewableCert') @mock.patch('certbot._internal.storage.renewal_file_for_certname') - def test_revoke_by_certname_and_server(self, unused_mock_renewal_file_for_certname, - mock_cert, mock_delete_if_appropriate): + @mock.patch('certbot._internal.client.acme_from_config_key') + @mock.patch('certbot._internal.cli.set_by_cli') + def test_revoke_by_certname_and_server(self, mock_set_by_cli, mock_acme_from_config, + unused_mock_renewal_file_for_certname, mock_cert, + mock_delete_if_appropriate): """Revoking with --server should use the server from the CLI""" + self._mock_set_by_cli(mock_set_by_cli, "server", True) mock_cert.return_value = mock.MagicMock(cert_path=self.tmp_cert_path, server="https://acme.example") args = 'revoke --cert-name=example.com --server https://other.example'.split() mock_delete_if_appropriate.return_value = False self._call(args) - self.mock_acme_client.assert_called_once_with(mock.ANY, mock.ANY, 'https://other.example') + self.assertEqual(mock_acme_from_config.call_args_list[0][0][0].server, + 'https://other.example') self.mock_success_revoke.assert_called_once_with(self.tmp_cert_path) @mock.patch('certbot._internal.main._delete_if_appropriate') @mock.patch('certbot._internal.storage.RenewableCert') @mock.patch('certbot._internal.storage.renewal_file_for_certname') - def test_revoke_by_certname_empty_server(self, unused_mock_renewal_file_for_certname, + @mock.patch('certbot._internal.client.acme_from_config_key') + @mock.patch('certbot._internal.cli.set_by_cli') + def test_revoke_by_certname_empty_server(self, mock_set_by_cli, mock_acme_from_config, + unused_mock_renewal_file_for_certname, mock_cert, mock_delete_if_appropriate): """Revoking with --cert-name where the lineage server is empty shouldn't crash """ mock_cert.return_value = mock.MagicMock(cert_path=self.tmp_cert_path, server=None) args = 'revoke --cert-name=example.com'.split() mock_delete_if_appropriate.return_value = False self._call(args) - self.mock_acme_client.assert_called_once_with( - mock.ANY, mock.ANY, constants.CLI_DEFAULTS['server']) + self.assertEqual(mock_acme_from_config.call_args_list[0][0][0].server, + constants.CLI_DEFAULTS['server']) self.mock_success_revoke.assert_called_once_with(self.tmp_cert_path) @mock.patch('certbot._internal.main._delete_if_appropriate') @@ -1034,9 +1066,7 @@ class MainTest(test_util.ConfigTestCase): plugins.visible().ifaces.assert_called_once_with(ifaces) filtered = plugins.visible().ifaces() self.assertEqual(filtered.init.call_count, 1) - filtered.verify.assert_called_once_with(ifaces) - verified = filtered.verify() - self.assertEqual(stdout.getvalue().strip(), str(verified)) + self.assertEqual(stdout.getvalue().strip(), str(filtered)) @mock.patch('certbot._internal.main.plugins_disco') @mock.patch('certbot._internal.main.cli.HelpfulArgumentParser.determine_help_topics') @@ -1052,11 +1082,9 @@ class MainTest(test_util.ConfigTestCase): plugins.visible().ifaces.assert_called_once_with(ifaces) filtered = plugins.visible().ifaces() self.assertEqual(filtered.init.call_count, 1) - filtered.verify.assert_called_once_with(ifaces) - verified = filtered.verify() - verified.prepare.assert_called_once_with() - verified.available.assert_called_once_with() - available = verified.available() + filtered.prepare.assert_called_once_with() + filtered.available.assert_called_once_with() + available = filtered.available() self.assertEqual(stdout.getvalue().strip(), str(available)) def test_certonly_abspath(self): @@ -1191,7 +1219,9 @@ class MainTest(test_util.ConfigTestCase): mock_lineage.should_autorenew.return_value = due_for_renewal mock_lineage.has_pending_deployment.return_value = False mock_lineage.names.return_value = ['isnot.org'] - mock_lineage.private_key_type = 'RSA' + mock_lineage.private_key_type = 'ecdsa' + mock_lineage.elliptic_curve = 'secp256r1' + mock_lineage.reuse_key = reuse_key mock_certr = mock.MagicMock() mock_key = mock.MagicMock(pem='pem_key') mock_client = mock.MagicMock() @@ -1235,11 +1265,11 @@ class MainTest(test_util.ConfigTestCase): if reuse_key and not new_key: # The location of the previous live privkey.pem is passed # to obtain_certificate - mock_client.obtain_certificate.assert_called_once_with(['isnot.org'], + mock_client.obtain_certificate.assert_called_once_with([mock.ANY], os.path.normpath(os.path.join( self.config.config_dir, "live/sample-renewal/privkey.pem"))) else: - mock_client.obtain_certificate.assert_called_once_with(['isnot.org'], None) + mock_client.obtain_certificate.assert_called_once_with([mock.ANY], None) else: self.assertEqual(mock_client.obtain_certificate.call_count, 0) except: @@ -1566,11 +1596,12 @@ class MainTest(test_util.ConfigTestCase): self._call_no_clientmock(['--cert-path', SS_CERT_PATH, '--key-path', RSA2048_KEY_PATH, '--server', server, 'revoke']) with open(RSA2048_KEY_PATH, 'rb') as f: - mock_acme_client.BackwardsCompatibleClientV2.assert_called_once_with( - mock.ANY, jose.JWK.load(f.read()), server) + self.assertEqual(mock_acme_client.ClientV2.call_count, 1) + self.assertEqual(mock_acme_client.ClientNetwork.call_args[0][0], + jose.JWK.load(f.read())) with open(SS_CERT_PATH, 'rb') as f: cert = crypto_util.pyopenssl_load_certificate(f.read())[0] - mock_revoke = mock_acme_client.BackwardsCompatibleClientV2().revoke + mock_revoke = mock_acme_client.ClientV2().revoke mock_revoke.assert_called_once_with( jose.ComparableX509(cert), mock.ANY) diff --git a/certbot/tests/ocsp_test.py b/certbot/tests/ocsp_test.py index c102667bc..802787e02 100644 --- a/certbot/tests/ocsp_test.py +++ b/certbot/tests/ocsp_test.py @@ -4,6 +4,7 @@ import contextlib from datetime import datetime from datetime import timedelta import unittest +from unittest import mock from cryptography import x509 from cryptography.exceptions import InvalidSignature @@ -16,11 +17,6 @@ import pytz from certbot import errors from certbot.tests import util as test_util -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock - out = """Missing = in header key=value ocsp: Use -help for summary. diff --git a/certbot/tests/plugins/common_test.py b/certbot/tests/plugins/common_test.py index 46d766bcf..215faaea3 100644 --- a/certbot/tests/plugins/common_test.py +++ b/certbot/tests/plugins/common_test.py @@ -2,12 +2,9 @@ import functools import shutil import unittest +from unittest import mock import josepy as jose -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock from acme import challenges from certbot import achallenges diff --git a/certbot/tests/plugins/disco_test.py b/certbot/tests/plugins/disco_test.py index 7c2dda0da..673a8d04b 100644 --- a/certbot/tests/plugins/disco_test.py +++ b/certbot/tests/plugins/disco_test.py @@ -3,9 +3,9 @@ import functools import string from typing import List import unittest +from unittest import mock import pkg_resources -import zope.interface from certbot import errors from certbot import interfaces @@ -13,11 +13,6 @@ from certbot._internal.plugins import null from certbot._internal.plugins import standalone from certbot._internal.plugins import webroot -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock - EP_SA = pkg_resources.EntryPoint( "sa", "certbot._internal.plugins.standalone", @@ -60,21 +55,7 @@ class PluginEntryPointTest(unittest.TestCase): for entry_point, name in names.items(): self.assertEqual( - name, PluginEntryPoint.entry_point_to_plugin_name(entry_point, with_prefix=False)) - - def test_entry_point_to_plugin_name_prefixed(self): - from certbot._internal.plugins.disco import PluginEntryPoint - - names = { - self.ep1: "p1:ep1", - self.ep1prim: "p2:ep1", - self.ep2: "p2:ep2", - self.ep3: "p3:ep3", - } - - for entry_point, name in names.items(): - self.assertEqual( - name, PluginEntryPoint.entry_point_to_plugin_name(entry_point, with_prefix=True)) + name, PluginEntryPoint.entry_point_to_plugin_name(entry_point)) def test_description(self): self.assertIn("temporary webserver", self.plugin_ep.description) @@ -129,29 +110,6 @@ class PluginEntryPointTest(unittest.TestCase): self.assertIs(self.plugin_ep.misconfigured, False) self.assertIs(self.plugin_ep.available, False) - def test_verify(self): - iface1 = mock.MagicMock(__name__="iface1") - iface2 = mock.MagicMock(__name__="iface2") - iface3 = mock.MagicMock(__name__="iface3") - # pylint: disable=protected-access - self.plugin_ep._initialized = plugin = mock.MagicMock() - - exceptions = zope.interface.exceptions - with mock.patch("certbot._internal.plugins.disco._verify") as mock_verify: - mock_verify.exceptions = exceptions - - def verify_object(obj, cls, iface): # pylint: disable=missing-docstring - assert obj is plugin - assert iface is iface1 or iface is iface2 or iface is iface3 - if iface is iface3: - return False - return True - mock_verify.side_effect = verify_object - self.assertTrue(self.plugin_ep.verify((iface1,))) - self.assertTrue(self.plugin_ep.verify((iface1, iface2))) - self.assertFalse(self.plugin_ep.verify((iface3,))) - self.assertFalse(self.plugin_ep.verify((iface1, iface3))) - def test_prepare(self): config = mock.MagicMock() self.plugin_ep.init(config=config) @@ -194,6 +152,12 @@ class PluginEntryPointTest(unittest.TestCase): self.assertIs(self.plugin_ep.misconfigured, False) self.assertIs(self.plugin_ep.available, False) + def test_str(self): + output = str(self.plugin_ep) + self.assertIn("Authenticator", output) + self.assertNotIn("Installer", output) + self.assertIn("Plugin", output) + def test_repr(self): self.assertEqual("PluginEntryPoint#sa", repr(self.plugin_ep)) @@ -232,8 +196,7 @@ class PluginsRegistryTest(unittest.TestCase): self.assertIs(plugins["wr"].entry_point, EP_WR) self.assertIs(plugins["ep1"].plugin_cls, null.Installer) self.assertIs(plugins["ep1"].entry_point, self.ep1) - self.assertIs(plugins["p1:ep1"].plugin_cls, null.Installer) - self.assertIs(plugins["p1:ep1"].entry_point, self.ep1) + self.assertNotIn("p1:ep1", plugins) def test_getitem(self): self.assertEqual(self.plugin_ep, self.reg["mock"]) @@ -264,14 +227,6 @@ class PluginsRegistryTest(unittest.TestCase): self.plugin_ep.ifaces.return_value = False self.assertEqual({}, self.reg.ifaces()._plugins) - def test_verify(self): - self.plugin_ep.verify.return_value = True - # pylint: disable=protected-access - self.assertEqual( - self.plugins, self.reg.verify(mock.MagicMock())._plugins) - self.plugin_ep.verify.return_value = False - self.assertEqual({}, self.reg.verify(mock.MagicMock())._plugins) - def test_prepare(self): self.plugin_ep.prepare.return_value = "baz" self.assertEqual(["baz"], self.reg.prepare()) diff --git a/certbot/tests/plugins/dns_common_lexicon_test.py b/certbot/tests/plugins/dns_common_lexicon_test.py index 40afd107b..4634c2057 100644 --- a/certbot/tests/plugins/dns_common_lexicon_test.py +++ b/certbot/tests/plugins/dns_common_lexicon_test.py @@ -1,11 +1,7 @@ """Tests for certbot.plugins.dns_common_lexicon.""" import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock +from unittest import mock from certbot.plugins import dns_common_lexicon from certbot.plugins import dns_test_common_lexicon diff --git a/certbot/tests/plugins/dns_common_test.py b/certbot/tests/plugins/dns_common_test.py index f68d36137..97bc5dea6 100644 --- a/certbot/tests/plugins/dns_common_test.py +++ b/certbot/tests/plugins/dns_common_test.py @@ -3,11 +3,7 @@ import collections import logging import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock +from unittest import mock from certbot import errors from certbot import util diff --git a/certbot/tests/plugins/enhancements_test.py b/certbot/tests/plugins/enhancements_test.py index 62289d95b..903d3e095 100644 --- a/certbot/tests/plugins/enhancements_test.py +++ b/certbot/tests/plugins/enhancements_test.py @@ -1,10 +1,6 @@ """Tests for new style enhancements""" import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock +from unittest import mock from certbot._internal.plugins import null from certbot.plugins import enhancements diff --git a/certbot/tests/plugins/manual_test.py b/certbot/tests/plugins/manual_test.py index cfe2f60fa..a5dc69c32 100644 --- a/certbot/tests/plugins/manual_test.py +++ b/certbot/tests/plugins/manual_test.py @@ -2,11 +2,7 @@ import sys import textwrap import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock +from unittest import mock from acme import challenges from certbot import errors diff --git a/certbot/tests/plugins/null_test.py b/certbot/tests/plugins/null_test.py index dfdd0a7de..ce3440e5b 100644 --- a/certbot/tests/plugins/null_test.py +++ b/certbot/tests/plugins/null_test.py @@ -1,10 +1,6 @@ """Tests for certbot._internal.plugins.null.""" import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock +from unittest import mock class InstallerTest(unittest.TestCase): diff --git a/certbot/tests/plugins/selection_test.py b/certbot/tests/plugins/selection_test.py index fb090f05d..6aed9ec8d 100644 --- a/certbot/tests/plugins/selection_test.py +++ b/certbot/tests/plugins/selection_test.py @@ -2,7 +2,7 @@ import sys from typing import List import unittest - +from unittest import mock from certbot import errors from certbot import interfaces @@ -11,11 +11,6 @@ from certbot._internal.plugins.disco import PluginsRegistry from certbot.display import util as display_util from certbot.tests import util as test_util -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock - class ConveniencePickPluginTest(unittest.TestCase): """Tests for certbot._internal.plugins.selection.pick_*.""" @@ -77,7 +72,7 @@ class PickPluginTest(unittest.TestCase): plugin_ep.init.return_value = "foo" plugin_ep.misconfigured = False - self.reg.visible().ifaces().verify().available.return_value = { + self.reg.visible().ifaces().available.return_value = { "bar": plugin_ep} self.assertEqual("foo", self._call()) @@ -86,14 +81,14 @@ class PickPluginTest(unittest.TestCase): plugin_ep.init.return_value = "foo" plugin_ep.misconfigured = True - self.reg.visible().ifaces().verify().available.return_value = { + self.reg.visible().ifaces().available.return_value = { "bar": plugin_ep} self.assertIsNone(self._call()) def test_multiple(self): plugin_ep = mock.MagicMock() plugin_ep.init.return_value = "foo" - self.reg.visible().ifaces().verify().available.return_value = { + self.reg.visible().ifaces().available.return_value = { "bar": plugin_ep, "baz": plugin_ep, } @@ -104,7 +99,7 @@ class PickPluginTest(unittest.TestCase): [plugin_ep, plugin_ep], self.question) def test_choose_plugin_none(self): - self.reg.visible().ifaces().verify().available.return_value = { + self.reg.visible().ifaces().available.return_value = { "bar": None, "baz": None, } diff --git a/certbot/tests/plugins/standalone_test.py b/certbot/tests/plugins/standalone_test.py index 2649abae9..39454570e 100644 --- a/certbot/tests/plugins/standalone_test.py +++ b/certbot/tests/plugins/standalone_test.py @@ -5,6 +5,7 @@ from typing import Dict from typing import Set from typing import Tuple import unittest +from unittest import mock import josepy as jose import OpenSSL.crypto @@ -16,11 +17,6 @@ from certbot import errors from certbot.tests import acme_util from certbot.tests import util as test_util -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock - class ServerManagerTest(unittest.TestCase): """Tests for certbot._internal.plugins.standalone.ServerManager.""" diff --git a/certbot/tests/plugins/storage_test.py b/certbot/tests/plugins/storage_test.py index 66034b09e..a63ef7795 100644 --- a/certbot/tests/plugins/storage_test.py +++ b/certbot/tests/plugins/storage_test.py @@ -4,17 +4,13 @@ from typing import Iterable from typing import List from typing import Optional import unittest +from unittest import mock from certbot import errors from certbot.compat import filesystem from certbot.compat import os from certbot.tests import util as test_util -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock - class PluginStorageTest(test_util.ConfigTestCase): diff --git a/certbot/tests/plugins/util_test.py b/certbot/tests/plugins/util_test.py index 1b4fcd652..faac01165 100644 --- a/certbot/tests/plugins/util_test.py +++ b/certbot/tests/plugins/util_test.py @@ -1,10 +1,6 @@ """Tests for certbot.plugins.util.""" import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock +from unittest import mock from certbot.compat import os diff --git a/certbot/tests/plugins/webroot_test.py b/certbot/tests/plugins/webroot_test.py index d7e961596..d5ccc4b4f 100644 --- a/certbot/tests/plugins/webroot_test.py +++ b/certbot/tests/plugins/webroot_test.py @@ -8,12 +8,9 @@ import json import shutil import tempfile import unittest +from unittest import mock import josepy as jose -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock from acme import challenges from certbot import achallenges diff --git a/certbot/tests/renewal_test.py b/certbot/tests/renewal_test.py index d6e2866dc..ce6065091 100644 --- a/certbot/tests/renewal_test.py +++ b/certbot/tests/renewal_test.py @@ -1,18 +1,13 @@ """Tests for certbot._internal.renewal""" import copy import unittest +from unittest import mock from acme import challenges from certbot import errors, configuration from certbot._internal import storage import certbot.tests.util as test_util -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock - - class RenewalTest(test_util.ConfigTestCase): @mock.patch('certbot._internal.cli.set_by_cli') @@ -55,8 +50,9 @@ class RenewalTest(test_util.ConfigTestCase): self.assertEqual(self.config.webroot_map, {}) self.assertEqual(self.config.webroot_path, ['/var/www/test']) - def test_reuse_key_renewal_params(self): - self.config.rsa_key_size = 'INVALID_VALUE' + @mock.patch('certbot._internal.renewal._avoid_reuse_key_conflicts') + def test_reuse_key_renewal_params(self, unused_mock_avoid_reuse_conflicts): + self.config.elliptic_curve = 'INVALID_VALUE' self.config.reuse_key = True self.config.dry_run = True config = configuration.NamespaceConfig(self.config) @@ -73,9 +69,10 @@ class RenewalTest(test_util.ConfigTestCase): with mock.patch('certbot._internal.renewal.hooks.renew_hook'): renewal.renew_cert(self.config, None, le_client, lineage) - assert self.config.rsa_key_size == 2048 + assert self.config.elliptic_curve == 'secp256r1' - def test_reuse_ec_key_renewal_params(self): + @mock.patch('certbot._internal.renewal._avoid_reuse_key_conflicts') + def test_reuse_ec_key_renewal_params(self, unused_mock_avoid_reuse_conflicts): self.config.elliptic_curve = 'INVALID_CURVE' self.config.reuse_key = True self.config.dry_run = True @@ -99,7 +96,9 @@ class RenewalTest(test_util.ConfigTestCase): assert self.config.elliptic_curve == 'secp256r1' - def test_new_key(self): + @mock.patch('certbot._internal.renewal.cli.set_by_cli') + def test_new_key(self, mock_set_by_cli): + mock_set_by_cli.return_value = False # When renewing with both reuse_key and new_key, the key should be regenerated, # the key type, key parameters and reuse_key should be kept. self.config.reuse_key = True @@ -119,12 +118,44 @@ class RenewalTest(test_util.ConfigTestCase): with mock.patch('certbot._internal.renewal.hooks.renew_hook'): renewal.renew_cert(self.config, None, le_client, lineage) - self.assertEqual(self.config.rsa_key_size, 2048) - self.assertEqual(self.config.key_type, 'rsa') + self.assertEqual(self.config.elliptic_curve, 'secp256r1') + self.assertEqual(self.config.key_type, 'ecdsa') self.assertTrue(self.config.reuse_key) # None is passed as the existing key, i.e. the key is not actually being reused. le_client.obtain_certificate.assert_called_with(mock.ANY, None) + @mock.patch('certbot._internal.renewal.hooks.renew_hook') + @mock.patch('certbot._internal.renewal.cli.set_by_cli') + def test_reuse_key_conflicts(self, mock_set_by_cli, unused_mock_renew_hook): + mock_set_by_cli.return_value = False + + # When renewing with reuse_key and a conflicting key parameter (size, curve) + # an error should be raised ... + self.config.reuse_key = True + self.config.key_type = "rsa" + self.config.rsa_key_size = 4096 + self.config.dry_run = True + + config = configuration.NamespaceConfig(self.config) + + rc_path = test_util.make_lineage( + self.config.config_dir, 'sample-renewal.conf') + lineage = storage.RenewableCert(rc_path, config) + lineage.configuration["renewalparams"]["reuse_key"] = True + + le_client = mock.MagicMock() + le_client.obtain_certificate.return_value = (None, None, None, None) + + from certbot._internal import renewal + + with self.assertRaisesRegex(errors.Error, "Unable to change the --key-type"): + renewal.renew_cert(self.config, None, le_client, lineage) + + # ... unless --no-reuse-key is set + mock_set_by_cli.side_effect = lambda var: var == "reuse_key" + self.config.reuse_key = False + renewal.renew_cert(self.config, None, le_client, lineage) + @test_util.patch_display_util() @mock.patch('certbot._internal.renewal.cli.set_by_cli') def test_remove_deprecated_config_elements(self, mock_set_by_cli, unused_mock_get_utility): diff --git a/certbot/tests/renewupdater_test.py b/certbot/tests/renewupdater_test.py index f086e3cf3..30a7b0f46 100644 --- a/certbot/tests/renewupdater_test.py +++ b/certbot/tests/renewupdater_test.py @@ -1,10 +1,6 @@ """Tests for renewal updater interfaces""" import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock +from unittest import mock from certbot import interfaces from certbot._internal import main diff --git a/certbot/tests/reporter_test.py b/certbot/tests/reporter_test.py deleted file mode 100644 index 0270ad3f6..000000000 --- a/certbot/tests/reporter_test.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Tests for certbot._internal.reporter.""" -import io -import sys -import unittest - - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock - - -class ReporterTest(unittest.TestCase): - """Tests for certbot._internal.reporter.Reporter.""" - def setUp(self): - from certbot._internal import reporter - self.reporter = reporter.Reporter(mock.MagicMock(quiet=False)) - - self.old_stdout = sys.stdout - sys.stdout = io.StringIO() - - def tearDown(self): - sys.stdout = self.old_stdout - - def test_multiline_message(self): - self.reporter.add_message("Line 1\nLine 2", self.reporter.LOW_PRIORITY) - self.reporter.print_messages() - output = sys.stdout.getvalue() - self.assertIn("Line 1\n", output) - self.assertIn("Line 2", output) - - def test_tty_print_empty(self): - sys.stdout.isatty = lambda: True - self.test_no_tty_print_empty() - - def test_no_tty_print_empty(self): - self.reporter.print_messages() - self.assertEqual(sys.stdout.getvalue(), "") - try: - raise ValueError - except ValueError: - self.reporter.print_messages() - self.assertEqual(sys.stdout.getvalue(), "") - - def test_tty_successful_exit(self): - sys.stdout.isatty = lambda: True - self._successful_exit_common() - - def test_no_tty_successful_exit(self): - self._successful_exit_common() - - def test_tty_unsuccessful_exit(self): - sys.stdout.isatty = lambda: True - self._unsuccessful_exit_common() - - def test_no_tty_unsuccessful_exit(self): - self._unsuccessful_exit_common() - - def _successful_exit_common(self): - self._add_messages() - self.reporter.print_messages() - output = sys.stdout.getvalue() - self.assertIn("IMPORTANT NOTES:", output) - self.assertIn("High", output) - self.assertIn("Med", output) - self.assertIn("Low", output) - - def _unsuccessful_exit_common(self): - self._add_messages() - try: - raise ValueError - except ValueError: - self.reporter.print_messages() - output = sys.stdout.getvalue() - self.assertIn("IMPORTANT NOTES:", output) - self.assertIn("High", output) - self.assertNotIn("Med", output) - self.assertNotIn("Low", output) - - def _add_messages(self): - self.reporter.add_message("High", self.reporter.HIGH_PRIORITY) - self.reporter.add_message( - "Med", self.reporter.MEDIUM_PRIORITY, on_crash=False) - self.reporter.add_message( - "Low", self.reporter.LOW_PRIORITY, on_crash=False) - - -if __name__ == "__main__": - unittest.main() # pragma: no cover diff --git a/certbot/tests/reverter_test.py b/certbot/tests/reverter_test.py index e8d85d4d1..5124c7d9f 100644 --- a/certbot/tests/reverter_test.py +++ b/certbot/tests/reverter_test.py @@ -4,11 +4,7 @@ import logging import shutil import tempfile import unittest - -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock +from unittest import mock from certbot import errors from certbot.compat import os diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py index c4e42ec37..3a1f2b7b4 100644 --- a/certbot/tests/storage_test.py +++ b/certbot/tests/storage_test.py @@ -4,12 +4,9 @@ import datetime import shutil import stat import unittest +from unittest import mock import configobj -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock import pytz import certbot diff --git a/certbot/tests/util_test.py b/certbot/tests/util_test.py index 3af87f85a..e9b5ddef2 100644 --- a/certbot/tests/util_test.py +++ b/certbot/tests/util_test.py @@ -5,18 +5,13 @@ from importlib import reload as reload_module import io import sys import unittest +from unittest import mock from certbot import errors from certbot.compat import filesystem from certbot.compat import os import certbot.tests.util as test_util -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock - - class EnvNoSnapForExternalCallsTest(unittest.TestCase): """Tests for certbot.util.env_no_snap_for_external_calls.""" @@ -592,19 +587,6 @@ class OsInfoTest(unittest.TestCase): self.assertEqual(cbutil.get_python_os_info(), ("testdist", "42")) -class GetStrictVersionTest(unittest.TestCase): - """Test for certbot.util.get_strict_version.""" - - @classmethod - def _call(cls, *args, **kwargs): - from certbot.util import get_strict_version - return get_strict_version(*args, **kwargs) - - def test_it(self): - with self.assertWarnsRegex(DeprecationWarning, "get_strict_version"): - self._call("1.2.3") - - class AtexitRegisterTest(unittest.TestCase): """Tests for certbot.util.atexit_register.""" def setUp(self): diff --git a/letstest/README.md b/letstest/README.md index c569d1e8f..3f6017af3 100644 --- a/letstest/README.md +++ b/letstest/README.md @@ -19,16 +19,16 @@ This package is installed in the Certbot development environment that is created by following the instructions at https://certbot.eff.org/docs/contributing.html#running-a-local-copy-of-the-client. -After activating that virtual environment, you can then configure AWS -credentials and create a key by running: -``` ->aws configure --profile -[interactive: enter secrets for IAM role] ->aws ec2 create-key-pair --profile --key-name --query 'KeyMaterial' --output text > whatever/path/you/want.pem -``` -Note: whatever you pick for `` will be shown to other users with AWS access. +These tests use the AWS SDK for Python (boto3) to manipulate EC2 instances. +Before running the tests, you'll need to set up credentials by following the +instructions at +https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html#configuration. +You will also want to create a `~/.aws/config` file setting the region for your +profile to `us-east-1`, following the instructions in the boto3 quickstart guide above. -When prompted for a default region name, enter: `us-east-1`. +Lastly, you will want to create a file on your system containing a trusted SSH key +by following the instructions at +https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/create-key-pairs.html. ## Usage To run tests, activate the virtual environment you created above and from this directory run: @@ -36,18 +36,6 @@ To run tests, activate the virtual environment you created above and from this d >letstest targets/targets.yaml /path/to/your/key.pem scripts/ ``` -You can only run up to two tests at once. The following error is often indicative of there being too many AWS instances running on our account: -``` -NameError: name 'instances' is not defined -``` - -If you see this, you can run the following command to shut down all running instances: -``` -aws ec2 terminate-instances --profile --instance-ids $(aws ec2 describe-instances --profile | grep | cut -f8) -``` - -It will take a minute for these instances to shut down and become available again. Running this will invalidate any in progress tests. - A temporary directory whose name is output by the tests is also created with a log file from each instance of the test and a file named "results" containing the output above. The tests take quite a while to run. diff --git a/letstest/setup.py b/letstest/setup.py index 4f05c8754..60434431d 100644 --- a/letstest/setup.py +++ b/letstest/setup.py @@ -20,6 +20,7 @@ setup( 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', ], @@ -27,10 +28,6 @@ setup( packages=find_packages(), include_package_data=True, install_requires=[ - # awscli isn't required by the tests themselves, but it is a useful - # tool to have when using these tests to generate keys and control - # running instances so the dependency is declared here for convenience. - 'awscli', 'boto3', 'botocore', # The API from Fabric 2.0+ is used instead of the 1.0 API. diff --git a/pytest.ini b/pytest.ini index 84d1d398b..e498766ec 100644 --- a/pytest.ini +++ b/pytest.ini @@ -11,33 +11,11 @@ # we release breaking changes. # # The current warnings being ignored are: -# 1) The warning raised when importing certbot.tests.util and the external mock -# library is installed. -# 2) An ImportWarning is raised with older versions of setuptools and -# zope.interface. See -# https://github.com/zopefoundation/zope.interface/issues/68 for more info. -# 3) The deprecation warning raised when importing old Zope interfaces from -# the certbot.interfaces module. -# 4) The deprecation warning raised when importing deprecated attributes from -# the certbot.display.util module. -# 5) A deprecation warning is raised in dnspython==1.15.0 in the oldest tests for +# 1) A deprecation warning is raised in dnspython==1.15.0 in the oldest tests for # certbot-dns-rfc2136. -# 6) botocore is currently using deprecated urllib3 functionality. See -# https://github.com/boto/botocore/issues/2744. -# 7) ACMEv1 deprecations in acme.client which will be resolved by Certbot 2.0. -# 8) acme.mixins deprecation in acme.client which will be resolved by Certbot 2.0. -# 9) acme.messages.Authorization.combinations which will be resolved by Certbot 2.0. -# 10) pytest-cov uses deprecated functionality in pytest-xdist, to be resolved by -# https://github.com/pytest-dev/pytest-cov/issues/557. +# 2) pytest-cov uses deprecated functionality in pytest-xdist, to be resolved by +# https://github.com/pytest-dev/pytest-cov/issues/557. filterwarnings = error - ignore:The external mock module:PendingDeprecationWarning - ignore:.*zope. missing __init__:ImportWarning - ignore:.*attribute in certbot.interfaces module is deprecated:DeprecationWarning - ignore:.*attribute in certbot.display.util module is deprecated:DeprecationWarning ignore:decodestring\(\) is a deprecated alias:DeprecationWarning:dns - ignore:'urllib3.contrib.pyopenssl:DeprecationWarning:botocore - ignore:.*attribute in acme.client is deprecated:DeprecationWarning - ignore:.*acme.mixins is deprecated:DeprecationWarning - ignore:.*Authorization.combinations is deprecated:DeprecationWarning ignore:.*rsyncdir:DeprecationWarning diff --git a/tools/_release.sh b/tools/_release.sh index a9ed017a7..368ed21d5 100755 --- a/tools/_release.sh +++ b/tools/_release.sh @@ -57,7 +57,7 @@ export GPG_TTY=$(tty) PORT=${PORT:-1234} # subpackages to be released (the way the script thinks about them) -SUBPKGS_NO_CERTBOT="acme certbot-apache certbot-nginx certbot-dns-cloudflare certbot-dns-cloudxns \ +SUBPKGS_NO_CERTBOT="acme certbot-apache certbot-nginx certbot-dns-cloudflare \ certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-dnsmadeeasy \ certbot-dns-gehirn certbot-dns-google certbot-dns-linode certbot-dns-luadns \ certbot-dns-nsone certbot-dns-ovh certbot-dns-rfc2136 certbot-dns-route53 \ diff --git a/tools/docker/lib/common b/tools/docker/lib/common index 4ba86cac0..bf3598f92 100644 --- a/tools/docker/lib/common +++ b/tools/docker/lib/common @@ -20,7 +20,6 @@ export CERTBOT_PLUGINS=( "dns-dnsimple" "dns-ovh" "dns-cloudflare" - "dns-cloudxns" "dns-digitalocean" "dns-google" "dns-luadns" diff --git a/tools/oldest_constraints.txt b/tools/oldest_constraints.txt index 75fc47aba..1783b0d37 100644 --- a/tools/oldest_constraints.txt +++ b/tools/oldest_constraints.txt @@ -2,7 +2,7 @@ # that script. apacheconfig==0.3.2 ; python_full_version < "3.8.0" and python_version >= "3.7" asn1crypto==0.24.0 ; python_full_version >= "3.7.0" and python_full_version < "3.8.0" -astroid==2.11.7 ; python_full_version < "3.8.0" and python_version >= "3.7" +astroid==2.12.12 ; python_full_version >= "3.7.2" and python_full_version < "3.8.0" attrs==22.1.0 ; python_version >= "3.7" and python_full_version < "3.8.0" boto3==1.15.15 ; python_full_version < "3.8.0" and python_version >= "3.7" botocore==1.18.15 ; python_full_version < "3.8.0" and python_version >= "3.7" @@ -10,17 +10,18 @@ certifi==2022.9.24 ; python_full_version < "3.8.0" and python_version >= "3.7" cffi==1.9.1 ; python_full_version < "3.8.0" and python_version >= "3.7" chardet==3.0.4 ; python_full_version < "3.8.0" and python_version >= "3.7" cloudflare==1.5.1 ; python_full_version < "3.8.0" and python_version >= "3.7" -colorama==0.4.5 ; python_full_version < "3.8.0" and sys_platform == "win32" and python_version >= "3.7" +colorama==0.4.6 ; python_full_version < "3.8.0" and sys_platform == "win32" and python_version >= "3.7" configargparse==0.10.0 ; python_full_version < "3.8.0" and python_version >= "3.7" configobj==5.0.6 ; python_full_version < "3.8.0" and python_version >= "3.7" coverage==6.5.0 ; python_version >= "3.7" and python_full_version < "3.8.0" cryptography==3.2.1 ; python_full_version < "3.8.0" and python_version >= "3.7" cython==0.29.32 ; python_full_version >= "3.7.0" and python_full_version < "3.8.0" -dill==0.3.5.1 ; python_full_version < "3.8.0" and python_version >= "3.7" +dill==0.3.6 ; python_full_version >= "3.7.2" and python_full_version < "3.8.0" distlib==0.3.6 ; python_version >= "3.7" and python_full_version < "3.8.0" distro==1.0.1 ; python_full_version < "3.8.0" and python_version >= "3.7" dns-lexicon==3.2.1 ; python_full_version < "3.8.0" and python_version >= "3.7" dnspython==1.15.0 ; python_full_version < "3.8.0" and python_version >= "3.7" +exceptiongroup==1.0.4 ; python_version >= "3.7" and python_full_version < "3.8.0" execnet==1.9.0 ; python_version >= "3.7" and python_full_version < "3.8.0" filelock==3.8.0 ; python_version >= "3.7" and python_full_version < "3.8.0" funcsigs==0.4 ; python_full_version >= "3.7.0" and python_full_version < "3.8.0" @@ -31,71 +32,64 @@ idna==2.6 ; python_full_version < "3.8.0" and python_version >= "3.7" importlib-metadata==5.0.0 ; python_version >= "3.7" and python_version < "3.8" iniconfig==1.1.1 ; python_version >= "3.7" and python_full_version < "3.8.0" ipaddress==1.0.16 ; python_full_version >= "3.7.0" and python_full_version < "3.8.0" -isort==5.10.1 ; python_full_version < "3.8.0" and python_version >= "3.7" +isort==5.10.1 ; python_full_version >= "3.7.2" and python_full_version < "3.8.0" jmespath==0.10.0 ; python_full_version < "3.8.0" and python_version >= "3.7" josepy==1.13.0 ; python_version >= "3.7" and python_full_version < "3.8.0" -lazy-object-proxy==1.7.1 ; python_full_version < "3.8.0" and python_version >= "3.7" +lazy-object-proxy==1.8.0 ; python_full_version >= "3.7.2" and python_full_version < "3.8.0" logger==1.4 ; python_full_version < "3.8.0" and python_version >= "3.7" -mccabe==0.7.0 ; python_full_version < "3.8.0" and python_version >= "3.7" -mock==1.0.1 ; python_full_version >= "3.7.0" and python_full_version < "3.8.0" +mccabe==0.7.0 ; python_full_version >= "3.7.2" and python_full_version < "3.8.0" mypy-extensions==0.4.3 ; python_version >= "3.7" and python_full_version < "3.8.0" -mypy==0.982 ; python_version >= "3.7" and python_full_version < "3.8.0" +mypy==0.991 ; python_version >= "3.7" and python_full_version < "3.8.0" ndg-httpsclient==0.3.2 ; python_full_version >= "3.7.0" and python_full_version < "3.8.0" oauth2client==4.0.0 ; python_full_version < "3.8.0" and python_version >= "3.7" packaging==21.3 ; python_version >= "3.7" and python_full_version < "3.8.0" parsedatetime==2.4 ; python_full_version < "3.8.0" and python_version >= "3.7" pbr==1.8.0 ; python_full_version >= "3.7.0" and python_full_version < "3.8.0" -pip==22.3 ; python_version >= "3.7" and python_full_version < "3.8.0" -platformdirs==2.5.2 ; python_version >= "3.7" and python_full_version < "3.8.0" +pip==22.3.1 ; python_version >= "3.7" and python_full_version < "3.8.0" +platformdirs==2.5.4 ; python_full_version < "3.8.0" and python_version >= "3.7" pluggy==1.0.0 ; python_version >= "3.7" and python_full_version < "3.8.0" ply==3.4 ; python_full_version < "3.8.0" and python_version >= "3.7" py==1.11.0 ; python_version >= "3.7" and python_full_version < "3.8.0" pyasn1-modules==0.0.10 ; python_full_version < "3.8.0" and python_version >= "3.7" pyasn1==0.1.9 ; python_full_version < "3.8.0" and python_version >= "3.7" pycparser==2.14 ; python_full_version < "3.8.0" and python_version >= "3.7" -pylint==2.13.9 ; python_full_version < "3.8.0" and python_version >= "3.7" +pylint==2.15.5 ; python_full_version >= "3.7.2" and python_full_version < "3.8.0" pyopenssl==17.5.0 ; python_full_version < "3.8.0" and python_version >= "3.7" pyparsing==2.2.1 ; python_full_version < "3.8.0" and python_version >= "3.7" pyrfc3339==1.0 ; python_full_version < "3.8.0" and python_version >= "3.7" pytest-cov==4.0.0 ; python_version >= "3.7" and python_full_version < "3.8.0" -pytest-forked==1.4.0 ; python_version >= "3.7" and python_full_version < "3.8.0" -pytest-xdist==2.5.0 ; python_version >= "3.7" and python_full_version < "3.8.0" -pytest==7.1.3 ; python_version >= "3.7" and python_full_version < "3.8.0" +pytest-xdist==3.0.2 ; python_version >= "3.7" and python_full_version < "3.8.0" +pytest==7.2.0 ; python_version >= "3.7" and python_full_version < "3.8.0" python-augeas==0.5.0 ; python_full_version < "3.8.0" and python_version >= "3.7" python-dateutil==2.8.2 ; python_full_version < "3.8.0" and python_version >= "3.7" python-digitalocean==1.11 ; python_full_version < "3.8.0" and python_version >= "3.7" pytz==2019.3 ; python_full_version < "3.8.0" and python_version >= "3.7" -pywin32==304 ; python_version >= "3.7" and python_full_version < "3.8.0" and sys_platform == "win32" +pywin32==305 ; python_version >= "3.7" and python_full_version < "3.8.0" and sys_platform == "win32" pyyaml==6.0 ; python_full_version < "3.8.0" and python_version >= "3.7" requests-file==1.5.1 ; python_version >= "3.7" and python_full_version < "3.8.0" -requests-toolbelt==0.10.0 ; python_version >= "3.7" and python_full_version < "3.8.0" requests==2.20.0 ; python_full_version < "3.8.0" and python_version >= "3.7" rsa==4.9 ; python_full_version < "3.8.0" and python_version >= "3.7" s3transfer==0.3.7 ; python_full_version < "3.8.0" and python_version >= "3.7" setuptools==41.6.0 ; python_full_version < "3.8.0" and python_version >= "3.7" six==1.11.0 ; python_full_version < "3.8.0" and python_version >= "3.7" tldextract==3.4.0 ; python_version >= "3.7" and python_full_version < "3.8.0" -tomli==2.0.1 ; python_version >= "3.7" and python_full_version < "3.8.0" +tomli==2.0.1 ; python_full_version < "3.8.0" and python_version >= "3.7" +tomlkit==0.11.6 ; python_full_version >= "3.7.2" and python_full_version < "3.8.0" tox==1.9.2 ; python_version >= "3.7" and python_full_version < "3.8.0" typed-ast==1.5.4 ; python_version < "3.8" and python_version >= "3.7" -types-cryptography==3.3.23.1 ; python_version >= "3.7" and python_full_version < "3.8.0" -types-mock==4.0.15.2 ; python_version >= "3.7" and python_full_version < "3.8.0" -types-pyopenssl==22.1.0.1 ; python_version >= "3.7" and python_full_version < "3.8.0" +types-cryptography==3.3.23.2 ; python_version >= "3.7" and python_full_version < "3.8.0" +types-pyopenssl==22.1.0.2 ; python_version >= "3.7" and python_full_version < "3.8.0" types-pyrfc3339==1.1.1 ; python_version >= "3.7" and python_full_version < "3.8.0" -types-python-dateutil==2.8.19.2 ; python_version >= "3.7" and python_full_version < "3.8.0" -types-pytz==2022.5.0.0 ; python_version >= "3.7" and python_full_version < "3.8.0" -types-requests==2.28.11.2 ; python_version >= "3.7" and python_full_version < "3.8.0" -types-setuptools==65.5.0.1 ; python_version >= "3.7" and python_full_version < "3.8.0" -types-six==1.16.21 ; python_version >= "3.7" and python_full_version < "3.8.0" -types-urllib3==1.26.25.1 ; python_version >= "3.7" and python_full_version < "3.8.0" +types-python-dateutil==2.8.19.4 ; python_version >= "3.7" and python_full_version < "3.8.0" +types-pytz==2022.6.0.1 ; python_version >= "3.7" and python_full_version < "3.8.0" +types-requests==2.28.11.5 ; python_version >= "3.7" and python_full_version < "3.8.0" +types-setuptools==65.5.0.3 ; python_version >= "3.7" and python_full_version < "3.8.0" +types-six==1.16.21.3 ; python_version >= "3.7" and python_full_version < "3.8.0" +types-urllib3==1.26.25.4 ; python_version >= "3.7" and python_full_version < "3.8.0" typing-extensions==4.4.0 ; python_version >= "3.7" and python_version < "3.8" uritemplate==3.0.1 ; python_full_version < "3.8.0" and python_version >= "3.7" urllib3==1.24.2 ; python_full_version < "3.8.0" and python_version >= "3.7" -virtualenv==20.16.5 ; python_version >= "3.7" and python_full_version < "3.8.0" +virtualenv==20.16.7 ; python_version >= "3.7" and python_full_version < "3.8.0" wheel==0.33.6 ; python_full_version < "3.8.0" and python_version >= "3.7" -wrapt==1.14.1 ; python_full_version < "3.8.0" and python_version >= "3.7" -zipp==3.9.0 ; python_version >= "3.7" and python_version < "3.8" -zope-component==4.1.0 ; python_full_version < "3.8.0" and python_version >= "3.7" -zope-event==4.0.3 ; python_full_version < "3.8.0" and python_version >= "3.7" -zope-hookable==4.0.4 ; python_full_version >= "3.7.0" and python_full_version < "3.8.0" -zope-interface==4.0.5 ; python_full_version < "3.8.0" and python_version >= "3.7" +wrapt==1.14.1 ; python_full_version >= "3.7.2" and python_full_version < "3.8.0" +zipp==3.10.0 ; python_version >= "3.7" and python_version < "3.8" diff --git a/tools/pinning/current/pyproject.toml b/tools/pinning/current/pyproject.toml index ae3df1a01..7db80d7fa 100644 --- a/tools/pinning/current/pyproject.toml +++ b/tools/pinning/current/pyproject.toml @@ -15,7 +15,6 @@ python = "^3.7" certbot-ci = {path = "../../../certbot-ci"} certbot-compatibility-test = {path = "../../../certbot-compatibility-test"} certbot-dns-cloudflare = {path = "../../../certbot-dns-cloudflare", extras = ["docs"]} -certbot-dns-cloudxns = {path = "../../../certbot-dns-cloudxns", extras = ["docs"]} certbot-dns-digitalocean = {path = "../../../certbot-dns-digitalocean", extras = ["docs"]} certbot-dns-dnsimple = {path = "../../../certbot-dns-dnsimple", extras = ["docs"]} certbot-dns-dnsmadeeasy = {path = "../../../certbot-dns-dnsmadeeasy", extras = ["docs"]} @@ -36,14 +35,6 @@ letstest = {path = "../../../letstest"} windows-installer = {path = "../../../windows-installer"} # Extra dependencies -# awscli is just listed here as a performance optimization. As of writing this, -# there are some conflicts in shared dependencies between it and other packages -# we depend on. To try and resolve them, poetry searches through older versions -# of awscli to see if that resolves the conflict, but there are over 1000 -# versions of awscli on PyPI so this process takes too long. Providing a high -# minimum version here prevents poetry from searching this path and it fairly -# quickly resolves the dependency conflict in another way. -awscli = ">=1.22.76" # As of writing this, cython is a build dependency of pyyaml. Since there # doesn't appear to be a good way to automatically track down and pin build # dependencies in Python (see @@ -51,13 +42,6 @@ awscli = ">=1.22.76" # as a dependency here to ensure a version of cython is pinned for extra # stability. cython = "*" -# We install mock in our "external-mock" tox environment to test that we didn't -# break Certbot's test API which used to always use mock objects from the 3rd -# party mock library. We list the mock dependency here so that is pinned, but -# we don't depend on it in Certbot to avoid installing mock when it's not -# needed. This dependency can be removed here once Certbot's support for the -# 3rd party mock library has been dropped. -mock = "*" # setuptools-rust is a build dependency of cryptography, and since we don't have # a great way of pinning build dependencies, we simply list it here to ensure a # working version. Note: if build dependencies of setuptools-rust break at some @@ -70,7 +54,7 @@ setuptools-rust = "*" # # If this pinning is removed, we may still need to add a lower bound for the # pylint version. See https://github.com/certbot/certbot/pull/9229. -pylint = "2.13.9" +pylint = { version="2.15.5", python = ">=3.7.2" } # Bug in poetry, where still installes yanked versions from pypi (source: https://github.com/python-poetry/poetry/issues/2453) # this version of cryptography introduced a security vulnrability. diff --git a/tools/pinning/oldest/pyproject.toml b/tools/pinning/oldest/pyproject.toml index 77721e259..6ab7c628c 100644 --- a/tools/pinning/oldest/pyproject.toml +++ b/tools/pinning/oldest/pyproject.toml @@ -18,7 +18,6 @@ python = "3.7.*" # on acme so certbot must be listed before acme. certbot-ci = {path = "../../../certbot-ci"} certbot-dns-cloudflare = {path = "../../../certbot-dns-cloudflare"} -certbot-dns-cloudxns = {path = "../../../certbot-dns-cloudxns"} certbot-dns-digitalocean = {path = "../../../certbot-dns-digitalocean"} certbot-dns-dnsimple = {path = "../../../certbot-dns-dnsimple"} certbot-dns-dnsmadeeasy = {path = "../../../certbot-dns-dnsmadeeasy"} @@ -62,7 +61,6 @@ google-api-python-client = "1.5.5" httplib2 = "0.9.2" idna = "2.6" ipaddress = "1.0.16" -mock = "1.0.1" ndg-httpsclient = "0.3.2" oauth2client = "4.0.0" parsedatetime = "2.4" @@ -80,11 +78,6 @@ requests = "2.20.0" setuptools = "41.6.0" six = "1.11.0" urllib3 = "1.24.2" -# Package names containing "." need to be quoted. -"zope.component" = "4.1.0" -"zope.event" = "4.0.3" -"zope.hookable" = "4.0.4" -"zope.interface" = "4.0.5" # Build dependencies # Since there doesn't appear to @@ -104,14 +97,6 @@ cython = "*" # wheel 0.34.0 is buggy). wheel = "<0.34.0" -# pylint often adds new checks that we need to conform our code to when -# upgrading our dependencies. To help control when this needs to be done, we -# pin pylint to a compatible version here. -# -# If this pinning is removed, we may still need to add a lower bound for the -# pylint version. See https://github.com/certbot/certbot/pull/9229. -pylint = "2.13.9" - [tool.poetry.dev-dependencies] [build-system] diff --git a/tools/requirements.txt b/tools/requirements.txt index cbfe6f865..905d96cc7 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -8,9 +8,8 @@ alabaster==0.7.12 ; python_version >= "3.7" and python_version < "4.0" apacheconfig==0.3.2 ; python_version >= "3.7" and python_version < "4.0" appnope==0.1.3 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "darwin" -astroid==2.11.7 ; python_version >= "3.7" and python_version < "4.0" +astroid==2.12.13 ; python_full_version >= "3.7.2" and python_version < "4.0" attrs==22.1.0 ; python_version >= "3.7" and python_version < "4.0" -awscli==1.27.0 ; python_version >= "3.7" and python_version < "4.0" azure-devops==6.0.0b4 ; python_version >= "3.7" and python_version < "4.0" babel==2.11.0 ; python_version >= "3.7" and python_version < "4.0" backcall==0.2.0 ; python_version >= "3.7" and python_version < "4.0" @@ -18,8 +17,8 @@ backports-cached-property==1.0.2 ; python_version >= "3.7" and python_version < bcrypt==4.0.1 ; python_version >= "3.7" and python_version < "4.0" beautifulsoup4==4.11.1 ; python_version >= "3.7" and python_version < "4.0" bleach==5.0.1 ; python_version >= "3.7" and python_version < "4.0" -boto3==1.26.0 ; python_version >= "3.7" and python_version < "4.0" -botocore==1.29.0 ; python_version >= "3.7" and python_version < "4.0" +boto3==1.26.13 ; python_version >= "3.7" and python_version < "4.0" +botocore==1.29.13 ; python_version >= "3.7" and python_version < "4.0" cachecontrol==0.12.11 ; python_version >= "3.7" and python_version < "4.0" cachetools==5.2.0 ; python_version >= "3.7" and python_version < "4.0" cachy==0.3.0 ; python_version >= "3.7" and python_version < "4.0" @@ -27,8 +26,8 @@ certifi==2022.9.24 ; python_version >= "3.7" and python_version < "4" cffi==1.15.1 ; python_version >= "3.7" and python_version < "4.0" charset-normalizer==2.1.1 ; python_version >= "3.7" and python_version < "4" cleo==1.0.0a5 ; python_version >= "3.7" and python_version < "4.0" -cloudflare==2.10.2 ; python_version >= "3.7" and python_version < "4.0" -colorama==0.4.4 ; python_version >= "3.7" and python_version < "4.0" +cloudflare==2.10.5 ; python_version >= "3.7" and python_version < "4.0" +colorama==0.4.6 ; python_version < "4.0" and sys_platform == "win32" and python_version >= "3.7" or python_version >= "3.7" and python_version < "4.0" and platform_system == "Windows" commonmark==0.9.1 ; python_version >= "3.7" and python_version < "4.0" configargparse==1.5.3 ; python_version >= "3.7" and python_version < "4.0" configobj==5.0.6 ; python_version >= "3.7" and python_version < "4.0" @@ -37,22 +36,22 @@ crashtest==0.3.1 ; python_version >= "3.7" and python_version < "4.0" cryptography==38.0.3 ; python_version >= "3.7" and python_version < "4.0" cython==0.29.32 ; python_version >= "3.7" and python_version < "4.0" decorator==5.1.1 ; python_version >= "3.7" and python_version < "4.0" -dill==0.3.6 ; python_version >= "3.7" and python_version < "4.0" +dill==0.3.6 ; python_full_version >= "3.7.2" and python_version < "4.0" distlib==0.3.6 ; python_version >= "3.7" and python_version < "4.0" distro==1.8.0 ; python_version >= "3.7" and python_version < "4.0" dns-lexicon==3.11.7 ; python_version >= "3.7" and python_version < "4.0" dnspython==2.2.1 ; python_version >= "3.7" and python_version < "4.0" -docutils==0.16 ; python_version >= "3.7" and python_version < "4.0" +docutils==0.17.1 ; python_version >= "3.7" and python_version < "4.0" dulwich==0.20.50 ; python_version >= "3.7" and python_version < "4.0" -exceptiongroup==1.0.0 ; python_version >= "3.7" and python_version < "3.11" +exceptiongroup==1.0.4 ; python_version >= "3.7" and python_version < "3.11" execnet==1.9.0 ; python_version >= "3.7" and python_version < "4.0" fabric==2.7.1 ; python_version >= "3.7" and python_version < "4.0" filelock==3.8.0 ; python_version >= "3.7" and python_version < "4.0" google-api-core==2.10.2 ; python_version >= "3.7" and python_version < "4.0" -google-api-python-client==2.65.0 ; python_version >= "3.7" and python_version < "4.0" +google-api-python-client==2.66.0 ; python_version >= "3.7" and python_version < "4.0" google-auth-httplib2==0.1.0 ; python_version >= "3.7" and python_version < "4.0" -google-auth==2.14.0 ; python_version >= "3.7" and python_version < "4.0" -googleapis-common-protos==1.56.4 ; python_version >= "3.7" and python_version < "4.0" +google-auth==2.14.1 ; python_version >= "3.7" and python_version < "4.0" +googleapis-common-protos==1.57.0 ; python_version >= "3.7" and python_version < "4.0" html5lib==1.1 ; python_version >= "3.7" and python_version < "4.0" httplib2==0.21.0 ; python_version >= "3.7" and python_version < "4.0" idna==3.4 ; python_version >= "3.7" and python_version < "4" @@ -64,7 +63,7 @@ invoke==1.7.3 ; python_version >= "3.7" and python_version < "4.0" ipdb==0.13.9 ; python_version >= "3.7" and python_version < "4.0" ipython==7.34.0 ; python_version >= "3.7" and python_version < "4.0" isodate==0.6.1 ; python_version >= "3.7" and python_version < "4.0" -isort==5.10.1 ; python_version >= "3.7" and python_version < "4.0" +isort==5.10.1 ; python_full_version >= "3.7.2" and python_version < "4.0" jaraco-classes==3.2.3 ; python_version >= "3.7" and python_version < "4.0" jedi==0.18.1 ; python_version >= "3.7" and python_version < "4.0" jeepney==0.8.0 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "linux" @@ -74,37 +73,36 @@ josepy==1.13.0 ; python_version >= "3.7" and python_version < "4.0" jsonlines==3.1.0 ; python_version >= "3.7" and python_version < "4.0" jsonpickle==2.2.0 ; python_version >= "3.7" and python_version < "4.0" jsonschema==4.17.0 ; python_version >= "3.7" and python_version < "4.0" -keyring==23.9.3 ; python_version >= "3.7" and python_version < "4.0" -lazy-object-proxy==1.8.0 ; python_version >= "3.7" and python_version < "4.0" +keyring==23.11.0 ; python_version >= "3.7" and python_version < "4.0" +lazy-object-proxy==1.8.0 ; python_full_version >= "3.7.2" and python_version < "4.0" lockfile==0.12.2 ; python_version >= "3.7" and python_version < "4.0" markupsafe==2.1.1 ; python_version >= "3.7" and python_version < "4.0" matplotlib-inline==0.1.6 ; python_version >= "3.7" and python_version < "4.0" -mccabe==0.7.0 ; python_version >= "3.7" and python_version < "4.0" -mock==4.0.3 ; python_version >= "3.7" and python_version < "4.0" +mccabe==0.7.0 ; python_full_version >= "3.7.2" and python_version < "4.0" more-itertools==9.0.0 ; python_version >= "3.7" and python_version < "4.0" msgpack==1.0.4 ; python_version >= "3.7" and python_version < "4.0" msrest==0.6.21 ; python_version >= "3.7" and python_version < "4.0" mypy-extensions==0.4.3 ; python_version >= "3.7" and python_version < "4.0" -mypy==0.982 ; python_version >= "3.7" and python_version < "4.0" +mypy==0.991 ; python_version >= "3.7" and python_version < "4.0" oauth2client==4.1.3 ; python_version >= "3.7" and python_version < "4.0" oauthlib==3.2.2 ; python_version >= "3.7" and python_version < "4.0" packaging==21.3 ; python_version >= "3.7" and python_version < "4.0" -paramiko==2.11.0 ; python_version >= "3.7" and python_version < "4.0" +paramiko==2.12.0 ; python_version >= "3.7" and python_version < "4.0" parsedatetime==2.6 ; python_version >= "3.7" and python_version < "4.0" parso==0.8.3 ; python_version >= "3.7" and python_version < "4.0" pathlib2==2.3.7.post1 ; python_version >= "3.7" and python_version < "4.0" pexpect==4.8.0 ; python_version >= "3.7" and python_version < "4.0" pickleshare==0.7.5 ; python_version >= "3.7" and python_version < "4.0" -pip==22.3 ; python_version >= "3.7" and python_version < "4.0" +pip==22.3.1 ; python_version >= "3.7" and python_version < "4.0" pkginfo==1.8.3 ; python_version >= "3.7" and python_version < "4.0" pkgutil-resolve-name==1.3.10 ; python_version >= "3.7" and python_version < "3.9" -platformdirs==2.5.2 ; python_version >= "3.7" and python_version < "4.0" +platformdirs==2.5.4 ; python_version < "4.0" and python_version >= "3.7" pluggy==1.0.0 ; python_version >= "3.7" and python_version < "4.0" ply==3.11 ; python_version >= "3.7" and python_version < "4.0" poetry-core==1.3.2 ; python_version >= "3.7" and python_version < "4.0" -poetry-plugin-export==1.1.2 ; python_version >= "3.7" and python_version < "4.0" +poetry-plugin-export==1.2.0 ; python_version >= "3.7" and python_version < "4.0" poetry==1.2.2 ; python_version >= "3.7" and python_version < "4.0" -prompt-toolkit==3.0.31 ; python_version >= "3.7" and python_version < "4.0" +prompt-toolkit==3.0.33 ; python_version >= "3.7" and python_version < "4.0" protobuf==4.21.9 ; python_version >= "3.7" and python_version < "4.0" ptyprocess==0.7.0 ; python_version >= "3.7" and python_version < "4.0" py==1.11.0 ; python_version >= "3.7" and python_version < "4.0" @@ -113,13 +111,13 @@ pyasn1==0.4.8 ; python_version >= "3.7" and python_version < "4.0" pycparser==2.21 ; python_version >= "3.7" and python_version < "4.0" pygments==2.13.0 ; python_version >= "3.7" and python_version < "4.0" pylev==1.4.0 ; python_version >= "3.7" and python_version < "4.0" -pylint==2.13.9 ; python_version >= "3.7" and python_version < "4.0" +pylint==2.15.5 ; python_full_version >= "3.7.2" and python_version < "4.0" pynacl==1.5.0 ; python_version >= "3.7" and python_version < "4.0" pynsist==2.7 ; python_version >= "3.7" and python_version < "4.0" pyopenssl==22.1.0 ; python_version >= "3.7" and python_version < "4.0" pyparsing==3.0.9 ; python_version >= "3.7" and python_version < "4.0" pyrfc3339==1.1 ; python_version >= "3.7" and python_version < "4.0" -pyrsistent==0.19.1 ; python_version >= "3.7" and python_version < "4.0" +pyrsistent==0.19.2 ; python_version >= "3.7" and python_version < "4.0" pytest-cov==4.0.0 ; python_version >= "3.7" and python_version < "4.0" pytest-xdist==3.0.2 ; python_version >= "3.7" and python_version < "4.0" pytest==7.2.0 ; python_version >= "3.7" and python_version < "4.0" @@ -128,8 +126,8 @@ python-dateutil==2.8.2 ; python_version >= "3.7" and python_version < "4.0" python-digitalocean==1.17.0 ; python_version >= "3.7" and python_version < "4.0" pytz==2022.6 ; python_version >= "3.7" and python_version < "4.0" pywin32-ctypes==0.2.0 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32" -pywin32==304 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32" -pyyaml==5.4.1 ; python_version >= "3.7" and python_version < "4.0" +pywin32==305 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32" +pyyaml==6.0 ; python_version >= "3.7" and python_version < "4.0" readme-renderer==37.3 ; python_version >= "3.7" and python_version < "4.0" requests-download==0.1.2 ; python_version >= "3.7" and python_version < "4.0" requests-file==1.5.1 ; python_version >= "3.7" and python_version < "4.0" @@ -138,18 +136,18 @@ requests-toolbelt==0.9.1 ; python_version >= "3.7" and python_version < "4.0" requests==2.28.1 ; python_version >= "3.7" and python_version < "4" rfc3986==2.0.0 ; python_version >= "3.7" and python_version < "4.0" rich==12.6.0 ; python_version >= "3.7" and python_version < "4.0" -rsa==4.7.2 ; python_version >= "3.7" and python_version < "4" +rsa==4.9 ; python_version >= "3.7" and python_version < "4" s3transfer==0.6.0 ; python_version >= "3.7" and python_version < "4.0" secretstorage==3.3.3 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "linux" semantic-version==2.10.0 ; python_version >= "3.7" and python_version < "4.0" setuptools-rust==1.5.2 ; python_version >= "3.7" and python_version < "4.0" -setuptools==65.5.0 ; python_version >= "3.7" and python_version < "4.0" +setuptools==65.6.0 ; python_version >= "3.7" and python_version < "4.0" shellingham==1.5.0 ; python_version >= "3.7" and python_version < "4.0" six==1.16.0 ; python_version >= "3.7" and python_version < "4.0" snowballstemmer==2.2.0 ; python_version >= "3.7" and python_version < "4.0" soupsieve==2.3.2.post1 ; python_version >= "3.7" and python_version < "4.0" -sphinx-rtd-theme==1.1.0 ; python_version >= "3.7" and python_version < "4.0" -sphinx==5.1.1 ; python_version >= "3.7" and python_version < "4.0" +sphinx-rtd-theme==1.1.1 ; python_version >= "3.7" and python_version < "4.0" +sphinx==5.3.0 ; python_version >= "3.7" and python_version < "4.0" sphinxcontrib-applehelp==1.0.2 ; python_version >= "3.7" and python_version < "4.0" sphinxcontrib-devhelp==1.0.2 ; python_version >= "3.7" and python_version < "4.0" sphinxcontrib-htmlhelp==2.0.0 ; python_version >= "3.7" and python_version < "4.0" @@ -159,33 +157,28 @@ sphinxcontrib-serializinghtml==1.1.5 ; python_version >= "3.7" and python_versio tldextract==3.4.0 ; python_version >= "3.7" and python_version < "4.0" toml==0.10.2 ; python_version >= "3.7" and python_version < "4.0" tomli==2.0.1 ; python_version >= "3.7" and python_full_version <= "3.11.0a6" -tomlkit==0.11.6 ; python_version >= "3.7" and python_version < "4.0" -tox==3.27.0 ; python_version >= "3.7" and python_version < "4.0" +tomlkit==0.11.6 ; python_version < "4.0" and python_version >= "3.7" +tox==3.27.1 ; python_version >= "3.7" and python_version < "4.0" traitlets==5.5.0 ; python_version >= "3.7" and python_version < "4.0" twine==4.0.1 ; python_version >= "3.7" and python_version < "4.0" -typed-ast==1.5.4 ; python_version >= "3.7" and python_version < "3.8" -types-cryptography==3.3.23.1 ; python_version >= "3.7" and python_version < "4.0" -types-mock==4.0.15.2 ; python_version >= "3.7" and python_version < "4.0" -types-pyopenssl==22.1.0.1 ; python_version >= "3.7" and python_version < "4.0" -types-pyrfc3339==1.1.1 ; python_version >= "3.7" and python_version < "4.0" -types-python-dateutil==2.8.19.2 ; python_version >= "3.7" and python_version < "4.0" +typed-ast==1.5.4 ; python_version < "3.8" and python_version >= "3.7" +types-cryptography==3.3.23.2 ; python_version >= "3.7" and python_version < "4.0" +types-pyopenssl==22.1.0.2 ; python_version >= "3.7" and python_version < "4.0" +types-pyrfc3339==1.1.1.1 ; python_version >= "3.7" and python_version < "4.0" +types-python-dateutil==2.8.19.4 ; python_version >= "3.7" and python_version < "4.0" types-pytz==2022.6.0.1 ; python_version >= "3.7" and python_version < "4.0" -types-requests==2.28.11.2 ; python_version >= "3.7" and python_version < "4.0" -types-setuptools==65.5.0.2 ; python_version >= "3.7" and python_version < "4.0" -types-six==1.16.21 ; python_version >= "3.7" and python_version < "4.0" -types-urllib3==1.26.25.1 ; python_version >= "3.7" and python_version < "4.0" +types-requests==2.28.11.5 ; python_version >= "3.7" and python_version < "4.0" +types-setuptools==65.6.0.0 ; python_version >= "3.7" and python_version < "4.0" +types-six==1.16.21.4 ; python_version >= "3.7" and python_version < "4.0" +types-urllib3==1.26.25.4 ; python_version >= "3.7" and python_version < "4.0" typing-extensions==4.4.0 ; python_version >= "3.7" and python_version < "4.0" uritemplate==4.1.1 ; python_version >= "3.7" and python_version < "4.0" urllib3==1.26.12 ; python_version >= "3.7" and python_version < "4" -virtualenv==20.16.6 ; python_version >= "3.7" and python_version < "4.0" +virtualenv==20.16.7 ; python_version >= "3.7" and python_version < "4.0" wcwidth==0.2.5 ; python_version >= "3.7" and python_version < "4.0" webencodings==0.5.1 ; python_version >= "3.7" and python_version < "4.0" -wheel==0.37.1 ; python_version >= "3.7" and python_version < "4.0" -wrapt==1.14.1 ; python_version >= "3.7" and python_version < "4.0" +wheel==0.38.4 ; python_version >= "3.7" and python_version < "4.0" +wrapt==1.14.1 ; python_full_version >= "3.7.2" and python_version < "4.0" xattr==0.9.9 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "darwin" yarg==0.1.9 ; python_version >= "3.7" and python_version < "4.0" zipp==3.10.0 ; python_version >= "3.7" and python_version < "4.0" -zope-component==5.0.1 ; python_version >= "3.7" and python_version < "4.0" -zope-event==4.5.0 ; python_version >= "3.7" and python_version < "4.0" -zope-hookable==5.2 ; python_version >= "3.7" and python_version < "4.0" -zope-interface==5.5.0 ; python_version >= "3.7" and python_version < "4.0" diff --git a/tools/venv.py b/tools/venv.py index 2aa5b0a2d..0721b2b25 100755 --- a/tools/venv.py +++ b/tools/venv.py @@ -27,7 +27,6 @@ REQUIREMENTS = [ '-e certbot[all]', '-e certbot-apache', '-e certbot-dns-cloudflare', - '-e certbot-dns-cloudxns', '-e certbot-dns-digitalocean', '-e certbot-dns-dnsimple', '-e certbot-dns-dnsmadeeasy', diff --git a/tox.cover.py b/tox.cover.py index 1f8f9dcce..f1e400eb8 100755 --- a/tox.cover.py +++ b/tox.cover.py @@ -7,7 +7,7 @@ import subprocess import sys DEFAULT_PACKAGES = [ - 'certbot', 'acme', 'certbot_apache', 'certbot_dns_cloudflare', 'certbot_dns_cloudxns', + 'certbot', 'acme', 'certbot_apache', 'certbot_dns_cloudflare', 'certbot_dns_digitalocean', 'certbot_dns_dnsimple', 'certbot_dns_dnsmadeeasy', 'certbot_dns_gehirn', 'certbot_dns_google', 'certbot_dns_linode', 'certbot_dns_luadns', 'certbot_dns_nsone', 'certbot_dns_ovh', 'certbot_dns_rfc2136', 'certbot_dns_route53', @@ -18,7 +18,6 @@ COVER_THRESHOLDS = { 'acme': {'linux': 100, 'windows': 99}, 'certbot_apache': {'linux': 100, 'windows': 100}, 'certbot_dns_cloudflare': {'linux': 98, 'windows': 98}, - 'certbot_dns_cloudxns': {'linux': 98, 'windows': 98}, 'certbot_dns_digitalocean': {'linux': 98, 'windows': 98}, 'certbot_dns_dnsimple': {'linux': 98, 'windows': 98}, 'certbot_dns_dnsmadeeasy': {'linux': 99, 'windows': 99}, diff --git a/tox.ini b/tox.ini index 03f638c0e..8125304f7 100644 --- a/tox.ini +++ b/tox.ini @@ -17,10 +17,10 @@ install_and_test = python {toxinidir}/tools/install_and_test.py # Packages are listed on one line because tox seems to have inconsistent # behavior with substitutions that contain line continuations, see # https://github.com/tox-dev/tox/issues/2069 for more info. -dns_packages = certbot-dns-cloudflare certbot-dns-cloudxns certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-dnsmadeeasy certbot-dns-gehirn certbot-dns-google certbot-dns-linode certbot-dns-luadns certbot-dns-nsone certbot-dns-ovh certbot-dns-rfc2136 certbot-dns-route53 certbot-dns-sakuracloud +dns_packages = certbot-dns-cloudflare certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-dnsmadeeasy certbot-dns-gehirn certbot-dns-google certbot-dns-linode certbot-dns-luadns certbot-dns-nsone certbot-dns-ovh certbot-dns-rfc2136 certbot-dns-route53 certbot-dns-sakuracloud win_all_packages = acme[test] certbot[test] {[base]dns_packages} certbot-nginx all_packages = {[base]win_all_packages} certbot-apache -source_paths = acme/acme certbot/certbot certbot-apache/certbot_apache certbot-ci/certbot_integration_tests certbot-ci/snap_integration_tests certbot-ci/windows_installer_integration_tests certbot-compatibility-test/certbot_compatibility_test certbot-dns-cloudflare/certbot_dns_cloudflare certbot-dns-cloudxns/certbot_dns_cloudxns certbot-dns-digitalocean/certbot_dns_digitalocean certbot-dns-dnsimple/certbot_dns_dnsimple certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy certbot-dns-gehirn/certbot_dns_gehirn certbot-dns-google/certbot_dns_google certbot-dns-linode/certbot_dns_linode certbot-dns-luadns/certbot_dns_luadns certbot-dns-nsone/certbot_dns_nsone certbot-dns-ovh/certbot_dns_ovh certbot-dns-rfc2136/certbot_dns_rfc2136 certbot-dns-route53/certbot_dns_route53 certbot-dns-sakuracloud/certbot_dns_sakuracloud certbot-nginx/certbot_nginx tests/lock_test.py +source_paths = acme/acme certbot/certbot certbot-apache/certbot_apache certbot-ci/certbot_integration_tests certbot-ci/snap_integration_tests certbot-ci/windows_installer_integration_tests certbot-compatibility-test/certbot_compatibility_test certbot-dns-cloudflare/certbot_dns_cloudflare certbot-dns-digitalocean/certbot_dns_digitalocean certbot-dns-dnsimple/certbot_dns_dnsimple certbot-dns-dnsmadeeasy/certbot_dns_dnsmadeeasy certbot-dns-gehirn/certbot_dns_gehirn certbot-dns-google/certbot_dns_google certbot-dns-linode/certbot_dns_linode certbot-dns-luadns/certbot_dns_luadns certbot-dns-nsone/certbot_dns_nsone certbot-dns-ovh/certbot_dns_ovh certbot-dns-rfc2136/certbot_dns_rfc2136 certbot-dns-route53/certbot_dns_route53 certbot-dns-sakuracloud/certbot_dns_sakuracloud certbot-nginx/certbot_nginx tests/lock_test.py [testenv] passenv = @@ -112,11 +112,6 @@ commands = setenv = {[testenv:oldest]setenv} -[testenv:external-mock] -commands = - python {toxinidir}/tools/pip_install.py mock - {[base]install_and_test} {[base]all_packages} - [testenv:lint{,-win,-posix}] basepython = python3 # separating into multiple invocations disables cross package diff --git a/windows-installer/setup.py b/windows-installer/setup.py index 3729052a0..c1f038749 100644 --- a/windows-installer/setup.py +++ b/windows-installer/setup.py @@ -22,6 +22,7 @@ setup( 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Operating System :: Microsoft :: Windows', 'Topic :: Software Development :: Build Tools', ],