mirror of
https://github.com/certbot/certbot.git
synced 2026-08-02 19:22:16 +02:00
Remove the Postfix plugin (#7097)
* Remove the postfix plugin. * Remove references to postfix plugin in code. * Remove reference to postfix plugin in docs.
This commit is contained in:
committed by
Adrien Ferrand
parent
5385375571
commit
0c5f526f8b
+1
-1
@@ -57,7 +57,7 @@ matrix:
|
|||||||
env: TOXENV=mypy
|
env: TOXENV=mypy
|
||||||
<<: *not-on-master
|
<<: *not-on-master
|
||||||
- python: "2.7"
|
- python: "2.7"
|
||||||
env: TOXENV='py27-{acme,apache,certbot,dns,nginx,postfix}-oldest'
|
env: TOXENV='py27-{acme,apache,certbot,dns,nginx}-oldest'
|
||||||
sudo: required
|
sudo: required
|
||||||
services: docker
|
services: docker
|
||||||
<<: *not-on-master
|
<<: *not-on-master
|
||||||
|
|||||||
@@ -1,190 +0,0 @@
|
|||||||
Copyright 2017 Electronic Frontier Foundation and others
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
include LICENSE.txt
|
|
||||||
include README.rst
|
|
||||||
recursive-include certbot_postfix/testdata *
|
|
||||||
recursive-include certbot_postfix/docs *
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
==========================
|
|
||||||
Postfix plugin for Certbot
|
|
||||||
==========================
|
|
||||||
|
|
||||||
Note: this MTA installer is in **developer beta**-- we appreciate any testing, feedback, or
|
|
||||||
feature requests for this plugin.
|
|
||||||
|
|
||||||
To install this plugin, in the root of this repo, run::
|
|
||||||
|
|
||||||
python tools/venv.py
|
|
||||||
source venv/bin/activate
|
|
||||||
|
|
||||||
You can use this installer with any `authenticator plugin
|
|
||||||
<https://certbot.eff.org/docs/using.html#getting-certificates-and-choosing-plugins>`_.
|
|
||||||
For instance, with the `standalone authenticator
|
|
||||||
<https://certbot.eff.org/docs/using.html#standalone>`_, which requires no extra server
|
|
||||||
software, you might run::
|
|
||||||
|
|
||||||
sudo ./venv/bin/certbot run --standalone -i postfix -d <domain name>
|
|
||||||
|
|
||||||
To just install existing certs with this plugin, run::
|
|
||||||
|
|
||||||
sudo ./venv/bin/certbot install -i postfix --cert-path <path to cert> --key-path <path to key> -d <domain name>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""Certbot Postfix plugin."""
|
|
||||||
|
|
||||||
from certbot_postfix.installer import Installer
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
"""Postfix plugin constants."""
|
|
||||||
|
|
||||||
# pylint: disable=unused-import, no-name-in-module
|
|
||||||
from acme.magic_typing import Dict, Tuple, Union
|
|
||||||
# pylint: enable=unused-import, no-name-in-module
|
|
||||||
|
|
||||||
MINIMUM_VERSION = (2, 11,)
|
|
||||||
|
|
||||||
# If the value of a default VAR is a tuple, then the values which
|
|
||||||
# come LATER in the tuple are more strict/more secure.
|
|
||||||
# Certbot will default to the first value in the tuple, but will
|
|
||||||
# not override "more secure" settings.
|
|
||||||
|
|
||||||
ACCEPTABLE_SERVER_SECURITY_LEVELS = ("may", "encrypt")
|
|
||||||
ACCEPTABLE_CLIENT_SECURITY_LEVELS = ("may", "encrypt",
|
|
||||||
"dane", "dane-only",
|
|
||||||
"fingerprint",
|
|
||||||
"verify", "secure")
|
|
||||||
ACCEPTABLE_CIPHER_LEVELS = ("medium", "high")
|
|
||||||
|
|
||||||
# Exporting certain ciphers to prevent logjam: https://weakdh.org/sysadmin.html
|
|
||||||
EXCLUDE_CIPHERS = ("aNULL, eNULL, EXPORT, DES, RC4, MD5, PSK, aECDH, "
|
|
||||||
"EDH-DSS-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA, KRB5-DES, CBC3-SHA",)
|
|
||||||
|
|
||||||
|
|
||||||
TLS_VERSIONS = ("SSLv2", "SSLv3", "TLSv1", "TLSv1.1", "TLSv1.2")
|
|
||||||
# Should NOT use SSLv2/3.
|
|
||||||
ACCEPTABLE_TLS_VERSIONS = ("TLSv1", "TLSv1.1", "TLSv1.2")
|
|
||||||
|
|
||||||
# Variables associated with enabling opportunistic TLS.
|
|
||||||
TLS_SERVER_VARS = {
|
|
||||||
"smtpd_tls_security_level": ACCEPTABLE_SERVER_SECURITY_LEVELS,
|
|
||||||
} # type:Dict[str, Tuple[str, ...]]
|
|
||||||
TLS_CLIENT_VARS = {
|
|
||||||
"smtp_tls_security_level": ACCEPTABLE_CLIENT_SECURITY_LEVELS,
|
|
||||||
} # type:Dict[str, Tuple[str, ...]]
|
|
||||||
# Default variables for a secure MTA server [receiver].
|
|
||||||
DEFAULT_SERVER_VARS = {
|
|
||||||
"smtpd_tls_auth_only": ("yes",),
|
|
||||||
"smtpd_tls_mandatory_protocols": ("!SSLv2, !SSLv3",),
|
|
||||||
"smtpd_tls_protocols": ("!SSLv2, !SSLv3",),
|
|
||||||
"smtpd_tls_ciphers": ACCEPTABLE_CIPHER_LEVELS,
|
|
||||||
"smtpd_tls_mandatory_ciphers": ACCEPTABLE_CIPHER_LEVELS,
|
|
||||||
"smtpd_tls_exclude_ciphers": EXCLUDE_CIPHERS,
|
|
||||||
"smtpd_tls_eecdh_grade": ("strong",),
|
|
||||||
} # type:Dict[str, Tuple[str, ...]]
|
|
||||||
|
|
||||||
# Default variables for a secure MTA client [sender].
|
|
||||||
DEFAULT_CLIENT_VARS = {
|
|
||||||
"smtp_tls_ciphers": ACCEPTABLE_CIPHER_LEVELS,
|
|
||||||
"smtp_tls_exclude_ciphers": EXCLUDE_CIPHERS,
|
|
||||||
"smtp_tls_mandatory_ciphers": ACCEPTABLE_CIPHER_LEVELS,
|
|
||||||
} # type:Dict[str, Tuple[str, ...]]
|
|
||||||
|
|
||||||
CLI_DEFAULTS = dict(
|
|
||||||
config_dir="/etc/postfix",
|
|
||||||
ctl="postfix",
|
|
||||||
config_utility="postconf",
|
|
||||||
tls_only=False,
|
|
||||||
ignore_master_overrides=False,
|
|
||||||
server_only=False,
|
|
||||||
)
|
|
||||||
"""CLI defaults."""
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
"""certbot installer plugin for postfix."""
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import six
|
|
||||||
import zope.component
|
|
||||||
import zope.interface
|
|
||||||
|
|
||||||
# pylint: disable=unused-import, no-name-in-module
|
|
||||||
from acme.magic_typing import Callable, Dict, List
|
|
||||||
# pylint: enable=unused-import, no-name-in-module
|
|
||||||
|
|
||||||
from certbot import errors
|
|
||||||
from certbot import interfaces
|
|
||||||
from certbot import util as certbot_util
|
|
||||||
from certbot.compat import os
|
|
||||||
from certbot.plugins import common as plugins_common
|
|
||||||
|
|
||||||
from certbot_postfix import constants
|
|
||||||
from certbot_postfix import postconf
|
|
||||||
from certbot_postfix import util
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
@zope.interface.implementer(interfaces.IInstaller)
|
|
||||||
@zope.interface.provider(interfaces.IPluginFactory)
|
|
||||||
class Installer(plugins_common.Installer):
|
|
||||||
"""Certbot installer plugin for Postfix.
|
|
||||||
|
|
||||||
:ivar str config_dir: Postfix configuration directory to modify
|
|
||||||
:ivar list save_notes: documentation for proposed changes. This is
|
|
||||||
cleared and stored in Certbot checkpoints when save() is called
|
|
||||||
|
|
||||||
:ivar postconf: Wrapper for Postfix configuration command-line tool.
|
|
||||||
:type postconf: :class: `certbot_postfix.postconf.ConfigMain`
|
|
||||||
:ivar postfix: Wrapper for Postfix command-line tool.
|
|
||||||
:type postfix: :class: `certbot_postfix.util.PostfixUtil`
|
|
||||||
"""
|
|
||||||
|
|
||||||
description = "Configure TLS with the Postfix MTA"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def add_parser_arguments(cls, add):
|
|
||||||
add("ctl", default=constants.CLI_DEFAULTS["ctl"],
|
|
||||||
help="Path to the 'postfix' control program.")
|
|
||||||
# This directory points to Postfix's configuration directory.
|
|
||||||
add("config-dir", default=constants.CLI_DEFAULTS["config_dir"],
|
|
||||||
help="Path to the directory containing the "
|
|
||||||
"Postfix main.cf file to modify instead of using the "
|
|
||||||
"default configuration paths.")
|
|
||||||
add("config-utility", default=constants.CLI_DEFAULTS["config_utility"],
|
|
||||||
help="Path to the 'postconf' executable.")
|
|
||||||
add("tls-only", action="store_true", default=constants.CLI_DEFAULTS["tls_only"],
|
|
||||||
help="Only set params to enable opportunistic TLS and install certificates.")
|
|
||||||
add("server-only", action="store_true", default=constants.CLI_DEFAULTS["server_only"],
|
|
||||||
help="Only set server params (prefixed with smtpd*)")
|
|
||||||
add("ignore-master-overrides", action="store_true",
|
|
||||||
default=constants.CLI_DEFAULTS["ignore_master_overrides"],
|
|
||||||
help="Ignore errors reporting overridden TLS parameters in master.cf.")
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(Installer, self).__init__(*args, **kwargs)
|
|
||||||
# Wrapper around postconf commands
|
|
||||||
self.postfix = None
|
|
||||||
self.postconf = None
|
|
||||||
|
|
||||||
# Files to save
|
|
||||||
self.save_notes = [] # type: List[str]
|
|
||||||
|
|
||||||
self._enhance_func = {} # type: Dict[str, Callable[[str, str], None]]
|
|
||||||
# Since we only need to enable TLS once for all domains,
|
|
||||||
# keep track of whether this enhancement was already called.
|
|
||||||
self._tls_enabled = False
|
|
||||||
|
|
||||||
def prepare(self):
|
|
||||||
"""Prepare the installer.
|
|
||||||
|
|
||||||
:raises errors.PluginError: when an unexpected error occurs
|
|
||||||
:raises errors.MisconfigurationError: when the config is invalid
|
|
||||||
:raises errors.NoInstallationError: when can't find installation
|
|
||||||
:raises errors.NotSupportedError: when version is not supported
|
|
||||||
"""
|
|
||||||
# Verify postfix and postconf are installed
|
|
||||||
for param in ("ctl", "config_utility",):
|
|
||||||
util.verify_exe_exists(self.conf(param),
|
|
||||||
"Cannot find executable '{0}'. You can provide the "
|
|
||||||
"path to this command with --{1}".format(
|
|
||||||
self.conf(param),
|
|
||||||
self.option_name(param)))
|
|
||||||
|
|
||||||
# Set up CLI tools
|
|
||||||
self.postfix = util.PostfixUtil(self.conf('config-dir'))
|
|
||||||
self.postconf = postconf.ConfigMain(self.conf('config-utility'),
|
|
||||||
self.conf('ignore-master-overrides'),
|
|
||||||
self.conf('config-dir'))
|
|
||||||
|
|
||||||
# Ensure current configuration is valid.
|
|
||||||
self.config_test()
|
|
||||||
|
|
||||||
# Check Postfix version
|
|
||||||
self._check_version()
|
|
||||||
self._lock_config_dir()
|
|
||||||
self.install_ssl_dhparams()
|
|
||||||
|
|
||||||
def config_test(self):
|
|
||||||
"""Test to see that the current Postfix configuration is valid.
|
|
||||||
|
|
||||||
:raises errors.MisconfigurationError: If the configuration is invalid.
|
|
||||||
"""
|
|
||||||
self.postfix.test()
|
|
||||||
|
|
||||||
def _check_version(self):
|
|
||||||
"""Verifies that the installed Postfix version is supported.
|
|
||||||
|
|
||||||
:raises errors.NotSupportedError: if the version is unsupported
|
|
||||||
"""
|
|
||||||
if self._get_version() < constants.MINIMUM_VERSION:
|
|
||||||
version_string = '.'.join([str(n) for n in constants.MINIMUM_VERSION])
|
|
||||||
raise errors.NotSupportedError('Postfix version must be at least %s' % version_string)
|
|
||||||
|
|
||||||
def _lock_config_dir(self):
|
|
||||||
"""Stop two Postfix plugins from modifying the config at once.
|
|
||||||
|
|
||||||
:raises .PluginError: if unable to acquire the lock
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
certbot_util.lock_dir_until_exit(self.conf('config-dir'))
|
|
||||||
except (OSError, errors.LockError):
|
|
||||||
logger.debug("Encountered error:", exc_info=True)
|
|
||||||
raise errors.PluginError(
|
|
||||||
"Unable to lock %s" % self.conf('config-dir'))
|
|
||||||
|
|
||||||
def more_info(self):
|
|
||||||
"""Human-readable string to help the user. Describes steps taken and any relevant
|
|
||||||
info to help the user decide which plugin to use.
|
|
||||||
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
return (
|
|
||||||
"Configures Postfix to try to authenticate mail servers, use "
|
|
||||||
"installed certificates and disable weak ciphers and protocols.{0}"
|
|
||||||
"Server root: {root}{0}"
|
|
||||||
"Version: {version}".format(
|
|
||||||
os.linesep,
|
|
||||||
root=self.conf('config-dir'),
|
|
||||||
version='.'.join([str(i) for i in self._get_version()]))
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_version(self):
|
|
||||||
"""Return the version of Postfix, as a tuple. (e.g. '2.11.3' is (2, 11, 3))
|
|
||||||
|
|
||||||
:returns: version
|
|
||||||
:rtype: tuple
|
|
||||||
|
|
||||||
:raises errors.PluginError: Unable to find Postfix version.
|
|
||||||
"""
|
|
||||||
mail_version = self.postconf.get_default("mail_version")
|
|
||||||
return tuple(int(i) for i in mail_version.split('.'))
|
|
||||||
|
|
||||||
def get_all_names(self):
|
|
||||||
"""Returns all names that may be authenticated.
|
|
||||||
|
|
||||||
:rtype: `set` of `str`
|
|
||||||
|
|
||||||
"""
|
|
||||||
return certbot_util.get_filtered_names(self.postconf.get(var)
|
|
||||||
for var in ('mydomain', 'myhostname', 'myorigin',))
|
|
||||||
|
|
||||||
def _set_vars(self, var_dict):
|
|
||||||
"""Sets all parameters in var_dict to config file. If current value is already set
|
|
||||||
as more secure (acceptable), then don't set/overwrite it.
|
|
||||||
"""
|
|
||||||
for param, acceptable in six.iteritems(var_dict):
|
|
||||||
if not util.is_acceptable_value(param, self.postconf.get(param), acceptable):
|
|
||||||
self.postconf.set(param, acceptable[0], acceptable)
|
|
||||||
|
|
||||||
def _confirm_changes(self):
|
|
||||||
"""Confirming outstanding updates for configuration parameters.
|
|
||||||
|
|
||||||
:raises errors.PluginError: when user rejects the configuration changes.
|
|
||||||
"""
|
|
||||||
updates = self.postconf.get_changes()
|
|
||||||
output_string = "Postfix TLS configuration parameters to update in main.cf:\n"
|
|
||||||
for name, value in six.iteritems(updates):
|
|
||||||
output_string += "{0} = {1}\n".format(name, value)
|
|
||||||
output_string += "Is this okay?\n"
|
|
||||||
if not zope.component.getUtility(interfaces.IDisplay).yesno(output_string,
|
|
||||||
force_interactive=True, default=True):
|
|
||||||
raise errors.PluginError(
|
|
||||||
"Manually rejected configuration changes.\n"
|
|
||||||
"Try using --tls-only or --server-only to change a particular"
|
|
||||||
"subset of configuration parameters.")
|
|
||||||
|
|
||||||
def deploy_cert(self, domain, cert_path,
|
|
||||||
key_path, chain_path, fullchain_path): # pylint: disable=unused-argument
|
|
||||||
"""Configure the Postfix SMTP server to use the given TLS cert.
|
|
||||||
|
|
||||||
:param str domain: domain to deploy certificate file
|
|
||||||
:param str cert_path: absolute path to the certificate file
|
|
||||||
:param str key_path: absolute path to the private key file
|
|
||||||
:param str chain_path: absolute path to the certificate chain file
|
|
||||||
:param str fullchain_path: absolute path to the certificate fullchain
|
|
||||||
file (cert plus chain)
|
|
||||||
|
|
||||||
:raises .PluginError: when cert cannot be deployed
|
|
||||||
|
|
||||||
"""
|
|
||||||
if self._tls_enabled:
|
|
||||||
return
|
|
||||||
self._tls_enabled = True
|
|
||||||
self.save_notes.append("Configuring TLS for {0}".format(domain))
|
|
||||||
self.postconf.set("smtpd_tls_cert_file", cert_path)
|
|
||||||
self.postconf.set("smtpd_tls_key_file", key_path)
|
|
||||||
self._set_vars(constants.TLS_SERVER_VARS)
|
|
||||||
if not self.conf('server_only'):
|
|
||||||
self._set_vars(constants.TLS_CLIENT_VARS)
|
|
||||||
if not self.conf('tls_only'):
|
|
||||||
self._set_vars(constants.DEFAULT_SERVER_VARS)
|
|
||||||
if not self.conf('server_only'):
|
|
||||||
self._set_vars(constants.DEFAULT_CLIENT_VARS)
|
|
||||||
# Despite the name, this option also supports 2048-bit DH params.
|
|
||||||
# http://www.postfix.org/FORWARD_SECRECY_README.html#server_fs
|
|
||||||
self.postconf.set("smtpd_tls_dh1024_param_file", self.ssl_dhparams)
|
|
||||||
self._confirm_changes()
|
|
||||||
|
|
||||||
def enhance(self, domain, enhancement, options=None): # pylint: disable=unused-argument
|
|
||||||
"""Raises an exception since this installer doesn't support any enhancements.
|
|
||||||
"""
|
|
||||||
raise errors.PluginError(
|
|
||||||
"Unsupported enhancement: {0}".format(enhancement))
|
|
||||||
|
|
||||||
def supported_enhancements(self):
|
|
||||||
"""Returns a list of supported enhancements.
|
|
||||||
|
|
||||||
:rtype: list
|
|
||||||
|
|
||||||
"""
|
|
||||||
return []
|
|
||||||
|
|
||||||
def save(self, title=None, temporary=False):
|
|
||||||
"""Creates backups and writes changes to configuration files.
|
|
||||||
|
|
||||||
:param str title: The title of the save. If a title is given, the
|
|
||||||
configuration will be saved as a new checkpoint and put in a
|
|
||||||
timestamped directory. `title` has no effect if temporary is true.
|
|
||||||
|
|
||||||
:param bool temporary: Indicates whether the changes made will
|
|
||||||
be quickly reversed in the future (challenges)
|
|
||||||
|
|
||||||
:raises errors.PluginError: when save is unsuccessful
|
|
||||||
"""
|
|
||||||
save_files = set((os.path.join(self.conf('config-dir'), "main.cf"),))
|
|
||||||
self.add_to_checkpoint(save_files,
|
|
||||||
"\n".join(self.save_notes), temporary)
|
|
||||||
self.postconf.flush()
|
|
||||||
|
|
||||||
del self.save_notes[:]
|
|
||||||
|
|
||||||
if title and not temporary:
|
|
||||||
self.finalize_checkpoint(title)
|
|
||||||
|
|
||||||
def recovery_routine(self):
|
|
||||||
super(Installer, self).recovery_routine()
|
|
||||||
self.postconf = postconf.ConfigMain(self.conf('config-utility'),
|
|
||||||
self.conf('ignore-master-overrides'),
|
|
||||||
self.conf('config-dir'))
|
|
||||||
|
|
||||||
def rollback_checkpoints(self, rollback=1):
|
|
||||||
"""Rollback saved checkpoints.
|
|
||||||
|
|
||||||
:param int rollback: Number of checkpoints to revert
|
|
||||||
|
|
||||||
:raises .errors.PluginError: If there is a problem with the input or
|
|
||||||
the function is unable to correctly revert the configuration
|
|
||||||
"""
|
|
||||||
super(Installer, self).rollback_checkpoints(rollback)
|
|
||||||
self.postconf = postconf.ConfigMain(self.conf('config-utility'),
|
|
||||||
self.conf('ignore-master-overrides'),
|
|
||||||
self.conf('config-dir'))
|
|
||||||
|
|
||||||
def restart(self):
|
|
||||||
"""Restart or refresh the server content.
|
|
||||||
|
|
||||||
:raises .PluginError: when server cannot be restarted
|
|
||||||
"""
|
|
||||||
self.postfix.restart()
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
"""Classes that wrap the postconf command line utility.
|
|
||||||
"""
|
|
||||||
import six
|
|
||||||
from certbot import errors
|
|
||||||
from certbot_postfix import util
|
|
||||||
|
|
||||||
# pylint: disable=unused-import, no-name-in-module
|
|
||||||
from acme.magic_typing import Dict, List, Tuple
|
|
||||||
# pylint: enable=unused-import, no-name-in-module
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigMain(util.PostfixUtilBase):
|
|
||||||
"""A parser for Postfix's main.cf file."""
|
|
||||||
|
|
||||||
def __init__(self, executable, ignore_master_overrides=False, config_dir=None):
|
|
||||||
super(ConfigMain, self).__init__(executable, config_dir)
|
|
||||||
# Whether to ignore overrides from master.
|
|
||||||
self._ignore_master_overrides = ignore_master_overrides
|
|
||||||
# List of all current Postfix parameters, from `postconf` command.
|
|
||||||
self._db = {} # type: Dict[str, str]
|
|
||||||
# List of current master.cf overrides from Postfix config. Dictionary
|
|
||||||
# of parameter name => list of tuples (service name, paramter value)
|
|
||||||
# Note: We should never modify master without explicit permission.
|
|
||||||
self._master_db = {} # type: Dict[str, List[Tuple[str, str]]]
|
|
||||||
# List of all changes requested to the Postfix parameters as they are now
|
|
||||||
# in _db. These changes are flushed to `postconf` on `flush`.
|
|
||||||
self._updated = {} # type: Dict[str, str]
|
|
||||||
self._read_from_conf()
|
|
||||||
|
|
||||||
def _read_from_conf(self):
|
|
||||||
"""Reads initial parameter state from `main.cf` into this object.
|
|
||||||
"""
|
|
||||||
out = self._get_output()
|
|
||||||
for name, value in _parse_main_output(out):
|
|
||||||
self._db[name] = value
|
|
||||||
out = self._get_output_master()
|
|
||||||
for name, value in _parse_main_output(out):
|
|
||||||
service, param_name = name.rsplit("/", 1)
|
|
||||||
if param_name not in self._master_db:
|
|
||||||
self._master_db[param_name] = []
|
|
||||||
self._master_db[param_name].append((service, value))
|
|
||||||
|
|
||||||
def _get_output_master(self):
|
|
||||||
"""Retrieves output for `master.cf` parameters."""
|
|
||||||
return self._get_output('-P')
|
|
||||||
|
|
||||||
def get_default(self, name):
|
|
||||||
"""Retrieves default value of parameter `name` from postfix parameters.
|
|
||||||
|
|
||||||
:param str name: The name of the parameter to fetch.
|
|
||||||
:returns: The default value of parameter `name`.
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
out = self._get_output(['-d', name])
|
|
||||||
_, value = next(_parse_main_output(out), (None, None))
|
|
||||||
return value
|
|
||||||
|
|
||||||
def get(self, name):
|
|
||||||
"""Retrieves working value of parameter `name` from postfix parameters.
|
|
||||||
|
|
||||||
:param str name: The name of the parameter to fetch.
|
|
||||||
:returns: The value of parameter `name`.
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
if name in self._updated:
|
|
||||||
return self._updated[name]
|
|
||||||
return self._db[name]
|
|
||||||
|
|
||||||
def get_master_overrides(self, name):
|
|
||||||
"""Retrieves list of overrides for parameter `name` in postfix's Master config
|
|
||||||
file.
|
|
||||||
|
|
||||||
:returns: List of tuples (service, value), meaning that parameter `name`
|
|
||||||
is overridden as `value` for `service`.
|
|
||||||
:rtype: `list` of `tuple` of `str`
|
|
||||||
"""
|
|
||||||
if name in self._master_db:
|
|
||||||
return self._master_db[name]
|
|
||||||
return None
|
|
||||||
|
|
||||||
def set(self, name, value, acceptable_overrides=None):
|
|
||||||
"""Sets parameter `name` to `value`. If `name` is overridden by a particular service in
|
|
||||||
`master.cf`, reports any of these parameter conflicts as long as
|
|
||||||
`ignore_master_overrides` was not set.
|
|
||||||
|
|
||||||
.. note:: that this function does not flush these parameter values to main.cf;
|
|
||||||
To do that, use `flush`.
|
|
||||||
|
|
||||||
:param str name: The name of the parameter to set.
|
|
||||||
:param str value: The value of the parameter.
|
|
||||||
:param tuple acceptable_overrides: If the master configuration file overrides `value`
|
|
||||||
with a value in acceptable_overrides.
|
|
||||||
"""
|
|
||||||
if name not in self._db:
|
|
||||||
raise KeyError("Parameter name {0} is not a valid Postfix parameter name.".format(name))
|
|
||||||
# Check to see if this parameter is overridden by master.
|
|
||||||
overrides = self.get_master_overrides(name)
|
|
||||||
if not self._ignore_master_overrides and overrides is not None:
|
|
||||||
util.report_master_overrides(name, overrides, acceptable_overrides)
|
|
||||||
if value != self._db[name]:
|
|
||||||
# _db contains the "original" state of parameters. We only care about
|
|
||||||
# writes if they cause a delta from the original state.
|
|
||||||
self._updated[name] = value
|
|
||||||
elif name in self._updated:
|
|
||||||
# If this write reverts a previously updated parameter back to the
|
|
||||||
# original DB's state, we don't have to keep track of it in _updated.
|
|
||||||
del self._updated[name]
|
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
"""Flushes all parameter changes made using `self.set`, to `main.cf`
|
|
||||||
|
|
||||||
:raises error.PluginError: When flush to main.cf fails for some reason.
|
|
||||||
"""
|
|
||||||
if not self._updated:
|
|
||||||
return
|
|
||||||
args = ['-e']
|
|
||||||
for name, value in six.iteritems(self._updated):
|
|
||||||
args.append('{0}={1}'.format(name, value))
|
|
||||||
try:
|
|
||||||
self._get_output(args)
|
|
||||||
except IOError as e:
|
|
||||||
raise errors.PluginError("Unable to save to Postfix config: {0}".format(e))
|
|
||||||
for name, value in six.iteritems(self._updated):
|
|
||||||
self._db[name] = value
|
|
||||||
self._updated = {}
|
|
||||||
|
|
||||||
def get_changes(self):
|
|
||||||
""" Return queued changes to main.cf.
|
|
||||||
|
|
||||||
:rtype: dict[str, str]
|
|
||||||
"""
|
|
||||||
return self._updated
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_main_output(output):
|
|
||||||
"""Parses the raw output from Postconf about main.cf.
|
|
||||||
|
|
||||||
Expects the output to look like:
|
|
||||||
|
|
||||||
.. code-block:: none
|
|
||||||
|
|
||||||
name1 = value1
|
|
||||||
name2 = value2
|
|
||||||
|
|
||||||
:param str output: data postconf wrote to stdout about main.cf
|
|
||||||
|
|
||||||
:returns: generator providing key-value pairs from main.cf
|
|
||||||
:rtype: Iterator[tuple(str, str)]
|
|
||||||
"""
|
|
||||||
for line in output.splitlines():
|
|
||||||
name, _, value = line.partition(" =")
|
|
||||||
yield name, value.strip()
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
""" Certbot Postfix Tests """
|
|
||||||
@@ -1,316 +0,0 @@
|
|||||||
"""Tests for certbot_postfix.installer."""
|
|
||||||
import copy
|
|
||||||
import functools
|
|
||||||
import unittest
|
|
||||||
from contextlib import contextmanager
|
|
||||||
|
|
||||||
import mock
|
|
||||||
import pkg_resources
|
|
||||||
import six
|
|
||||||
from acme.magic_typing import Dict, Tuple # pylint: disable=unused-import,no-name-in-module
|
|
||||||
|
|
||||||
from certbot import errors
|
|
||||||
from certbot.compat import os
|
|
||||||
from certbot.tests import util as certbot_test_util
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_MAIN_CF = {
|
|
||||||
"smtpd_tls_cert_file": "",
|
|
||||||
"smtpd_tls_key_file": "",
|
|
||||||
"smtpd_tls_dh1024_param_file": "",
|
|
||||||
"smtpd_tls_security_level": "none",
|
|
||||||
"smtpd_tls_auth_only": "",
|
|
||||||
"smtpd_tls_mandatory_protocols": "",
|
|
||||||
"smtpd_tls_protocols": "",
|
|
||||||
"smtpd_tls_ciphers": "",
|
|
||||||
"smtpd_tls_exclude_ciphers": "",
|
|
||||||
"smtpd_tls_mandatory_ciphers": "",
|
|
||||||
"smtpd_tls_eecdh_grade": "medium",
|
|
||||||
"smtp_tls_security_level": "",
|
|
||||||
"smtp_tls_ciphers": "",
|
|
||||||
"smtp_tls_exclude_ciphers": "",
|
|
||||||
"smtp_tls_mandatory_ciphers": "",
|
|
||||||
"mail_version": "3.2.3"
|
|
||||||
}
|
|
||||||
|
|
||||||
def _main_cf_with(obj):
|
|
||||||
main_cf = copy.copy(DEFAULT_MAIN_CF)
|
|
||||||
main_cf.update(obj)
|
|
||||||
return main_cf
|
|
||||||
|
|
||||||
class InstallerTest(certbot_test_util.ConfigTestCase):
|
|
||||||
# pylint: disable=too-many-public-methods
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(InstallerTest, self).setUp()
|
|
||||||
_config_file = pkg_resources.resource_filename("certbot_postfix.tests",
|
|
||||||
os.path.join("testdata", "config.json"))
|
|
||||||
self.config.postfix_ctl = "postfix"
|
|
||||||
self.config.postfix_config_dir = self.tempdir
|
|
||||||
self.config.postfix_config_utility = "postconf"
|
|
||||||
self.config.postfix_tls_only = False
|
|
||||||
self.config.postfix_server_only = False
|
|
||||||
self.config.config_dir = self.tempdir
|
|
||||||
|
|
||||||
@mock.patch("certbot_postfix.installer.util.is_acceptable_value")
|
|
||||||
def test_set_vars(self, mock_is_acceptable_value):
|
|
||||||
mock_is_acceptable_value.return_value = True
|
|
||||||
with create_installer(self.config) as installer:
|
|
||||||
installer.prepare()
|
|
||||||
mock_is_acceptable_value.return_value = False
|
|
||||||
|
|
||||||
@mock.patch("certbot_postfix.installer.util.is_acceptable_value")
|
|
||||||
def test_acceptable_value(self, mock_is_acceptable_value):
|
|
||||||
mock_is_acceptable_value.return_value = True
|
|
||||||
with create_installer(self.config) as installer:
|
|
||||||
installer.prepare()
|
|
||||||
mock_is_acceptable_value.return_value = False
|
|
||||||
|
|
||||||
@certbot_test_util.patch_get_utility()
|
|
||||||
def test_confirm_changes_no_raises_error(self, mock_util):
|
|
||||||
mock_util().yesno.return_value = False
|
|
||||||
with create_installer(self.config) as installer:
|
|
||||||
installer.prepare()
|
|
||||||
self.assertRaises(errors.PluginError, installer.deploy_cert,
|
|
||||||
"example.com", "cert_path", "key_path",
|
|
||||||
"chain_path", "fullchain_path")
|
|
||||||
|
|
||||||
@certbot_test_util.patch_get_utility()
|
|
||||||
def test_save(self, mock_util):
|
|
||||||
mock_util().yesno.return_value = True
|
|
||||||
with create_installer(self.config) as installer:
|
|
||||||
installer.prepare()
|
|
||||||
installer.postconf.flush = mock.Mock()
|
|
||||||
installer.reverter = mock.Mock()
|
|
||||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
|
||||||
"chain_path", "fullchain_path")
|
|
||||||
installer.save()
|
|
||||||
self.assertEqual(installer.save_notes, [])
|
|
||||||
self.assertEqual(installer.postconf.flush.call_count, 1)
|
|
||||||
self.assertEqual(installer.reverter.add_to_checkpoint.call_count, 1)
|
|
||||||
|
|
||||||
@certbot_test_util.patch_get_utility()
|
|
||||||
def test_save_with_title(self, mock_util):
|
|
||||||
mock_util().yesno.return_value = True
|
|
||||||
with create_installer(self.config) as installer:
|
|
||||||
installer.prepare()
|
|
||||||
installer.postconf.flush = mock.Mock()
|
|
||||||
installer.reverter = mock.Mock()
|
|
||||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
|
||||||
"chain_path", "fullchain_path")
|
|
||||||
installer.save(title="new_file!")
|
|
||||||
self.assertEqual(installer.reverter.finalize_checkpoint.call_count, 1)
|
|
||||||
|
|
||||||
@certbot_test_util.patch_get_utility()
|
|
||||||
def test_rollback_checkpoints_resets_postconf(self, mock_util):
|
|
||||||
mock_util().yesno.return_value = True
|
|
||||||
with create_installer(self.config) as installer:
|
|
||||||
installer.prepare()
|
|
||||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
|
||||||
"chain_path", "fullchain_path")
|
|
||||||
installer.rollback_checkpoints()
|
|
||||||
self.assertEqual(installer.postconf.get_changes(), {})
|
|
||||||
|
|
||||||
@certbot_test_util.patch_get_utility()
|
|
||||||
def test_recovery_routine_resets_postconf(self, mock_util):
|
|
||||||
mock_util().yesno.return_value = True
|
|
||||||
with create_installer(self.config) as installer:
|
|
||||||
installer.prepare()
|
|
||||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
|
||||||
"chain_path", "fullchain_path")
|
|
||||||
installer.recovery_routine()
|
|
||||||
self.assertEqual(installer.postconf.get_changes(), {})
|
|
||||||
|
|
||||||
def test_restart(self):
|
|
||||||
with create_installer(self.config) as installer:
|
|
||||||
installer.prepare()
|
|
||||||
installer.restart()
|
|
||||||
self.assertEqual(installer.postfix.restart.call_count, 1) # pylint: disable=no-member
|
|
||||||
|
|
||||||
def test_add_parser_arguments(self):
|
|
||||||
options = set(("ctl", "config-dir", "config-utility",
|
|
||||||
"tls-only", "server-only", "ignore-master-overrides"))
|
|
||||||
mock_add = mock.MagicMock()
|
|
||||||
|
|
||||||
from certbot_postfix import installer
|
|
||||||
installer.Installer.add_parser_arguments(mock_add)
|
|
||||||
|
|
||||||
for call in mock_add.call_args_list:
|
|
||||||
self.assertTrue(call[0][0] in options)
|
|
||||||
|
|
||||||
def test_no_postconf_prepare(self):
|
|
||||||
with create_installer(self.config) as installer:
|
|
||||||
installer_path = "certbot_postfix.installer"
|
|
||||||
exe_exists_path = installer_path + ".certbot_util.exe_exists"
|
|
||||||
path_surgery_path = "certbot_postfix.util.plugins_util.path_surgery"
|
|
||||||
with mock.patch(path_surgery_path, return_value=False):
|
|
||||||
with mock.patch(exe_exists_path, return_value=False):
|
|
||||||
self.assertRaises(errors.NoInstallationError,
|
|
||||||
installer.prepare)
|
|
||||||
|
|
||||||
def test_old_version(self):
|
|
||||||
with create_installer(self.config, main_cf=_main_cf_with({"mail_version": "0.0.1"}))\
|
|
||||||
as installer:
|
|
||||||
self.assertRaises(errors.NotSupportedError, installer.prepare)
|
|
||||||
|
|
||||||
def test_lock_error(self):
|
|
||||||
with create_installer(self.config) as installer:
|
|
||||||
assert_raises = functools.partial(self.assertRaises,
|
|
||||||
errors.PluginError,
|
|
||||||
installer.prepare)
|
|
||||||
certbot_test_util.lock_and_call(assert_raises, self.tempdir)
|
|
||||||
|
|
||||||
|
|
||||||
@mock.patch('certbot.util.lock_dir_until_exit')
|
|
||||||
def test_dir_locked(self, lock_dir):
|
|
||||||
with create_installer(self.config) as installer:
|
|
||||||
lock_dir.side_effect = errors.LockError
|
|
||||||
self.assertRaises(errors.PluginError, installer.prepare)
|
|
||||||
|
|
||||||
def test_more_info(self):
|
|
||||||
with create_installer(self.config) as installer:
|
|
||||||
installer.prepare()
|
|
||||||
output = installer.more_info()
|
|
||||||
self.assertTrue("Postfix" in output)
|
|
||||||
self.assertTrue(self.tempdir in output)
|
|
||||||
self.assertTrue(DEFAULT_MAIN_CF["mail_version"] in output)
|
|
||||||
|
|
||||||
def test_get_all_names(self):
|
|
||||||
config = {"mydomain": "example.org",
|
|
||||||
"myhostname": "mail.example.org",
|
|
||||||
"myorigin": "example.org"}
|
|
||||||
with create_installer(self.config, main_cf=_main_cf_with(config)) as installer:
|
|
||||||
installer.prepare()
|
|
||||||
result = installer.get_all_names()
|
|
||||||
self.assertEqual(result, set(config.values()))
|
|
||||||
|
|
||||||
@certbot_test_util.patch_get_utility()
|
|
||||||
def test_deploy(self, mock_util):
|
|
||||||
mock_util().yesno.return_value = True
|
|
||||||
from certbot_postfix import constants
|
|
||||||
with create_installer(self.config) as installer:
|
|
||||||
installer.prepare()
|
|
||||||
|
|
||||||
# pylint: disable=protected-access
|
|
||||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
|
||||||
"chain_path", "fullchain_path")
|
|
||||||
changes = installer.postconf.get_changes()
|
|
||||||
expected = {} # type: Dict[str, Tuple[str, ...]]
|
|
||||||
expected.update(constants.TLS_SERVER_VARS)
|
|
||||||
expected.update(constants.DEFAULT_SERVER_VARS)
|
|
||||||
expected.update(constants.DEFAULT_CLIENT_VARS)
|
|
||||||
self.assertEqual(changes["smtpd_tls_key_file"], "key_path")
|
|
||||||
self.assertEqual(changes["smtpd_tls_cert_file"], "cert_path")
|
|
||||||
for name, value in six.iteritems(expected):
|
|
||||||
self.assertEqual(changes[name], value[0])
|
|
||||||
|
|
||||||
@certbot_test_util.patch_get_utility()
|
|
||||||
def test_tls_only(self, mock_util):
|
|
||||||
mock_util().yesno.return_value = True
|
|
||||||
with create_installer(self.config) as installer:
|
|
||||||
installer.prepare()
|
|
||||||
installer.conf = lambda x: x == "tls_only"
|
|
||||||
installer.postconf.set = mock.Mock()
|
|
||||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
|
||||||
"chain_path", "fullchain_path")
|
|
||||||
self.assertEqual(installer.postconf.set.call_count, 4)
|
|
||||||
|
|
||||||
@certbot_test_util.patch_get_utility()
|
|
||||||
def test_server_only(self, mock_util):
|
|
||||||
mock_util().yesno.return_value = True
|
|
||||||
with create_installer(self.config) as installer:
|
|
||||||
installer.prepare()
|
|
||||||
installer.conf = lambda x: x == "server_only"
|
|
||||||
installer.postconf.set = mock.Mock()
|
|
||||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
|
||||||
"chain_path", "fullchain_path")
|
|
||||||
self.assertEqual(installer.postconf.set.call_count, 11)
|
|
||||||
|
|
||||||
@certbot_test_util.patch_get_utility()
|
|
||||||
def test_tls_and_server_only(self, mock_util):
|
|
||||||
mock_util().yesno.return_value = True
|
|
||||||
with create_installer(self.config) as installer:
|
|
||||||
installer.prepare()
|
|
||||||
installer.conf = lambda x: True
|
|
||||||
installer.postconf.set = mock.Mock()
|
|
||||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
|
||||||
"chain_path", "fullchain_path")
|
|
||||||
self.assertEqual(installer.postconf.set.call_count, 3)
|
|
||||||
|
|
||||||
@certbot_test_util.patch_get_utility()
|
|
||||||
def test_deploy_twice(self, mock_util):
|
|
||||||
# Deploying twice on the same installer shouldn't do anything!
|
|
||||||
mock_util().yesno.return_value = True
|
|
||||||
with create_installer(self.config) as installer:
|
|
||||||
installer.prepare()
|
|
||||||
from certbot_postfix.postconf import ConfigMain
|
|
||||||
with mock.patch.object(ConfigMain, "set", wraps=installer.postconf.set) as fake_set:
|
|
||||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
|
||||||
"chain_path", "fullchain_path")
|
|
||||||
self.assertEqual(fake_set.call_count, 15)
|
|
||||||
fake_set.reset_mock()
|
|
||||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
|
||||||
"chain_path", "fullchain_path")
|
|
||||||
self.assertFalse(fake_set.called)
|
|
||||||
|
|
||||||
@certbot_test_util.patch_get_utility()
|
|
||||||
def test_deploy_already_secure(self, mock_util):
|
|
||||||
# Should not overwrite "more-secure" parameters
|
|
||||||
mock_util().yesno.return_value = True
|
|
||||||
more_secure = {
|
|
||||||
"smtpd_tls_security_level": "encrypt",
|
|
||||||
"smtpd_tls_protocols": "!SSLv3, !SSLv2, !TLSv1",
|
|
||||||
"smtpd_tls_eecdh_grade": "strong"
|
|
||||||
}
|
|
||||||
with create_installer(self.config,\
|
|
||||||
main_cf=_main_cf_with(more_secure)) as installer:
|
|
||||||
installer.prepare()
|
|
||||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
|
||||||
"chain_path", "fullchain_path")
|
|
||||||
for param in more_secure:
|
|
||||||
self.assertFalse(param in installer.postconf.get_changes())
|
|
||||||
|
|
||||||
def test_enhance(self):
|
|
||||||
with create_installer(self.config) as installer:
|
|
||||||
installer.prepare()
|
|
||||||
self.assertRaises(errors.PluginError,
|
|
||||||
installer.enhance,
|
|
||||||
"example.org", "redirect")
|
|
||||||
|
|
||||||
def test_supported_enhancements(self):
|
|
||||||
with create_installer(self.config) as installer:
|
|
||||||
installer.prepare()
|
|
||||||
self.assertEqual(installer.supported_enhancements(), [])
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def create_installer(config, main_cf=None):
|
|
||||||
"""Creates a Postfix installer with calls to `postconf` and `postfix` mocked out.
|
|
||||||
|
|
||||||
In particular, creates a ConfigMain object that does regular things, but seeds it
|
|
||||||
with values from `main_cf` and `master_cf` dicts.
|
|
||||||
"""
|
|
||||||
if main_cf is None:
|
|
||||||
main_cf = DEFAULT_MAIN_CF
|
|
||||||
|
|
||||||
from certbot_postfix.postconf import ConfigMain
|
|
||||||
from certbot_postfix import installer
|
|
||||||
|
|
||||||
def _mock_init_postconf(postconf, executable, ignore_master_overrides=False, config_dir=None):
|
|
||||||
# pylint: disable=protected-access,unused-argument
|
|
||||||
postconf._ignore_master_overrides = ignore_master_overrides
|
|
||||||
postconf._db = main_cf
|
|
||||||
postconf._master_db = {}
|
|
||||||
postconf._updated = {}
|
|
||||||
# override get_default to get from main
|
|
||||||
postconf.get_default = lambda name: main_cf[name]
|
|
||||||
with mock.patch.object(ConfigMain, "__init__", _mock_init_postconf):
|
|
||||||
exe_exists_path = "certbot_postfix.installer.certbot_util.exe_exists"
|
|
||||||
with mock.patch(exe_exists_path, return_value=True):
|
|
||||||
with mock.patch("certbot_postfix.installer.util.PostfixUtil",
|
|
||||||
return_value=mock.Mock()):
|
|
||||||
yield installer.Installer(config, "postfix")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main() # pragma: no cover
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
"""Tests for certbot_postfix.postconf."""
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import mock
|
|
||||||
|
|
||||||
from certbot import errors
|
|
||||||
|
|
||||||
|
|
||||||
class PostConfTest(unittest.TestCase):
|
|
||||||
"""Tests for certbot_postfix.util.PostConf."""
|
|
||||||
def setUp(self):
|
|
||||||
from certbot_postfix.postconf import ConfigMain
|
|
||||||
super(PostConfTest, self).setUp()
|
|
||||||
with mock.patch('certbot_postfix.util.PostfixUtilBase._get_output') as mock_call:
|
|
||||||
with mock.patch('certbot_postfix.postconf.ConfigMain._get_output_master') as \
|
|
||||||
mock_master_call:
|
|
||||||
with mock.patch('certbot_postfix.postconf.util.verify_exe_exists') as verify_exe:
|
|
||||||
verify_exe.return_value = True
|
|
||||||
mock_call.return_value = ('default_parameter = value\n'
|
|
||||||
'extra_param =\n'
|
|
||||||
'overridden_by_master = default\n')
|
|
||||||
mock_master_call.return_value = (
|
|
||||||
'service/type/overridden_by_master = master_value\n'
|
|
||||||
'service2/type/overridden_by_master = master_value2\n'
|
|
||||||
)
|
|
||||||
self.config = ConfigMain('postconf', False)
|
|
||||||
|
|
||||||
@mock.patch('certbot_postfix.util.PostfixUtilBase._get_output')
|
|
||||||
@mock.patch('certbot_postfix.postconf.util.verify_exe_exists')
|
|
||||||
def test_get_output_master(self, mock_verify_exe, mock_get_output):
|
|
||||||
from certbot_postfix.postconf import ConfigMain
|
|
||||||
mock_verify_exe.return_value = True
|
|
||||||
ConfigMain('postconf', lambda x, y, z: None)
|
|
||||||
mock_get_output.assert_called_with('-P')
|
|
||||||
|
|
||||||
@mock.patch('certbot_postfix.util.PostfixUtilBase._get_output')
|
|
||||||
def test_read_default(self, mock_get_output):
|
|
||||||
mock_get_output.return_value = 'param = default_value'
|
|
||||||
self.assertEqual(self.config.get_default('param'), 'default_value')
|
|
||||||
|
|
||||||
@mock.patch('certbot_postfix.util.PostfixUtilBase._call')
|
|
||||||
def test_set(self, mock_call):
|
|
||||||
self.config.set('extra_param', 'other_value')
|
|
||||||
self.assertEqual(self.config.get('extra_param'), 'other_value')
|
|
||||||
self.config.flush()
|
|
||||||
mock_call.assert_called_with(['-e', 'extra_param=other_value'])
|
|
||||||
|
|
||||||
def test_set_bad_param_name(self):
|
|
||||||
self.assertRaises(KeyError, self.config.set, 'nonexistent_param', 'some_value')
|
|
||||||
|
|
||||||
@mock.patch('certbot_postfix.util.PostfixUtilBase._call')
|
|
||||||
def test_write_revert(self, mock_call):
|
|
||||||
self.config.set('default_parameter', 'fake_news')
|
|
||||||
# revert config set
|
|
||||||
self.config.set('default_parameter', 'value')
|
|
||||||
self.config.flush()
|
|
||||||
mock_call.assert_not_called()
|
|
||||||
|
|
||||||
@mock.patch('certbot_postfix.util.PostfixUtilBase._call')
|
|
||||||
def test_write_default(self, mock_call):
|
|
||||||
self.config.set('default_parameter', 'value')
|
|
||||||
self.config.flush()
|
|
||||||
mock_call.assert_not_called()
|
|
||||||
|
|
||||||
def test_master_overrides(self):
|
|
||||||
self.assertEqual(self.config.get_master_overrides('overridden_by_master'),
|
|
||||||
[('service/type', 'master_value'),
|
|
||||||
('service2/type', 'master_value2')])
|
|
||||||
|
|
||||||
def test_set_check_override(self):
|
|
||||||
self.assertRaises(errors.PluginError, self.config.set,
|
|
||||||
'overridden_by_master', 'new_value')
|
|
||||||
|
|
||||||
def test_ignore_check_override(self):
|
|
||||||
# pylint: disable=protected-access
|
|
||||||
self.config._ignore_master_overrides = True
|
|
||||||
self.config.set('overridden_by_master', 'new_value')
|
|
||||||
|
|
||||||
def test_check_acceptable_overrides(self):
|
|
||||||
self.config.set('overridden_by_master', 'new_value',
|
|
||||||
('master_value', 'master_value2'))
|
|
||||||
|
|
||||||
@mock.patch('certbot_postfix.util.PostfixUtilBase._get_output')
|
|
||||||
def test_flush(self, mock_out):
|
|
||||||
self.config.set('default_parameter', 'new_value')
|
|
||||||
self.config.set('extra_param', 'another_value')
|
|
||||||
self.config.flush()
|
|
||||||
arguments = mock_out.call_args_list[-1][0][0]
|
|
||||||
self.assertEqual('-e', arguments[0])
|
|
||||||
self.assertTrue('default_parameter=new_value' in arguments)
|
|
||||||
self.assertTrue('extra_param=another_value' in arguments)
|
|
||||||
|
|
||||||
@mock.patch('certbot_postfix.util.PostfixUtilBase._get_output')
|
|
||||||
def test_flush_updates_object(self, mock_out):
|
|
||||||
self.config.set('default_parameter', 'new_value')
|
|
||||||
self.config.flush()
|
|
||||||
mock_out.reset_mock()
|
|
||||||
self.config.set('default_parameter', 'new_value')
|
|
||||||
mock_out.assert_not_called()
|
|
||||||
|
|
||||||
@mock.patch('certbot_postfix.util.PostfixUtilBase._get_output')
|
|
||||||
def test_flush_throws_error_on_fail(self, mock_out):
|
|
||||||
mock_out.side_effect = [IOError("oh no!")]
|
|
||||||
self.config.set('default_parameter', 'new_value')
|
|
||||||
self.assertRaises(errors.PluginError, self.config.flush)
|
|
||||||
|
|
||||||
if __name__ == '__main__': # pragma: no cover
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
"""Tests for certbot_postfix.util."""
|
|
||||||
|
|
||||||
import subprocess
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import mock
|
|
||||||
|
|
||||||
from certbot import errors
|
|
||||||
|
|
||||||
|
|
||||||
class PostfixUtilBaseTest(unittest.TestCase):
|
|
||||||
"""Tests for certbot_postfix.util.PostfixUtilBase."""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _create_object(cls, *args, **kwargs):
|
|
||||||
from certbot_postfix.util import PostfixUtilBase
|
|
||||||
return PostfixUtilBase(*args, **kwargs)
|
|
||||||
|
|
||||||
@mock.patch('certbot_postfix.util.verify_exe_exists')
|
|
||||||
def test_no_exe(self, mock_verify):
|
|
||||||
expected_error = errors.NoInstallationError
|
|
||||||
mock_verify.side_effect = expected_error
|
|
||||||
self.assertRaises(expected_error, self._create_object, 'nonexistent')
|
|
||||||
|
|
||||||
def test_object_creation(self):
|
|
||||||
with mock.patch('certbot_postfix.util.verify_exe_exists'):
|
|
||||||
self._create_object('existent')
|
|
||||||
|
|
||||||
@mock.patch('certbot_postfix.util.check_all_output')
|
|
||||||
def test_call_extends_args(self, mock_output):
|
|
||||||
# pylint: disable=protected-access
|
|
||||||
with mock.patch('certbot_postfix.util.verify_exe_exists'):
|
|
||||||
mock_output.return_value = 'expected'
|
|
||||||
postfix = self._create_object('executable')
|
|
||||||
postfix._call(['many', 'extra', 'args'])
|
|
||||||
mock_output.assert_called_with(['executable', 'many', 'extra', 'args'])
|
|
||||||
postfix._call()
|
|
||||||
mock_output.assert_called_with(['executable'])
|
|
||||||
|
|
||||||
def test_create_with_config(self):
|
|
||||||
# pylint: disable=protected-access
|
|
||||||
with mock.patch('certbot_postfix.util.verify_exe_exists'):
|
|
||||||
postfix = self._create_object('exec', 'config_dir')
|
|
||||||
self.assertEqual(postfix._base_command, ['exec', '-c', 'config_dir'])
|
|
||||||
|
|
||||||
class PostfixUtilTest(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
# pylint: disable=protected-access
|
|
||||||
from certbot_postfix.util import PostfixUtil
|
|
||||||
with mock.patch('certbot_postfix.util.verify_exe_exists'):
|
|
||||||
self.postfix = PostfixUtil()
|
|
||||||
self.postfix._call = mock.Mock()
|
|
||||||
self.mock_call = self.postfix._call
|
|
||||||
|
|
||||||
def test_test(self):
|
|
||||||
self.postfix.test()
|
|
||||||
self.mock_call.assert_called_with(['check'])
|
|
||||||
|
|
||||||
def test_test_raises_error_when_check_fails(self):
|
|
||||||
self.mock_call.side_effect = [subprocess.CalledProcessError(1, "")]
|
|
||||||
self.assertRaises(errors.MisconfigurationError, self.postfix.test)
|
|
||||||
self.mock_call.assert_called_with(['check'])
|
|
||||||
|
|
||||||
def test_restart_while_running(self):
|
|
||||||
self.mock_call.side_effect = [subprocess.CalledProcessError(1, ""), None]
|
|
||||||
self.postfix.restart()
|
|
||||||
self.mock_call.assert_called_with(['start'])
|
|
||||||
|
|
||||||
def test_restart_while_not_running(self):
|
|
||||||
self.postfix.restart()
|
|
||||||
self.mock_call.assert_called_with(['reload'])
|
|
||||||
|
|
||||||
def test_restart_raises_error_when_reload_fails(self):
|
|
||||||
self.mock_call.side_effect = [None, subprocess.CalledProcessError(1, "")]
|
|
||||||
self.assertRaises(errors.PluginError, self.postfix.restart)
|
|
||||||
self.mock_call.assert_called_with(['reload'])
|
|
||||||
|
|
||||||
def test_restart_raises_error_when_start_fails(self):
|
|
||||||
self.mock_call.side_effect = [
|
|
||||||
subprocess.CalledProcessError(1, ""),
|
|
||||||
subprocess.CalledProcessError(1, "")]
|
|
||||||
self.assertRaises(errors.PluginError, self.postfix.restart)
|
|
||||||
self.mock_call.assert_called_with(['start'])
|
|
||||||
|
|
||||||
class CheckAllOutputTest(unittest.TestCase):
|
|
||||||
"""Tests for certbot_postfix.util.check_all_output."""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _call(cls, *args, **kwargs):
|
|
||||||
from certbot_postfix.util import check_all_output
|
|
||||||
return check_all_output(*args, **kwargs)
|
|
||||||
|
|
||||||
@mock.patch('certbot_postfix.util.logger')
|
|
||||||
@mock.patch('certbot_postfix.util.subprocess.Popen')
|
|
||||||
def test_command_error(self, mock_popen, mock_logger):
|
|
||||||
command = 'foo'
|
|
||||||
retcode = 42
|
|
||||||
output = 'bar'
|
|
||||||
err = 'baz'
|
|
||||||
|
|
||||||
mock_popen().communicate.return_value = (output, err)
|
|
||||||
mock_popen().poll.return_value = 42
|
|
||||||
|
|
||||||
self.assertRaises(subprocess.CalledProcessError, self._call, command)
|
|
||||||
log_args = mock_logger.debug.call_args[0]
|
|
||||||
for value in (command, retcode, output, err,):
|
|
||||||
self.assertTrue(value in log_args)
|
|
||||||
|
|
||||||
@mock.patch('certbot_postfix.util.subprocess.Popen')
|
|
||||||
def test_success(self, mock_popen):
|
|
||||||
command = 'foo'
|
|
||||||
expected = ('bar', '')
|
|
||||||
mock_popen().communicate.return_value = expected
|
|
||||||
mock_popen().poll.return_value = 0
|
|
||||||
|
|
||||||
self.assertEqual(self._call(command), expected)
|
|
||||||
|
|
||||||
def test_stdout_error(self):
|
|
||||||
self.assertRaises(ValueError, self._call, stdout=None)
|
|
||||||
|
|
||||||
def test_stderr_error(self):
|
|
||||||
self.assertRaises(ValueError, self._call, stderr=None)
|
|
||||||
|
|
||||||
def test_universal_newlines_error(self):
|
|
||||||
self.assertRaises(ValueError, self._call, universal_newlines=False)
|
|
||||||
|
|
||||||
|
|
||||||
class VerifyExeExistsTest(unittest.TestCase):
|
|
||||||
"""Tests for certbot_postfix.util.verify_exe_exists."""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _call(cls, *args, **kwargs):
|
|
||||||
from certbot_postfix.util import verify_exe_exists
|
|
||||||
return verify_exe_exists(*args, **kwargs)
|
|
||||||
|
|
||||||
@mock.patch('certbot_postfix.util.certbot_util.exe_exists')
|
|
||||||
@mock.patch('certbot_postfix.util.plugins_util.path_surgery')
|
|
||||||
def test_failure(self, mock_exe_exists, mock_path_surgery):
|
|
||||||
mock_exe_exists.return_value = mock_path_surgery.return_value = False
|
|
||||||
self.assertRaises(errors.NoInstallationError, self._call, 'foo')
|
|
||||||
|
|
||||||
@mock.patch('certbot_postfix.util.certbot_util.exe_exists')
|
|
||||||
def test_simple_success(self, mock_exe_exists):
|
|
||||||
mock_exe_exists.return_value = True
|
|
||||||
self._call('foo')
|
|
||||||
|
|
||||||
@mock.patch('certbot_postfix.util.certbot_util.exe_exists')
|
|
||||||
@mock.patch('certbot_postfix.util.plugins_util.path_surgery')
|
|
||||||
def test_successful_surgery(self, mock_exe_exists, mock_path_surgery):
|
|
||||||
mock_exe_exists.return_value = False
|
|
||||||
mock_path_surgery.return_value = True
|
|
||||||
self._call('foo')
|
|
||||||
|
|
||||||
class TestUtils(unittest.TestCase):
|
|
||||||
""" Testing random utility functions in util.py
|
|
||||||
"""
|
|
||||||
def test_report_master_overrides(self):
|
|
||||||
from certbot_postfix.util import report_master_overrides
|
|
||||||
self.assertRaises(errors.PluginError, report_master_overrides, 'name',
|
|
||||||
[('service/type', 'value')])
|
|
||||||
# Shouldn't raise error
|
|
||||||
report_master_overrides('name', [('service/type', 'value')],
|
|
||||||
acceptable_overrides=('value',))
|
|
||||||
|
|
||||||
def test_no_acceptable_value(self):
|
|
||||||
from certbot_postfix.util import is_acceptable_value
|
|
||||||
self.assertFalse(is_acceptable_value('name', 'value', None))
|
|
||||||
|
|
||||||
def test_is_acceptable_value(self):
|
|
||||||
from certbot_postfix.util import is_acceptable_value
|
|
||||||
self.assertTrue(is_acceptable_value('name', 'value', ('value',)))
|
|
||||||
self.assertFalse(is_acceptable_value('name', 'bad', ('value',)))
|
|
||||||
|
|
||||||
def test_is_acceptable_tuples(self):
|
|
||||||
from certbot_postfix.util import is_acceptable_value
|
|
||||||
self.assertTrue(is_acceptable_value('name', 'value', ('value', 'value1')))
|
|
||||||
self.assertFalse(is_acceptable_value('name', 'bad', ('value', 'value1')))
|
|
||||||
|
|
||||||
def test_is_acceptable_protocols(self):
|
|
||||||
from certbot_postfix.util import is_acceptable_value
|
|
||||||
# SSLv2 and SSLv3 are both not supported, unambiguously
|
|
||||||
self.assertFalse(is_acceptable_value('tls_mandatory_protocols_lol',
|
|
||||||
'SSLv2, SSLv3', None))
|
|
||||||
self.assertFalse(is_acceptable_value('tls_protocols_lol',
|
|
||||||
'SSLv2, SSLv3', None))
|
|
||||||
self.assertFalse(is_acceptable_value('tls_protocols_lol',
|
|
||||||
'!SSLv2, !TLSv1', None))
|
|
||||||
self.assertFalse(is_acceptable_value('tls_protocols_lol',
|
|
||||||
'!SSLv2, SSLv3, !SSLv3, ', None))
|
|
||||||
self.assertTrue(is_acceptable_value('tls_protocols_lol',
|
|
||||||
'!SSLv2, !SSLv3', None))
|
|
||||||
self.assertTrue(is_acceptable_value('tls_protocols_lol',
|
|
||||||
'!SSLv3, !TLSv1, !SSLv2', None))
|
|
||||||
# TLSv1.2 is supported unambiguously
|
|
||||||
self.assertFalse(is_acceptable_value('tls_protocols_lol',
|
|
||||||
'TLSv1, TLSv1.1,', None))
|
|
||||||
self.assertFalse(is_acceptable_value('tls_protocols_lol',
|
|
||||||
'TLSv1.2, !TLSv1.2,', None))
|
|
||||||
self.assertTrue(is_acceptable_value('tls_protocols_lol',
|
|
||||||
'TLSv1.2, ', None))
|
|
||||||
self.assertTrue(is_acceptable_value('tls_protocols_lol',
|
|
||||||
'TLSv1, TLSv1.1, TLSv1.2', None))
|
|
||||||
|
|
||||||
if __name__ == '__main__': # pragma: no cover
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,292 +0,0 @@
|
|||||||
"""Utility functions for use in the Postfix installer."""
|
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
from certbot import errors
|
|
||||||
from certbot import util as certbot_util
|
|
||||||
from certbot.plugins import util as plugins_util
|
|
||||||
from certbot_postfix import constants
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
COMMAND = "postfix"
|
|
||||||
|
|
||||||
|
|
||||||
class PostfixUtilBase(object):
|
|
||||||
"""A base class for wrapping Postfix command line utilities."""
|
|
||||||
|
|
||||||
def __init__(self, executable, config_dir=None):
|
|
||||||
"""Sets up the Postfix utility class.
|
|
||||||
|
|
||||||
:param str executable: name or path of the Postfix utility
|
|
||||||
:param str config_dir: path to an alternative Postfix config
|
|
||||||
|
|
||||||
:raises .NoInstallationError: when the executable isn't found
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.executable = executable
|
|
||||||
verify_exe_exists(executable)
|
|
||||||
self._set_base_command(config_dir)
|
|
||||||
self.config_dir = None
|
|
||||||
|
|
||||||
def _set_base_command(self, config_dir):
|
|
||||||
self._base_command = [self.executable]
|
|
||||||
if config_dir is not None:
|
|
||||||
self._base_command.extend(('-c', config_dir,))
|
|
||||||
|
|
||||||
def _call(self, extra_args=None):
|
|
||||||
"""Runs the Postfix utility and returns the result.
|
|
||||||
|
|
||||||
:param list extra_args: additional arguments for the command
|
|
||||||
|
|
||||||
:returns: data written to stdout and stderr
|
|
||||||
:rtype: `tuple` of `str`
|
|
||||||
|
|
||||||
:raises subprocess.CalledProcessError: if the command fails
|
|
||||||
|
|
||||||
"""
|
|
||||||
args = list(self._base_command)
|
|
||||||
if extra_args is not None:
|
|
||||||
args.extend(extra_args)
|
|
||||||
return check_all_output(args)
|
|
||||||
|
|
||||||
def _get_output(self, extra_args=None):
|
|
||||||
"""Runs the Postfix utility and returns only stdout output.
|
|
||||||
|
|
||||||
This function relies on self._call for running the utility.
|
|
||||||
|
|
||||||
:param list extra_args: additional arguments for the command
|
|
||||||
|
|
||||||
:returns: data written to stdout
|
|
||||||
:rtype: str
|
|
||||||
|
|
||||||
:raises subprocess.CalledProcessError: if the command fails
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self._call(extra_args)[0]
|
|
||||||
|
|
||||||
class PostfixUtil(PostfixUtilBase):
|
|
||||||
"""Wrapper around Postfix CLI tool.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, config_dir=None):
|
|
||||||
super(PostfixUtil, self).__init__(COMMAND, config_dir)
|
|
||||||
|
|
||||||
def test(self):
|
|
||||||
"""Make sure the configuration is valid.
|
|
||||||
|
|
||||||
:raises .MisconfigurationError: if the config is invalid
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self._call(["check"])
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
logger.debug("Could not check postfix configuration:\n%s", e)
|
|
||||||
raise errors.MisconfigurationError(
|
|
||||||
"Postfix failed internal configuration check.")
|
|
||||||
|
|
||||||
def restart(self):
|
|
||||||
"""Restart or refresh the server content.
|
|
||||||
|
|
||||||
:raises .PluginError: when server cannot be restarted
|
|
||||||
|
|
||||||
"""
|
|
||||||
logger.info("Reloading Postfix configuration...")
|
|
||||||
if self._is_running():
|
|
||||||
self._reload()
|
|
||||||
else:
|
|
||||||
self._start()
|
|
||||||
|
|
||||||
|
|
||||||
def _is_running(self):
|
|
||||||
"""Is Postfix currently running?
|
|
||||||
|
|
||||||
Uses the 'postfix status' command to determine if Postfix is
|
|
||||||
currently running using the specified configuration files.
|
|
||||||
|
|
||||||
:returns: True if Postfix is running, otherwise, False
|
|
||||||
:rtype: bool
|
|
||||||
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self._call(["status"])
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _start(self):
|
|
||||||
"""Instructions Postfix to start running.
|
|
||||||
|
|
||||||
:raises .PluginError: when Postfix cannot start
|
|
||||||
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self._call(["start"])
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
raise errors.PluginError("Postfix failed to start")
|
|
||||||
|
|
||||||
def _reload(self):
|
|
||||||
"""Instructs Postfix to reload its configuration.
|
|
||||||
|
|
||||||
If Postfix isn't currently running, this method will fail.
|
|
||||||
|
|
||||||
:raises .PluginError: when Postfix cannot reload
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self._call(["reload"])
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
raise errors.PluginError(
|
|
||||||
"Postfix failed to reload its configuration")
|
|
||||||
|
|
||||||
def check_all_output(*args, **kwargs):
|
|
||||||
"""A version of subprocess.check_output that also captures stderr.
|
|
||||||
|
|
||||||
This is the same as :func:`subprocess.check_output` except output
|
|
||||||
written to stderr is also captured and returned to the caller. The
|
|
||||||
return value is a tuple of two strings (rather than byte strings).
|
|
||||||
To accomplish this, the caller cannot set the stdout, stderr, or
|
|
||||||
universal_newlines parameters to :class:`subprocess.Popen`.
|
|
||||||
|
|
||||||
Additionally, if the command exits with a nonzero status, output is
|
|
||||||
not included in the raised :class:`subprocess.CalledProcessError`
|
|
||||||
because Python 2.6 does not support this. Instead, the failure
|
|
||||||
including the output is logged.
|
|
||||||
|
|
||||||
:param tuple args: positional arguments for Popen
|
|
||||||
:param dict kwargs: keyword arguments for Popen
|
|
||||||
|
|
||||||
:returns: data written to stdout and stderr
|
|
||||||
:rtype: `tuple` of `str`
|
|
||||||
|
|
||||||
:raises ValueError: if arguments are invalid
|
|
||||||
:raises subprocess.CalledProcessError: if the command fails
|
|
||||||
|
|
||||||
"""
|
|
||||||
for keyword in ('stdout', 'stderr', 'universal_newlines',):
|
|
||||||
if keyword in kwargs:
|
|
||||||
raise ValueError(
|
|
||||||
keyword + ' argument not allowed, it will be overridden.')
|
|
||||||
|
|
||||||
kwargs['stdout'] = subprocess.PIPE
|
|
||||||
kwargs['stderr'] = subprocess.PIPE
|
|
||||||
kwargs['universal_newlines'] = True
|
|
||||||
|
|
||||||
process = subprocess.Popen(*args, **kwargs)
|
|
||||||
output, err = process.communicate()
|
|
||||||
retcode = process.poll()
|
|
||||||
if retcode:
|
|
||||||
cmd = kwargs.get('args')
|
|
||||||
if cmd is None:
|
|
||||||
cmd = args[0]
|
|
||||||
logger.debug(
|
|
||||||
"'%s' exited with %d. stdout output was:\n%s\nstderr output was:\n%s",
|
|
||||||
cmd, retcode, output, err)
|
|
||||||
raise subprocess.CalledProcessError(retcode, cmd)
|
|
||||||
return (output, err)
|
|
||||||
|
|
||||||
|
|
||||||
def verify_exe_exists(exe, message=None):
|
|
||||||
"""Ensures an executable with the given name is available.
|
|
||||||
|
|
||||||
If an executable isn't found for the given path or name, extra
|
|
||||||
directories are added to the user's PATH to help find system
|
|
||||||
utilities that may not be available in the default cron PATH.
|
|
||||||
|
|
||||||
:param str exe: executable path or name
|
|
||||||
:param str message: Error message to print.
|
|
||||||
|
|
||||||
:raises .NoInstallationError: when the executable isn't found
|
|
||||||
|
|
||||||
"""
|
|
||||||
if message is None:
|
|
||||||
message = "Cannot find executable '{0}'.".format(exe)
|
|
||||||
if not (certbot_util.exe_exists(exe) or plugins_util.path_surgery(exe)):
|
|
||||||
raise errors.NoInstallationError(message)
|
|
||||||
|
|
||||||
def report_master_overrides(name, overrides, acceptable_overrides=None):
|
|
||||||
"""If the value for a parameter `name` is overridden by other services,
|
|
||||||
report a warning to notify the user. If `parameter` is a TLS version parameter
|
|
||||||
(i.e., `parameter` contains 'tls_protocols' or 'tls_mandatory_protocols'), then
|
|
||||||
`acceptable_overrides` isn't used each value in overrides is inspected for secure TLS
|
|
||||||
versions.
|
|
||||||
|
|
||||||
:param str name: The name of the parameter that is being overridden.
|
|
||||||
:param list overrides: The values that other services are setting for `name`.
|
|
||||||
Each override is a tuple: (service name, value)
|
|
||||||
:param tuple acceptable_overrides: Override values that are acceptable. For instance, if
|
|
||||||
another service is overriding our parameter with a more secure option, we don't have
|
|
||||||
to warn. If this is set to None, errors are raised for *any* overrides of `name`!
|
|
||||||
"""
|
|
||||||
error_string = ""
|
|
||||||
for override in overrides:
|
|
||||||
service, value = override
|
|
||||||
# If this override is acceptable:
|
|
||||||
if acceptable_overrides is not None and \
|
|
||||||
is_acceptable_value(name, value, acceptable_overrides):
|
|
||||||
continue
|
|
||||||
error_string += " {0}: {1}\n".format(service, value)
|
|
||||||
if error_string:
|
|
||||||
raise errors.PluginError("{0} is overridden with less secure options by the "
|
|
||||||
"following services in master.cf:\n".format(name) + error_string)
|
|
||||||
|
|
||||||
|
|
||||||
def is_acceptable_value(parameter, value, acceptable=None):
|
|
||||||
""" Returns whether the `value` for this `parameter` is acceptable,
|
|
||||||
given a tuple of `acceptable` values. If `parameter` is a TLS version parameter
|
|
||||||
(i.e., `parameter` contains 'tls_protocols' or 'tls_mandatory_protocols'), then
|
|
||||||
`acceptable` isn't used and `value` is inspected for secure TLS versions.
|
|
||||||
|
|
||||||
:param str parameter: The name of the parameter being set.
|
|
||||||
:param str value: Proposed new value for parameter.
|
|
||||||
:param tuple acceptable: List of acceptable values for parameter.
|
|
||||||
"""
|
|
||||||
# Check if param value is a comma-separated list of protocols.
|
|
||||||
# Otherwise, just check whether the value is in the acceptable list.
|
|
||||||
if 'tls_protocols' in parameter or 'tls_mandatory_protocols' in parameter:
|
|
||||||
return _has_acceptable_tls_versions(value)
|
|
||||||
if acceptable is not None:
|
|
||||||
return value in acceptable
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _has_acceptable_tls_versions(parameter_string):
|
|
||||||
"""
|
|
||||||
Checks to see if the list of TLS protocols is acceptable.
|
|
||||||
This requires that TLSv1.2 is supported, and neither SSLv2 nor SSLv3 are supported.
|
|
||||||
|
|
||||||
Should be a string of protocol names delimited by commas, spaces, or colons.
|
|
||||||
|
|
||||||
Postfix's documents suggest listing protocols to exclude, like "!SSLv2, !SSLv3".
|
|
||||||
Listing the protocols to include, like "TLSv1, TLSv1.1, TLSv1.2" is okay as well,
|
|
||||||
though not recommended
|
|
||||||
|
|
||||||
When these two modes are interspersed, the presence of a single non-negated protocol name
|
|
||||||
(i.e. "TLSv1" rather than "!TLSv1") automatically excludes all other unnamed protocols.
|
|
||||||
|
|
||||||
In addition, the presence of both a protocol name inclusion and exclusion isn't explicitly
|
|
||||||
documented, so this method should return False if it encounters contradicting statements
|
|
||||||
about TLSv1.2, SSLv2, or SSLv3. (for instance, "SSLv3, !SSLv3").
|
|
||||||
"""
|
|
||||||
if not parameter_string:
|
|
||||||
return False
|
|
||||||
bad_versions = list(constants.TLS_VERSIONS)
|
|
||||||
for version in constants.ACCEPTABLE_TLS_VERSIONS:
|
|
||||||
del bad_versions[bad_versions.index(version)]
|
|
||||||
supported_version_list = re.split("[, :]+", parameter_string)
|
|
||||||
# The presence of any non-"!" protocol listing excludes the others by default.
|
|
||||||
inclusion_list = False
|
|
||||||
for version in supported_version_list:
|
|
||||||
if not version:
|
|
||||||
continue
|
|
||||||
if version in bad_versions: # short-circuit if we recognize any bad version
|
|
||||||
return False
|
|
||||||
if version[0] != "!":
|
|
||||||
inclusion_list = True
|
|
||||||
if inclusion_list: # For any inclusion list, we still require TLS 1.2.
|
|
||||||
if "TLSv1.2" not in supported_version_list or "!TLSv1.2" in supported_version_list:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
for bad_version in bad_versions:
|
|
||||||
if "!" + bad_version not in supported_version_list:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/_build/
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# Minimal makefile for Sphinx documentation
|
|
||||||
#
|
|
||||||
|
|
||||||
# You can set these variables from the command line.
|
|
||||||
SPHINXOPTS =
|
|
||||||
SPHINXBUILD = sphinx-build
|
|
||||||
SPHINXPROJ = certbot-postfix
|
|
||||||
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)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
=================
|
|
||||||
API Documentation
|
|
||||||
=================
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:glob:
|
|
||||||
|
|
||||||
api/**
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
:mod:`certbot_postfix.installer`
|
|
||||||
--------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: certbot_postfix.installer
|
|
||||||
:members:
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
:mod:`certbot_postfix.postconf`
|
|
||||||
-------------------------------
|
|
||||||
|
|
||||||
.. automodule:: certbot_postfix.postconf
|
|
||||||
:members:
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# Configuration file for the Sphinx documentation builder.
|
|
||||||
#
|
|
||||||
# This file does only contain a selection of the most common options. For a
|
|
||||||
# full list see the documentation:
|
|
||||||
# http://www.sphinx-doc.org/en/master/config
|
|
||||||
|
|
||||||
# -- Path setup --------------------------------------------------------------
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
from certbot.compat import os
|
|
||||||
# import sys
|
|
||||||
# sys.path.insert(0, os.path.abspath('.'))
|
|
||||||
|
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
|
||||||
|
|
||||||
project = u'certbot-postfix'
|
|
||||||
copyright = u'2018, Certbot Project'
|
|
||||||
author = u'Certbot Project'
|
|
||||||
|
|
||||||
# The short X.Y version
|
|
||||||
version = u'0'
|
|
||||||
# The full version, including alpha/beta/rc tags
|
|
||||||
release = u'0'
|
|
||||||
|
|
||||||
|
|
||||||
# -- 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', 'private-members']
|
|
||||||
|
|
||||||
# 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'
|
|
||||||
|
|
||||||
# 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 = u'en'
|
|
||||||
|
|
||||||
# List of patterns, relative to source directory, that match files and
|
|
||||||
# directories to ignore when looking for source files.
|
|
||||||
# This pattern also affects html_static_path and html_extra_path .
|
|
||||||
exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store']
|
|
||||||
|
|
||||||
default_role = 'py:obj'
|
|
||||||
|
|
||||||
# The name of the Pygments (syntax highlighting) style to use.
|
|
||||||
pygments_style = 'sphinx'
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output -------------------------------------------------
|
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
|
||||||
# a list of builtin themes.
|
|
||||||
#
|
|
||||||
|
|
||||||
# http://docs.readthedocs.org/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs
|
|
||||||
# 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']
|
|
||||||
|
|
||||||
# Custom sidebar templates, must be a dictionary that maps document names
|
|
||||||
# to template names.
|
|
||||||
#
|
|
||||||
# The default sidebars (for documents that don't match any pattern) are
|
|
||||||
# defined by theme itself. Builtin themes are using these templates by
|
|
||||||
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
|
|
||||||
# 'searchbox.html']``.
|
|
||||||
#
|
|
||||||
# html_sidebars = {}
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTMLHelp output ---------------------------------------------
|
|
||||||
|
|
||||||
# Output file base name for HTML help builder.
|
|
||||||
htmlhelp_basename = 'certbot-postfixdoc'
|
|
||||||
|
|
||||||
|
|
||||||
# -- 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-postfix.tex', u'certbot-postfix 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-postfix', u'certbot-postfix 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-postfix', u'certbot-postfix Documentation',
|
|
||||||
author, 'certbot-postfix', 'One line description of project.',
|
|
||||||
'Miscellaneous'),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# -- Extension configuration -------------------------------------------------
|
|
||||||
|
|
||||||
# -- Options for intersphinx extension ---------------------------------------
|
|
||||||
|
|
||||||
# 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://certbot.eff.org/docs/', None),
|
|
||||||
}
|
|
||||||
|
|
||||||
# -- Options for todo extension ----------------------------------------------
|
|
||||||
|
|
||||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
|
||||||
todo_include_todos = True
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
.. certbot-postfix documentation master file, created by
|
|
||||||
sphinx-quickstart on Wed May 2 16:01:06 2018.
|
|
||||||
You can adapt this file completely to your liking, but it should at least
|
|
||||||
contain the root `toctree` directive.
|
|
||||||
|
|
||||||
Welcome to certbot-postfix's documentation!
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
:caption: Contents:
|
|
||||||
|
|
||||||
.. automodule:: certbot_postfix
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 1
|
|
||||||
|
|
||||||
api
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Indices and tables
|
|
||||||
==================
|
|
||||||
|
|
||||||
* :ref:`genindex`
|
|
||||||
* :ref:`modindex`
|
|
||||||
* :ref:`search`
|
|
||||||
@@ -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-postfix
|
|
||||||
|
|
||||||
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.http://sphinx-doc.org/
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
|
||||||
goto end
|
|
||||||
|
|
||||||
:help
|
|
||||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
|
||||||
|
|
||||||
:end
|
|
||||||
popd
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# Remember to update setup.py to match the package versions below.
|
|
||||||
acme[dev]==0.29.0
|
|
||||||
-e .[dev]
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
[bdist_wheel]
|
|
||||||
universal = 1
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
from setuptools import setup
|
|
||||||
from setuptools import find_packages
|
|
||||||
|
|
||||||
|
|
||||||
version = '0.26.0.dev0'
|
|
||||||
|
|
||||||
install_requires = [
|
|
||||||
'acme>=0.29.0',
|
|
||||||
'certbot>=0.34.0.dev0',
|
|
||||||
'setuptools',
|
|
||||||
'six',
|
|
||||||
'zope.component',
|
|
||||||
'zope.interface',
|
|
||||||
]
|
|
||||||
|
|
||||||
docs_extras = [
|
|
||||||
'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags
|
|
||||||
'sphinx_rtd_theme',
|
|
||||||
]
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name='certbot-postfix',
|
|
||||||
version=version,
|
|
||||||
description="Postfix plugin for Certbot",
|
|
||||||
url='https://github.com/certbot/certbot',
|
|
||||||
author="Certbot Project",
|
|
||||||
author_email='client-dev@letsencrypt.org',
|
|
||||||
license='Apache License 2.0',
|
|
||||||
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
|
|
||||||
classifiers=[
|
|
||||||
'Development Status :: 3 - Alpha',
|
|
||||||
'Environment :: Plugins',
|
|
||||||
'Intended Audience :: System Administrators',
|
|
||||||
'License :: OSI Approved :: Apache Software License',
|
|
||||||
'Operating System :: POSIX :: Linux',
|
|
||||||
'Programming Language :: Python',
|
|
||||||
'Programming Language :: Python :: 2',
|
|
||||||
'Programming Language :: Python :: 2.7',
|
|
||||||
'Programming Language :: Python :: 3',
|
|
||||||
'Programming Language :: Python :: 3.4',
|
|
||||||
'Programming Language :: Python :: 3.5',
|
|
||||||
'Programming Language :: Python :: 3.6',
|
|
||||||
'Programming Language :: Python :: 3.7',
|
|
||||||
'Topic :: Communications :: Email :: Mail Transport Agents',
|
|
||||||
'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': [
|
|
||||||
'postfix = certbot_postfix:Installer',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
test_suite='certbot_postfix',
|
|
||||||
)
|
|
||||||
@@ -39,7 +39,6 @@ class PluginEntryPoint(object):
|
|||||||
"certbot-dns-route53",
|
"certbot-dns-route53",
|
||||||
"certbot-dns-sakuracloud",
|
"certbot-dns-sakuracloud",
|
||||||
"certbot-nginx",
|
"certbot-nginx",
|
||||||
"certbot-postfix",
|
|
||||||
]
|
]
|
||||||
"""Distributions for which prefix will be omitted."""
|
"""Distributions for which prefix will be omitted."""
|
||||||
|
|
||||||
|
|||||||
@@ -280,7 +280,6 @@ external_ Y N A plugin for convenient scripting (See also ticket 2782_)
|
|||||||
icecast_ N Y Deploy certificates to Icecast 2 streaming media servers
|
icecast_ N Y Deploy certificates to Icecast 2 streaming media servers
|
||||||
pritunl_ N Y Install certificates in pritunl distributed OpenVPN servers
|
pritunl_ N Y Install certificates in pritunl distributed OpenVPN servers
|
||||||
proxmox_ N Y Install certificates in Proxmox Virtualization servers
|
proxmox_ N Y Install certificates in Proxmox Virtualization servers
|
||||||
postfix_ N Y STARTTLS Everywhere is becoming a Certbot Postfix/Exim plugin
|
|
||||||
heroku_ Y Y Integration with Heroku SSL
|
heroku_ Y Y Integration with Heroku SSL
|
||||||
=========== ==== ==== ===============================================================
|
=========== ==== ==== ===============================================================
|
||||||
|
|
||||||
@@ -294,7 +293,6 @@ heroku_ Y Y Integration with Heroku SSL
|
|||||||
.. _pritunl: https://github.com/kharkevich/letsencrypt-pritunl
|
.. _pritunl: https://github.com/kharkevich/letsencrypt-pritunl
|
||||||
.. _proxmox: https://github.com/kharkevich/letsencrypt-proxmox
|
.. _proxmox: https://github.com/kharkevich/letsencrypt-proxmox
|
||||||
.. _external: https://github.com/marcan/letsencrypt-external
|
.. _external: https://github.com/marcan/letsencrypt-external
|
||||||
.. _postfix: https://github.com/EFForg/starttls-everywhere
|
|
||||||
.. _heroku: https://github.com/gboudreau/certbot-heroku
|
.. _heroku: https://github.com/gboudreau/certbot-heroku
|
||||||
|
|
||||||
If you're interested, you can also :ref:`write your own plugin <dev-plugin>`.
|
If you're interested, you can also :ref:`write your own plugin <dev-plugin>`.
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ REQUIREMENTS = [
|
|||||||
'-e certbot-dns-route53',
|
'-e certbot-dns-route53',
|
||||||
'-e certbot-dns-sakuracloud',
|
'-e certbot-dns-sakuracloud',
|
||||||
'-e certbot-nginx',
|
'-e certbot-nginx',
|
||||||
'-e certbot-postfix',
|
|
||||||
'-e letshelp-certbot',
|
'-e letshelp-certbot',
|
||||||
'-e certbot-compatibility-test',
|
'-e certbot-compatibility-test',
|
||||||
'-e certbot-ci',
|
'-e certbot-ci',
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ import shutil
|
|||||||
import subprocess
|
import subprocess
|
||||||
import re
|
import re
|
||||||
|
|
||||||
SKIP_PROJECTS_ON_WINDOWS = [
|
SKIP_PROJECTS_ON_WINDOWS = ['certbot-apache', 'letshelp-certbot']
|
||||||
'certbot-apache', 'certbot-postfix', 'letshelp-certbot']
|
|
||||||
|
|
||||||
|
|
||||||
def call_with_print(command, cwd=None):
|
def call_with_print(command, cwd=None):
|
||||||
|
|||||||
+2
-4
@@ -9,7 +9,7 @@ DEFAULT_PACKAGES = [
|
|||||||
'certbot_dns_digitalocean', 'certbot_dns_dnsimple', 'certbot_dns_dnsmadeeasy',
|
'certbot_dns_digitalocean', 'certbot_dns_dnsimple', 'certbot_dns_dnsmadeeasy',
|
||||||
'certbot_dns_gehirn', 'certbot_dns_google', 'certbot_dns_linode', 'certbot_dns_luadns',
|
'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_nsone', 'certbot_dns_ovh', 'certbot_dns_rfc2136', 'certbot_dns_route53',
|
||||||
'certbot_dns_sakuracloud', 'certbot_nginx', 'certbot_postfix', 'letshelp_certbot']
|
'certbot_dns_sakuracloud', 'certbot_nginx', 'letshelp_certbot']
|
||||||
|
|
||||||
COVER_THRESHOLDS = {
|
COVER_THRESHOLDS = {
|
||||||
'certbot': {'linux': 98, 'windows': 93},
|
'certbot': {'linux': 98, 'windows': 93},
|
||||||
@@ -30,12 +30,10 @@ COVER_THRESHOLDS = {
|
|||||||
'certbot_dns_route53': {'linux': 92, 'windows': 92},
|
'certbot_dns_route53': {'linux': 92, 'windows': 92},
|
||||||
'certbot_dns_sakuracloud': {'linux': 97, 'windows': 97},
|
'certbot_dns_sakuracloud': {'linux': 97, 'windows': 97},
|
||||||
'certbot_nginx': {'linux': 97, 'windows': 97},
|
'certbot_nginx': {'linux': 97, 'windows': 97},
|
||||||
'certbot_postfix': {'linux': 100, 'windows': 100},
|
|
||||||
'letshelp_certbot': {'linux': 100, 'windows': 100}
|
'letshelp_certbot': {'linux': 100, 'windows': 100}
|
||||||
}
|
}
|
||||||
|
|
||||||
SKIP_PROJECTS_ON_WINDOWS = [
|
SKIP_PROJECTS_ON_WINDOWS = ['certbot-apache', 'letshelp-certbot']
|
||||||
'certbot-apache', 'certbot-postfix', 'letshelp-certbot']
|
|
||||||
|
|
||||||
|
|
||||||
def cover(package):
|
def cover(package):
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ all_packages =
|
|||||||
certbot-apache \
|
certbot-apache \
|
||||||
{[base]dns_packages} \
|
{[base]dns_packages} \
|
||||||
certbot-nginx \
|
certbot-nginx \
|
||||||
certbot-postfix \
|
|
||||||
letshelp-certbot
|
letshelp-certbot
|
||||||
install_packages =
|
install_packages =
|
||||||
python {toxinidir}/tools/pip_install_editable.py {[base]all_packages}
|
python {toxinidir}/tools/pip_install_editable.py {[base]all_packages}
|
||||||
@@ -59,7 +58,6 @@ source_paths =
|
|||||||
certbot-dns-route53/certbot_dns_route53
|
certbot-dns-route53/certbot_dns_route53
|
||||||
certbot-dns-sakuracloud/certbot_dns_sakuracloud
|
certbot-dns-sakuracloud/certbot_dns_sakuracloud
|
||||||
certbot-nginx/certbot_nginx
|
certbot-nginx/certbot_nginx
|
||||||
certbot-postfix/certbot_postfix
|
|
||||||
letshelp-certbot/letshelp_certbot
|
letshelp-certbot/letshelp_certbot
|
||||||
tests/lock_test.py
|
tests/lock_test.py
|
||||||
|
|
||||||
@@ -111,12 +109,6 @@ commands =
|
|||||||
setenv =
|
setenv =
|
||||||
{[testenv:py27-oldest]setenv}
|
{[testenv:py27-oldest]setenv}
|
||||||
|
|
||||||
[testenv:py27-postfix-oldest]
|
|
||||||
commands =
|
|
||||||
{[base]install_and_test} certbot-postfix
|
|
||||||
setenv =
|
|
||||||
{[testenv:py27-oldest]setenv}
|
|
||||||
|
|
||||||
[testenv:py27-cover]
|
[testenv:py27-cover]
|
||||||
basepython = python2.7
|
basepython = python2.7
|
||||||
commands =
|
commands =
|
||||||
|
|||||||
Reference in New Issue
Block a user