commit a5a7cb9f9bb24e677898ab0369a3973174ef4ffa Author: Robert Kaussow Date: Sun Apr 19 17:57:48 2020 +0200 initial commit diff --git a/.drone.jsonnet b/.drone.jsonnet new file mode 100644 index 0000000..7711cd7 --- /dev/null +++ b/.drone.jsonnet @@ -0,0 +1,212 @@ +local PythonVersion(pyversion='2.7') = { + name: 'python' + std.strReplace(pyversion, '.', ''), + image: 'python:' + pyversion, + environment: { + PY_COLORS: 1, + }, + commands: [ + 'pip install -r dev-requirements.txt -qq', + 'pip install -qq .', + 'pytest tests --cov=certbot_dns_corenetworks --no-cov-on-fail', + ], + depends_on: [ + 'clone', + ], +}; + +local PipelineLint = { + kind: 'pipeline', + name: 'lint', + platform: { + os: 'linux', + arch: 'amd64', + }, + steps: [ + { + name: 'flake8', + image: 'python:3.8', + environment: { + PY_COLORS: 1, + }, + commands: [ + 'pip install -r dev-requirements.txt -qq', + 'pip install -qq .', + 'flake8 ./certbot_dns_corenetworks', + ], + }, + ], + trigger: { + ref: ['refs/heads/master', 'refs/tags/**', 'refs/pull/**'], + }, +}; + +local PipelineTest = { + kind: 'pipeline', + name: 'test', + platform: { + os: 'linux', + arch: 'amd64', + }, + steps: [ + PythonVersion(pyversion='2.7'), + PythonVersion(pyversion='3.5'), + PythonVersion(pyversion='3.6'), + PythonVersion(pyversion='3.7'), + PythonVersion(pyversion='3.8'), + { + name: 'codecov', + image: 'python:3.8', + environment: { + PY_COLORS: 1, + CODECOV_TOKEN: { from_secret: 'codecov_token' }, + }, + commands: [ + 'pip install codecov -qq', + 'codecov --required -X gcov', + ], + depends_on: [ + 'python27', + 'python35', + 'python36', + 'python37', + 'python38', + ], + }, + ], + depends_on: [ + 'lint', + ], + trigger: { + ref: ['refs/heads/master', 'refs/tags/**', 'refs/pull/**'], + }, +}; + +local PipelineSecurity = { + kind: 'pipeline', + name: 'security', + platform: { + os: 'linux', + arch: 'amd64', + }, + steps: [ + { + name: 'bandit', + image: 'python:3.8', + environment: { + PY_COLORS: 1, + }, + commands: [ + 'pip install -r dev-requirements.txt -qq', + 'pip install -qq .', + 'bandit -r ./certbot_dns_corenetworks -x ./certbot_dns_corenetworks/test', + ], + }, + ], + depends_on: [ + 'test', + ], + trigger: { + ref: ['refs/heads/master', 'refs/tags/**', 'refs/pull/**'], + }, +}; + +local PipelineBuildPackage = { + kind: 'pipeline', + name: 'build-package', + platform: { + os: 'linux', + arch: 'amd64', + }, + steps: [ + { + name: 'build', + image: 'python:3.8', + environment: { + SETUPTOOLS_SCM_PRETEND_VERSION: '${DRONE_TAG##v}', + }, + commands: [ + 'python setup.py sdist bdist_wheel', + ], + }, + { + name: 'checksum', + image: 'alpine', + commands: [ + 'cd dist/ && sha256sum * > ../sha256sum.txt', + ], + }, + { + name: 'publish-github', + image: 'plugins/github-release', + settings: { + overwrite: true, + api_key: { from_secret: 'github_token' }, + files: ['dist/*', 'sha256sum.txt'], + title: '${DRONE_TAG}', + note: 'CHANGELOG.md', + }, + when: { + ref: ['refs/tags/**'], + }, + }, + { + name: 'publish-pypi', + image: 'plugins/pypi', + settings: { + username: { from_secret: 'pypi_username' }, + password: { from_secret: 'pypi_password' }, + repository: 'https://upload.pypi.org/legacy/', + skip_build: true, + }, + when: { + ref: ['refs/tags/**'], + }, + }, + ], + depends_on: [ + 'security', + ], + trigger: { + ref: ['refs/heads/master', 'refs/tags/**', 'refs/pull/**'], + }, +}; + +local PipelineNotifications = { + kind: 'pipeline', + name: 'notifications', + platform: { + os: 'linux', + arch: 'amd64', + }, + steps: [ + { + name: 'matrix', + image: 'plugins/matrix', + settings: { + homeserver: { from_secret: 'matrix_homeserver' }, + roomid: { from_secret: 'matrix_roomid' }, + template: 'Status: **{{ build.status }}**
Build: [{{ repo.Owner }}/{{ repo.Name }}]({{ build.link }}) ({{ build.branch }}) by {{ build.author }}
Message: {{ build.message }}', + username: { from_secret: 'matrix_username' }, + password: { from_secret: 'matrix_password' }, + }, + when: { + status: ['success', 'failure'], + }, + }, + ], + depends_on: [ + 'build-package', + ], + trigger: { + ref: ['refs/heads/master', 'refs/tags/**'], + status: ['success', 'failure'], + }, +}; + +[ + PipelineLint, + PipelineTest, + PipelineSecurity, + PipelineBuildPackage, + PipelineNotifications, +] diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..1df8d4a --- /dev/null +++ b/.drone.yml @@ -0,0 +1,240 @@ +--- +kind: pipeline +name: lint + +platform: + os: linux + arch: amd64 + +steps: +- name: flake8 + image: python:3.8 + commands: + - pip install -r dev-requirements.txt -qq + - pip install -qq . + - flake8 ./certbot_dns_corenetworks + environment: + PY_COLORS: 1 + +trigger: + ref: + - refs/heads/master + - refs/tags/** + - refs/pull/** + +--- +kind: pipeline +name: test + +platform: + os: linux + arch: amd64 + +steps: +- name: python27 + image: python:2.7 + commands: + - pip install -r dev-requirements.txt -qq + - pip install -qq . + - pytest tests --cov=certbot_dns_corenetworks --no-cov-on-fail + environment: + PY_COLORS: 1 + depends_on: + - clone + +- name: python35 + image: python:3.5 + commands: + - pip install -r dev-requirements.txt -qq + - pip install -qq . + - pytest tests --cov=certbot_dns_corenetworks --no-cov-on-fail + environment: + PY_COLORS: 1 + depends_on: + - clone + +- name: python36 + image: python:3.6 + commands: + - pip install -r dev-requirements.txt -qq + - pip install -qq . + - pytest tests --cov=certbot_dns_corenetworks --no-cov-on-fail + environment: + PY_COLORS: 1 + depends_on: + - clone + +- name: python37 + image: python:3.7 + commands: + - pip install -r dev-requirements.txt -qq + - pip install -qq . + - pytest tests --cov=certbot_dns_corenetworks --no-cov-on-fail + environment: + PY_COLORS: 1 + depends_on: + - clone + +- name: python38 + image: python:3.8 + commands: + - pip install -r dev-requirements.txt -qq + - pip install -qq . + - pytest tests --cov=certbot_dns_corenetworks --no-cov-on-fail + environment: + PY_COLORS: 1 + depends_on: + - clone + +- name: codecov + image: python:3.8 + commands: + - pip install codecov -qq + - codecov --required -X gcov + environment: + CODECOV_TOKEN: + from_secret: codecov_token + PY_COLORS: 1 + depends_on: + - python27 + - python35 + - python36 + - python37 + - python38 + +trigger: + ref: + - refs/heads/master + - refs/tags/** + - refs/pull/** + +depends_on: +- lint + +--- +kind: pipeline +name: security + +platform: + os: linux + arch: amd64 + +steps: +- name: bandit + image: python:3.8 + commands: + - pip install -r dev-requirements.txt -qq + - pip install -qq . + - bandit -r ./certbot_dns_corenetworks -x ./certbot_dns_corenetworks/test + environment: + PY_COLORS: 1 + +trigger: + ref: + - refs/heads/master + - refs/tags/** + - refs/pull/** + +depends_on: +- test + +--- +kind: pipeline +name: build-package + +platform: + os: linux + arch: amd64 + +steps: +- name: build + image: python:3.8 + commands: + - python setup.py sdist bdist_wheel + environment: + SETUPTOOLS_SCM_PRETEND_VERSION: ${DRONE_TAG##v} + +- name: checksum + image: alpine + commands: + - cd dist/ && sha256sum * > ../sha256sum.txt + +- name: publish-github + image: plugins/github-release + settings: + api_key: + from_secret: github_token + files: + - dist/* + - sha256sum.txt + note: CHANGELOG.md + overwrite: true + title: ${DRONE_TAG} + when: + ref: + - refs/tags/** + +- name: publish-pypi + image: plugins/pypi + settings: + password: + from_secret: pypi_password + repository: https://upload.pypi.org/legacy/ + skip_build: true + username: + from_secret: pypi_username + when: + ref: + - refs/tags/** + +trigger: + ref: + - refs/heads/master + - refs/tags/** + - refs/pull/** + +depends_on: +- security + +--- +kind: pipeline +name: notifications + +platform: + os: linux + arch: amd64 + +steps: +- name: matrix + image: plugins/matrix + settings: + homeserver: + from_secret: matrix_homeserver + password: + from_secret: matrix_password + roomid: + from_secret: matrix_roomid + template: "Status: **{{ build.status }}**
Build: [{{ repo.Owner }}/{{ repo.Name }}]({{ build.link }}) ({{ build.branch }}) by {{ build.author }}
Message: {{ build.message }}" + username: + from_secret: matrix_username + when: + status: + - success + - failure + +trigger: + ref: + - refs/heads/master + - refs/tags/** + status: + - success + - failure + +depends_on: +- build-package + +--- +kind: signature +hmac: 09f1f5c2f46dd90b159b5c5de705afce849ea7fb4772b96aa9827a6d6bb97e3e + +... diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..472dc43 --- /dev/null +++ b/.flake8 @@ -0,0 +1,19 @@ +[flake8] +ignore = D103, D107, W503 +max-line-length = 99 +inline-quotes = double +exclude = + .git + .tox + __pycache__ + build + dist + test + tests + *.pyc + *.egg-info + .cache + .eggs + env* +application-import-names = corenetworks +format = ${cyan}%(path)s:%(row)d:%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s diff --git a/.github/settings.yml b/.github/settings.yml new file mode 100644 index 0000000..8309b73 --- /dev/null +++ b/.github/settings.yml @@ -0,0 +1,60 @@ +--- +repository: + name: corenetworks + description: Python library for the core-networks.de DNS API + homepage: https://corenetworks.geekdocs.de + topics: corenetworks, api, dns, python + + private: false + has_issues: true + has_projects: false + has_wiki: false + has_downloads: false + + default_branch: master + + allow_squash_merge: true + allow_merge_commit: true + allow_rebase_merge: true + +labels: + - name: bug + color: d73a4a + description: Something isn't working + - name: documentation + color: 0075ca + description: Improvements or additions to documentation + - name: duplicate + color: cfd3d7 + description: This issue or pull request already exists + - name: enhancement + color: a2eeef + description: New feature or request + - name: good first issue + color: 7057ff + description: Good for newcomers + - name: help wanted + color: 008672 + description: Extra attention is needed + - name: invalid + color: e4e669 + description: This doesn't seem right + - name: question + color: d876e3 + description: Further information is requested + - name: wontfix + color: ffffff + description: This will not be worked on + +branches: + - name: master + protection: + required_pull_request_reviews: null + required_status_checks: + strict: true + contexts: + - continuous-integration/drone/pr + enforce_admins: null + restrictions: null + +... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e5079dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,113 @@ +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env* + +# virtualenv +.venv +venv/ +ENV/ +env/ +env*/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + +# Ignore ide addons +.server-script +.on-save.json +.vscode +.pytest_cache + +pip-wheel-metadata + +# Hugo documentation +docs/themes/ +docs/public/ +resources/_gen/ + +# Misc +.local/ +.corenetworks* +docs/content/api/corenetworks/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..de4f062 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +* initial release diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bb3e998 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Robert Kaussow + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f6bf2c4 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# certbot-dns-corenetworks + +[![Build Status](https://img.shields.io/drone/build/xoxys/certbot-dns-corenetworks?logo=drone)](https://cloud.drone.io/xoxys/certbot-dns-corenetworks) +[![Python Version](https://img.shields.io/pypi/pyversions/certbot-dns-corenetworks.svg)](https://pypi.org/project/certbot-dns-corenetworks/) +[![PyPi Status](https://img.shields.io/pypi/status/certbot-dns-corenetworks.svg)](https://pypi.org/project/certbot-dns-corenetworks/) +[![PyPi Release](https://img.shields.io/pypi/v/certbot-dns-corenetworks.svg)](https://pypi.org/project/certbot-dns-corenetworks/) +[![License: MIT](https://img.shields.io/github/license/xoxys/certbot-dns-corenetworks)](LICENSE) + +## License + +This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. + +## Maintainers and Contributors + +[Robert Kaussow](https://github.com/xoxys) diff --git a/certbot_dns_corenetworks/__init__.py b/certbot_dns_corenetworks/__init__.py new file mode 100644 index 0000000..37f93ee --- /dev/null +++ b/certbot_dns_corenetworks/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +"""Default package.""" + +__author__ = "Robert Kaussow" +__project__ = "certbot_dns_corenetworks" +__license__ = "MIT" +__maintainer__ = "Robert Kaussow" +__email__ = "mail@geeklabor.de" +__url__ = "https://github.com/xoxys/certbot-dns-corenetworks" +__version__ = "0.1.0" diff --git a/certbot_dns_corenetworks/dns_corenetworks.py b/certbot_dns_corenetworks/dns_corenetworks.py new file mode 100644 index 0000000..37ab43f --- /dev/null +++ b/certbot_dns_corenetworks/dns_corenetworks.py @@ -0,0 +1,228 @@ +"""DNS Authenticator for Core Networks.""" +import logging +import re + +import zope.interface # noqa +from certbot import errors +from certbot import interfaces +from certbot.plugins import dns_common +from corenetworks import CoreNetworks +from corenetworks.exceptions import AuthError +from corenetworks.exceptions import CoreNetworksException + +logger = logging.getLogger(__name__) + + +@zope.interface.implementer(interfaces.IAuthenticator) +@zope.interface.provider(interfaces.IPluginFactory) +class Authenticator(dns_common.DNSAuthenticator): + """DNS Authenticator for Core Networks DNS API.""" + + description = ( + "Obtain certificates using a DNS TXT record " + "(if you are using Core Networks for your domains)." + ) + ttl = 300 + + clientCache = {} # noqa + nameCache = {} # noqa + + def __init__(self, *args, **kwargs): + """Initialize an Core Networks Authenticator.""" + super(Authenticator, self).__init__(*args, **kwargs) + self.credentials = None + + @classmethod + def add_parser_arguments(cls, add): # noqa + super(Authenticator, cls).add_parser_arguments(add, default_propagation_seconds=60) + add( + "credentials", + help=("Path to Core Networks account credentials INI file"), + default="/etc/letsencrypt/corenetworks.cfg" + ) + + def more_info(self): # noqa + return "This plugin configures a DNS TXT record to respond to a dns-01 challenge using " \ + "the Core Networks DNS API." + + def _setup_credentials(self): + self.credentials = self._configure_credentials( + "credentials", "path to Core Networks API credentials INI file", { + "username": "Username of the Core Networks API account.", + "password": "Password of the Core Networks API account.", + } + ) + + def _follow_cnames(self, domain, validation_name): + """ + Perform recursive CNAME lookups. + + In case there exist a CNAME for the given validation name a recursive CNAME lookup + will be performed automatically. If the optional dependency dnspython is not installed, + the given name is simply returned. + """ + try: + import dns.exception # noqa + import dns.resolver # noqa + import dns.name # noqa + except ImportError: + return validation_name + + resolver = dns.resolver.Resolver() + name = dns.name.from_text(validation_name) + while 1: + try: + answer = resolver.query(name, "CNAME") + if 1 <= len(answer): + name = answer[0].target + else: + break + except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): + break + except (dns.exception.Timeout, dns.resolver.YXDOMAIN, dns.resolver.NoNameservers): + raise errors.PluginError( + "Failed to lookup CNAMEs on your requested domain {0}".format(domain) + ) + return name.to_text(True) + + def _perform(self, domain, validation_name, validation): + if validation_name in Authenticator.nameCache: + resolved = Authenticator.nameCache[validation_name] + else: + resolved = self._follow_cnames(domain, validation_name) + Authenticator.nameCache[validation_name] = resolved + + if resolved != validation_name: + logger.info("Validation record for %s redirected by CNAME(s) to %s", domain, resolved) + + self._get_corenetworks_client().add_txt_record(domain, resolved, validation, self.ttl) + + def _cleanup(self, domain, validation_name, validation): + resolved = Authenticator.nameCache[validation_name] + self._get_corenetworks_client().del_txt_record(domain, resolved, validation) + + def _get_corenetworks_client(self): + key = self.conf("credentials") + + if key in Authenticator.clientCache: + client = Authenticator.clientCache[key] + else: + client = _CorenetworksClient( + self.credentials.conf("username"), self.credentials.conf("password") + ) + Authenticator.clientCache[key] = client + + return client + + +class _CorenetworksClient(object): + """Encapsulates all communication with the Core Networks API.""" + + def __init__(self, user, password, auto_commit=True): + try: + self.client = CoreNetworks(user, password, auto_commit=auto_commit) + except AuthError as e: + raise errors.PluginError("Login failed: {0}".format(str(e))) + + def add_txt_record(self, domain_name, record_name, record_content, record_ttl): + """ + Add a TXT record using the supplied information. + + Args: + domain_name (str): The requested domain for validation. + record_name (str): The record name (typically beginning with "_acme-challenge."). + record_content (str): The record content (typically the challenge validation). + record_ttl(int): The record TTL (number of seconds that the record may be cached). + + Raises: + certbot.errors.PluginError: If an error occurs communicating with the DNS server + + """ + zone = self._find_zone(record_name) + name = re.sub(r"\.{}$".format(zone), "", record_name) + + try: + self.client.add_record( + zone, { + "name": name, + "type": "TXT", + "data": record_content, + "ttl": record_ttl + } + ) + except CoreNetworksException: + raise errors.PluginError( + "Failed to add TXT DNS record {record} to {zone} for {domain}".format( + record=record_name, zone=zone, domain=domain_name + ) + ) + + def del_txt_record(self, domain_name, record_name, record_content): + """ + Delete a TXT record using the supplied information. + + Args: + domain_name (str): The requested domain for validation. + record_name (str): The record name (typically beginning with "_acme-challenge."). + record_content (str): The record content (typically the challenge validation). + + Returns: + certbot.errors.PluginError: if an error occurs communicating with the DNS server + + """ + zone = self._find_zone(record_name) + name = re.sub(r"\.{}$".format(zone), "", record_name) + + try: + info = self.client.records(zone, {"name": name, "type": "TXT", "data": record_content}) + if (len(info) != 1 or info[0]["name"] != name): + raise NameError("Unknown record") + except NameError as e: + raise errors.PluginError( + "Record {record} not found: {err}".format(record=record_name, err=e) + ) + except CoreNetworksException as e: + raise errors.PluginError( + "Could not lookup record {record}: {err}".format(record=record_name, err=e) + ) + + try: + self.client.delete_record(zone, {"name": name, "type": "TXT", "data": record_content}) + except CoreNetworksException: + raise errors.PluginError( + "Failed to delete TXT DNS record {record} of {zone} for {domain}".format( + record=record_name, zone=zone, domain=domain_name + ) + ) + + def _find_zone(self, domain_name): + """ + Find the base domain name for a given domain name. + + :param str domain_name: The domain name for which to find the corresponding base domain. + :returns: The base domain name, if found. + :rtype: str + :raises certbot.errors.PluginError: if no matching domain is found. + """ + domain_name_guesses = dns_common.base_domain_name_guesses(domain_name) + + for guess in domain_name_guesses: + logger.debug("Testing {0} for domain {1}...".format(guess, domain_name)) + try: + info = self.client.zone(guess)[0] + except Exception: + continue + + logger.debug("Found zone '{zone}': {info}".format(zone=guess, info=info)) + if not info.get("active"): + raise errors.PluginError("Zone {0} is not active".format(guess)) + if info.get("type") != "master": + raise errors.PluginError("Zone {0} is not a master zone".format(guess)) + + return guess + + raise errors.PluginError( + "Unable to determine base domain for {0} using names: {1}".format( + domain_name, domain_name_guesses + ) + ) diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..ea113a4 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,19 @@ +pydocstyle +flake8 +flake8-colors +flake8-blind-except +flake8-builtins +flake8-docstrings +flake8-isort +flake8-logging-format +flake8-polyfill +flake8-quotes +flake8-pep3101 +flake8-eradicate; python_version >= "3.6" +pep8-naming +wheel +pytest +pytest-mock +pytest-cov +bandit +yapf diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..301fa79 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,30 @@ +[metadata] +description-file = README.md +license_file = LICENSE + +[bdist_wheel] +universal = 1 + +[isort] +default_section = THIRDPARTY +known_first_party = certbot_dns_corenetworks +sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER +force_single_line = true +line_length = 99 +skip_glob = **/.env*,**/env/*,**/docs/* + +[yapf] +based_on_style = google +column_limit = 99 +dedent_closing_brackets = true +coalesce_brackets = true +split_before_logical_operator = true + +[tool:pytest] +filterwarnings = + ignore::FutureWarning + ignore:.*collections.*:DeprecationWarning + ignore:.*pep8.*:FutureWarning + +[coverage:run] +omit = **/test/* diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..404c311 --- /dev/null +++ b/setup.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Setup script for the package.""" + +import io +import os +import re + +from setuptools import find_packages +from setuptools import setup + +PACKAGE_NAME = "certbot_dns_corenetworks" + + +def get_property(prop, project): + current_dir = os.path.dirname(os.path.realpath(__file__)) + result = re.search( + r'{}\s*=\s*[\'"]([^\'"]*)[\'"]'.format(prop), + open(os.path.join(current_dir, project, "__init__.py")).read(), + ) + return result.group(1) + + +def get_readme(filename="README.md"): + this = os.path.abspath(os.path.dirname(__file__)) + with io.open(os.path.join(this, filename), encoding="utf-8") as f: + long_description = f.read() + return long_description + + +setup( + name=get_property("__project__", PACKAGE_NAME), + description="Core Networks DNS Authenticator plugin for Certbot", + keywords="dns, certbot, automation, corenetworks", + version=get_property("__version__", PACKAGE_NAME), + author=get_property("__author__", PACKAGE_NAME), + author_email=get_property("__email__", PACKAGE_NAME), + url=get_property("__url__", PACKAGE_NAME), + license=get_property("__license__", PACKAGE_NAME), + long_description=get_readme(), + long_description_content_type="text/markdown", + packages=find_packages(exclude=["*.test", "test", "test.*"]), + include_package_data=True, + zip_safe=False, + python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,<4", + entry_points={ + "certbot.plugins": [ + "dns-corenetworks = certbot_dns_corenetworks.dns_corenetworks:Authenticator" + ], + }, + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Environment :: Plugins", + "Intended Audience :: System Administrators", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: POSIX", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Topic :: Security", + "Topic :: System :: Networking", + "Topic :: Utilities", + ], + install_requires=[ + "acme", + "certbot>=0.15", + "setuptools", + "zope.interface", + "corenetworks", + ], + dependency_links=[], +) diff --git a/tests/dns_corenetwork_test.py b/tests/dns_corenetwork_test.py new file mode 100644 index 0000000..dfbf2ca --- /dev/null +++ b/tests/dns_corenetwork_test.py @@ -0,0 +1,84 @@ +"""Tests for certbot_dns_ispconfig.dns_ispconfig.""" + +import unittest + +import mock +from certbot import errors +from certbot.compat import os +from certbot.plugins import dns_test_common +from certbot.plugins.dns_test_common import DOMAIN +from certbot.tests import util as test_util + +API_USER = "my_user" +API_PASSWORD = "secure" + + +class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest): + """Test for Hetzner DNS Authenticator.""" + + def setUp(self): + from certbot_dns_corenetworks.dns_corenetworks import Authenticator + + super(AuthenticatorTest, self).setUp() + + path = os.path.join(self.tempdir, "file.ini") + dns_test_common.write({ + "corenetworks_username": API_USER, + "corenetworks_password": API_PASSWORD + }, path) + + self.config = mock.MagicMock( + corenetworks_credentials=path, corenetworks_propagation_seconds=0 + ) # don't wait during tests + + self.auth = Authenticator(self.config, "corenetworks") + + self.mock_client = mock.MagicMock() + # _get_corenetworks_client | pylint: disable=protected-access + self.auth._get_corenetworks_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.nameCache["_acme-challenge." + DOMAIN] = "_acme-challenge." + DOMAIN + 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) + + def test_creds(self): + dns_test_common.write({ + "corenetworks_username": API_USER, + "corenetworks_password": API_PASSWORD + }, self.config.corenetworks_credentials) + 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_no_creds(self): + dns_test_common.write({}, self.config.corenetworks_credentials) + self.assertRaises(errors.PluginError, self.auth.perform, [self.achall]) + + def test_missing_user_or_password(self): + dns_test_common.write({"corenetworks_username": API_USER}, + self.config.corenetworks_credentials) + self.assertRaises(errors.PluginError, self.auth.perform, [self.achall]) + + dns_test_common.write({"corenetworks_password": API_PASSWORD}, + self.config.corenetworks_credentials) + self.assertRaises(errors.PluginError, self.auth.perform, [self.achall]) + + +if __name__ == "__main__": + unittest.main()