mirror of
https://github.com/certbot/certbot.git
synced 2026-01-26 07:41:33 +03:00
merge with latest master
This commit is contained in:
46
CHANGELOG.md
46
CHANGELOG.md
@@ -2,6 +2,52 @@
|
||||
|
||||
Certbot adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## 0.14.1 - 2017-05-16
|
||||
|
||||
### Fixed
|
||||
|
||||
* Certbot now works with configargparse 0.12.0.
|
||||
* Issues with the Apache plugin and Augeas 1.7+ have been resolved.
|
||||
* A problem where the Nginx plugin would fail to install certificates on
|
||||
systems that had the plugin's SSL/TLS options file from 7+ months ago has been
|
||||
fixed.
|
||||
|
||||
More details about these changes can be found on our GitHub repo:
|
||||
https://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.14.1+is%3Aclosed
|
||||
|
||||
## 0.14.0 - 2017-05-04
|
||||
|
||||
### Added
|
||||
|
||||
* Python 3.3+ support for all Certbot packages. `certbot-auto` still currently
|
||||
only supports Python 2, but the `acme`, `certbot`, `certbot-apache`, and
|
||||
`certbot-nginx` packages on PyPI now fully support Python 2.6, 2.7, and 3.3+.
|
||||
* Certbot's Apache plugin now handles multiple virtual hosts per file.
|
||||
* Lockfiles to prevent multiple versions of Certbot running simultaneously.
|
||||
|
||||
### Changed
|
||||
|
||||
* When converting an HTTP virtual host to HTTPS in Apache, Certbot only copies
|
||||
the virtual host rather than the entire contents of the file it's contained
|
||||
in.
|
||||
* The Nginx plugin now includes SSL/TLS directives in a separate file located
|
||||
in Certbot's configuration directory rather than copying the contents of the
|
||||
file into every modified `server` block.
|
||||
|
||||
### Fixed
|
||||
|
||||
* Ensure logging is configured before parts of Certbot attempt to log any
|
||||
messages.
|
||||
* Support for the `--quiet` flag in `certbot-auto`.
|
||||
* Reverted a change made in a previous release to make the `acme` and `certbot`
|
||||
packages always depend on `argparse`. This dependency is conditional again on
|
||||
the user's Python version.
|
||||
* Small bugs in the Nginx plugin such as properly handling empty `server`
|
||||
blocks and setting `server_names_hash_bucket_size` during challenges.
|
||||
|
||||
As always, a more complete list of changes can be found on GitHub:
|
||||
https://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.14.0+is%3Aclosed
|
||||
|
||||
## 0.13.0 - 2017-04-06
|
||||
|
||||
### Added
|
||||
|
||||
@@ -23,7 +23,7 @@ WORKDIR /opt/certbot/src
|
||||
# TODO: Install Apache/Nginx for plugin development.
|
||||
COPY letsencrypt-auto-source/letsencrypt-auto /opt/certbot/src/letsencrypt-auto-source/letsencrypt-auto
|
||||
RUN /opt/certbot/src/letsencrypt-auto-source/letsencrypt-auto --os-packages-only && \
|
||||
apt-get install python3-dev -y && \
|
||||
apt-get install python3-dev git -y && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* \
|
||||
/tmp/* \
|
||||
|
||||
12
README.rst
12
README.rst
@@ -96,18 +96,10 @@ ACME spec: http://ietf-wg-acme.github.io/acme/
|
||||
|
||||
ACME working area in github: https://github.com/ietf-wg-acme/acme
|
||||
|
||||
|
||||
Mailing list: `client-dev`_ (to subscribe without a Google account, send an
|
||||
email to client-dev+subscribe@letsencrypt.org)
|
||||
|
||||
|build-status| |coverage| |docs| |container|
|
||||
|
||||
.. _Freenode: https://webchat.freenode.net?channels=%23letsencrypt
|
||||
|
||||
.. _OFTC: https://webchat.oftc.net?channels=%23certbot
|
||||
|
||||
.. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev
|
||||
|
||||
.. |build-status| image:: https://travis-ci.org/certbot/certbot.svg?branch=master
|
||||
:target: https://travis-ci.org/certbot/certbot
|
||||
:alt: Travis CI status
|
||||
@@ -141,7 +133,7 @@ Current Features
|
||||
* Supports multiple web servers:
|
||||
|
||||
- apache/2.x (beta support for auto-configuration)
|
||||
- nginx/0.8.48+ (alpha support for auto-configuration)
|
||||
- nginx/0.8.48+ (alpha support for auto-configuration, beta support in 0.14.0)
|
||||
- webroot (adds files to webroot directories in order to prove control of
|
||||
domains and obtain certs)
|
||||
- standalone (runs its own simple webserver to prove you control a domain)
|
||||
@@ -157,7 +149,7 @@ Current Features
|
||||
runs https only (Apache only)
|
||||
* Fully automated.
|
||||
* Configuration changes are logged and can be reverted.
|
||||
* Supports ncurses and text (-t) UI, or can be driven entirely from the
|
||||
* Supports an interactive text UI, or can be driven entirely from the
|
||||
command line.
|
||||
* Free and Open Source Software, made with Python.
|
||||
|
||||
|
||||
@@ -564,6 +564,9 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
|
||||
except ValueError:
|
||||
jobj = None
|
||||
|
||||
if response.status_code == 409:
|
||||
raise errors.ConflictError(response.headers.get('Location'))
|
||||
|
||||
if not response.ok:
|
||||
if jobj is not None:
|
||||
if response_ct != cls.JSON_ERROR_CONTENT_TYPE:
|
||||
|
||||
@@ -513,6 +513,12 @@ class ClientNetworkTest(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
self.response, self.net._check_response(self.response))
|
||||
|
||||
def test_check_response_conflict(self):
|
||||
self.response.ok = False
|
||||
self.response.status_code = 409
|
||||
# pylint: disable=protected-access
|
||||
self.assertRaises(errors.ConflictError, self.net._check_response, self.response)
|
||||
|
||||
def test_check_response_jobj(self):
|
||||
self.response.json.return_value = {}
|
||||
for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']:
|
||||
|
||||
@@ -82,3 +82,14 @@ class PollError(ClientError):
|
||||
def __repr__(self):
|
||||
return '{0}(exhausted={1!r}, updated={2!r})'.format(
|
||||
self.__class__.__name__, self.exhausted, self.updated)
|
||||
|
||||
class ConflictError(ClientError):
|
||||
"""Error for when the server returns a 409 (Conflict) HTTP status.
|
||||
|
||||
In the version of ACME implemented by Boulder, this is used to find an
|
||||
account if you only have the private key, but don't know the account URL.
|
||||
"""
|
||||
def __init__(self, location):
|
||||
self.location = location
|
||||
super(ConflictError, self).__init__()
|
||||
|
||||
|
||||
@@ -222,7 +222,8 @@ class Signature(json_util.JSONObjectWithFields):
|
||||
|
||||
protected_params = {}
|
||||
for header in protect:
|
||||
protected_params[header] = header_params.pop(header)
|
||||
if header in header_params:
|
||||
protected_params[header] = header_params.pop(header)
|
||||
if protected_params:
|
||||
# pylint: disable=star-args
|
||||
protected = cls.header_cls(**protected_params).json_dumps()
|
||||
|
||||
@@ -49,6 +49,6 @@ class JWS(jose.JWS):
|
||||
# jwk field if kid is not provided.
|
||||
include_jwk = kid is None
|
||||
return super(JWS, cls).sign(payload, key=key, alg=alg,
|
||||
protect=frozenset(['nonce', 'url', 'kid']),
|
||||
protect=frozenset(['nonce', 'url', 'kid', 'jwk', 'alg']),
|
||||
nonce=nonce, url=url, kid=kid,
|
||||
include_jwk=include_jwk)
|
||||
|
||||
@@ -4,11 +4,10 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.14.0.dev0'
|
||||
version = '0.15.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
'argparse',
|
||||
# load_pem_private/public_key (>=0.6)
|
||||
# rsa_recover_prime_factors (>=0.8)
|
||||
'cryptography>=0.8',
|
||||
@@ -28,6 +27,13 @@ install_requires = [
|
||||
'six',
|
||||
]
|
||||
|
||||
# env markers cause problems with older pip and setuptools
|
||||
if sys.version_info < (2, 7):
|
||||
install_requires.extend([
|
||||
'argparse',
|
||||
'ordereddict',
|
||||
])
|
||||
|
||||
dev_extras = [
|
||||
'nose',
|
||||
'tox',
|
||||
@@ -59,6 +65,7 @@ setup(
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
],
|
||||
|
||||
@@ -47,7 +47,9 @@ class AugeasConfigurator(common.Plugin):
|
||||
loadpath=constants.AUGEAS_LENS_DIR,
|
||||
# Do not save backup (we do it ourselves), do not load
|
||||
# anything by default
|
||||
flags=(augeas.Augeas.NONE | augeas.Augeas.NO_MODL_AUTOLOAD))
|
||||
flags=(augeas.Augeas.NONE |
|
||||
augeas.Augeas.NO_MODL_AUTOLOAD |
|
||||
augeas.Augeas.ENABLE_SPAN))
|
||||
self.recovery_routine()
|
||||
|
||||
def check_parsing_errors(self, lens):
|
||||
@@ -118,8 +120,8 @@ class AugeasConfigurator(common.Plugin):
|
||||
|
||||
# If the augeas tree didn't change, no files were saved and a backup
|
||||
# should not be created
|
||||
save_files = set()
|
||||
if save_paths:
|
||||
save_files = set()
|
||||
for path in save_paths:
|
||||
save_files.add(self.aug.get(path)[6:])
|
||||
|
||||
@@ -138,6 +140,12 @@ class AugeasConfigurator(common.Plugin):
|
||||
self.save_notes = ""
|
||||
self.aug.save()
|
||||
|
||||
# Force reload if files were modified
|
||||
# This is needed to recalculate augeas directive span
|
||||
if save_files:
|
||||
for sf in save_files:
|
||||
self.aug.remove("/files/"+sf)
|
||||
self.aug.load()
|
||||
if title and not temporary:
|
||||
try:
|
||||
self.reverter.finalize_checkpoint(title)
|
||||
|
||||
@@ -197,6 +197,14 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
|
||||
install_ssl_options_conf(self.mod_ssl_conf)
|
||||
|
||||
# Prevent two Apache plugins from modifying a config at once
|
||||
try:
|
||||
util.lock_dir_until_exit(self.conf("server-root"))
|
||||
except (OSError, errors.LockError):
|
||||
logger.debug("Encountered error:", exc_info=True)
|
||||
raise errors.PluginError(
|
||||
"Unable to lock %s", self.conf("server-root"))
|
||||
|
||||
def _check_aug_version(self):
|
||||
""" Checks that we have recent enough version of libaugeas.
|
||||
If augeas version is recent enough, it will support case insensitive
|
||||
@@ -494,6 +502,32 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
|
||||
return ""
|
||||
|
||||
def _get_vhost_names(self, path):
|
||||
"""Helper method for getting the ServerName and
|
||||
ServerAlias values from vhost in path
|
||||
|
||||
:param path: Path to read ServerName and ServerAliases from
|
||||
|
||||
:returns: Tuple including ServerName and `list` of ServerAlias strings
|
||||
"""
|
||||
|
||||
servername_match = self.parser.find_dir(
|
||||
"ServerName", None, start=path, exclude=False)
|
||||
serveralias_match = self.parser.find_dir(
|
||||
"ServerAlias", None, start=path, exclude=False)
|
||||
|
||||
serveraliases = []
|
||||
for alias in serveralias_match:
|
||||
serveralias = self.parser.get_arg(alias)
|
||||
serveraliases.append(serveralias)
|
||||
|
||||
servername = None
|
||||
if servername_match:
|
||||
# Get last ServerName as each overwrites the previous
|
||||
servername = self.parser.get_arg(servername_match[-1])
|
||||
|
||||
return (servername, serveraliases)
|
||||
|
||||
def _add_servernames(self, host):
|
||||
"""Helper function for get_virtual_hosts().
|
||||
|
||||
@@ -501,22 +535,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
:type host: :class:`~certbot_apache.obj.VirtualHost`
|
||||
|
||||
"""
|
||||
# Take the final ServerName as each overrides the previous
|
||||
servername_match = self.parser.find_dir(
|
||||
"ServerName", None, start=host.path, exclude=False)
|
||||
serveralias_match = self.parser.find_dir(
|
||||
"ServerAlias", None, start=host.path, exclude=False)
|
||||
|
||||
for alias in serveralias_match:
|
||||
serveralias = self.parser.get_arg(alias)
|
||||
if not host.modmacro:
|
||||
host.aliases.add(serveralias)
|
||||
servername, serveraliases = self._get_vhost_names(host.path)
|
||||
|
||||
if servername_match:
|
||||
# Get last ServerName as each overwrites the previous
|
||||
servername = self.parser.get_arg(servername_match[-1])
|
||||
for alias in serveraliases:
|
||||
if not host.modmacro:
|
||||
host.name = servername
|
||||
host.aliases.add(alias)
|
||||
|
||||
if not host.modmacro:
|
||||
host.name = servername
|
||||
|
||||
def _create_vhost(self, path):
|
||||
"""Used by get_virtual_hosts to create vhost objects
|
||||
@@ -573,30 +600,48 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
|
||||
"""
|
||||
# Search base config, and all included paths for VirtualHosts
|
||||
file_paths = {}
|
||||
internal_paths = defaultdict(set)
|
||||
vhs = []
|
||||
vhost_paths = {}
|
||||
for vhost_path in self.parser.parser_paths.keys():
|
||||
# Make a list of parser paths because the parser_paths
|
||||
# dictionary may be modified during the loop.
|
||||
for vhost_path in list(self.parser.parser_paths):
|
||||
paths = self.aug.match(
|
||||
("/files%s//*[label()=~regexp('%s')]" %
|
||||
(vhost_path, parser.case_i("VirtualHost"))))
|
||||
paths = [path for path in paths if
|
||||
os.path.basename(path.lower()) == "virtualhost"]
|
||||
"virtualhost" in os.path.basename(path).lower()]
|
||||
for path in paths:
|
||||
new_vhost = self._create_vhost(path)
|
||||
if not new_vhost:
|
||||
continue
|
||||
internal_path = get_internal_aug_path(new_vhost.path)
|
||||
realpath = os.path.realpath(new_vhost.filep)
|
||||
if realpath not in vhost_paths.keys():
|
||||
if realpath not in file_paths:
|
||||
file_paths[realpath] = new_vhost.filep
|
||||
internal_paths[realpath].add(internal_path)
|
||||
vhs.append(new_vhost)
|
||||
vhost_paths[realpath] = new_vhost.filep
|
||||
elif realpath == new_vhost.filep:
|
||||
elif (realpath == new_vhost.filep and
|
||||
realpath != file_paths[realpath]):
|
||||
# Prefer "real" vhost paths instead of symlinked ones
|
||||
# ex: sites-enabled/vh.conf -> sites-available/vh.conf
|
||||
|
||||
# remove old (most likely) symlinked one
|
||||
vhs = [v for v in vhs if v.filep != vhost_paths[realpath]]
|
||||
new_vhs = []
|
||||
for v in vhs:
|
||||
if v.filep == file_paths[realpath]:
|
||||
internal_paths[realpath].remove(
|
||||
get_internal_aug_path(v.path))
|
||||
else:
|
||||
new_vhs.append(v)
|
||||
vhs = new_vhs
|
||||
|
||||
file_paths[realpath] = realpath
|
||||
internal_paths[realpath].add(internal_path)
|
||||
vhs.append(new_vhost)
|
||||
elif internal_path not in internal_paths[realpath]:
|
||||
internal_paths[realpath].add(internal_path)
|
||||
vhs.append(new_vhost)
|
||||
vhost_paths[realpath] = realpath
|
||||
|
||||
return vhs
|
||||
|
||||
@@ -792,24 +837,27 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
avail_fp = nonssl_vhost.filep
|
||||
ssl_fp = self._get_ssl_vhost_path(avail_fp)
|
||||
|
||||
self._copy_create_ssl_vhost_skeleton(avail_fp, ssl_fp)
|
||||
orig_matches = self.aug.match("/files%s//* [label()=~regexp('%s')]" %
|
||||
(self._escape(ssl_fp),
|
||||
parser.case_i("VirtualHost")))
|
||||
|
||||
self._copy_create_ssl_vhost_skeleton(nonssl_vhost, ssl_fp)
|
||||
|
||||
# Reload augeas to take into account the new vhost
|
||||
self.aug.load()
|
||||
# Get Vhost augeas path for new vhost
|
||||
vh_p = self.aug.match("/files%s//* [label()=~regexp('%s')]" %
|
||||
(self._escape(ssl_fp), parser.case_i("VirtualHost")))
|
||||
if len(vh_p) != 1:
|
||||
logger.error("Error: should only be one vhost in %s", avail_fp)
|
||||
raise errors.PluginError("Currently, we only support "
|
||||
"configurations with one vhost per file")
|
||||
else:
|
||||
# This simplifies the process
|
||||
vh_p = vh_p[0]
|
||||
new_matches = self.aug.match("/files%s//* [label()=~regexp('%s')]" %
|
||||
(self._escape(ssl_fp),
|
||||
parser.case_i("VirtualHost")))
|
||||
|
||||
vh_p = self._get_new_vh_path(orig_matches, new_matches)
|
||||
|
||||
if not vh_p:
|
||||
raise errors.PluginError(
|
||||
"Could not reverse map the HTTPS VirtualHost to the original")
|
||||
|
||||
# Update Addresses
|
||||
self._update_ssl_vhosts_addrs(vh_p)
|
||||
|
||||
# Add directives
|
||||
self._add_dummy_ssl_directives(vh_p)
|
||||
self.save()
|
||||
@@ -822,6 +870,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
# We know the length is one because of the assertion above
|
||||
# Create the Vhost object
|
||||
ssl_vhost = self._create_vhost(vh_p)
|
||||
ssl_vhost.ancestor = nonssl_vhost
|
||||
self.vhosts.append(ssl_vhost)
|
||||
|
||||
# NOTE: Searches through Augeas seem to ruin changes to directives
|
||||
@@ -835,6 +884,20 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
|
||||
return ssl_vhost
|
||||
|
||||
def _get_new_vh_path(self, orig_matches, new_matches):
|
||||
""" Helper method for make_vhost_ssl for matching augeas paths. Returns
|
||||
VirtualHost path from new_matches that's not present in orig_matches.
|
||||
|
||||
Paths are normalized, because augeas leaves indices out for paths
|
||||
with only single directive with a similar key """
|
||||
|
||||
orig_matches = [i.replace("[1]", "") for i in orig_matches]
|
||||
for match in new_matches:
|
||||
if match.replace("[1]", "") not in orig_matches:
|
||||
# Return the unmodified path
|
||||
return match
|
||||
return None
|
||||
|
||||
def _get_ssl_vhost_path(self, non_ssl_vh_fp):
|
||||
# Get filepath of new ssl_vhost
|
||||
if non_ssl_vh_fp.endswith(".conf"):
|
||||
@@ -859,7 +922,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
if not line.lstrip().startswith("RewriteRule"):
|
||||
if not line.lower().lstrip().startswith("rewriterule"):
|
||||
return False
|
||||
|
||||
# According to: http://httpd.apache.org/docs/2.4/rewrite/flags.html
|
||||
@@ -875,10 +938,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
# Sift line if it redirects the request to a HTTPS site
|
||||
return target.startswith("https://")
|
||||
|
||||
def _copy_create_ssl_vhost_skeleton(self, avail_fp, ssl_fp):
|
||||
def _copy_create_ssl_vhost_skeleton(self, vhost, ssl_fp):
|
||||
"""Copies over existing Vhost with IfModule mod_ssl.c> skeleton.
|
||||
|
||||
:param str avail_fp: Pointer to the original available non-ssl vhost
|
||||
:param obj.VirtualHost vhost: Original VirtualHost object
|
||||
:param str ssl_fp: Full path where the new ssl_vhost will reside.
|
||||
|
||||
A new file is created on the filesystem.
|
||||
@@ -886,70 +949,25 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
"""
|
||||
# First register the creation so that it is properly removed if
|
||||
# configuration is rolled back
|
||||
self.reverter.register_file_creation(False, ssl_fp)
|
||||
if os.path.exists(ssl_fp):
|
||||
notes = "Appended new VirtualHost directive to file %s" % ssl_fp
|
||||
files = set()
|
||||
files.add(ssl_fp)
|
||||
self.reverter.add_to_checkpoint(files, notes)
|
||||
else:
|
||||
self.reverter.register_file_creation(False, ssl_fp)
|
||||
sift = False
|
||||
|
||||
try:
|
||||
with open(avail_fp, "r") as orig_file:
|
||||
with open(ssl_fp, "w") as new_file:
|
||||
new_file.write("<IfModule mod_ssl.c>\n")
|
||||
orig_contents = self._get_vhost_block(vhost)
|
||||
ssl_vh_contents, sift = self._sift_rewrite_rules(orig_contents)
|
||||
|
||||
comment = ("# Some rewrite rules in this file were "
|
||||
"disabled on your HTTPS site,\n"
|
||||
"# because they have the potential to create "
|
||||
"redirection loops.\n")
|
||||
|
||||
for line in orig_file:
|
||||
A = line.lstrip().startswith("RewriteCond")
|
||||
B = line.lstrip().startswith("RewriteRule")
|
||||
|
||||
if not (A or B):
|
||||
new_file.write(line)
|
||||
continue
|
||||
|
||||
# A RewriteRule that doesn't need filtering
|
||||
if B and not self._sift_rewrite_rule(line):
|
||||
new_file.write(line)
|
||||
continue
|
||||
|
||||
# A RewriteRule that does need filtering
|
||||
if B and self._sift_rewrite_rule(line):
|
||||
if not sift:
|
||||
new_file.write(comment)
|
||||
sift = True
|
||||
new_file.write("# " + line)
|
||||
continue
|
||||
|
||||
# We save RewriteCond(s) and their corresponding
|
||||
# RewriteRule in 'chunk'.
|
||||
# We then decide whether we comment out the entire
|
||||
# chunk based on its RewriteRule.
|
||||
chunk = []
|
||||
if A:
|
||||
chunk.append(line)
|
||||
line = next(orig_file)
|
||||
|
||||
# RewriteCond(s) must be followed by one RewriteRule
|
||||
while not line.lstrip().startswith("RewriteRule"):
|
||||
chunk.append(line)
|
||||
line = next(orig_file)
|
||||
|
||||
# Now, current line must start with a RewriteRule
|
||||
chunk.append(line)
|
||||
|
||||
if self._sift_rewrite_rule(line):
|
||||
if not sift:
|
||||
new_file.write(comment)
|
||||
sift = True
|
||||
|
||||
new_file.write(''.join(
|
||||
['# ' + l for l in chunk]))
|
||||
continue
|
||||
else:
|
||||
new_file.write(''.join(chunk))
|
||||
continue
|
||||
|
||||
new_file.write("</IfModule>\n")
|
||||
with open(ssl_fp, "a") as new_file:
|
||||
new_file.write("<IfModule mod_ssl.c>\n")
|
||||
new_file.write("\n".join(ssl_vh_contents))
|
||||
# The content does not include the closing tag, so add it
|
||||
new_file.write("</VirtualHost>\n")
|
||||
new_file.write("</IfModule>\n")
|
||||
except IOError:
|
||||
logger.fatal("Error writing/reading to file in make_vhost_ssl")
|
||||
raise errors.PluginError("Unable to write/read in make_vhost_ssl")
|
||||
@@ -959,9 +977,116 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
reporter.add_message(
|
||||
"Some rewrite rules copied from {0} were disabled in the "
|
||||
"vhost for your HTTPS site located at {1} because they have "
|
||||
"the potential to create redirection loops.".format(avail_fp,
|
||||
ssl_fp),
|
||||
reporter.MEDIUM_PRIORITY)
|
||||
"the potential to create redirection loops.".format(
|
||||
vhost.filep, ssl_fp), reporter.MEDIUM_PRIORITY)
|
||||
self.aug.set("/augeas/files%s/mtime" % (self._escape(ssl_fp)), "0")
|
||||
self.aug.set("/augeas/files%s/mtime" % (self._escape(vhost.filep)), "0")
|
||||
|
||||
def _sift_rewrite_rules(self, contents):
|
||||
""" Helper function for _copy_create_ssl_vhost_skeleton to prepare the
|
||||
new HTTPS VirtualHost contents. Currently disabling the rewrites """
|
||||
|
||||
result = []
|
||||
sift = False
|
||||
contents = iter(contents)
|
||||
|
||||
comment = ("# Some rewrite rules in this file were "
|
||||
"disabled on your HTTPS site,\n"
|
||||
"# because they have the potential to create "
|
||||
"redirection loops.\n")
|
||||
|
||||
for line in contents:
|
||||
A = line.lower().lstrip().startswith("rewritecond")
|
||||
B = line.lower().lstrip().startswith("rewriterule")
|
||||
|
||||
if not (A or B):
|
||||
result.append(line)
|
||||
continue
|
||||
|
||||
# A RewriteRule that doesn't need filtering
|
||||
if B and not self._sift_rewrite_rule(line):
|
||||
result.append(line)
|
||||
continue
|
||||
|
||||
# A RewriteRule that does need filtering
|
||||
if B and self._sift_rewrite_rule(line):
|
||||
if not sift:
|
||||
result.append(comment)
|
||||
sift = True
|
||||
result.append("# " + line)
|
||||
continue
|
||||
|
||||
# We save RewriteCond(s) and their corresponding
|
||||
# RewriteRule in 'chunk'.
|
||||
# We then decide whether we comment out the entire
|
||||
# chunk based on its RewriteRule.
|
||||
chunk = []
|
||||
if A:
|
||||
chunk.append(line)
|
||||
line = next(contents)
|
||||
|
||||
# RewriteCond(s) must be followed by one RewriteRule
|
||||
while not line.lower().lstrip().startswith("rewriterule"):
|
||||
chunk.append(line)
|
||||
line = next(contents)
|
||||
|
||||
# Now, current line must start with a RewriteRule
|
||||
chunk.append(line)
|
||||
|
||||
if self._sift_rewrite_rule(line):
|
||||
if not sift:
|
||||
result.append(comment)
|
||||
sift = True
|
||||
|
||||
result.append('\n'.join(
|
||||
['# ' + l for l in chunk]))
|
||||
continue
|
||||
else:
|
||||
result.append('\n'.join(chunk))
|
||||
continue
|
||||
return result, sift
|
||||
|
||||
def _get_vhost_block(self, vhost):
|
||||
""" Helper method to get VirtualHost contents from the original file.
|
||||
This is done with help of augeas span, which returns the span start and
|
||||
end positions
|
||||
|
||||
:returns: `list` of VirtualHost block content lines without closing tag
|
||||
"""
|
||||
|
||||
try:
|
||||
span_val = self.aug.span(vhost.path)
|
||||
except ValueError:
|
||||
logger.fatal("Error while reading the VirtualHost %s from "
|
||||
"file %s", vhost.name, vhost.filep, exc_info=True)
|
||||
raise errors.PluginError("Unable to read VirtualHost from file")
|
||||
span_filep = span_val[0]
|
||||
span_start = span_val[5]
|
||||
span_end = span_val[6]
|
||||
with open(span_filep, 'r') as fh:
|
||||
fh.seek(span_start)
|
||||
vh_contents = fh.read(span_end-span_start).split("\n")
|
||||
self._remove_closing_vhost_tag(vh_contents)
|
||||
return vh_contents
|
||||
|
||||
def _remove_closing_vhost_tag(self, vh_contents):
|
||||
"""Removes the closing VirtualHost tag if it exists.
|
||||
|
||||
This method modifies vh_contents directly to remove the closing
|
||||
tag. If the closing vhost tag is found, everything on the line
|
||||
after it is also removed. Whether or not this tag is included
|
||||
in the result of span depends on the Augeas version.
|
||||
|
||||
:param list vh_contents: VirtualHost block contents to check
|
||||
|
||||
"""
|
||||
for offset, line in enumerate(reversed(vh_contents)):
|
||||
if line:
|
||||
line_index = line.lower().find("</virtualhost>")
|
||||
if line_index != -1:
|
||||
content_index = len(vh_contents) - offset - 1
|
||||
vh_contents[content_index] = line[:line_index]
|
||||
break
|
||||
|
||||
def _update_ssl_vhosts_addrs(self, vh_path):
|
||||
ssl_addrs = set()
|
||||
@@ -1008,16 +1133,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
self.parser.add_dir(vh_path, "Include", self.mod_ssl_conf)
|
||||
|
||||
def _add_servername_alias(self, target_name, vhost):
|
||||
fp = self._escape(vhost.filep)
|
||||
vh_p = self.aug.match("/files%s//* [label()=~regexp('%s')]" %
|
||||
(fp, parser.case_i("VirtualHost")))
|
||||
if not vh_p:
|
||||
return
|
||||
vh_path = vh_p[0]
|
||||
if (self.parser.find_dir("ServerName", target_name,
|
||||
start=vh_path, exclude=False) or
|
||||
self.parser.find_dir("ServerAlias", target_name,
|
||||
start=vh_path, exclude=False)):
|
||||
vh_path = vhost.path
|
||||
sname, saliases = self._get_vhost_names(vh_path)
|
||||
if target_name == sname or target_name in saliases:
|
||||
return
|
||||
if self._has_matching_wildcard(vh_path, target_name):
|
||||
return
|
||||
@@ -1417,7 +1535,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
for re_path in rewrite_engine_path_list:
|
||||
# A RewriteEngine directive may also be included in per
|
||||
# directory .htaccess files. We only care about the VirtualHost.
|
||||
if 'VirtualHost' in re_path:
|
||||
if 'virtualhost' in re_path.lower():
|
||||
return self.parser.get_arg(re_path)
|
||||
return False
|
||||
|
||||
@@ -1508,6 +1626,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
def _get_http_vhost(self, ssl_vhost):
|
||||
"""Find appropriate HTTP vhost for ssl_vhost."""
|
||||
# First candidate vhosts filter
|
||||
if ssl_vhost.ancestor:
|
||||
return ssl_vhost.ancestor
|
||||
candidate_http_vhs = [
|
||||
vhost for vhost in self.vhosts if not vhost.ssl
|
||||
]
|
||||
@@ -1819,25 +1939,46 @@ def get_file_path(vhost_path):
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
# Strip off /files/
|
||||
try:
|
||||
if vhost_path.startswith("/files/"):
|
||||
avail_fp = vhost_path[7:].split("/")
|
||||
else:
|
||||
return None
|
||||
except AttributeError:
|
||||
# If we received a None path
|
||||
if not vhost_path or not vhost_path.startswith("/files/"):
|
||||
return None
|
||||
|
||||
last_good = ""
|
||||
# Loop through the path parts and validate after every addition
|
||||
for p in avail_fp:
|
||||
cur_path = last_good+"/"+p
|
||||
if os.path.exists(cur_path):
|
||||
last_good = cur_path
|
||||
else:
|
||||
break
|
||||
return last_good
|
||||
return _split_aug_path(vhost_path)[0]
|
||||
|
||||
|
||||
def get_internal_aug_path(vhost_path):
|
||||
"""Get the Augeas path for a vhost with the file path removed.
|
||||
|
||||
:param str vhost_path: Augeas virtual host path
|
||||
|
||||
:returns: Augeas path to vhost relative to the containing file
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
return _split_aug_path(vhost_path)[1]
|
||||
|
||||
|
||||
def _split_aug_path(vhost_path):
|
||||
"""Splits an Augeas path into a file path and an internal path.
|
||||
|
||||
After removing "/files", this function splits vhost_path into the
|
||||
file path and the remaining Augeas path.
|
||||
|
||||
:param str vhost_path: Augeas virtual host path
|
||||
|
||||
:returns: file path and internal Augeas path
|
||||
:rtype: `tuple` of `str`
|
||||
|
||||
"""
|
||||
# Strip off /files
|
||||
file_path = vhost_path[6:]
|
||||
internal_path = []
|
||||
|
||||
# Remove components from the end of file_path until it becomes valid
|
||||
while not os.path.exists(file_path):
|
||||
file_path, _, internal_path_part = file_path.rpartition("/")
|
||||
internal_path.append(internal_path_part)
|
||||
|
||||
return file_path, "/".join(reversed(internal_path))
|
||||
|
||||
|
||||
def install_ssl_options_conf(options_ssl):
|
||||
|
||||
@@ -91,7 +91,7 @@ def _vhost_menu(domain, vhosts):
|
||||
msg = ("Encountered vhost ambiguity but unable to ask for user guidance in "
|
||||
"non-interactive mode. Currently Certbot needs each vhost to be "
|
||||
"in its own conf file, and may need vhosts to be explicitly "
|
||||
"labelled with ServerName or ServerAlias directories.")
|
||||
"labelled with ServerName or ServerAlias directives.")
|
||||
logger.warning(msg)
|
||||
raise errors.MissingCommandlineFlag(msg)
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
|
||||
:ivar bool ssl: SSLEngine on in vhost
|
||||
:ivar bool enabled: Virtual host is enabled
|
||||
:ivar bool modmacro: VirtualHost is using mod_macro
|
||||
:ivar VirtualHost ancestor: A non-SSL VirtualHost this is based on
|
||||
|
||||
https://httpd.apache.org/docs/2.4/vhosts/details.html
|
||||
|
||||
@@ -123,7 +124,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
|
||||
strip_name = re.compile(r"^(?:.+://)?([^ :$]*)")
|
||||
|
||||
def __init__(self, filep, path, addrs, ssl, enabled, name=None,
|
||||
aliases=None, modmacro=False):
|
||||
aliases=None, modmacro=False, ancestor=None):
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
"""Initialize a VH."""
|
||||
@@ -135,6 +136,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
|
||||
self.ssl = ssl
|
||||
self.enabled = enabled
|
||||
self.modmacro = modmacro
|
||||
self.ancestor = ancestor
|
||||
|
||||
def get_names(self):
|
||||
"""Return a set of all names."""
|
||||
|
||||
@@ -95,6 +95,23 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
self.assertRaises(
|
||||
errors.NotSupportedError, self.config.prepare)
|
||||
|
||||
def test_prepare_locked(self):
|
||||
server_root = self.config.conf("server-root")
|
||||
self.config.config_test = mock.Mock()
|
||||
os.remove(os.path.join(server_root, ".certbot.lock"))
|
||||
certbot_util.lock_and_call(self._test_prepare_locked, server_root)
|
||||
|
||||
@mock.patch("certbot_apache.parser.ApacheParser")
|
||||
@mock.patch("certbot_apache.configurator.util.exe_exists")
|
||||
def _test_prepare_locked(self, unused_parser, unused_exe_exists):
|
||||
try:
|
||||
self.config.prepare()
|
||||
except errors.PluginError as err:
|
||||
err_msg = str(err)
|
||||
self.assertTrue("lock" in err_msg)
|
||||
self.assertTrue(self.config.conf("server-root") in err_msg)
|
||||
else: # pragma: no cover
|
||||
self.fail("Exception wasn't raised!")
|
||||
|
||||
def test_add_parser_arguments(self): # pylint: disable=no-self-use
|
||||
from certbot_apache.configurator import ApacheConfigurator
|
||||
@@ -138,6 +155,17 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
self.assertEqual(get_file_path("nonexistent"), None)
|
||||
self.assertEqual(self.config._create_vhost("nonexistent"), None) # pylint: disable=protected-access
|
||||
|
||||
def test_get_aug_internal_path(self):
|
||||
from certbot_apache.configurator import get_internal_aug_path
|
||||
internal_paths = [
|
||||
"VirtualHost", "IfModule/VirtualHost", "VirtualHost", "VirtualHost",
|
||||
"Macro/VirtualHost", "IfModule/VirtualHost", "VirtualHost",
|
||||
"IfModule/VirtualHost"]
|
||||
|
||||
for i, internal_path in enumerate(internal_paths):
|
||||
self.assertEqual(
|
||||
get_internal_aug_path(self.vh_truth[i].path), internal_path)
|
||||
|
||||
def test_bad_servername_alias(self):
|
||||
ssl_vh1 = obj.VirtualHost(
|
||||
"fp1", "ap1", set([obj.Addr(("*", "443"))]),
|
||||
@@ -569,6 +597,22 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
# already listens to the correct port
|
||||
self.assertEqual(mock_add_dir.call_count, 0)
|
||||
|
||||
def test_make_vhost_ssl_with_mock_span(self):
|
||||
# span excludes the closing </VirtualHost> tag in older versions
|
||||
# of Augeas
|
||||
return_value = [self.vh_truth[0].filep, 1, 12, 0, 0, 0, 1142]
|
||||
with mock.patch.object(self.config.aug, 'span') as mock_span:
|
||||
mock_span.return_value = return_value
|
||||
self.test_make_vhost_ssl()
|
||||
|
||||
def test_make_vhost_ssl_with_mock_span2(self):
|
||||
# span includes the closing </VirtualHost> tag in newer versions
|
||||
# of Augeas
|
||||
return_value = [self.vh_truth[0].filep, 1, 12, 0, 0, 0, 1157]
|
||||
with mock.patch.object(self.config.aug, 'span') as mock_span:
|
||||
mock_span.return_value = return_value
|
||||
self.test_make_vhost_ssl()
|
||||
|
||||
def test_make_vhost_ssl(self):
|
||||
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0])
|
||||
|
||||
@@ -655,11 +699,6 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
len(self.config.parser.find_dir(
|
||||
directive, None, self.vh_truth[1].path, False)), 0)
|
||||
|
||||
def test_make_vhost_ssl_extra_vhs(self):
|
||||
self.config.aug.match = mock.Mock(return_value=["p1", "p2"])
|
||||
self.assertRaises(
|
||||
errors.PluginError, self.config.make_vhost_ssl, self.vh_truth[0])
|
||||
|
||||
def test_make_vhost_ssl_bad_write(self):
|
||||
mock_open = mock.mock_open()
|
||||
# This calls open
|
||||
@@ -794,6 +833,15 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
def test_supported_enhancements(self):
|
||||
self.assertTrue(isinstance(self.config.supported_enhancements(), list))
|
||||
|
||||
def test_find_http_vhost_without_ancestor(self):
|
||||
# pylint: disable=protected-access
|
||||
vhost = self.vh_truth[0]
|
||||
vhost.ssl = True
|
||||
vhost.ancestor = None
|
||||
res = self.config._get_http_vhost(vhost)
|
||||
self.assertEqual(self.vh_truth[0].name, res.name)
|
||||
self.assertEqual(self.vh_truth[0].aliases, res.aliases)
|
||||
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator._get_http_vhost")
|
||||
@mock.patch("certbot_apache.display_ops.select_vhost")
|
||||
@mock.patch("certbot.util.exe_exists")
|
||||
@@ -993,8 +1041,9 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
# three args to rw_rule
|
||||
self.assertEqual(len(rw_rule), 3)
|
||||
|
||||
self.assertTrue(rw_engine[0].startswith(self.vh_truth[3].path))
|
||||
self.assertTrue(rw_rule[0].startswith(self.vh_truth[3].path))
|
||||
# [:-3] to remove the vhost index number
|
||||
self.assertTrue(rw_engine[0].startswith(self.vh_truth[3].path[:-3]))
|
||||
self.assertTrue(rw_rule[0].startswith(self.vh_truth[3].path[:-3]))
|
||||
|
||||
self.assertTrue("rewrite_module" in self.config.parser.modules)
|
||||
|
||||
@@ -1042,9 +1091,9 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
self.assertEqual(len(rw_engine), 1)
|
||||
# three args to rw_rule + 1 arg for the pre existing rewrite
|
||||
self.assertEqual(len(rw_rule), 5)
|
||||
|
||||
self.assertTrue(rw_engine[0].startswith(self.vh_truth[3].path))
|
||||
self.assertTrue(rw_rule[0].startswith(self.vh_truth[3].path))
|
||||
# [:-3] to remove the vhost index number
|
||||
self.assertTrue(rw_engine[0].startswith(self.vh_truth[3].path[:-3]))
|
||||
self.assertTrue(rw_rule[0].startswith(self.vh_truth[3].path[:-3]))
|
||||
|
||||
self.assertTrue("rewrite_module" in self.config.parser.modules)
|
||||
|
||||
@@ -1152,89 +1201,6 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
not_rewriterule = "NotRewriteRule ^ ..."
|
||||
self.assertFalse(self.config._sift_rewrite_rule(not_rewriterule))
|
||||
|
||||
@certbot_util.patch_get_utility()
|
||||
def test_make_vhost_ssl_with_existing_rewrite_rule(self, mock_get_utility):
|
||||
self.config.parser.modules.add("rewrite_module")
|
||||
|
||||
http_vhost = self.vh_truth[0]
|
||||
|
||||
self.config.parser.add_dir(
|
||||
http_vhost.path, "RewriteEngine", "on")
|
||||
|
||||
self.config.parser.add_dir(
|
||||
http_vhost.path, "RewriteRule",
|
||||
["^",
|
||||
"https://%{SERVER_NAME}%{REQUEST_URI}",
|
||||
"[L,NE,R=permanent]"])
|
||||
self.config.save()
|
||||
|
||||
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0])
|
||||
|
||||
self.assertTrue(self.config.parser.find_dir(
|
||||
"RewriteEngine", "on", ssl_vhost.path, False))
|
||||
|
||||
conf_text = open(ssl_vhost.filep).read()
|
||||
commented_rewrite_rule = ("# RewriteRule ^ "
|
||||
"https://%{SERVER_NAME}%{REQUEST_URI} "
|
||||
"[L,NE,R=permanent]")
|
||||
self.assertTrue(commented_rewrite_rule in conf_text)
|
||||
mock_get_utility().add_message.assert_called_once_with(mock.ANY,
|
||||
|
||||
mock.ANY)
|
||||
@certbot_util.patch_get_utility()
|
||||
def test_make_vhost_ssl_with_existing_rewrite_conds(self, mock_get_utility):
|
||||
self.config.parser.modules.add("rewrite_module")
|
||||
|
||||
http_vhost = self.vh_truth[0]
|
||||
|
||||
self.config.parser.add_dir(
|
||||
http_vhost.path, "RewriteEngine", "on")
|
||||
|
||||
# Add a chunk that should not be commented out.
|
||||
self.config.parser.add_dir(http_vhost.path,
|
||||
"RewriteCond", ["%{DOCUMENT_ROOT}/%{REQUEST_FILENAME}", "!-f"])
|
||||
self.config.parser.add_dir(
|
||||
http_vhost.path, "RewriteRule",
|
||||
["^(.*)$", "b://u%{REQUEST_URI}", "[P,NE,L]"])
|
||||
|
||||
# Add a chunk that should be commented out.
|
||||
self.config.parser.add_dir(http_vhost.path,
|
||||
"RewriteCond", ["%{HTTPS}", "!=on"])
|
||||
self.config.parser.add_dir(http_vhost.path,
|
||||
"RewriteCond", ["%{HTTPS}", "!^$"])
|
||||
self.config.parser.add_dir(
|
||||
http_vhost.path, "RewriteRule",
|
||||
["^",
|
||||
"https://%{SERVER_NAME}%{REQUEST_URI}",
|
||||
"[L,NE,R=permanent]"])
|
||||
|
||||
self.config.save()
|
||||
|
||||
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0])
|
||||
|
||||
conf_line_set = set(open(ssl_vhost.filep).read().splitlines())
|
||||
|
||||
not_commented_cond1 = ("RewriteCond "
|
||||
"%{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f")
|
||||
not_commented_rewrite_rule = ("RewriteRule "
|
||||
"^(.*)$ b://u%{REQUEST_URI} [P,NE,L]")
|
||||
|
||||
commented_cond1 = "# RewriteCond %{HTTPS} !=on"
|
||||
commented_cond2 = "# RewriteCond %{HTTPS} !^$"
|
||||
commented_rewrite_rule = ("# RewriteRule ^ "
|
||||
"https://%{SERVER_NAME}%{REQUEST_URI} "
|
||||
"[L,NE,R=permanent]")
|
||||
|
||||
self.assertTrue(not_commented_cond1 in conf_line_set)
|
||||
self.assertTrue(not_commented_rewrite_rule in conf_line_set)
|
||||
|
||||
self.assertTrue(commented_cond1 in conf_line_set)
|
||||
self.assertTrue(commented_cond2 in conf_line_set)
|
||||
self.assertTrue(commented_rewrite_rule in conf_line_set)
|
||||
mock_get_utility().add_message.assert_called_once_with(mock.ANY,
|
||||
mock.ANY)
|
||||
|
||||
|
||||
def get_achalls(self):
|
||||
"""Return testing achallenges."""
|
||||
account_key = self.rsa512jwk
|
||||
@@ -1345,6 +1311,126 @@ class AugeasVhostsTest(util.ApacheTest):
|
||||
self.config.choose_vhost(name)
|
||||
self.assertEqual(mock_select.call_count, 0)
|
||||
|
||||
def test_augeas_span_error(self):
|
||||
broken_vhost = self.config.vhosts[0]
|
||||
broken_vhost.path = broken_vhost.path + "/nonexistent"
|
||||
self.assertRaises(errors.PluginError, self.config.make_vhost_ssl,
|
||||
broken_vhost)
|
||||
|
||||
class MultiVhostsTest(util.ApacheTest):
|
||||
"""Test vhosts with illegal names dependant on augeas version."""
|
||||
# pylint: disable=protected-access
|
||||
|
||||
def setUp(self): # pylint: disable=arguments-differ
|
||||
td = "debian_apache_2_4/multi_vhosts"
|
||||
cr = "debian_apache_2_4/multi_vhosts/apache2"
|
||||
vr = "debian_apache_2_4/multi_vhosts/apache2/sites-available"
|
||||
super(MultiVhostsTest, self).setUp(test_dir=td,
|
||||
config_root=cr,
|
||||
vhost_root=vr)
|
||||
|
||||
self.config = util.get_apache_configurator(
|
||||
self.config_path, self.vhost_path, self.config_dir, self.work_dir)
|
||||
self.vh_truth = util.get_vh_truth(
|
||||
self.temp_dir, "debian_apache_2_4/multi_vhosts")
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_dir)
|
||||
shutil.rmtree(self.config_dir)
|
||||
shutil.rmtree(self.work_dir)
|
||||
|
||||
def test_make_vhost_ssl(self):
|
||||
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[1])
|
||||
|
||||
self.assertEqual(
|
||||
ssl_vhost.filep,
|
||||
os.path.join(self.config_path, "sites-available",
|
||||
"default-le-ssl.conf"))
|
||||
|
||||
self.assertEqual(ssl_vhost.path,
|
||||
"/files" + ssl_vhost.filep + "/IfModule/VirtualHost")
|
||||
self.assertEqual(len(ssl_vhost.addrs), 1)
|
||||
self.assertEqual(set([obj.Addr.fromstring("*:443")]), ssl_vhost.addrs)
|
||||
self.assertEqual(ssl_vhost.name, "banana.vomit.com")
|
||||
self.assertTrue(ssl_vhost.ssl)
|
||||
self.assertFalse(ssl_vhost.enabled)
|
||||
|
||||
self.assertTrue(self.config.parser.find_dir(
|
||||
"SSLCertificateFile", None, ssl_vhost.path, False))
|
||||
self.assertTrue(self.config.parser.find_dir(
|
||||
"SSLCertificateKeyFile", None, ssl_vhost.path, False))
|
||||
|
||||
self.assertEqual(self.config.is_name_vhost(self.vh_truth[1]),
|
||||
self.config.is_name_vhost(ssl_vhost))
|
||||
|
||||
mock_path = "certbot_apache.configurator.ApacheConfigurator._get_new_vh_path"
|
||||
with mock.patch(mock_path) as mock_getpath:
|
||||
mock_getpath.return_value = None
|
||||
self.assertRaises(errors.PluginError, self.config.make_vhost_ssl,
|
||||
self.vh_truth[1])
|
||||
|
||||
def test_get_new_path(self):
|
||||
with_index_1 = ["/path[1]/section[1]"]
|
||||
without_index = ["/path/section"]
|
||||
with_index_2 = ["/path[2]/section[2]"]
|
||||
self.assertEqual(self.config._get_new_vh_path(without_index,
|
||||
with_index_1),
|
||||
None)
|
||||
self.assertEqual(self.config._get_new_vh_path(without_index,
|
||||
with_index_2),
|
||||
with_index_2[0])
|
||||
|
||||
both = with_index_1 + with_index_2
|
||||
self.assertEqual(self.config._get_new_vh_path(without_index, both),
|
||||
with_index_2[0])
|
||||
|
||||
@certbot_util.patch_get_utility()
|
||||
def test_make_vhost_ssl_with_existing_rewrite_rule(self, mock_get_utility):
|
||||
self.config.parser.modules.add("rewrite_module")
|
||||
|
||||
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[4])
|
||||
|
||||
self.assertTrue(self.config.parser.find_dir(
|
||||
"RewriteEngine", "on", ssl_vhost.path, False))
|
||||
|
||||
conf_text = open(ssl_vhost.filep).read()
|
||||
commented_rewrite_rule = ("# RewriteRule \"^/secrets/(.+)\" "
|
||||
"\"https://new.example.com/docs/$1\" [R,L]")
|
||||
uncommented_rewrite_rule = ("RewriteRule \"^/docs/(.+)\" "
|
||||
"\"http://new.example.com/docs/$1\" [R,L]")
|
||||
self.assertTrue(commented_rewrite_rule in conf_text)
|
||||
self.assertTrue(uncommented_rewrite_rule in conf_text)
|
||||
mock_get_utility().add_message.assert_called_once_with(mock.ANY,
|
||||
mock.ANY)
|
||||
|
||||
@certbot_util.patch_get_utility()
|
||||
def test_make_vhost_ssl_with_existing_rewrite_conds(self, mock_get_utility):
|
||||
self.config.parser.modules.add("rewrite_module")
|
||||
|
||||
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[3])
|
||||
|
||||
conf_lines = open(ssl_vhost.filep).readlines()
|
||||
conf_line_set = [l.strip() for l in conf_lines]
|
||||
not_commented_cond1 = ("RewriteCond "
|
||||
"%{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f")
|
||||
not_commented_rewrite_rule = ("RewriteRule "
|
||||
"^(.*)$ b://u%{REQUEST_URI} [P,NE,L]")
|
||||
|
||||
commented_cond1 = "# RewriteCond %{HTTPS} !=on"
|
||||
commented_cond2 = "# RewriteCond %{HTTPS} !^$"
|
||||
commented_rewrite_rule = ("# RewriteRule ^ "
|
||||
"https://%{SERVER_NAME}%{REQUEST_URI} "
|
||||
"[L,NE,R=permanent]")
|
||||
|
||||
self.assertTrue(not_commented_cond1 in conf_line_set)
|
||||
self.assertTrue(not_commented_rewrite_rule in conf_line_set)
|
||||
|
||||
self.assertTrue(commented_cond1 in conf_line_set)
|
||||
self.assertTrue(commented_cond2 in conf_line_set)
|
||||
self.assertTrue(commented_rewrite_rule in conf_line_set)
|
||||
mock_get_utility().add_message.assert_called_once_with(mock.ANY,
|
||||
mock.ANY)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
|
||||
196
certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multi_vhosts/apache2/apache2.conf
vendored
Normal file
196
certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multi_vhosts/apache2/apache2.conf
vendored
Normal file
@@ -0,0 +1,196 @@
|
||||
# This is the main Apache server configuration file. It contains the
|
||||
# configuration directives that give the server its instructions.
|
||||
# See http://httpd.apache.org/docs/2.4/ for detailed information about
|
||||
# the directives and /usr/share/doc/apache2/README.Debian about Debian specific
|
||||
# hints.
|
||||
#
|
||||
#
|
||||
# Summary of how the Apache 2 configuration works in Debian:
|
||||
# The Apache 2 web server configuration in Debian is quite different to
|
||||
# upstream's suggested way to configure the web server. This is because Debian's
|
||||
# default Apache2 installation attempts to make adding and removing modules,
|
||||
# virtual hosts, and extra configuration directives as flexible as possible, in
|
||||
# order to make automating the changes and administering the server as easy as
|
||||
# possible.
|
||||
|
||||
# It is split into several files forming the configuration hierarchy outlined
|
||||
# below, all located in the /etc/apache2/ directory:
|
||||
#
|
||||
# /etc/apache2/
|
||||
# |-- apache2.conf
|
||||
# | `-- ports.conf
|
||||
# |-- mods-enabled
|
||||
# | |-- *.load
|
||||
# | `-- *.conf
|
||||
# |-- conf-enabled
|
||||
# | `-- *.conf
|
||||
# `-- sites-enabled
|
||||
# `-- *.conf
|
||||
#
|
||||
#
|
||||
# * apache2.conf is the main configuration file (this file). It puts the pieces
|
||||
# together by including all remaining configuration files when starting up the
|
||||
# web server.
|
||||
#
|
||||
# * ports.conf is always included from the main configuration file. It is
|
||||
# supposed to determine listening ports for incoming connections which can be
|
||||
# customized anytime.
|
||||
#
|
||||
# * Configuration files in the mods-enabled/, conf-enabled/ and sites-enabled/
|
||||
# directories contain particular configuration snippets which manage modules,
|
||||
# global configuration fragments, or virtual host configurations,
|
||||
# respectively.
|
||||
#
|
||||
# They are activated by symlinking available configuration files from their
|
||||
# respective *-available/ counterparts. These should be managed by using our
|
||||
# helpers a2enmod/a2dismod, a2ensite/a2dissite and a2enconf/a2disconf. See
|
||||
# their respective man pages for detailed information.
|
||||
#
|
||||
# * The binary is called apache2. Due to the use of environment variables, in
|
||||
# the default configuration, apache2 needs to be started/stopped with
|
||||
# /etc/init.d/apache2 or apache2ctl. Calling /usr/bin/apache2 directly will not
|
||||
# work with the default configuration.
|
||||
|
||||
|
||||
# Global configuration
|
||||
|
||||
#
|
||||
# The accept serialization lock file MUST BE STORED ON A LOCAL DISK.
|
||||
#
|
||||
Mutex file:${APACHE_LOCK_DIR} default
|
||||
|
||||
#
|
||||
# PidFile: The file in which the server should record its process
|
||||
# identification number when it starts.
|
||||
# This needs to be set in /etc/apache2/envvars
|
||||
#
|
||||
PidFile ${APACHE_PID_FILE}
|
||||
|
||||
#
|
||||
# Timeout: The number of seconds before receives and sends time out.
|
||||
#
|
||||
Timeout 300
|
||||
|
||||
#
|
||||
# KeepAlive: Whether or not to allow persistent connections (more than
|
||||
# one request per connection). Set to "Off" to deactivate.
|
||||
#
|
||||
KeepAlive On
|
||||
|
||||
#
|
||||
# MaxKeepAliveRequests: The maximum number of requests to allow
|
||||
# during a persistent connection. Set to 0 to allow an unlimited amount.
|
||||
# We recommend you leave this number high, for maximum performance.
|
||||
#
|
||||
MaxKeepAliveRequests 100
|
||||
|
||||
#
|
||||
# KeepAliveTimeout: Number of seconds to wait for the next request from the
|
||||
# same client on the same connection.
|
||||
#
|
||||
KeepAliveTimeout 5
|
||||
|
||||
|
||||
# These need to be set in /etc/apache2/envvars
|
||||
User ${APACHE_RUN_USER}
|
||||
Group ${APACHE_RUN_GROUP}
|
||||
|
||||
#
|
||||
# HostnameLookups: Log the names of clients or just their IP addresses
|
||||
# e.g., www.apache.org (on) or 204.62.129.132 (off).
|
||||
# The default is off because it'd be overall better for the net if people
|
||||
# had to knowingly turn this feature on, since enabling it means that
|
||||
# each client request will result in AT LEAST one lookup request to the
|
||||
# nameserver.
|
||||
#
|
||||
HostnameLookups Off
|
||||
|
||||
# ErrorLog: The location of the error log file.
|
||||
# If you do not specify an ErrorLog directive within a <VirtualHost>
|
||||
# container, error messages relating to that virtual host will be
|
||||
# logged here. If you *do* define an error logfile for a <VirtualHost>
|
||||
# container, that host's errors will be logged there and not here.
|
||||
#
|
||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||
|
||||
#
|
||||
# LogLevel: Control the severity of messages logged to the error_log.
|
||||
# Available values: trace8, ..., trace1, debug, info, notice, warn,
|
||||
# error, crit, alert, emerg.
|
||||
# It is also possible to configure the log level for particular modules, e.g.
|
||||
# "LogLevel info ssl:warn"
|
||||
#
|
||||
LogLevel warn
|
||||
|
||||
# Include module configuration:
|
||||
IncludeOptional mods-enabled/*.load
|
||||
IncludeOptional mods-enabled/*.conf
|
||||
|
||||
# Include list of ports to listen on
|
||||
Include ports.conf
|
||||
|
||||
|
||||
# Sets the default security model of the Apache2 HTTPD server. It does
|
||||
# not allow access to the root filesystem outside of /usr/share and /var/www.
|
||||
# The former is used by web applications packaged in Debian,
|
||||
# the latter may be used for local directories served by the web server. If
|
||||
# your system is serving content from a sub-directory in /srv you must allow
|
||||
# access here, or in any related virtual host.
|
||||
<Directory />
|
||||
Options FollowSymLinks
|
||||
AllowOverride None
|
||||
Require all denied
|
||||
</Directory>
|
||||
|
||||
<Directory /usr/share>
|
||||
AllowOverride None
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
<Directory /var/>
|
||||
Options Indexes FollowSymLinks
|
||||
AllowOverride None
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
# AccessFileName: The name of the file to look for in each directory
|
||||
# for additional configuration directives. See also the AllowOverride
|
||||
# directive.
|
||||
#
|
||||
AccessFileName .htaccess
|
||||
|
||||
#
|
||||
# The following lines prevent .htaccess and .htpasswd files from being
|
||||
# viewed by Web clients.
|
||||
#
|
||||
<FilesMatch "^\.ht">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
# The following directives define some format nicknames for use with
|
||||
# a CustomLog directive.
|
||||
#
|
||||
# These deviate from the Common Log Format definitions in that they use %O
|
||||
# (the actual bytes sent including headers) instead of %b (the size of the
|
||||
# requested file), because the latter makes it impossible to detect partial
|
||||
# requests.
|
||||
#
|
||||
# Note that the use of %{X-Forwarded-For}i instead of %h is not recommended.
|
||||
# Use mod_remoteip instead.
|
||||
#
|
||||
LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
|
||||
LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
|
||||
LogFormat "%h %l %u %t \"%r\" %>s %O" common
|
||||
LogFormat "%{Referer}i -> %U" referer
|
||||
LogFormat "%{User-agent}i" agent
|
||||
|
||||
# Include of directories ignores editors' and dpkg's backup files,
|
||||
# see README.Debian for details.
|
||||
|
||||
# Include generic snippets of statements
|
||||
IncludeOptional conf-enabled/*.conf
|
||||
|
||||
# Include the virtual host configurations:
|
||||
IncludeOptional sites-enabled/*.conf
|
||||
|
||||
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
|
||||
29
certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multi_vhosts/apache2/envvars
vendored
Normal file
29
certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multi_vhosts/apache2/envvars
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# envvars - default environment variables for apache2ctl
|
||||
|
||||
# this won't be correct after changing uid
|
||||
unset HOME
|
||||
|
||||
# for supporting multiple apache2 instances
|
||||
if [ "${APACHE_CONFDIR##/etc/apache2-}" != "${APACHE_CONFDIR}" ] ; then
|
||||
SUFFIX="-${APACHE_CONFDIR##/etc/apache2-}"
|
||||
else
|
||||
SUFFIX=
|
||||
fi
|
||||
|
||||
# Since there is no sane way to get the parsed apache2 config in scripts, some
|
||||
# settings are defined via environment variables and then used in apache2ctl,
|
||||
# /etc/init.d/apache2, /etc/logrotate.d/apache2, etc.
|
||||
export APACHE_RUN_USER=www-data
|
||||
export APACHE_RUN_GROUP=www-data
|
||||
# temporary state file location. This might be changed to /run in Wheezy+1
|
||||
export APACHE_PID_FILE=/var/run/apache2/apache2$SUFFIX.pid
|
||||
export APACHE_RUN_DIR=/var/run/apache2$SUFFIX
|
||||
export APACHE_LOCK_DIR=/var/lock/apache2$SUFFIX
|
||||
# Only /var/log/apache2 is handled by /etc/logrotate.d/apache2.
|
||||
export APACHE_LOG_DIR=/var/log/apache2$SUFFIX
|
||||
|
||||
## The locale used by some modules like mod_dav
|
||||
export LANG=C
|
||||
|
||||
export LANG
|
||||
|
||||
15
certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multi_vhosts/apache2/ports.conf
vendored
Normal file
15
certbot-apache/certbot_apache/tests/testdata/debian_apache_2_4/multi_vhosts/apache2/ports.conf
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# If you just change the port or add more ports here, you will likely also
|
||||
# have to change the VirtualHost statement in
|
||||
# /etc/apache2/sites-enabled/000-default.conf
|
||||
|
||||
Listen 80
|
||||
|
||||
<IfModule ssl_module>
|
||||
Listen 443
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_gnutls.c>
|
||||
Listen 443
|
||||
</IfModule>
|
||||
|
||||
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
|
||||
@@ -0,0 +1,22 @@
|
||||
<VirtualHost *:80>
|
||||
|
||||
ServerName banana.vomit.net
|
||||
ServerAdmin webmaster@localhost
|
||||
DocumentRoot /var/www/html
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||
|
||||
</VirtualHost>
|
||||
|
||||
<VirtualHost *:80>
|
||||
|
||||
ServerName banana.vomit.com
|
||||
ServerAdmin webmaster@localhost
|
||||
DocumentRoot /var/www/html
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||
|
||||
</VirtualHost>
|
||||
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
|
||||
@@ -0,0 +1,38 @@
|
||||
<VirtualHost *:80>
|
||||
ServerName 1.multi.vhost.tld
|
||||
ServerAlias first.multi.vhost.tld
|
||||
ServerAdmin webmaster@localhost
|
||||
DocumentRoot /var/www/html
|
||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||
</VirtualHost>
|
||||
|
||||
<IfModule mod_ssl.c>
|
||||
<VirtualHost *:80>
|
||||
ServerName 2.multi.vhost.tld
|
||||
ServerAlias second.multi.vhost.tld
|
||||
ServerAdmin webmaster@localhost
|
||||
DocumentRoot /var/www/html
|
||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||
RewriteEngine on
|
||||
RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^(.*)$ b://u%{REQUEST_URI} [P,NE,L]
|
||||
RewriteCond %{HTTPS} !=on
|
||||
RewriteCond %{HTTPS} !^$
|
||||
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [L,NE,R=permanent]
|
||||
</VirtualHost>
|
||||
</IfModule>
|
||||
|
||||
<VirtualHost *:80>
|
||||
ServerName 3.multi.vhost.tld
|
||||
ServerAlias third.multi.vhost.tld
|
||||
ServerAdmin webmaster@localhost
|
||||
DocumentRoot /var/www/html
|
||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||
RewriteEngine on
|
||||
RewriteRule "^/secrets/(.+)" "https://new.example.com/docs/$1" [R,L]
|
||||
RewriteRule "^/docs/(.+)" "http://new.example.com/docs/$1" [R,L]
|
||||
</VirtualHost>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
../sites-available/default.conf
|
||||
@@ -0,0 +1 @@
|
||||
../sites-available/multi-vhost.conf
|
||||
@@ -164,5 +164,35 @@ def get_vh_truth(temp_dir, config_name):
|
||||
set([obj.Addr.fromstring("10.2.3.4:443")]), True, True,
|
||||
"ocspvhost.com")]
|
||||
return vh_truth
|
||||
|
||||
if config_name == "debian_apache_2_4/multi_vhosts":
|
||||
prefix = os.path.join(
|
||||
temp_dir, config_name, "apache2/sites-available")
|
||||
aug_pre = "/files" + prefix
|
||||
vh_truth = [
|
||||
obj.VirtualHost(
|
||||
os.path.join(prefix, "default.conf"),
|
||||
os.path.join(aug_pre, "default.conf/VirtualHost[1]"),
|
||||
set([obj.Addr.fromstring("*:80")]),
|
||||
False, True, "ip-172-30-0-17"),
|
||||
obj.VirtualHost(
|
||||
os.path.join(prefix, "default.conf"),
|
||||
os.path.join(aug_pre, "default.conf/VirtualHost[2]"),
|
||||
set([obj.Addr.fromstring("*:80")]),
|
||||
False, True, "banana.vomit.com"),
|
||||
obj.VirtualHost(
|
||||
os.path.join(prefix, "multi-vhost.conf"),
|
||||
os.path.join(aug_pre, "multi-vhost.conf/VirtualHost[1]"),
|
||||
set([obj.Addr.fromstring("*:80")]),
|
||||
False, True, "1.multi.vhost.tld"),
|
||||
obj.VirtualHost(
|
||||
os.path.join(prefix, "multi-vhost.conf"),
|
||||
os.path.join(aug_pre, "multi-vhost.conf/IfModule/VirtualHost"),
|
||||
set([obj.Addr.fromstring("*:80")]),
|
||||
False, True, "2.multi.vhost.tld"),
|
||||
obj.VirtualHost(
|
||||
os.path.join(prefix, "multi-vhost.conf"),
|
||||
os.path.join(aug_pre, "multi-vhost.conf/VirtualHost[2]"),
|
||||
set([obj.Addr.fromstring("*:80")]),
|
||||
False, True, "3.multi.vhost.tld")]
|
||||
return vh_truth
|
||||
return None # pragma: no cover
|
||||
|
||||
2
certbot-apache/setup.cfg
Normal file
2
certbot-apache/setup.cfg
Normal file
@@ -0,0 +1,2 @@
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
@@ -4,14 +4,14 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.14.0.dev0'
|
||||
version = '0.15.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
'acme=={0}'.format(version),
|
||||
'certbot=={0}'.format(version),
|
||||
'mock',
|
||||
'python-augeas<=0.5.0',
|
||||
'python-augeas',
|
||||
# For pkg_resources. >=1.0 so pip resolves it to a version cryptography
|
||||
# will tolerate; see #2599:
|
||||
'setuptools>=1.0',
|
||||
@@ -42,6 +42,11 @@ setup(
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
|
||||
183
certbot-auto
183
certbot-auto
@@ -15,6 +15,11 @@ set -e # Work even if somebody does "sh thisscript.sh".
|
||||
|
||||
# Note: you can set XDG_DATA_HOME or VENV_PATH before running this script,
|
||||
# if you want to change where the virtual environment will be installed
|
||||
|
||||
# HOME might not be defined when being run through something like systemd
|
||||
if [ -z "$HOME" ]; then
|
||||
HOME=~root
|
||||
fi
|
||||
if [ -z "$XDG_DATA_HOME" ]; then
|
||||
XDG_DATA_HOME=~/.local/share
|
||||
fi
|
||||
@@ -23,7 +28,7 @@ if [ -z "$VENV_PATH" ]; then
|
||||
VENV_PATH="$XDG_DATA_HOME/$VENV_NAME"
|
||||
fi
|
||||
VENV_BIN="$VENV_PATH/bin"
|
||||
LE_AUTO_VERSION="0.13.0"
|
||||
LE_AUTO_VERSION="0.14.1"
|
||||
BASENAME=$(basename $0)
|
||||
USAGE="Usage: $BASENAME [OPTIONS]
|
||||
A self-updating wrapper script for the Certbot ACME client. When run, updates
|
||||
@@ -59,7 +64,7 @@ for arg in "$@" ; do
|
||||
NO_BOOTSTRAP=1;;
|
||||
--help)
|
||||
HELP=1;;
|
||||
--noninteractive|--non-interactive)
|
||||
--noninteractive|--non-interactive|renew)
|
||||
ASSUME_YES=1;;
|
||||
--quiet)
|
||||
QUIET=1;;
|
||||
@@ -93,6 +98,16 @@ if [ "$QUIET" = 1 ]; then
|
||||
ASSUME_YES=1
|
||||
fi
|
||||
|
||||
say() {
|
||||
if [ "$QUIET" != 1 ]; then
|
||||
echo "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
error() {
|
||||
echo "$@"
|
||||
}
|
||||
|
||||
# Support for busybox and others where there is no "command",
|
||||
# but "which" instead
|
||||
if command -v command > /dev/null 2>&1 ; then
|
||||
@@ -100,7 +115,7 @@ if command -v command > /dev/null 2>&1 ; then
|
||||
elif which which > /dev/null 2>&1 ; then
|
||||
export EXISTS="which"
|
||||
else
|
||||
echo "Cannot find command nor which... please install one!"
|
||||
error "Cannot find command nor which... please install one!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -145,17 +160,17 @@ if [ -n "${LE_AUTO_SUDO+x}" ]; then
|
||||
;;
|
||||
'') ;; # Nothing to do for plain root method.
|
||||
*)
|
||||
echo "Error: unknown root authorization mechanism '$LE_AUTO_SUDO'."
|
||||
error "Error: unknown root authorization mechanism '$LE_AUTO_SUDO'."
|
||||
exit 1
|
||||
esac
|
||||
echo "Using preset root authorization mechanism '$LE_AUTO_SUDO'."
|
||||
say "Using preset root authorization mechanism '$LE_AUTO_SUDO'."
|
||||
else
|
||||
if test "`id -u`" -ne "0" ; then
|
||||
if $EXISTS sudo 1>/dev/null 2>&1; then
|
||||
SUDO=sudo
|
||||
SUDO_ENV="CERTBOT_AUTO=$0"
|
||||
else
|
||||
echo \"sudo\" is not available, will use \"su\" for installation steps...
|
||||
say \"sudo\" is not available, will use \"su\" for installation steps...
|
||||
SUDO=su_sudo
|
||||
fi
|
||||
else
|
||||
@@ -165,7 +180,7 @@ fi
|
||||
|
||||
BootstrapMessage() {
|
||||
# Arguments: Platform name
|
||||
echo "Bootstrapping dependencies for $1... (you can skip this with --no-bootstrap)"
|
||||
say "Bootstrapping dependencies for $1... (you can skip this with --no-bootstrap)"
|
||||
}
|
||||
|
||||
ExperimentalBootstrap() {
|
||||
@@ -176,11 +191,11 @@ ExperimentalBootstrap() {
|
||||
$2
|
||||
fi
|
||||
else
|
||||
echo "FATAL: $1 support is very experimental at present..."
|
||||
echo "if you would like to work on improving it, please ensure you have backups"
|
||||
echo "and then run this script again with the --debug flag!"
|
||||
echo "Alternatively, you can install OS dependencies yourself and run this script"
|
||||
echo "again with --no-bootstrap."
|
||||
error "FATAL: $1 support is very experimental at present..."
|
||||
error "if you would like to work on improving it, please ensure you have backups"
|
||||
error "and then run this script again with the --debug flag!"
|
||||
error "Alternatively, you can install OS dependencies yourself and run this script"
|
||||
error "again with --no-bootstrap."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
@@ -191,15 +206,15 @@ DeterminePythonVersion() {
|
||||
$EXISTS "$LE_PYTHON" > /dev/null && break
|
||||
done
|
||||
if [ "$?" != "0" ]; then
|
||||
echo "Cannot find any Pythons; please install one!"
|
||||
error "Cannot find any Pythons; please install one!"
|
||||
exit 1
|
||||
fi
|
||||
export LE_PYTHON
|
||||
|
||||
PYVER=`"$LE_PYTHON" -V 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'`
|
||||
if [ "$PYVER" -lt 26 ]; then
|
||||
echo "You have an ancient version of Python entombed in your operating system..."
|
||||
echo "This isn't going to work; you'll need at least version 2.6."
|
||||
error "You have an ancient version of Python entombed in your operating system..."
|
||||
error "This isn't going to work; you'll need at least version 2.6."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
@@ -227,7 +242,7 @@ BootstrapDebCommon() {
|
||||
QUIET_FLAG='-qq'
|
||||
fi
|
||||
|
||||
$SUDO apt-get $QUIET_FLAG update || echo apt-get update hit problems but continuing anyway...
|
||||
$SUDO apt-get $QUIET_FLAG update || error apt-get update hit problems but continuing anyway...
|
||||
|
||||
# virtualenv binary can be found in different packages depending on
|
||||
# distro version (#346)
|
||||
@@ -255,7 +270,7 @@ BootstrapDebCommon() {
|
||||
# ARGS:
|
||||
BACKPORT_NAME="$1"
|
||||
BACKPORT_SOURCELINE="$2"
|
||||
echo "To use the Apache Certbot plugin, augeas needs to be installed from $BACKPORT_NAME."
|
||||
say "To use the Apache Certbot plugin, augeas needs to be installed from $BACKPORT_NAME."
|
||||
if ! grep -v -e ' *#' /etc/apt/sources.list | grep -q "$BACKPORT_NAME" ; then
|
||||
# This can theoretically error if sources.list.d is empty, but in that case we don't care.
|
||||
if ! grep -v -e ' *#' /etc/apt/sources.list.d/* 2>/dev/null | grep -q "$BACKPORT_NAME"; then
|
||||
@@ -315,7 +330,7 @@ BootstrapDebCommon() {
|
||||
|
||||
|
||||
if ! $EXISTS virtualenv > /dev/null ; then
|
||||
echo Failed to install a working \"virtualenv\" command, exiting
|
||||
error Failed to install a working \"virtualenv\" command, exiting
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
@@ -335,7 +350,7 @@ BootstrapRpmCommon() {
|
||||
tool=yum
|
||||
|
||||
else
|
||||
echo "Neither yum nor dnf found. Aborting bootstrap!"
|
||||
error "Neither yum nor dnf found. Aborting bootstrap!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -349,7 +364,7 @@ BootstrapRpmCommon() {
|
||||
if ! $SUDO $tool list *virtualenv >/dev/null 2>&1; then
|
||||
echo "To use Certbot, packages from the EPEL repository need to be installed."
|
||||
if ! $SUDO $tool list epel-release >/dev/null 2>&1; then
|
||||
echo "Please enable this repository and try running Certbot again."
|
||||
error "Enable the EPEL repository and try running Certbot again."
|
||||
exit 1
|
||||
fi
|
||||
if [ "$ASSUME_YES" = 1 ]; then
|
||||
@@ -361,7 +376,7 @@ BootstrapRpmCommon() {
|
||||
sleep 1s
|
||||
fi
|
||||
if ! $SUDO $tool install $yes_flag $QUIET_FLAG epel-release; then
|
||||
echo "Could not enable EPEL. Aborting bootstrap!"
|
||||
error "Could not enable EPEL. Aborting bootstrap!"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -403,7 +418,7 @@ BootstrapRpmCommon() {
|
||||
fi
|
||||
|
||||
if ! $SUDO $tool install $yes_flag $QUIET_FLAG $pkgs; then
|
||||
echo "Could not install OS dependencies. Aborting bootstrap!"
|
||||
error "Could not install OS dependencies. Aborting bootstrap!"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
@@ -508,15 +523,15 @@ BootstrapFreeBsd() {
|
||||
|
||||
BootstrapMac() {
|
||||
if hash brew 2>/dev/null; then
|
||||
echo "Using Homebrew to install dependencies..."
|
||||
say "Using Homebrew to install dependencies..."
|
||||
pkgman=brew
|
||||
pkgcmd="brew install"
|
||||
elif hash port 2>/dev/null; then
|
||||
echo "Using MacPorts to install dependencies..."
|
||||
say "Using MacPorts to install dependencies..."
|
||||
pkgman=port
|
||||
pkgcmd="$SUDO port install"
|
||||
else
|
||||
echo "No Homebrew/MacPorts; installing Homebrew..."
|
||||
say "No Homebrew/MacPorts; installing Homebrew..."
|
||||
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
||||
pkgman=brew
|
||||
pkgcmd="brew install"
|
||||
@@ -527,26 +542,26 @@ BootstrapMac() {
|
||||
-o "$(which python)" = "/usr/bin/python" ]; then
|
||||
# We want to avoid using the system Python because it requires root to use pip.
|
||||
# python.org, MacPorts or HomeBrew Python installations should all be OK.
|
||||
echo "Installing python..."
|
||||
say "Installing python..."
|
||||
$pkgcmd python
|
||||
fi
|
||||
|
||||
# Workaround for _dlopen not finding augeas on macOS
|
||||
if [ "$pkgman" = "port" ] && ! [ -e "/usr/local/lib/libaugeas.dylib" ] && [ -e "/opt/local/lib/libaugeas.dylib" ]; then
|
||||
echo "Applying augeas workaround"
|
||||
say "Applying augeas workaround"
|
||||
$SUDO mkdir -p /usr/local/lib/
|
||||
$SUDO ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib/
|
||||
fi
|
||||
|
||||
if ! hash pip 2>/dev/null; then
|
||||
echo "pip not installed"
|
||||
echo "Installing pip..."
|
||||
say "pip not installed"
|
||||
say "Installing pip..."
|
||||
curl --silent --show-error --retry 5 https://bootstrap.pypa.io/get-pip.py | python
|
||||
fi
|
||||
|
||||
if ! hash virtualenv 2>/dev/null; then
|
||||
echo "virtualenv not installed."
|
||||
echo "Installing with pip..."
|
||||
say "virtualenv not installed."
|
||||
say "Installing with pip..."
|
||||
pip install virtualenv
|
||||
fi
|
||||
}
|
||||
@@ -566,7 +581,7 @@ BootstrapMageiaCommon() {
|
||||
libpython-devel \
|
||||
python-virtualenv
|
||||
then
|
||||
echo "Could not install Python dependencies. Aborting bootstrap!"
|
||||
error "Could not install Python dependencies. Aborting bootstrap!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -578,7 +593,7 @@ BootstrapMageiaCommon() {
|
||||
libffi-devel \
|
||||
rootcerts
|
||||
then
|
||||
echo "Could not install additional dependencies. Aborting bootstrap!"
|
||||
error "Could not install additional dependencies. Aborting bootstrap!"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
@@ -605,11 +620,11 @@ Bootstrap() {
|
||||
BootstrapMessage "Archlinux"
|
||||
BootstrapArchCommon
|
||||
else
|
||||
echo "Please use pacman to install letsencrypt packages:"
|
||||
echo "# pacman -S certbot certbot-apache"
|
||||
echo
|
||||
echo "If you would like to use the virtualenv way, please run the script again with the"
|
||||
echo "--debug flag."
|
||||
error "Please use pacman to install letsencrypt packages:"
|
||||
error "# pacman -S certbot certbot-apache"
|
||||
error
|
||||
error "If you would like to use the virtualenv way, please run the script again with the"
|
||||
error "--debug flag."
|
||||
exit 1
|
||||
fi
|
||||
elif [ -f /etc/manjaro-release ]; then
|
||||
@@ -625,11 +640,11 @@ Bootstrap() {
|
||||
elif [ -f /etc/product ] && grep -q "Joyent Instance" /etc/product ; then
|
||||
ExperimentalBootstrap "Joyent SmartOS Zone" BootstrapSmartOS
|
||||
else
|
||||
echo "Sorry, I don't know how to bootstrap Certbot on your operating system!"
|
||||
echo
|
||||
echo "You will need to install OS dependencies, configure virtualenv, and run pip install manually."
|
||||
echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites"
|
||||
echo "for more info."
|
||||
error "Sorry, I don't know how to bootstrap Certbot on your operating system!"
|
||||
error
|
||||
error "You will need to install OS dependencies, configure virtualenv, and run pip install manually."
|
||||
error "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites"
|
||||
error "for more info."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
@@ -649,7 +664,7 @@ if [ "$1" = "--le-auto-phase2" ]; then
|
||||
# grep for both certbot and letsencrypt until certbot and shim packages have been released
|
||||
INSTALLED_VERSION=$("$VENV_BIN/letsencrypt" --version 2>&1 | grep "^certbot\|^letsencrypt" | cut -d " " -f 2)
|
||||
if [ -z "$INSTALLED_VERSION" ]; then
|
||||
echo "Error: couldn't get currently installed version for $VENV_BIN/letsencrypt: " 1>&2
|
||||
error "Error: couldn't get currently installed version for $VENV_BIN/letsencrypt: " 1>&2
|
||||
"$VENV_BIN/letsencrypt" --version
|
||||
exit 1
|
||||
fi
|
||||
@@ -657,7 +672,7 @@ if [ "$1" = "--le-auto-phase2" ]; then
|
||||
INSTALLED_VERSION="none"
|
||||
fi
|
||||
if [ "$LE_AUTO_VERSION" != "$INSTALLED_VERSION" ]; then
|
||||
echo "Creating virtual environment..."
|
||||
say "Creating virtual environment..."
|
||||
DeterminePythonVersion
|
||||
rm -rf "$VENV_PATH"
|
||||
if [ "$VERBOSE" = 1 ]; then
|
||||
@@ -666,7 +681,7 @@ if [ "$1" = "--le-auto-phase2" ]; then
|
||||
virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" > /dev/null
|
||||
fi
|
||||
|
||||
echo "Installing Python packages..."
|
||||
say "Installing Python packages..."
|
||||
TEMP_DIR=$(TempDir)
|
||||
trap 'rm -rf "$TEMP_DIR"' EXIT
|
||||
# There is no $ interpolation due to quotes on starting heredoc delimiter.
|
||||
@@ -845,18 +860,18 @@ letsencrypt==0.7.0 \
|
||||
|
||||
# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE.
|
||||
|
||||
acme==0.13.0 \
|
||||
--hash=sha256:103ce8bed43aad1a9655ed815df09bbeab86ee16cc82137b44d9dac68faa394f \
|
||||
--hash=sha256:7489b3e20d02da0a389aedb82408ffb6b76294e41d833db85591b9f779539815
|
||||
certbot==0.13.0 \
|
||||
--hash=sha256:65d0d9d158972aff7746d4ef80a20465a14c54ae8bcb879216970c2a1b34503c \
|
||||
--hash=sha256:f63ad7747edaca2fb7d60c28882e44d2f48ff1cca9b9c7c251ad47e2189c00f3
|
||||
certbot-apache==0.13.0 \
|
||||
--hash=sha256:22f7c1dc93439384c0874960081d66957910c6dc737a9facbd9fcbc46c545874 \
|
||||
--hash=sha256:b43b04b53005e7218a09a0ba4d97581fab369e929472fa49fb55d29d0ab54589
|
||||
certbot-nginx==0.13.0 \
|
||||
--hash=sha256:9d0ab4eeb98b0ebad70ba116b32268342ad343d82d64990a652ff8072959b044 \
|
||||
--hash=sha256:f026a8faee8397a22c5d4a7623a6ef7c7e780ed63a3bdf9940f43f7823aa2a72
|
||||
acme==0.14.1 \
|
||||
--hash=sha256:f535d6459dcafa436749a8d2fdfafed21b792efa05b8bd3263fcd739c2e1497c \
|
||||
--hash=sha256:0e6d9d1bbb71d80c61c8d10ab9a40bcf38e25f0fa016b9769e96ebf5a79b552b
|
||||
certbot==0.14.1 \
|
||||
--hash=sha256:f950a058d4f657160de4ad163d9f781fe7adeec0c0a44556841adb03ad135d13 \
|
||||
--hash=sha256:519b28124869d97116cb1f2f04ccc2937c0b2fd32fce43576eb80c0e4ff1ab65
|
||||
certbot-apache==0.14.1 \
|
||||
--hash=sha256:1dda9b4dcf66f6dfba37c787d849e69ad25a344572f74a76fc4447bb1a5417b2 \
|
||||
--hash=sha256:da84996e345fc5789da3575225536b27fa3b35f89b2db2d8f494a34bced14f9b
|
||||
certbot-nginx==0.14.1 \
|
||||
--hash=sha256:bd3d4a1dcd6fa9e8ead19a9da88693f08b63464c86be2442e42cd60565c3f05f \
|
||||
--hash=sha256:f0c19f667072e4cfa6b92abf8312b6bee3ed1d2432676b211593034e7d1abb7e
|
||||
|
||||
UNLIKELY_EOF
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -1022,42 +1037,40 @@ UNLIKELY_EOF
|
||||
set -e
|
||||
if [ "$PIP_STATUS" != 0 ]; then
|
||||
# Report error. (Otherwise, be quiet.)
|
||||
echo "Had a problem while installing Python packages."
|
||||
error "Had a problem while installing Python packages."
|
||||
if [ "$VERBOSE" != 1 ]; then
|
||||
echo
|
||||
echo "pip prints the following errors: "
|
||||
echo "====================================================="
|
||||
echo "$PIP_OUT"
|
||||
echo "====================================================="
|
||||
echo
|
||||
echo "Certbot has problem setting up the virtual environment."
|
||||
error
|
||||
error "pip prints the following errors: "
|
||||
error "====================================================="
|
||||
error "$PIP_OUT"
|
||||
error "====================================================="
|
||||
error
|
||||
error "Certbot has problem setting up the virtual environment."
|
||||
|
||||
if `echo $PIP_OUT | grep -q Killed` || `echo $PIP_OUT | grep -q "allocate memory"` ; then
|
||||
echo
|
||||
echo "Based on your pip output, the problem can likely be fixed by "
|
||||
echo "increasing the available memory."
|
||||
error
|
||||
error "Based on your pip output, the problem can likely be fixed by "
|
||||
error "increasing the available memory."
|
||||
else
|
||||
echo
|
||||
echo "We were not be able to guess the right solution from your pip "
|
||||
echo "output."
|
||||
error
|
||||
error "We were not be able to guess the right solution from your pip "
|
||||
error "output."
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Consult https://certbot.eff.org/docs/install.html#problems-with-python-virtual-environment"
|
||||
echo "for possible solutions."
|
||||
echo "You may also find some support resources at https://certbot.eff.org/support/ ."
|
||||
error
|
||||
error "Consult https://certbot.eff.org/docs/install.html#problems-with-python-virtual-environment"
|
||||
error "for possible solutions."
|
||||
error "You may also find some support resources at https://certbot.eff.org/support/ ."
|
||||
fi
|
||||
rm -rf "$VENV_PATH"
|
||||
exit 1
|
||||
fi
|
||||
echo "Installation succeeded."
|
||||
say "Installation succeeded."
|
||||
fi
|
||||
if [ -n "$SUDO" ]; then
|
||||
# SUDO is su wrapper or sudo
|
||||
if [ "$QUIET" != 1 ]; then
|
||||
echo "Requesting root privileges to run certbot..."
|
||||
echo " $VENV_BIN/letsencrypt" "$@"
|
||||
fi
|
||||
say "Requesting root privileges to run certbot..."
|
||||
say " $VENV_BIN/letsencrypt" "$@"
|
||||
fi
|
||||
if [ -z "$SUDO_ENV" ] ; then
|
||||
# SUDO is su wrapper / noop
|
||||
@@ -1084,7 +1097,7 @@ else
|
||||
Bootstrap
|
||||
fi
|
||||
if [ "$OS_PACKAGES_ONLY" = 1 ]; then
|
||||
echo "OS packages installed."
|
||||
say "OS packages installed."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -1227,9 +1240,9 @@ UNLIKELY_EOF
|
||||
# ---------------------------------------------------------------------------
|
||||
DeterminePythonVersion
|
||||
if ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then
|
||||
echo "WARNING: unable to check for updates."
|
||||
error "WARNING: unable to check for updates."
|
||||
elif [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then
|
||||
echo "Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION..."
|
||||
say "Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION..."
|
||||
|
||||
# Now we drop into Python so we don't have to install even more
|
||||
# dependencies (curl, etc.), for better flow control, and for the option of
|
||||
@@ -1238,7 +1251,7 @@ UNLIKELY_EOF
|
||||
|
||||
# Install new copy of certbot-auto.
|
||||
# TODO: Deal with quotes in pathnames.
|
||||
echo "Replacing certbot-auto..."
|
||||
say "Replacing certbot-auto..."
|
||||
# Clone permissions with cp. chmod and chown don't have a --reference
|
||||
# option on macOS or BSD, and stat -c on Linux is stat -f on macOS and BSD:
|
||||
$SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone"
|
||||
|
||||
2
certbot-compatibility-test/setup.cfg
Normal file
2
certbot-compatibility-test/setup.cfg
Normal file
@@ -0,0 +1,2 @@
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
@@ -4,7 +4,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.14.0.dev0'
|
||||
version = '0.15.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
'certbot',
|
||||
@@ -42,6 +42,11 @@ setup(
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
],
|
||||
|
||||
190
certbot-dns-cloudflare/LICENSE.txt
Normal file
190
certbot-dns-cloudflare/LICENSE.txt
Normal file
@@ -0,0 +1,190 @@
|
||||
Copyright 2015 Electronic Frontier Foundation and others
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
3
certbot-dns-cloudflare/MANIFEST.in
Normal file
3
certbot-dns-cloudflare/MANIFEST.in
Normal file
@@ -0,0 +1,3 @@
|
||||
include LICENSE.txt
|
||||
include README.rst
|
||||
recursive-include docs *
|
||||
1
certbot-dns-cloudflare/README.rst
Normal file
1
certbot-dns-cloudflare/README.rst
Normal file
@@ -0,0 +1 @@
|
||||
Cloudflare DNS Authenticator plugin for Certbot
|
||||
@@ -0,0 +1 @@
|
||||
"""Cloudflare DNS Authenticator"""
|
||||
198
certbot-dns-cloudflare/certbot_dns_cloudflare/dns_cloudflare.py
Normal file
198
certbot-dns-cloudflare/certbot_dns_cloudflare/dns_cloudflare.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""DNS Authenticator for Cloudflare."""
|
||||
import logging
|
||||
|
||||
import CloudFlare
|
||||
import zope.interface
|
||||
|
||||
from certbot import errors
|
||||
from certbot import interfaces
|
||||
from certbot.plugins import dns_common
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ACCOUNT_URL = 'https://www.cloudflare.com/a/account/my-account'
|
||||
|
||||
|
||||
@zope.interface.implementer(interfaces.IAuthenticator)
|
||||
@zope.interface.provider(interfaces.IPluginFactory)
|
||||
class Authenticator(dns_common.DNSAuthenticator):
|
||||
"""DNS Authenticator for Cloudflare
|
||||
|
||||
This Authenticator uses the Cloudflare API to fulfill a dns-01 challenge.
|
||||
"""
|
||||
|
||||
description = 'Obtain certs using a DNS TXT record (if you are using Cloudflare for DNS).'
|
||||
ttl = 120
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Authenticator, self).__init__(*args, **kwargs)
|
||||
self.credentials = None
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add): # pylint: disable=arguments-differ
|
||||
super(Authenticator, cls).add_parser_arguments(add)
|
||||
add('credentials', help='Cloudflare credentials INI file.')
|
||||
|
||||
def more_info(self): # pylint: disable=missing-docstring,no-self-use
|
||||
return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \
|
||||
'the Cloudflare API.'
|
||||
|
||||
def _setup_credentials(self):
|
||||
self.credentials = self._configure_credentials(
|
||||
'credentials',
|
||||
'Cloudflare credentials INI file',
|
||||
{
|
||||
'email': 'email address associated with Cloudflare account',
|
||||
'api-key': 'API key for Cloudflare account, obtained from {0}'.format(ACCOUNT_URL)
|
||||
}
|
||||
)
|
||||
|
||||
def _perform(self, domain, validation_name, validation):
|
||||
self._get_cloudflare_client().add_txt_record(domain, validation_name, validation, self.ttl)
|
||||
|
||||
def _cleanup(self, domain, validation_name, validation):
|
||||
self._get_cloudflare_client().del_txt_record(domain, validation_name, validation)
|
||||
|
||||
def _get_cloudflare_client(self):
|
||||
return _CloudflareClient(self.credentials.conf('email'), self.credentials.conf('api-key'))
|
||||
|
||||
|
||||
class _CloudflareClient(object):
|
||||
"""
|
||||
Encapsulates all communication with the Cloudflare API.
|
||||
"""
|
||||
|
||||
def __init__(self, email, api_key):
|
||||
self.cf = CloudFlare.CloudFlare(email, api_key)
|
||||
|
||||
def add_txt_record(self, domain, record_name, record_content, record_ttl):
|
||||
"""
|
||||
Add a TXT record using the supplied information.
|
||||
|
||||
:param str domain: The domain to use to look up the Cloudflare zone.
|
||||
:param str record_name: The record name (typically beginning with '_acme-challenge.').
|
||||
:param str record_content: The record content (typically the challenge validation).
|
||||
:param int record_ttl: The record TTL (number of seconds that the record may be cached).
|
||||
:raises certbot.errors.PluginError: if an error occurs communicating with the Cloudflare API
|
||||
"""
|
||||
|
||||
zone_id = self._find_zone_id(domain)
|
||||
|
||||
data = {'type': 'TXT',
|
||||
'name': record_name,
|
||||
'content': record_content,
|
||||
'ttl': record_ttl}
|
||||
|
||||
try:
|
||||
logger.debug('Attempting to add record to zone %s: %s', zone_id, data)
|
||||
self.cf.zones.dns_records.post(zone_id, data=data) # zones | pylint: disable=no-member
|
||||
except CloudFlare.exceptions.CloudFlareAPIError as e:
|
||||
logger.error('Encountered CloudFlareAPIError adding TXT record: %d %s', e, e)
|
||||
raise errors.PluginError('Error communicating with the Cloudflare API: {0}'.format(e))
|
||||
|
||||
record_id = self._find_txt_record_id(zone_id, record_name, record_content)
|
||||
logger.debug('Successfully added TXT record with record_id: %s', record_id)
|
||||
|
||||
def del_txt_record(self, domain, record_name, record_content):
|
||||
"""
|
||||
Delete a TXT record using the supplied information.
|
||||
|
||||
Note that both the record's name and content are used to ensure that similar records
|
||||
created concurrently (e.g., due to concurrent invocations of this plugin) are not deleted.
|
||||
|
||||
Failures are logged, but not raised.
|
||||
|
||||
:param str domain: The domain to use to look up the Cloudflare zone.
|
||||
:param str record_name: The record name (typically beginning with '_acme-challenge.').
|
||||
:param str record_content: The record content (typically the challenge validation).
|
||||
"""
|
||||
|
||||
try:
|
||||
zone_id = self._find_zone_id(domain)
|
||||
except errors.PluginError as e:
|
||||
logger.debug('Encountered error finding zone_id during deletion: %s', e)
|
||||
return
|
||||
|
||||
if zone_id:
|
||||
record_id = self._find_txt_record_id(zone_id, record_name, record_content)
|
||||
if record_id:
|
||||
try:
|
||||
# zones | pylint: disable=no-member
|
||||
self.cf.zones.dns_records.delete(zone_id, record_id)
|
||||
logger.debug('Successfully deleted TXT record.')
|
||||
except CloudFlare.exceptions.CloudFlareAPIError as e:
|
||||
logger.warn('Encountered CloudFlareAPIError deleting TXT record: %s', e)
|
||||
else:
|
||||
logger.debug('TXT record not found; no cleanup needed.')
|
||||
else:
|
||||
logger.debug('Zone not found; no cleanup needed.')
|
||||
|
||||
def _find_zone_id(self, domain):
|
||||
"""
|
||||
Find the zone_id for a given domain.
|
||||
|
||||
:param str domain: The domain for which to find the zone_id.
|
||||
:returns: The zone_id, if found.
|
||||
:rtype: str
|
||||
:raises certbot.errors.PluginError: if no zone_id is found.
|
||||
"""
|
||||
|
||||
zone_name_guesses = dns_common.base_domain_name_guesses(domain)
|
||||
|
||||
for zone_name in zone_name_guesses:
|
||||
params = {'name': zone_name,
|
||||
'per_page': 1}
|
||||
|
||||
try:
|
||||
zones = self.cf.zones.get(params=params) # zones | pylint: disable=no-member
|
||||
except CloudFlare.exceptions.CloudFlareAPIError as e:
|
||||
code = int(e)
|
||||
hint = None
|
||||
|
||||
if code == 6003:
|
||||
hint = 'Did you copy your entire API key?'
|
||||
elif code == 9103:
|
||||
hint = 'Did you enter the correct email address?'
|
||||
|
||||
raise errors.PluginError('Error determining zone_id: {0} {1}. Please confirm that '
|
||||
'you have supplied valid Cloudflare API credentials.{2}'
|
||||
.format(code, e, ' ({0})'.format(hint) if hint else ''))
|
||||
|
||||
if len(zones) > 0:
|
||||
zone_id = zones[0]['id']
|
||||
logger.debug('Found zone_id of %s for %s using name %s', zone_id, domain, zone_name)
|
||||
return zone_id
|
||||
|
||||
raise errors.PluginError('Unable to determine zone_id for {0} using zone names: {1}. '
|
||||
'Please confirm that the domain name has been entered correctly '
|
||||
'and is already associated with the supplied Cloudflare account.'
|
||||
.format(domain, zone_name_guesses))
|
||||
|
||||
def _find_txt_record_id(self, zone_id, record_name, record_content):
|
||||
"""
|
||||
Find the record_id for a TXT record with the given name and content.
|
||||
|
||||
:param str zone_id: The zone_id which contains the record.
|
||||
:param str record_name: The record name (typically beginning with '_acme-challenge.').
|
||||
:param str record_content: The record content (typically the challenge validation).
|
||||
:returns: The record_id, if found.
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
params = {'type': 'TXT',
|
||||
'name': record_name,
|
||||
'content': record_content,
|
||||
'per_page': 1}
|
||||
try:
|
||||
# zones | pylint: disable=no-member
|
||||
records = self.cf.zones.dns_records.get(zone_id, params=params)
|
||||
except CloudFlare.exceptions.CloudFlareAPIError as e:
|
||||
logger.debug('Encountered CloudFlareAPIError getting TXT record_id: %s', e)
|
||||
records = []
|
||||
|
||||
if len(records) > 0:
|
||||
# Cleanup is returning the system to the state we found it. If, for some reason,
|
||||
# there are multiple matching records, we only delete one because we only added one.
|
||||
return records[0]['id']
|
||||
else:
|
||||
logger.debug('Unable to find TXT record.')
|
||||
@@ -0,0 +1,174 @@
|
||||
"""Tests for certbot_dns_cloudflare.dns_cloudflare."""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import CloudFlare
|
||||
import mock
|
||||
|
||||
from certbot import errors
|
||||
from certbot.plugins import dns_test_common
|
||||
from certbot.plugins.dns_test_common import DOMAIN
|
||||
from certbot.tests import util as test_util
|
||||
|
||||
API_ERROR = CloudFlare.exceptions.CloudFlareAPIError(1000, '', '')
|
||||
API_KEY = 'an-api-key'
|
||||
EMAIL = 'example@example.com'
|
||||
|
||||
|
||||
class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest):
|
||||
|
||||
def setUp(self):
|
||||
from certbot_dns_cloudflare.dns_cloudflare import Authenticator
|
||||
|
||||
super(AuthenticatorTest, self).setUp()
|
||||
|
||||
path = os.path.join(self.tempdir, 'file.ini')
|
||||
dns_test_common.write({"cloudflare_email": EMAIL, "cloudflare_api_key": API_KEY}, path)
|
||||
|
||||
self.config = mock.MagicMock(cloudflare_credentials=path,
|
||||
cloudflare_propagation_seconds=0) # don't wait during tests
|
||||
|
||||
self.auth = Authenticator(self.config, "cloudflare")
|
||||
|
||||
self.mock_client = mock.MagicMock()
|
||||
# _get_cloudflare_client | pylint: disable=protected-access
|
||||
self.auth._get_cloudflare_client = mock.MagicMock(return_value=self.mock_client)
|
||||
|
||||
def test_perform(self):
|
||||
self.auth.perform([self.achall])
|
||||
|
||||
expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)]
|
||||
self.assertEqual(expected, self.mock_client.mock_calls)
|
||||
|
||||
def test_cleanup(self):
|
||||
# _attempt_cleanup | pylint: disable=protected-access
|
||||
self.auth._attempt_cleanup = True
|
||||
self.auth.cleanup([self.achall])
|
||||
|
||||
expected = [mock.call.del_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY)]
|
||||
self.assertEqual(expected, self.mock_client.mock_calls)
|
||||
|
||||
|
||||
class CloudflareClientTest(unittest.TestCase):
|
||||
record_name = "foo"
|
||||
record_content = "bar"
|
||||
record_ttl = 42
|
||||
zone_id = 1
|
||||
record_id = 2
|
||||
|
||||
def setUp(self):
|
||||
from certbot_dns_cloudflare.dns_cloudflare import _CloudflareClient
|
||||
|
||||
self.cloudflare_client = _CloudflareClient(EMAIL, API_KEY)
|
||||
|
||||
self.cf = mock.MagicMock()
|
||||
self.cloudflare_client.cf = self.cf
|
||||
|
||||
def test_add_txt_record(self):
|
||||
self.cf.zones.get.return_value = [{'id': self.zone_id}]
|
||||
|
||||
self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content,
|
||||
self.record_ttl)
|
||||
|
||||
self.cf.zones.dns_records.post.assert_called_with(self.zone_id, data=mock.ANY)
|
||||
|
||||
post_data = self.cf.zones.dns_records.post.call_args[1]['data']
|
||||
|
||||
self.assertEqual('TXT', post_data['type'])
|
||||
self.assertEqual(self.record_name, post_data['name'])
|
||||
self.assertEqual(self.record_content, post_data['content'])
|
||||
self.assertEqual(self.record_ttl, post_data['ttl'])
|
||||
|
||||
def test_add_txt_record_error(self):
|
||||
self.cf.zones.get.return_value = [{'id': self.zone_id}]
|
||||
|
||||
self.cf.zones.dns_records.post.side_effect = API_ERROR
|
||||
|
||||
self.assertRaises(
|
||||
errors.PluginError,
|
||||
self.cloudflare_client.add_txt_record,
|
||||
DOMAIN, self.record_name, self.record_content, self.record_ttl)
|
||||
|
||||
def test_add_txt_record_error_during_zone_lookup(self):
|
||||
self.cf.zones.get.side_effect = API_ERROR
|
||||
|
||||
self.assertRaises(
|
||||
errors.PluginError,
|
||||
self.cloudflare_client.add_txt_record,
|
||||
DOMAIN, self.record_name, self.record_content, self.record_ttl)
|
||||
|
||||
def test_add_txt_record_zone_not_found(self):
|
||||
self.cf.zones.get.return_value = []
|
||||
|
||||
self.assertRaises(
|
||||
errors.PluginError,
|
||||
self.cloudflare_client.add_txt_record,
|
||||
DOMAIN, self.record_name, self.record_content, self.record_ttl)
|
||||
|
||||
def test_del_txt_record(self):
|
||||
self.cf.zones.get.return_value = [{'id': self.zone_id}]
|
||||
self.cf.zones.dns_records.get.return_value = [{'id': self.record_id}]
|
||||
|
||||
self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, self.record_content)
|
||||
|
||||
expected = [mock.call.zones.get(params=mock.ANY),
|
||||
mock.call.zones.dns_records.get(self.zone_id, params=mock.ANY),
|
||||
mock.call.zones.dns_records.delete(self.zone_id, self.record_id)]
|
||||
|
||||
self.assertEqual(expected, self.cf.mock_calls)
|
||||
|
||||
get_data = self.cf.zones.dns_records.get.call_args[1]['params']
|
||||
|
||||
self.assertEqual('TXT', get_data['type'])
|
||||
self.assertEqual(self.record_name, get_data['name'])
|
||||
self.assertEqual(self.record_content, get_data['content'])
|
||||
|
||||
def test_del_txt_record_error_during_zone_lookup(self):
|
||||
self.cf.zones.get.side_effect = API_ERROR
|
||||
|
||||
self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, self.record_content)
|
||||
|
||||
def test_del_txt_record_error_during_delete(self):
|
||||
self.cf.zones.get.return_value = [{'id': self.zone_id}]
|
||||
self.cf.zones.dns_records.get.return_value = [{'id': self.record_id}]
|
||||
self.cf.zones.dns_records.delete.side_effect = API_ERROR
|
||||
|
||||
self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, self.record_content)
|
||||
expected = [mock.call.zones.get(params=mock.ANY),
|
||||
mock.call.zones.dns_records.get(self.zone_id, params=mock.ANY),
|
||||
mock.call.zones.dns_records.delete(self.zone_id, self.record_id)]
|
||||
|
||||
self.assertEqual(expected, self.cf.mock_calls)
|
||||
|
||||
def test_del_txt_record_error_during_get(self):
|
||||
self.cf.zones.get.return_value = [{'id': self.zone_id}]
|
||||
self.cf.zones.dns_records.get.side_effect = API_ERROR
|
||||
|
||||
self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, self.record_content)
|
||||
expected = [mock.call.zones.get(params=mock.ANY),
|
||||
mock.call.zones.dns_records.get(self.zone_id, params=mock.ANY)]
|
||||
|
||||
self.assertEqual(expected, self.cf.mock_calls)
|
||||
|
||||
def test_del_txt_record_no_record(self):
|
||||
self.cf.zones.get.return_value = [{'id': self.zone_id}]
|
||||
self.cf.zones.dns_records.get.return_value = []
|
||||
|
||||
self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, self.record_content)
|
||||
expected = [mock.call.zones.get(params=mock.ANY),
|
||||
mock.call.zones.dns_records.get(self.zone_id, params=mock.ANY)]
|
||||
|
||||
self.assertEqual(expected, self.cf.mock_calls)
|
||||
|
||||
def test_del_txt_record_no_zone(self):
|
||||
self.cf.zones.get.return_value = [{'id': None}]
|
||||
|
||||
self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, self.record_content)
|
||||
expected = [mock.call.zones.get(params=mock.ANY)]
|
||||
|
||||
self.assertEqual(expected, self.cf.mock_calls)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
1
certbot-dns-cloudflare/docs/.gitignore
vendored
Normal file
1
certbot-dns-cloudflare/docs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/_build/
|
||||
20
certbot-dns-cloudflare/docs/Makefile
Normal file
20
certbot-dns-cloudflare/docs/Makefile
Normal file
@@ -0,0 +1,20 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SPHINXPROJ = certbot-dns-cloudflare
|
||||
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)
|
||||
8
certbot-dns-cloudflare/docs/api.rst
Normal file
8
certbot-dns-cloudflare/docs/api.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
=================
|
||||
API Documentation
|
||||
=================
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
|
||||
api/**
|
||||
5
certbot-dns-cloudflare/docs/api/dns_cloudflare.rst
Normal file
5
certbot-dns-cloudflare/docs/api/dns_cloudflare.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
:mod:`certbot_dns_cloudflare.dns_cloudflare`
|
||||
--------------------------------------
|
||||
|
||||
.. automodule:: certbot_dns_cloudflare.dns_cloudflare
|
||||
:members:
|
||||
180
certbot-dns-cloudflare/docs/conf.py
Normal file
180
certbot-dns-cloudflare/docs/conf.py
Normal file
@@ -0,0 +1,180 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# certbot-dns-cloudflare documentation build configuration file, created by
|
||||
# sphinx-quickstart on Tue May 9 10:20:04 2017.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = ['sphinx.ext.autodoc',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.viewcode']
|
||||
|
||||
autodoc_member_order = 'bysource'
|
||||
autodoc_default_flags = ['show-inheritance', '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'
|
||||
|
||||
# General information about the project.
|
||||
project = u'certbot-dns-cloudflare'
|
||||
copyright = u'2017, Certbot Project'
|
||||
author = u'Certbot Project'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = u'0'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = u'0'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = 'en'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
default_role = 'py:obj'
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = True
|
||||
|
||||
|
||||
# -- 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']
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'certbot-dns-cloudflaredoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'certbot-dns-cloudflare.tex', u'certbot-dns-cloudflare Documentation',
|
||||
u'Certbot Project', 'manual'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'certbot-dns-cloudflare', u'certbot-dns-cloudflare Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'certbot-dns-cloudflare', u'certbot-dns-cloudflare Documentation',
|
||||
author, 'certbot-dns-cloudflare', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/', None),
|
||||
'acme': ('https://acme-python.readthedocs.org/en/latest/', None),
|
||||
'certbot': ('https://certbot.eff.org/docs/', None),
|
||||
}
|
||||
27
certbot-dns-cloudflare/docs/index.rst
Normal file
27
certbot-dns-cloudflare/docs/index.rst
Normal file
@@ -0,0 +1,27 @@
|
||||
.. certbot-dns-cloudflare documentation master file, created by
|
||||
sphinx-quickstart on Tue May 9 10:20:04 2017.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to certbot-dns-cloudflare's documentation!
|
||||
==================================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
api
|
||||
|
||||
.. automodule:: certbot_dns_cloudflare
|
||||
:members:
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
36
certbot-dns-cloudflare/docs/make.bat
Normal file
36
certbot-dns-cloudflare/docs/make.bat
Normal file
@@ -0,0 +1,36 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
set SPHINXPROJ=certbot-dns-cloudflare
|
||||
|
||||
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
|
||||
2
certbot-dns-cloudflare/setup.cfg
Normal file
2
certbot-dns-cloudflare/setup.cfg
Normal file
@@ -0,0 +1,2 @@
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
69
certbot-dns-cloudflare/setup.py
Normal file
69
certbot-dns-cloudflare/setup.py
Normal file
@@ -0,0 +1,69 @@
|
||||
import sys
|
||||
|
||||
from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.15.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
'acme=={0}'.format(version),
|
||||
'certbot=={0}'.format(version),
|
||||
'cloudflare>=1.5.1',
|
||||
'mock',
|
||||
# For pkg_resources. >=1.0 so pip resolves it to a version cryptography
|
||||
# will tolerate; see #2599:
|
||||
'setuptools>=1.0',
|
||||
'zope.interface',
|
||||
]
|
||||
|
||||
docs_extras = [
|
||||
'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags
|
||||
'sphinx_rtd_theme',
|
||||
]
|
||||
|
||||
setup(
|
||||
name='certbot-dns-cloudflare',
|
||||
version=version,
|
||||
description="Cloudflare DNS Authenticator plugin for Certbot",
|
||||
url='https://github.com/certbot/certbot',
|
||||
author="Certbot Project",
|
||||
author_email='client-dev@letsencrypt.org',
|
||||
license='Apache License 2.0',
|
||||
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.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
'Topic :: System :: Networking',
|
||||
'Topic :: System :: Systems Administration',
|
||||
'Topic :: Utilities',
|
||||
],
|
||||
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=install_requires,
|
||||
extras_require={
|
||||
'docs': docs_extras,
|
||||
},
|
||||
entry_points={
|
||||
'certbot.plugins': [
|
||||
'dns-cloudflare = certbot_dns_cloudflare.dns_cloudflare:Authenticator',
|
||||
],
|
||||
},
|
||||
test_suite='certbot_dns_cloudflare',
|
||||
)
|
||||
190
certbot-dns-cloudxns/LICENSE.txt
Normal file
190
certbot-dns-cloudxns/LICENSE.txt
Normal file
@@ -0,0 +1,190 @@
|
||||
Copyright 2015 Electronic Frontier Foundation and others
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
3
certbot-dns-cloudxns/MANIFEST.in
Normal file
3
certbot-dns-cloudxns/MANIFEST.in
Normal file
@@ -0,0 +1,3 @@
|
||||
include LICENSE.txt
|
||||
include README.rst
|
||||
recursive-include docs *
|
||||
1
certbot-dns-cloudxns/README.rst
Normal file
1
certbot-dns-cloudxns/README.rst
Normal file
@@ -0,0 +1 @@
|
||||
CloudXNS DNS Authenticator plugin for Certbot
|
||||
1
certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py
Normal file
1
certbot-dns-cloudxns/certbot_dns_cloudxns/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""CloudXNS DNS Authenticator"""
|
||||
84
certbot-dns-cloudxns/certbot_dns_cloudxns/dns_cloudxns.py
Normal file
84
certbot-dns-cloudxns/certbot_dns_cloudxns/dns_cloudxns.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""DNS Authenticator for CloudXNS DNS."""
|
||||
import logging
|
||||
|
||||
import zope.interface
|
||||
from lexicon.providers import cloudxns
|
||||
|
||||
from certbot import errors
|
||||
from certbot import interfaces
|
||||
from certbot.plugins import dns_common
|
||||
from certbot.plugins import dns_common_lexicon
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ACCOUNT_URL = 'https://www.cloudxns.net/en/AccountManage/apimanage.html'
|
||||
|
||||
|
||||
@zope.interface.implementer(interfaces.IAuthenticator)
|
||||
@zope.interface.provider(interfaces.IPluginFactory)
|
||||
class Authenticator(dns_common.DNSAuthenticator):
|
||||
"""DNS Authenticator for CloudXNS DNS
|
||||
|
||||
This Authenticator uses the CloudXNS DNS API to fulfill a dns-01 challenge.
|
||||
"""
|
||||
|
||||
description = 'Obtain certs using a DNS TXT record (if you are using CloudXNS for DNS).'
|
||||
ttl = 60
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Authenticator, self).__init__(*args, **kwargs)
|
||||
self.credentials = None
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add): # pylint: disable=arguments-differ
|
||||
super(Authenticator, cls).add_parser_arguments(add, default_propagation_seconds=30)
|
||||
add('credentials', help='CloudXNS credentials INI file.')
|
||||
|
||||
def more_info(self): # pylint: disable=missing-docstring,no-self-use
|
||||
return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \
|
||||
'the CloudXNS API.'
|
||||
|
||||
def _setup_credentials(self):
|
||||
self.credentials = self._configure_credentials(
|
||||
'credentials',
|
||||
'CloudXNS credentials INI file',
|
||||
{
|
||||
'api-key': 'API key for CloudXNS account, obtained from {0}'.format(ACCOUNT_URL),
|
||||
'secret-key': 'Secret key for CloudXNS account, obtained from {0}'
|
||||
.format(ACCOUNT_URL)
|
||||
}
|
||||
)
|
||||
|
||||
def _perform(self, domain, validation_name, validation):
|
||||
self._get_cloudxns_client().add_txt_record(domain, validation_name, validation)
|
||||
|
||||
def _cleanup(self, domain, validation_name, validation):
|
||||
self._get_cloudxns_client().del_txt_record(domain, validation_name, validation)
|
||||
|
||||
def _get_cloudxns_client(self):
|
||||
return _CloudXNSLexiconClient(self.credentials.conf('api-key'),
|
||||
self.credentials.conf('secret-key'),
|
||||
self.ttl)
|
||||
|
||||
|
||||
class _CloudXNSLexiconClient(dns_common_lexicon.LexiconClient):
|
||||
"""
|
||||
Encapsulates all communication with the CloudXNS via Lexicon.
|
||||
"""
|
||||
|
||||
def __init__(self, api_key, secret_key, ttl):
|
||||
super(_CloudXNSLexiconClient, self).__init__()
|
||||
|
||||
self.provider = cloudxns.Provider({
|
||||
'auth_username': api_key,
|
||||
'auth_token': secret_key,
|
||||
'ttl': ttl,
|
||||
})
|
||||
|
||||
def _handle_http_error(self, e, domain_name):
|
||||
hint = None
|
||||
if str(e).startswith('400 Client Error:'):
|
||||
hint = 'Are your API key and Secret key values correct?'
|
||||
|
||||
return errors.PluginError('Error determining zone identifier for {0}: {1}.{2}'
|
||||
.format(domain_name, e, ' ({0})'.format(hint) if hint else ''))
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Tests for certbot_dns_cloudxns.dns_cloudxns."""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
from requests.exceptions import HTTPError, RequestException
|
||||
|
||||
from certbot.plugins import dns_test_common
|
||||
from certbot.plugins import dns_test_common_lexicon
|
||||
from certbot.tests import util as test_util
|
||||
|
||||
DOMAIN_NOT_FOUND = Exception('No domain found')
|
||||
GENERIC_ERROR = RequestException
|
||||
LOGIN_ERROR = HTTPError('400 Client Error: ...')
|
||||
|
||||
API_KEY = 'foo'
|
||||
SECRET = 'bar'
|
||||
|
||||
|
||||
class AuthenticatorTest(test_util.TempDirTestCase,
|
||||
dns_test_common_lexicon.BaseLexiconAuthenticatorTest):
|
||||
|
||||
def setUp(self):
|
||||
super(AuthenticatorTest, self).setUp()
|
||||
|
||||
from certbot_dns_cloudxns.dns_cloudxns import Authenticator
|
||||
|
||||
path = os.path.join(self.tempdir, 'file.ini')
|
||||
dns_test_common.write({"cloudxns_api_key": API_KEY, "cloudxns_secret_key": SECRET}, path)
|
||||
|
||||
self.config = mock.MagicMock(cloudxns_credentials=path,
|
||||
cloudxns_propagation_seconds=0) # don't wait during tests
|
||||
|
||||
self.auth = Authenticator(self.config, "cloudxns")
|
||||
|
||||
self.mock_client = mock.MagicMock()
|
||||
# _get_cloudxns_client | pylint: disable=protected-access
|
||||
self.auth._get_cloudxns_client = mock.MagicMock(return_value=self.mock_client)
|
||||
|
||||
|
||||
class CloudXNSLexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest):
|
||||
|
||||
def setUp(self):
|
||||
from certbot_dns_cloudxns.dns_cloudxns import _CloudXNSLexiconClient
|
||||
|
||||
self.client = _CloudXNSLexiconClient(API_KEY, SECRET, 0)
|
||||
|
||||
self.provider_mock = mock.MagicMock()
|
||||
self.client.provider = self.provider_mock
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
1
certbot-dns-cloudxns/docs/.gitignore
vendored
Normal file
1
certbot-dns-cloudxns/docs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/_build/
|
||||
20
certbot-dns-cloudxns/docs/Makefile
Normal file
20
certbot-dns-cloudxns/docs/Makefile
Normal file
@@ -0,0 +1,20 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SPHINXPROJ = certbot-dns-cloudxns
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
8
certbot-dns-cloudxns/docs/api.rst
Normal file
8
certbot-dns-cloudxns/docs/api.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
=================
|
||||
API Documentation
|
||||
=================
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
|
||||
api/**
|
||||
5
certbot-dns-cloudxns/docs/api/dns_cloudxns.rst
Normal file
5
certbot-dns-cloudxns/docs/api/dns_cloudxns.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
:mod:`certbot_dns_cloudxns.dns_cloudxns`
|
||||
----------------------------------------
|
||||
|
||||
.. automodule:: certbot_dns_cloudxns.dns_cloudxns
|
||||
:members:
|
||||
180
certbot-dns-cloudxns/docs/conf.py
Normal file
180
certbot-dns-cloudxns/docs/conf.py
Normal file
@@ -0,0 +1,180 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# certbot-dns-cloudxns documentation build configuration file, created by
|
||||
# sphinx-quickstart on Wed May 10 16:05:50 2017.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = ['sphinx.ext.autodoc',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.viewcode']
|
||||
|
||||
autodoc_member_order = 'bysource'
|
||||
autodoc_default_flags = ['show-inheritance', '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'
|
||||
|
||||
# General information about the project.
|
||||
project = u'certbot-dns-cloudxns'
|
||||
copyright = u'2017, Certbot Project'
|
||||
author = u'Certbot Project'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = u'0'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = u'0'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = 'en'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
default_role = 'py:obj'
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = True
|
||||
|
||||
|
||||
# -- 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']
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'certbot-dns-cloudxnsdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'certbot-dns-cloudxns.tex', u'certbot-dns-cloudxns Documentation',
|
||||
u'Certbot Project', 'manual'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'certbot-dns-cloudxns', u'certbot-dns-cloudxns Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'certbot-dns-cloudxns', u'certbot-dns-cloudxns Documentation',
|
||||
author, 'certbot-dns-cloudxns', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/', None),
|
||||
'acme': ('https://acme-python.readthedocs.org/en/latest/', None),
|
||||
'certbot': ('https://certbot.eff.org/docs/', None),
|
||||
}
|
||||
28
certbot-dns-cloudxns/docs/index.rst
Normal file
28
certbot-dns-cloudxns/docs/index.rst
Normal file
@@ -0,0 +1,28 @@
|
||||
.. certbot-dns-cloudxns documentation master file, created by
|
||||
sphinx-quickstart on Wed May 10 16:05:50 2017.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to certbot-dns-cloudxns's documentation!
|
||||
================================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
api
|
||||
|
||||
.. automodule:: certbot_dns_cloudxns
|
||||
:members:
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
36
certbot-dns-cloudxns/docs/make.bat
Normal file
36
certbot-dns-cloudxns/docs/make.bat
Normal file
@@ -0,0 +1,36 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
set SPHINXPROJ=certbot-dns-cloudxns
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
|
||||
:end
|
||||
popd
|
||||
2
certbot-dns-cloudxns/setup.cfg
Normal file
2
certbot-dns-cloudxns/setup.cfg
Normal file
@@ -0,0 +1,2 @@
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
68
certbot-dns-cloudxns/setup.py
Normal file
68
certbot-dns-cloudxns/setup.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import sys
|
||||
|
||||
from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.15.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
'acme=={0}'.format(version),
|
||||
'certbot=={0}'.format(version),
|
||||
'dns-lexicon',
|
||||
'mock',
|
||||
# For pkg_resources. >=1.0 so pip resolves it to a version cryptography
|
||||
# will tolerate; see #2599:
|
||||
'setuptools>=1.0',
|
||||
'zope.interface',
|
||||
]
|
||||
|
||||
docs_extras = [
|
||||
'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags
|
||||
'sphinx_rtd_theme',
|
||||
]
|
||||
|
||||
setup(
|
||||
name='certbot-dns-cloudxns',
|
||||
version=version,
|
||||
description="CloudXNS DNS Authenticator plugin for Certbot",
|
||||
url='https://github.com/certbot/certbot',
|
||||
author="Certbot Project",
|
||||
author_email='client-dev@letsencrypt.org',
|
||||
license='Apache License 2.0',
|
||||
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.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
'Topic :: System :: Networking',
|
||||
'Topic :: System :: Systems Administration',
|
||||
'Topic :: Utilities',
|
||||
],
|
||||
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=install_requires,
|
||||
extras_require={
|
||||
'docs': docs_extras,
|
||||
},
|
||||
entry_points={
|
||||
'certbot.plugins': [
|
||||
'dns-cloudxns = certbot_dns_cloudxns.dns_cloudxns:Authenticator',
|
||||
],
|
||||
},
|
||||
test_suite='certbot_dns_cloudxns',
|
||||
)
|
||||
190
certbot-dns-digitalocean/LICENSE.txt
Normal file
190
certbot-dns-digitalocean/LICENSE.txt
Normal file
@@ -0,0 +1,190 @@
|
||||
Copyright 2015 Electronic Frontier Foundation and others
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
3
certbot-dns-digitalocean/MANIFEST.in
Normal file
3
certbot-dns-digitalocean/MANIFEST.in
Normal file
@@ -0,0 +1,3 @@
|
||||
include LICENSE.txt
|
||||
include README.rst
|
||||
recursive-include docs *
|
||||
1
certbot-dns-digitalocean/README.rst
Normal file
1
certbot-dns-digitalocean/README.rst
Normal file
@@ -0,0 +1 @@
|
||||
DigitalOcean DNS Authenticator plugin for Certbot
|
||||
@@ -0,0 +1 @@
|
||||
"""DigitalOcean DNS Authenticator"""
|
||||
@@ -0,0 +1,168 @@
|
||||
"""DNS Authenticator for DigitalOcean."""
|
||||
import logging
|
||||
|
||||
import digitalocean
|
||||
import zope.interface
|
||||
|
||||
from certbot import errors
|
||||
from certbot import interfaces
|
||||
from certbot.plugins import dns_common
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@zope.interface.implementer(interfaces.IAuthenticator)
|
||||
@zope.interface.provider(interfaces.IPluginFactory)
|
||||
class Authenticator(dns_common.DNSAuthenticator):
|
||||
"""DNS Authenticator for DigitalOcean
|
||||
|
||||
This Authenticator uses the DigitalOcean API to fulfill a dns-01 challenge.
|
||||
"""
|
||||
|
||||
description = 'Obtain certs using a DNS TXT record (if you are using DigitalOcean for DNS).'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Authenticator, self).__init__(*args, **kwargs)
|
||||
self.credentials = None
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add): # pylint: disable=arguments-differ
|
||||
super(Authenticator, cls).add_parser_arguments(add)
|
||||
add('credentials', help='DigitalOcean credentials INI file.')
|
||||
|
||||
def more_info(self): # pylint: disable=missing-docstring,no-self-use
|
||||
return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \
|
||||
'the DigitalOcean API.'
|
||||
|
||||
def _setup_credentials(self):
|
||||
self.credentials = self._configure_credentials(
|
||||
'credentials',
|
||||
'DigitalOcean credentials INI file',
|
||||
{
|
||||
'token': 'API token for DigitalOcean account'
|
||||
}
|
||||
)
|
||||
|
||||
def _perform(self, domain, validation_name, validation):
|
||||
self._get_digitalocean_client().add_txt_record(domain, validation_name, validation)
|
||||
|
||||
def _cleanup(self, domain, validation_name, validation):
|
||||
self._get_digitalocean_client().del_txt_record(domain, validation_name, validation)
|
||||
|
||||
def _get_digitalocean_client(self):
|
||||
return _DigitalOceanClient(self.credentials.conf('token'))
|
||||
|
||||
|
||||
class _DigitalOceanClient(object):
|
||||
"""
|
||||
Encapsulates all communication with the DigitalOcean API.
|
||||
"""
|
||||
|
||||
def __init__(self, token):
|
||||
self.manager = digitalocean.Manager(token=token)
|
||||
|
||||
def add_txt_record(self, domain_name, record_name, record_content):
|
||||
"""
|
||||
Add a TXT record using the supplied information.
|
||||
|
||||
:param str domain_name: The domain to use to associate the record with.
|
||||
:param str record_name: The record name (typically beginning with '_acme-challenge.').
|
||||
:param str record_content: The record content (typically the challenge validation).
|
||||
:raises certbot.errors.PluginError: if an error occurs communicating with the DigitalOcean
|
||||
API
|
||||
"""
|
||||
|
||||
try:
|
||||
domain = self._find_domain(domain_name)
|
||||
except digitalocean.Error as e:
|
||||
hint = None
|
||||
|
||||
if str(e).startswith("Unable to authenticate"):
|
||||
hint = 'Did you provide a valid API token?'
|
||||
|
||||
logger.debug('Error finding domain using the DigitalOcean API: %s', e)
|
||||
raise errors.PluginError('Error finding domain using the DigitalOcean API: {0}{1}'
|
||||
.format(e, ' ({0})'.format(hint) if hint else ''))
|
||||
|
||||
try:
|
||||
result = domain.create_new_domain_record(
|
||||
type='TXT',
|
||||
name=self._compute_record_name(domain, record_name),
|
||||
data=record_content)
|
||||
|
||||
record_id = result['domain_record']['id']
|
||||
|
||||
logger.debug('Successfully added TXT record with id: %d', record_id)
|
||||
except digitalocean.Error as e:
|
||||
logger.debug('Error adding TXT record using the DigitalOcean API: %s', e)
|
||||
raise errors.PluginError('Error adding TXT record using the DigitalOcean API: {0}'
|
||||
.format(e))
|
||||
|
||||
def del_txt_record(self, domain_name, record_name, record_content):
|
||||
"""
|
||||
Delete a TXT record using the supplied information.
|
||||
|
||||
Note that both the record's name and content are used to ensure that similar records
|
||||
created concurrently (e.g., due to concurrent invocations of this plugin) are not deleted.
|
||||
|
||||
Failures are logged, but not raised.
|
||||
|
||||
:param str domain_name: The domain to use to associate the record with.
|
||||
:param str record_name: The record name (typically beginning with '_acme-challenge.').
|
||||
:param str record_content: The record content (typically the challenge validation).
|
||||
"""
|
||||
|
||||
try:
|
||||
domain = self._find_domain(domain_name)
|
||||
except digitalocean.Error as e:
|
||||
logger.debug('Error finding domain using the DigitalOcean API: %s', e)
|
||||
return
|
||||
|
||||
try:
|
||||
domain_records = domain.get_records()
|
||||
|
||||
matching_records = [record for record in domain_records
|
||||
if record.type == 'TXT'
|
||||
and record.name == self._compute_record_name(domain, record_name)
|
||||
and record.data == record_content]
|
||||
except digitalocean.Error as e:
|
||||
logger.debug('Error getting DNS records using the DigitalOcean API: %s', e)
|
||||
return
|
||||
|
||||
for record in matching_records:
|
||||
try:
|
||||
logger.debug('Removing TXT record with id: %s', record.id)
|
||||
record.destroy()
|
||||
except digitalocean.Error as e:
|
||||
logger.warn('Error deleting TXT record %s using the DigitalOcean API: %s',
|
||||
record.id, e)
|
||||
|
||||
def _find_domain(self, domain_name):
|
||||
"""
|
||||
Find the domain object for a given domain name.
|
||||
|
||||
:param str domain_name: The domain name for which to find the corresponding Domain.
|
||||
:returns: The Domain, if found.
|
||||
:rtype: `~digitalocean.Domain`
|
||||
:raises certbot.errors.PluginError: if no matching Domain is found.
|
||||
"""
|
||||
|
||||
domain_name_guesses = dns_common.base_domain_name_guesses(domain_name)
|
||||
|
||||
domains = self.manager.get_all_domains()
|
||||
|
||||
for guess in domain_name_guesses:
|
||||
matches = [domain for domain in domains if domain.name == guess]
|
||||
|
||||
if len(matches) > 0:
|
||||
domain = matches[0]
|
||||
logger.debug('Found base domain for %s using name %s', domain_name, guess)
|
||||
return domain
|
||||
|
||||
raise errors.PluginError('Unable to determine base domain for {0} using names: {1}.'
|
||||
.format(domain_name, domain_name_guesses))
|
||||
|
||||
@staticmethod
|
||||
def _compute_record_name(domain, full_record_name):
|
||||
# The domain, from DigitalOcean's point of view, is automatically appended.
|
||||
return full_record_name.rpartition("." + domain.name)[0]
|
||||
@@ -0,0 +1,170 @@
|
||||
"""Tests for certbot_dns_digitalocean.dns_digitalocean."""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import digitalocean
|
||||
import mock
|
||||
|
||||
from certbot import errors
|
||||
from certbot.plugins import dns_test_common
|
||||
from certbot.plugins.dns_test_common import DOMAIN
|
||||
from certbot.tests import util as test_util
|
||||
|
||||
API_ERROR = digitalocean.DataReadError()
|
||||
TOKEN = 'a-token'
|
||||
|
||||
|
||||
class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest):
|
||||
|
||||
def setUp(self):
|
||||
from certbot_dns_digitalocean.dns_digitalocean import Authenticator
|
||||
|
||||
super(AuthenticatorTest, self).setUp()
|
||||
|
||||
path = os.path.join(self.tempdir, 'file.ini')
|
||||
dns_test_common.write({"digitalocean_token": TOKEN}, path)
|
||||
|
||||
self.config = mock.MagicMock(digitalocean_credentials=path,
|
||||
digitalocean_propagation_seconds=0) # don't wait during tests
|
||||
|
||||
self.auth = Authenticator(self.config, "digitalocean")
|
||||
|
||||
self.mock_client = mock.MagicMock()
|
||||
# _get_digitalocean_client | pylint: disable=protected-access
|
||||
self.auth._get_digitalocean_client = mock.MagicMock(return_value=self.mock_client)
|
||||
|
||||
def test_perform(self):
|
||||
self.auth.perform([self.achall])
|
||||
|
||||
expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY)]
|
||||
self.assertEqual(expected, self.mock_client.mock_calls)
|
||||
|
||||
def test_cleanup(self):
|
||||
# _attempt_cleanup | pylint: disable=protected-access
|
||||
self.auth._attempt_cleanup = True
|
||||
self.auth.cleanup([self.achall])
|
||||
|
||||
expected = [mock.call.del_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY)]
|
||||
self.assertEqual(expected, self.mock_client.mock_calls)
|
||||
|
||||
|
||||
class DigitalOceanClientTest(unittest.TestCase):
|
||||
id = 1
|
||||
record_prefix = "_acme-challenge"
|
||||
record_name = record_prefix + "." + DOMAIN
|
||||
record_content = "bar"
|
||||
|
||||
def setUp(self):
|
||||
from certbot_dns_digitalocean.dns_digitalocean import _DigitalOceanClient
|
||||
|
||||
self.digitalocean_client = _DigitalOceanClient(TOKEN)
|
||||
|
||||
self.manager = mock.MagicMock()
|
||||
self.digitalocean_client.manager = self.manager
|
||||
|
||||
def test_add_txt_record(self):
|
||||
wrong_domain_mock = mock.MagicMock()
|
||||
wrong_domain_mock.name = "other.invalid"
|
||||
wrong_domain_mock.create_new_domain_record.side_effect = AssertionError('Wrong Domain')
|
||||
|
||||
domain_mock = mock.MagicMock()
|
||||
domain_mock.name = DOMAIN
|
||||
domain_mock.create_new_domain_record.return_value = {'domain_record': {'id': self.id}}
|
||||
|
||||
self.manager.get_all_domains.return_value = [wrong_domain_mock, domain_mock]
|
||||
|
||||
self.digitalocean_client.add_txt_record(DOMAIN, self.record_name, self.record_content)
|
||||
|
||||
domain_mock.create_new_domain_record.assert_called_with(type='TXT',
|
||||
name=self.record_prefix,
|
||||
data=self.record_content)
|
||||
|
||||
def test_add_txt_record_fail_to_find_domain(self):
|
||||
self.manager.get_all_domains.return_value = []
|
||||
|
||||
self.assertRaises(errors.PluginError,
|
||||
self.digitalocean_client.add_txt_record,
|
||||
DOMAIN, self.record_name, self.record_content)
|
||||
|
||||
def test_add_txt_record_error_finding_domain(self):
|
||||
self.manager.get_all_domains.side_effect = API_ERROR
|
||||
|
||||
self.assertRaises(errors.PluginError,
|
||||
self.digitalocean_client.add_txt_record,
|
||||
DOMAIN, self.record_name, self.record_content)
|
||||
|
||||
def test_add_txt_record_error_creating_record(self):
|
||||
domain_mock = mock.MagicMock()
|
||||
domain_mock.name = DOMAIN
|
||||
domain_mock.create_new_domain_record.side_effect = API_ERROR
|
||||
|
||||
self.manager.get_all_domains.return_value = [domain_mock]
|
||||
|
||||
self.assertRaises(errors.PluginError,
|
||||
self.digitalocean_client.add_txt_record,
|
||||
DOMAIN, self.record_name, self.record_content)
|
||||
|
||||
def test_del_txt_record(self):
|
||||
first_record_mock = mock.MagicMock()
|
||||
first_record_mock.type = 'TXT'
|
||||
first_record_mock.name = "DIFFERENT"
|
||||
first_record_mock.data = self.record_content
|
||||
|
||||
correct_record_mock = mock.MagicMock()
|
||||
correct_record_mock.type = 'TXT'
|
||||
correct_record_mock.name = self.record_prefix
|
||||
correct_record_mock.data = self.record_content
|
||||
|
||||
last_record_mock = mock.MagicMock()
|
||||
last_record_mock.type = 'TXT'
|
||||
last_record_mock.name = self.record_prefix
|
||||
last_record_mock.data = "DIFFERENT"
|
||||
|
||||
domain_mock = mock.MagicMock()
|
||||
domain_mock.name = DOMAIN
|
||||
domain_mock.get_records.return_value = [first_record_mock,
|
||||
correct_record_mock,
|
||||
last_record_mock]
|
||||
|
||||
self.manager.get_all_domains.return_value = [domain_mock]
|
||||
|
||||
self.digitalocean_client.del_txt_record(DOMAIN, self.record_name, self.record_content)
|
||||
|
||||
correct_record_mock.destroy.assert_called()
|
||||
|
||||
self.assertItemsEqual(first_record_mock.destroy.call_args_list, [])
|
||||
self.assertItemsEqual(last_record_mock.destroy.call_args_list, [])
|
||||
|
||||
def test_del_txt_record_error_finding_domain(self):
|
||||
self.manager.get_all_domains.side_effect = API_ERROR
|
||||
|
||||
self.digitalocean_client.del_txt_record(DOMAIN, self.record_name, self.record_content)
|
||||
|
||||
def test_del_txt_record_error_finding_record(self):
|
||||
domain_mock = mock.MagicMock()
|
||||
domain_mock.name = DOMAIN
|
||||
domain_mock.get_records.side_effect = API_ERROR
|
||||
|
||||
self.manager.get_all_domains.return_value = [domain_mock]
|
||||
|
||||
self.digitalocean_client.del_txt_record(DOMAIN, self.record_name, self.record_content)
|
||||
|
||||
def test_del_txt_record_error_deleting_record(self):
|
||||
record_mock = mock.MagicMock()
|
||||
record_mock.type = 'TXT'
|
||||
record_mock.name = self.record_prefix
|
||||
record_mock.data = self.record_content
|
||||
record_mock.destroy.side_effect = API_ERROR
|
||||
|
||||
domain_mock = mock.MagicMock()
|
||||
domain_mock.name = DOMAIN
|
||||
domain_mock.get_records.return_value = [record_mock]
|
||||
|
||||
self.manager.get_all_domains.return_value = [domain_mock]
|
||||
|
||||
self.digitalocean_client.del_txt_record(DOMAIN, self.record_name, self.record_content)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
1
certbot-dns-digitalocean/docs/.gitignore
vendored
Normal file
1
certbot-dns-digitalocean/docs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/_build/
|
||||
20
certbot-dns-digitalocean/docs/Makefile
Normal file
20
certbot-dns-digitalocean/docs/Makefile
Normal file
@@ -0,0 +1,20 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SPHINXPROJ = certbot-dns-digitalocean
|
||||
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)
|
||||
8
certbot-dns-digitalocean/docs/api.rst
Normal file
8
certbot-dns-digitalocean/docs/api.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
=================
|
||||
API Documentation
|
||||
=================
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
|
||||
api/**
|
||||
5
certbot-dns-digitalocean/docs/api/dns_digitalocean.rst
Normal file
5
certbot-dns-digitalocean/docs/api/dns_digitalocean.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
:mod:`certbot_dns_digitalocean.dns_digitalocean`
|
||||
------------------------------------------------
|
||||
|
||||
.. automodule:: certbot_dns_digitalocean.dns_digitalocean
|
||||
:members:
|
||||
180
certbot-dns-digitalocean/docs/conf.py
Normal file
180
certbot-dns-digitalocean/docs/conf.py
Normal file
@@ -0,0 +1,180 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# certbot-dns-digitalocean documentation build configuration file, created by
|
||||
# sphinx-quickstart on Wed May 10 10:52:06 2017.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = ['sphinx.ext.autodoc',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.viewcode']
|
||||
|
||||
autodoc_member_order = 'bysource'
|
||||
autodoc_default_flags = ['show-inheritance', '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'
|
||||
|
||||
# General information about the project.
|
||||
project = u'certbot-dns-digitalocean'
|
||||
copyright = u'2017, Certbot Project'
|
||||
author = u'Certbot Project'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = u'0'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = u'0'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = 'en'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
default_role = 'py:obj'
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = True
|
||||
|
||||
|
||||
# -- 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']
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'certbot-dns-digitaloceandoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'certbot-dns-digitalocean.tex', u'certbot-dns-digitalocean Documentation',
|
||||
u'Certbot Project', 'manual'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'certbot-dns-digitalocean', u'certbot-dns-digitalocean Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'certbot-dns-digitalocean', u'certbot-dns-digitalocean Documentation',
|
||||
author, 'certbot-dns-digitalocean', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/', None),
|
||||
'acme': ('https://acme-python.readthedocs.org/en/latest/', None),
|
||||
'certbot': ('https://certbot.eff.org/docs/', None),
|
||||
}
|
||||
28
certbot-dns-digitalocean/docs/index.rst
Normal file
28
certbot-dns-digitalocean/docs/index.rst
Normal file
@@ -0,0 +1,28 @@
|
||||
.. certbot-dns-digitalocean documentation master file, created by
|
||||
sphinx-quickstart on Wed May 10 10:52:06 2017.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to certbot-dns-digitalocean's documentation!
|
||||
====================================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
api
|
||||
|
||||
.. automodule:: certbot_dns_digitalocean
|
||||
:members:
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
36
certbot-dns-digitalocean/docs/make.bat
Normal file
36
certbot-dns-digitalocean/docs/make.bat
Normal file
@@ -0,0 +1,36 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
set SPHINXPROJ=certbot-dns-digitalocean
|
||||
|
||||
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
|
||||
2
certbot-dns-digitalocean/setup.cfg
Normal file
2
certbot-dns-digitalocean/setup.cfg
Normal file
@@ -0,0 +1,2 @@
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
69
certbot-dns-digitalocean/setup.py
Normal file
69
certbot-dns-digitalocean/setup.py
Normal file
@@ -0,0 +1,69 @@
|
||||
import sys
|
||||
|
||||
from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.15.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
'acme=={0}'.format(version),
|
||||
'certbot=={0}'.format(version),
|
||||
'mock',
|
||||
'python-digitalocean>=1.11',
|
||||
# For pkg_resources. >=1.0 so pip resolves it to a version cryptography
|
||||
# will tolerate; see #2599:
|
||||
'setuptools>=1.0',
|
||||
'zope.interface',
|
||||
]
|
||||
|
||||
docs_extras = [
|
||||
'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags
|
||||
'sphinx_rtd_theme',
|
||||
]
|
||||
|
||||
setup(
|
||||
name='certbot-dns-digitalocean',
|
||||
version=version,
|
||||
description="DigitalOcean DNS Authenticator plugin for Certbot",
|
||||
url='https://github.com/certbot/certbot',
|
||||
author="Certbot Project",
|
||||
author_email='client-dev@letsencrypt.org',
|
||||
license='Apache License 2.0',
|
||||
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.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
'Topic :: System :: Networking',
|
||||
'Topic :: System :: Systems Administration',
|
||||
'Topic :: Utilities',
|
||||
],
|
||||
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=install_requires,
|
||||
extras_require={
|
||||
'docs': docs_extras,
|
||||
},
|
||||
entry_points={
|
||||
'certbot.plugins': [
|
||||
'dns-digitalocean = certbot_dns_digitalocean.dns_digitalocean:Authenticator',
|
||||
],
|
||||
},
|
||||
test_suite='certbot_dns_digitalocean',
|
||||
)
|
||||
190
certbot-dns-dnsimple/LICENSE.txt
Normal file
190
certbot-dns-dnsimple/LICENSE.txt
Normal file
@@ -0,0 +1,190 @@
|
||||
Copyright 2015 Electronic Frontier Foundation and others
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
3
certbot-dns-dnsimple/MANIFEST.in
Normal file
3
certbot-dns-dnsimple/MANIFEST.in
Normal file
@@ -0,0 +1,3 @@
|
||||
include LICENSE.txt
|
||||
include README.rst
|
||||
recursive-include docs *
|
||||
1
certbot-dns-dnsimple/README.rst
Normal file
1
certbot-dns-dnsimple/README.rst
Normal file
@@ -0,0 +1 @@
|
||||
DNSimple DNS Authenticator plugin for Certbot
|
||||
1
certbot-dns-dnsimple/certbot_dns_dnsimple/__init__.py
Normal file
1
certbot-dns-dnsimple/certbot_dns_dnsimple/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""DNSimple DNS Authenticator"""
|
||||
79
certbot-dns-dnsimple/certbot_dns_dnsimple/dns_dnsimple.py
Normal file
79
certbot-dns-dnsimple/certbot_dns_dnsimple/dns_dnsimple.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""DNS Authenticator for DNSimple DNS."""
|
||||
import logging
|
||||
|
||||
import zope.interface
|
||||
from lexicon.providers import dnsimple
|
||||
|
||||
from certbot import errors
|
||||
from certbot import interfaces
|
||||
from certbot.plugins import dns_common
|
||||
from certbot.plugins import dns_common_lexicon
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ACCOUNT_URL = 'https://dnsimple.com/user'
|
||||
|
||||
|
||||
@zope.interface.implementer(interfaces.IAuthenticator)
|
||||
@zope.interface.provider(interfaces.IPluginFactory)
|
||||
class Authenticator(dns_common.DNSAuthenticator):
|
||||
"""DNS Authenticator for DNSimple
|
||||
|
||||
This Authenticator uses the DNSimple v2 API to fulfill a dns-01 challenge.
|
||||
"""
|
||||
|
||||
description = 'Obtain certs using a DNS TXT record (if you are using DNSimple for DNS).'
|
||||
ttl = 60
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Authenticator, self).__init__(*args, **kwargs)
|
||||
self.credentials = None
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add): # pylint: disable=arguments-differ
|
||||
super(Authenticator, cls).add_parser_arguments(add, default_propagation_seconds=30)
|
||||
add('credentials', help='DNSimple credentials INI file.')
|
||||
|
||||
def more_info(self): # pylint: disable=missing-docstring,no-self-use
|
||||
return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \
|
||||
'the DNSimple API.'
|
||||
|
||||
def _setup_credentials(self):
|
||||
self.credentials = self._configure_credentials(
|
||||
'credentials',
|
||||
'DNSimple credentials INI file',
|
||||
{
|
||||
'token': 'User access token for DNSimple v2 API. (See {0}.)'.format(ACCOUNT_URL)
|
||||
}
|
||||
)
|
||||
|
||||
def _perform(self, domain, validation_name, validation):
|
||||
self._get_dnsimple_client().add_txt_record(domain, validation_name, validation)
|
||||
|
||||
def _cleanup(self, domain, validation_name, validation):
|
||||
self._get_dnsimple_client().del_txt_record(domain, validation_name, validation)
|
||||
|
||||
def _get_dnsimple_client(self):
|
||||
return _DNSimpleLexiconClient(self.credentials.conf('token'), self.ttl)
|
||||
|
||||
|
||||
class _DNSimpleLexiconClient(dns_common_lexicon.LexiconClient):
|
||||
"""
|
||||
Encapsulates all communication with the DNSimple via Lexicon.
|
||||
"""
|
||||
|
||||
def __init__(self, token, ttl):
|
||||
super(_DNSimpleLexiconClient, self).__init__()
|
||||
|
||||
self.provider = dnsimple.Provider({
|
||||
'auth_token': token,
|
||||
'ttl': ttl,
|
||||
})
|
||||
|
||||
def _handle_http_error(self, e, domain_name):
|
||||
hint = None
|
||||
if str(e).startswith('401 Client Error: Unauthorized for url:'):
|
||||
hint = 'Is your API token value correct?'
|
||||
|
||||
return errors.PluginError('Error determining zone identifier for {0}: {1}.{2}'
|
||||
.format(domain_name, e, ' ({0})'.format(hint) if hint else ''))
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Tests for certbot_dns_dnsimple.dns_dnsimple."""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
from certbot.plugins import dns_test_common
|
||||
from certbot.plugins import dns_test_common_lexicon
|
||||
from certbot.tests import util as test_util
|
||||
|
||||
TOKEN = 'foo'
|
||||
|
||||
|
||||
class AuthenticatorTest(test_util.TempDirTestCase,
|
||||
dns_test_common_lexicon.BaseLexiconAuthenticatorTest):
|
||||
|
||||
def setUp(self):
|
||||
super(AuthenticatorTest, self).setUp()
|
||||
|
||||
from certbot_dns_dnsimple.dns_dnsimple import Authenticator
|
||||
|
||||
path = os.path.join(self.tempdir, 'file.ini')
|
||||
dns_test_common.write({"dnsimple_token": TOKEN}, path)
|
||||
|
||||
self.config = mock.MagicMock(dnsimple_credentials=path,
|
||||
dnsimple_propagation_seconds=0) # don't wait during tests
|
||||
|
||||
self.auth = Authenticator(self.config, "dnsimple")
|
||||
|
||||
self.mock_client = mock.MagicMock()
|
||||
# _get_dnsimple_client | pylint: disable=protected-access
|
||||
self.auth._get_dnsimple_client = mock.MagicMock(return_value=self.mock_client)
|
||||
|
||||
|
||||
class DNSimpleLexiconClientTest(unittest.TestCase, dns_test_common_lexicon.BaseLexiconClientTest):
|
||||
|
||||
LOGIN_ERROR = HTTPError('401 Client Error: Unauthorized for url: ...')
|
||||
|
||||
def setUp(self):
|
||||
from certbot_dns_dnsimple.dns_dnsimple import _DNSimpleLexiconClient
|
||||
|
||||
self.client = _DNSimpleLexiconClient(TOKEN, 0)
|
||||
|
||||
self.provider_mock = mock.MagicMock()
|
||||
self.client.provider = self.provider_mock
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
1
certbot-dns-dnsimple/docs/.gitignore
vendored
Normal file
1
certbot-dns-dnsimple/docs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/_build/
|
||||
20
certbot-dns-dnsimple/docs/Makefile
Normal file
20
certbot-dns-dnsimple/docs/Makefile
Normal file
@@ -0,0 +1,20 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SPHINXPROJ = certbot-dns-dnsimple
|
||||
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)
|
||||
8
certbot-dns-dnsimple/docs/api.rst
Normal file
8
certbot-dns-dnsimple/docs/api.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
=================
|
||||
API Documentation
|
||||
=================
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
|
||||
api/**
|
||||
5
certbot-dns-dnsimple/docs/api/dns_dnsimple.rst
Normal file
5
certbot-dns-dnsimple/docs/api/dns_dnsimple.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
:mod:`certbot_dns_dnsimple.dns_dnsimple`
|
||||
----------------------------------------
|
||||
|
||||
.. automodule:: certbot_dns_dnsimple.dns_dnsimple
|
||||
:members:
|
||||
180
certbot-dns-dnsimple/docs/conf.py
Normal file
180
certbot-dns-dnsimple/docs/conf.py
Normal file
@@ -0,0 +1,180 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# certbot-dns-dnsimple documentation build configuration file, created by
|
||||
# sphinx-quickstart on Wed May 10 18:23:41 2017.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = ['sphinx.ext.autodoc',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.viewcode']
|
||||
|
||||
autodoc_member_order = 'bysource'
|
||||
autodoc_default_flags = ['show-inheritance', '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'
|
||||
|
||||
# General information about the project.
|
||||
project = u'certbot-dns-dnsimple'
|
||||
copyright = u'2017, Certbot Project'
|
||||
author = u'Certbot Project'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = u'0'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = u'0'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = 'en'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
default_role = 'py:obj'
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = True
|
||||
|
||||
|
||||
# -- 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']
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'certbot-dns-dnsimpledoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'certbot-dns-dnsimple.tex', u'certbot-dns-dnsimple Documentation',
|
||||
u'Certbot Project', 'manual'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'certbot-dns-dnsimple', u'certbot-dns-dnsimple Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'certbot-dns-dnsimple', u'certbot-dns-dnsimple Documentation',
|
||||
author, 'certbot-dns-dnsimple', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/', None),
|
||||
'acme': ('https://acme-python.readthedocs.org/en/latest/', None),
|
||||
'certbot': ('https://certbot.eff.org/docs/', None),
|
||||
}
|
||||
28
certbot-dns-dnsimple/docs/index.rst
Normal file
28
certbot-dns-dnsimple/docs/index.rst
Normal file
@@ -0,0 +1,28 @@
|
||||
.. certbot-dns-dnsimple documentation master file, created by
|
||||
sphinx-quickstart on Wed May 10 18:23:41 2017.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to certbot-dns-dnsimple's documentation!
|
||||
================================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
api
|
||||
|
||||
.. automodule:: certbot_dns_dnsimple
|
||||
:members:
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
36
certbot-dns-dnsimple/docs/make.bat
Normal file
36
certbot-dns-dnsimple/docs/make.bat
Normal file
@@ -0,0 +1,36 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
set SPHINXPROJ=certbot-dns-dnsimple
|
||||
|
||||
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
|
||||
2
certbot-dns-dnsimple/setup.cfg
Normal file
2
certbot-dns-dnsimple/setup.cfg
Normal file
@@ -0,0 +1,2 @@
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
68
certbot-dns-dnsimple/setup.py
Normal file
68
certbot-dns-dnsimple/setup.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import sys
|
||||
|
||||
from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.15.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
'acme=={0}'.format(version),
|
||||
'certbot=={0}'.format(version),
|
||||
'dns-lexicon',
|
||||
'mock',
|
||||
# For pkg_resources. >=1.0 so pip resolves it to a version cryptography
|
||||
# will tolerate; see #2599:
|
||||
'setuptools>=1.0',
|
||||
'zope.interface',
|
||||
]
|
||||
|
||||
docs_extras = [
|
||||
'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags
|
||||
'sphinx_rtd_theme',
|
||||
]
|
||||
|
||||
setup(
|
||||
name='certbot-dns-dnsimple',
|
||||
version=version,
|
||||
description="DNSimple DNS Authenticator plugin for Certbot",
|
||||
url='https://github.com/certbot/certbot',
|
||||
author="Certbot Project",
|
||||
author_email='client-dev@letsencrypt.org',
|
||||
license='Apache License 2.0',
|
||||
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.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
'Topic :: System :: Networking',
|
||||
'Topic :: System :: Systems Administration',
|
||||
'Topic :: Utilities',
|
||||
],
|
||||
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=install_requires,
|
||||
extras_require={
|
||||
'docs': docs_extras,
|
||||
},
|
||||
entry_points={
|
||||
'certbot.plugins': [
|
||||
'dns-dnsimple = certbot_dns_dnsimple.dns_dnsimple:Authenticator',
|
||||
],
|
||||
},
|
||||
test_suite='certbot_dns_dnsimple',
|
||||
)
|
||||
190
certbot-dns-google/LICENSE.txt
Normal file
190
certbot-dns-google/LICENSE.txt
Normal file
@@ -0,0 +1,190 @@
|
||||
Copyright 2015 Electronic Frontier Foundation and others
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
3
certbot-dns-google/MANIFEST.in
Normal file
3
certbot-dns-google/MANIFEST.in
Normal file
@@ -0,0 +1,3 @@
|
||||
include LICENSE.txt
|
||||
include README.rst
|
||||
recursive-include docs *
|
||||
1
certbot-dns-google/README.rst
Normal file
1
certbot-dns-google/README.rst
Normal file
@@ -0,0 +1 @@
|
||||
Google Cloud DNS Authenticator plugin for Certbot
|
||||
1
certbot-dns-google/certbot_dns_google/__init__.py
Normal file
1
certbot-dns-google/certbot_dns_google/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Google Cloud DNS Authenticator"""
|
||||
184
certbot-dns-google/certbot_dns_google/dns_google.py
Normal file
184
certbot-dns-google/certbot_dns_google/dns_google.py
Normal file
@@ -0,0 +1,184 @@
|
||||
"""DNS Authenticator for Google Cloud DNS."""
|
||||
import json
|
||||
import logging
|
||||
|
||||
import zope.interface
|
||||
from googleapiclient import discovery
|
||||
from googleapiclient import errors as googleapiclient_errors
|
||||
from oauth2client.service_account import ServiceAccountCredentials
|
||||
|
||||
from certbot import errors
|
||||
from certbot import interfaces
|
||||
from certbot.plugins import dns_common
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ACCT_URL = 'https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatinganaccount'
|
||||
PERMISSIONS_URL = 'https://cloud.google.com/dns/access-control#permissions_and_roles'
|
||||
|
||||
|
||||
@zope.interface.implementer(interfaces.IAuthenticator)
|
||||
@zope.interface.provider(interfaces.IPluginFactory)
|
||||
class Authenticator(dns_common.DNSAuthenticator):
|
||||
"""DNS Authenticator for Google Cloud DNS
|
||||
|
||||
This Authenticator uses the Google Cloud DNS API to fulfill a dns-01 challenge.
|
||||
"""
|
||||
|
||||
description = 'Obtain certs using a DNS TXT record (if you are using Google Cloud DNS for DNS).'
|
||||
ttl = 60
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Authenticator, self).__init__(*args, **kwargs)
|
||||
self.credentials = None
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add): # pylint: disable=arguments-differ
|
||||
super(Authenticator, cls).add_parser_arguments(add, default_propagation_seconds=60)
|
||||
add('credentials',
|
||||
help=('Path to Google Cloud DNS service account JSON file. (See {0} for' +
|
||||
'information about creating a service account and {1} for information about the' +
|
||||
'required permissions.)').format(ACCT_URL, PERMISSIONS_URL))
|
||||
|
||||
def more_info(self): # pylint: disable=missing-docstring,no-self-use
|
||||
return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \
|
||||
'the Google Cloud DNS API.'
|
||||
|
||||
def _setup_credentials(self):
|
||||
self._configure_file('credentials', 'path to Google Cloud DNS service account JSON file')
|
||||
|
||||
dns_common.validate_file_permissions(self.conf('credentials'))
|
||||
|
||||
def _perform(self, domain, validation_name, validation):
|
||||
self._get_google_client().add_txt_record(domain, validation_name, validation, self.ttl)
|
||||
|
||||
def _cleanup(self, domain, validation_name, validation):
|
||||
self._get_google_client().del_txt_record(domain, validation_name, validation, self.ttl)
|
||||
|
||||
def _get_google_client(self):
|
||||
return _GoogleClient(self.conf('credentials'))
|
||||
|
||||
|
||||
class _GoogleClient(object):
|
||||
"""
|
||||
Encapsulates all communication with the Google Cloud DNS API.
|
||||
"""
|
||||
|
||||
def __init__(self, account_json):
|
||||
|
||||
scopes = ['https://www.googleapis.com/auth/ndev.clouddns.readwrite']
|
||||
credentials = ServiceAccountCredentials.from_json_keyfile_name(account_json, scopes)
|
||||
self.dns = discovery.build('dns', 'v1', credentials=credentials, cache_discovery=False)
|
||||
with open(account_json) as account:
|
||||
self.project_id = json.load(account)['project_id']
|
||||
|
||||
def add_txt_record(self, domain, record_name, record_content, record_ttl):
|
||||
"""
|
||||
Add a TXT record using the supplied information.
|
||||
|
||||
:param str domain: The domain to use to look up the managed zone.
|
||||
:param str record_name: The record name (typically beginning with '_acme-challenge.').
|
||||
:param str record_content: The record content (typically the challenge validation).
|
||||
:param int record_ttl: The record TTL (number of seconds that the record may be cached).
|
||||
:raises certbot.errors.PluginError: if an error occurs communicating with the Google API
|
||||
"""
|
||||
|
||||
zone_id = self._find_managed_zone_id(domain)
|
||||
|
||||
data = {
|
||||
"kind": "dns#change",
|
||||
"additions": [
|
||||
{
|
||||
"kind": "dns#resourceRecordSet",
|
||||
"type": "TXT",
|
||||
"name": record_name + ".",
|
||||
"rrdatas": [record_content, ],
|
||||
"ttl": record_ttl,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
changes = self.dns.changes() # changes | pylint: disable=no-member
|
||||
|
||||
try:
|
||||
request = changes.create(project=self.project_id, managedZone=zone_id, body=data)
|
||||
response = request.execute()
|
||||
|
||||
status = response['status']
|
||||
change = response['id']
|
||||
while status == 'pending':
|
||||
request = changes.get(project=self.project_id, managedZone=zone_id, changeId=change)
|
||||
response = request.execute()
|
||||
status = response['status']
|
||||
except googleapiclient_errors.Error as e:
|
||||
logger.error('Encountered error adding TXT record: %s', e)
|
||||
raise errors.PluginError('Error communicating with the Google Cloud DNS API: {0}'
|
||||
.format(e))
|
||||
|
||||
def del_txt_record(self, domain, record_name, record_content, record_ttl):
|
||||
"""
|
||||
Delete a TXT record using the supplied information.
|
||||
|
||||
:param str domain: The domain to use to look up the managed zone.
|
||||
:param str record_name: The record name (typically beginning with '_acme-challenge.').
|
||||
:param str record_content: The record content (typically the challenge validation).
|
||||
:param int record_ttl: The record TTL (number of seconds that the record may be cached).
|
||||
:raises certbot.errors.PluginError: if an error occurs communicating with the Google API
|
||||
"""
|
||||
|
||||
try:
|
||||
zone_id = self._find_managed_zone_id(domain)
|
||||
except errors.PluginError as e:
|
||||
logger.warn('Error finding zone. Skipping cleanup.')
|
||||
return
|
||||
|
||||
data = {
|
||||
"kind": "dns#change",
|
||||
"deletions": [
|
||||
{
|
||||
"kind": "dns#resourceRecordSet",
|
||||
"type": "TXT",
|
||||
"name": record_name + ".",
|
||||
"rrdatas": [record_content, ],
|
||||
"ttl": record_ttl,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
changes = self.dns.changes() # changes | pylint: disable=no-member
|
||||
|
||||
try:
|
||||
request = changes.create(project=self.project_id, managedZone=zone_id, body=data)
|
||||
request.execute()
|
||||
except googleapiclient_errors.Error as e:
|
||||
logger.warn('Encountered error deleting TXT record: %s', e)
|
||||
|
||||
def _find_managed_zone_id(self, domain):
|
||||
"""
|
||||
Find the managed zone for a given domain.
|
||||
|
||||
:param str domain: The domain for which to find the managed zone.
|
||||
:returns: The ID of the managed zone, if found.
|
||||
:rtype: str
|
||||
:raises certbot.errors.PluginError: if the managed zone cannot be found.
|
||||
"""
|
||||
|
||||
zone_dns_name_guesses = dns_common.base_domain_name_guesses(domain)
|
||||
|
||||
mz = self.dns.managedZones() # managedZones | pylint: disable=no-member
|
||||
for zone_name in zone_dns_name_guesses:
|
||||
try:
|
||||
request = mz.list(project=self.project_id, dnsName=zone_name + '.')
|
||||
response = request.execute()
|
||||
zones = response['managedZones']
|
||||
except googleapiclient_errors.Error as e:
|
||||
raise errors.PluginError('Encountered error finding managed zone: {0}'
|
||||
.format(e))
|
||||
|
||||
if len(zones) > 0:
|
||||
zone_id = zones[0]['id']
|
||||
logger.debug('Found id of %s for %s using name %s', zone_id, domain, zone_name)
|
||||
return zone_id
|
||||
|
||||
raise errors.PluginError('Unable to determine managed zone for {0} using zone names: {1}.'
|
||||
.format(domain, zone_dns_name_guesses))
|
||||
202
certbot-dns-google/certbot_dns_google/dns_google_test.py
Normal file
202
certbot-dns-google/certbot_dns_google/dns_google_test.py
Normal file
@@ -0,0 +1,202 @@
|
||||
"""Tests for certbot_dns_google.dns_google."""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
from googleapiclient.errors import Error
|
||||
|
||||
from certbot import errors
|
||||
from certbot.plugins import dns_test_common
|
||||
from certbot.plugins.dns_test_common import DOMAIN
|
||||
from certbot.tests import util as test_util
|
||||
|
||||
ACCOUNT_JSON_PATH = '/not/a/real/path.json'
|
||||
API_ERROR = Error()
|
||||
PROJECT_ID = "test-test-1"
|
||||
|
||||
|
||||
class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest):
|
||||
|
||||
def setUp(self):
|
||||
super(AuthenticatorTest, self).setUp()
|
||||
|
||||
from certbot_dns_google.dns_google import Authenticator
|
||||
|
||||
path = os.path.join(self.tempdir, 'file.json')
|
||||
open(path, "wb").close()
|
||||
|
||||
super(AuthenticatorTest, self).setUp()
|
||||
self.config = mock.MagicMock(google_credentials=path,
|
||||
google_propagation_seconds=0) # don't wait during tests
|
||||
|
||||
self.auth = Authenticator(self.config, "google")
|
||||
|
||||
self.mock_client = mock.MagicMock()
|
||||
# _get_google_client | pylint: disable=protected-access
|
||||
self.auth._get_google_client = mock.MagicMock(return_value=self.mock_client)
|
||||
|
||||
def test_perform(self):
|
||||
self.auth.perform([self.achall])
|
||||
|
||||
expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)]
|
||||
self.assertEqual(expected, self.mock_client.mock_calls)
|
||||
|
||||
def test_cleanup(self):
|
||||
# _attempt_cleanup | pylint: disable=protected-access
|
||||
self.auth._attempt_cleanup = True
|
||||
self.auth.cleanup([self.achall])
|
||||
|
||||
expected = [mock.call.del_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)]
|
||||
self.assertEqual(expected, self.mock_client.mock_calls)
|
||||
|
||||
|
||||
class GoogleClientTest(unittest.TestCase):
|
||||
record_name = "foo"
|
||||
record_content = "bar"
|
||||
record_ttl = 42
|
||||
zone = "ZONE_ID"
|
||||
change = "an-id"
|
||||
|
||||
def _setUp_client_with_mock(self, zone_request_side_effect):
|
||||
from certbot_dns_google.dns_google import _GoogleClient
|
||||
|
||||
client = _GoogleClient(ACCOUNT_JSON_PATH)
|
||||
|
||||
# Setup
|
||||
mock_mz = mock.MagicMock()
|
||||
mock_mz.list.return_value.execute.side_effect = zone_request_side_effect
|
||||
|
||||
mock_changes = mock.MagicMock()
|
||||
|
||||
client.dns.managedZones = mock.MagicMock(return_value=mock_mz)
|
||||
client.dns.changes = mock.MagicMock(return_value=mock_changes)
|
||||
|
||||
return client, mock_changes
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('certbot_dns_google.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'))
|
||||
def test_add_txt_record(self, unused_credential_mock):
|
||||
client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}])
|
||||
|
||||
client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)
|
||||
|
||||
expected_body = {
|
||||
"kind": "dns#change",
|
||||
"additions": [
|
||||
{
|
||||
"kind": "dns#resourceRecordSet",
|
||||
"type": "TXT",
|
||||
"name": self.record_name + ".",
|
||||
"rrdatas": [self.record_content, ],
|
||||
"ttl": self.record_ttl,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
changes.create.assert_called_with(body=expected_body,
|
||||
managedZone=self.zone,
|
||||
project=PROJECT_ID)
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('certbot_dns_google.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'))
|
||||
def test_add_txt_record_and_poll(self, unused_credential_mock):
|
||||
client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}])
|
||||
changes.create.return_value.execute.return_value = {'status': 'pending', 'id': self.change}
|
||||
changes.get.return_value.execute.return_value = {'status': 'done'}
|
||||
|
||||
client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)
|
||||
|
||||
changes.create.assert_called_with(body=mock.ANY,
|
||||
managedZone=self.zone,
|
||||
project=PROJECT_ID)
|
||||
|
||||
changes.get.assert_called_with(changeId=self.change,
|
||||
managedZone=self.zone,
|
||||
project=PROJECT_ID)
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('certbot_dns_google.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'))
|
||||
def test_add_txt_record_error_during_zone_lookup(self, unused_credential_mock):
|
||||
client, unused_changes = self._setUp_client_with_mock(API_ERROR)
|
||||
|
||||
self.assertRaises(errors.PluginError, client.add_txt_record,
|
||||
DOMAIN, self.record_name, self.record_content, self.record_ttl)
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('certbot_dns_google.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'))
|
||||
def test_add_txt_record_zone_not_found(self, unused_credential_mock):
|
||||
client, unused_changes = self._setUp_client_with_mock([{'managedZones': []},
|
||||
{'managedZones': []}])
|
||||
|
||||
self.assertRaises(errors.PluginError, client.add_txt_record,
|
||||
DOMAIN, self.record_name, self.record_content, self.record_ttl)
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('certbot_dns_google.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'))
|
||||
def test_add_txt_record_error_during_add(self, unused_credential_mock):
|
||||
client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}])
|
||||
changes.create.side_effect = API_ERROR
|
||||
|
||||
self.assertRaises(errors.PluginError, client.add_txt_record,
|
||||
DOMAIN, self.record_name, self.record_content, self.record_ttl)
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('certbot_dns_google.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'))
|
||||
def test_del_txt_record(self, unused_credential_mock):
|
||||
client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}])
|
||||
|
||||
client.del_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)
|
||||
|
||||
expected_body = {
|
||||
"kind": "dns#change",
|
||||
"deletions": [
|
||||
{
|
||||
"kind": "dns#resourceRecordSet",
|
||||
"type": "TXT",
|
||||
"name": self.record_name + ".",
|
||||
"rrdatas": [self.record_content, ],
|
||||
"ttl": self.record_ttl,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
changes.create.assert_called_with(body=expected_body,
|
||||
managedZone=self.zone,
|
||||
project=PROJECT_ID)
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('certbot_dns_google.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'))
|
||||
def test_del_txt_record_error_during_zone_lookup(self, unused_credential_mock):
|
||||
client, unused_changes = self._setUp_client_with_mock(API_ERROR)
|
||||
|
||||
client.del_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('certbot_dns_google.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'))
|
||||
def test_del_txt_record_zone_not_found(self, unused_credential_mock):
|
||||
client, unused_changes = self._setUp_client_with_mock([{'managedZones': []},
|
||||
{'managedZones': []}])
|
||||
|
||||
client.del_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)
|
||||
|
||||
@mock.patch('oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name')
|
||||
@mock.patch('certbot_dns_google.dns_google.open',
|
||||
mock.mock_open(read_data='{"project_id": "' + PROJECT_ID + '"}'))
|
||||
def test_del_txt_record_error_during_delete(self, unused_credential_mock):
|
||||
client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone}]}])
|
||||
changes.create.side_effect = API_ERROR
|
||||
|
||||
client.del_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
1
certbot-dns-google/docs/.gitignore
vendored
Normal file
1
certbot-dns-google/docs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/_build/
|
||||
20
certbot-dns-google/docs/Makefile
Normal file
20
certbot-dns-google/docs/Makefile
Normal file
@@ -0,0 +1,20 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SPHINXPROJ = certbot-dns-google
|
||||
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)
|
||||
8
certbot-dns-google/docs/api.rst
Normal file
8
certbot-dns-google/docs/api.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
=================
|
||||
API Documentation
|
||||
=================
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
|
||||
api/**
|
||||
5
certbot-dns-google/docs/api/dns_google.rst
Normal file
5
certbot-dns-google/docs/api/dns_google.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
:mod:`certbot_dns_google.dns_google`
|
||||
------------------------------------
|
||||
|
||||
.. automodule:: certbot_dns_google.dns_google
|
||||
:members:
|
||||
180
certbot-dns-google/docs/conf.py
Normal file
180
certbot-dns-google/docs/conf.py
Normal file
@@ -0,0 +1,180 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# certbot-dns-google documentation build configuration file, created by
|
||||
# sphinx-quickstart on Wed May 10 15:47:49 2017.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = ['sphinx.ext.autodoc',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.viewcode']
|
||||
|
||||
autodoc_member_order = 'bysource'
|
||||
autodoc_default_flags = ['show-inheritance', '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'
|
||||
|
||||
# General information about the project.
|
||||
project = u'certbot-dns-google'
|
||||
copyright = u'2017, Certbot Project'
|
||||
author = u'Certbot Project'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = u'0'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = u'0'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = 'en'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
default_role = 'py:obj'
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = True
|
||||
|
||||
|
||||
# -- 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']
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'certbot-dns-googledoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'certbot-dns-google.tex', u'certbot-dns-google Documentation',
|
||||
u'Certbot Project', 'manual'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'certbot-dns-google', u'certbot-dns-google Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'certbot-dns-google', u'certbot-dns-google Documentation',
|
||||
author, 'certbot-dns-google', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/', None),
|
||||
'acme': ('https://acme-python.readthedocs.org/en/latest/', None),
|
||||
'certbot': ('https://certbot.eff.org/docs/', None),
|
||||
}
|
||||
28
certbot-dns-google/docs/index.rst
Normal file
28
certbot-dns-google/docs/index.rst
Normal file
@@ -0,0 +1,28 @@
|
||||
.. certbot-dns-google documentation master file, created by
|
||||
sphinx-quickstart on Wed May 10 15:47:49 2017.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to certbot-dns-google's documentation!
|
||||
==============================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
api
|
||||
|
||||
.. automodule:: certbot_dns_google
|
||||
:members:
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
36
certbot-dns-google/docs/make.bat
Normal file
36
certbot-dns-google/docs/make.bat
Normal file
@@ -0,0 +1,36 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
set SPHINXPROJ=certbot-dns-google
|
||||
|
||||
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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user