Skip to content

Commit

Permalink
initial release
Browse files Browse the repository at this point in the history
  • Loading branch information
splashx committed Mar 19, 2019
0 parents commit 2b41645
Show file tree
Hide file tree
Showing 8 changed files with 368 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
**/__pycache__/
*.egg-info
venv
.venv
.idea
.coveralls.yml
28 changes: 28 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
language: python

python:
- "2.7"
- "3.4"
- "3.5"
- "3.6"
# PyPy versions
- "pypy3.5"

install:
- pip install python-coveralls
- pip install .

# run tests
script:
- pytest

deploy:
provider: pypi
user: $PYPI_USERNAME
password: $PYPI_PASSWORD
on:
tags: true
python: "3.6"

after_success:
- coveralls
86 changes: 86 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
[![Build Status](https://travis-ci.com/pan-net-security/certbot-powerdns.svg?branch=master)](https://travis-ci.com/pan-net-security/certbot-powerdns)
[![Coverage Status](https://coveralls.io/repos/github/pan-net-security/certbot-dns-powerdns/badge.svg?branch=master)](https://coveralls.io/github/pan-net-security/certbot-dns-powerdns?branch=master)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=6cfb0c4728624ebff38afc0b1ef91700795ea9ef&metric=alert_status)](https://sonarcloud.io/dashboard?id=6cfb0c4728624ebff38afc0b1ef91700795ea9ef)
![Libraries.io dependency status for latest release](https://img.shields.io/librariesio/release/github/pan-net-security/certbot-dns-powerdns.svg)
![PyPI - Status](https://img.shields.io/pypi/status/certbot-dns-powerdns.svg)

![PyPI - Python Version](https://img.shields.io/pypi/pyversions/certbot-dns-powerdns.svg)


certbot-dns-powerdns
============

PowerDNS DNS Authenticator plugin for [Certbot](https://certbot.eff.org/).

This plugin is built from the ground up and follows the development style and life-cycle
of other `certbot-dns-*` plugins found in the
[Official Certbot Repository](https://github.com/certbot/certbot).

Installation
------------

```
pip install --upgrade certbot
pip install certbot-dns-powerdns
```

Verify:

```
$ certbot plugins --text
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* certbot-dns-powerdns:dns-powerdns
Description: Obtain certificates using a DNS TXT record (if you are using
PowerDNS for DNS.)
Interfaces: IAuthenticator, IPlugin
Entry point: dns-powerdns = certbot_dns_powerdns.dns_powerdns:Authenticator
...
...
```

Configuration
-------------

The credentials file e.g. `~/pdns-credentials.ini` should look like this:

```
certbot_dns_powerdns:dns_powerdns_api_url = https://api.mypowerdns.example.org
certbot_dns_powerdns:dns_powerdns_api_key = AbCbASsd!@34
```

Usage
-----


```
certbot ... \
--authenticator certbot-dns-powerdns:dns-powerdns
--certbot-dns-powerdns:dns-powerdns-credentials ~/pdns-credentials.ini
certonly
```

FAQ
-----

##### Why such long name for a plugin?

This follows the upstream nomenclature: `certbot-dns-<dns-provider>`.

##### Why do I have to use `:` separator in the name? And why are the configuration file parameters so weird?

This is a limitation of the Certbot interface towards _third-party_ plugins.

For details read the discussions:

- https://github.com/certbot/certbot/issues/6504#issuecomment-473462138
- https://github.com/certbot/certbot/issues/6040
- https://github.com/certbot/certbot/issues/4351
- https://github.com/certbot/certbot/pull/6372


License
--------

Copyright (c) 2019 [DT Pan-Net s.r.o](https://github.com/pan-net-security)
21 changes: 21 additions & 0 deletions certbot_dns_powerdns/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Let's Encrypt PDNS plugin"""

# import inspect
#
# # https://github.com/certbot/certbot/issues/6504#issuecomment-473462138
# # https://github.com/certbot/certbot/issues/6040
# # https://github.com/certbot/certbot/issues/4351
# # https://github.com/certbot/certbot/pull/6372
# def _patch():
# for frame_obj, filename, line, func, _, _ in inspect.stack():
# if func == '__init__' and frame_obj.f_locals['self'].__class__.__name__ == 'PluginEntryPoint':
# frame_obj.f_locals['self'].name = frame_obj.f_locals['entry_point'].name
# module_name = frame_obj.f_locals['entry_point'].dist.key
# pre_free_dist = frame_obj.f_locals['self'].PREFIX_FREE_DISTRIBUTIONS
# if module_name not in pre_free_dist:
# pre_free_dist.append(module_name)
#
# _patch()
89 changes: 89 additions & 0 deletions certbot_dns_powerdns/dns_powerdns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""DNS Authenticator for PowerDNS."""

import logging

import zope.interface
from certbot import interfaces
from certbot import errors

from certbot.plugins import dns_common
from certbot.plugins import dns_common_lexicon

from lexicon.providers import powerdns

logger = logging.getLogger(__name__)

@zope.interface.implementer(interfaces.IAuthenticator)
@zope.interface.provider(interfaces.IPluginFactory)
class Authenticator(dns_common.DNSAuthenticator):
"""DNS Authenticator for PowerDNS DNS."""

description = 'Obtain certificates using a DNS TXT record ' + \
'(if you are using PowerDNS 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):
super(Authenticator, cls).add_parser_arguments(
add, default_propagation_seconds=60)
add("credentials", help="PowerDNS credentials 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 ' + \
'PowerDNS API'

def _setup_credentials(self):
self._configure_file('credentials',
'Absolute path to PowerDNS credentials file')
dns_common.validate_file_permissions(self.conf('credentials'))
self.credentials = self._configure_credentials(
'credentials',
'PowerDNS credentials file',
{
'api-url': 'PowerDNS-compatible API FQDN',
'api-key': 'PowerDNS-compatible API key (X-API-Key)'
}
)

def _perform(self, domain, validation_name, validation):
self._get_powerdns_client().add_txt_record(
domain, validation_name, validation)

def _cleanup(self, domain, validation_name, validation):
self._get_powerdns_client().del_txt_record(
domain, validation_name, validation)

def _get_powerdns_client(self):
return _PowerDNSLexiconClient(
self.credentials.conf('api-url'),
self.credentials.conf('api-key'),
self.ttl
)


class _PowerDNSLexiconClient(dns_common_lexicon.LexiconClient):
"""
Encapsulates all communication with the PowerDNS via Lexicon.
"""

def __init__(self, api_url, api_key, ttl):
super(_PowerDNSLexiconClient, self).__init__()

config = dns_common_lexicon.build_lexicon_config('powerdns', {
'ttl': ttl,
}, {
'auth_token': api_key,
'pdns_server': api_url,
})

self.provider = powerdns.Provider(config)

def _handle_http_error(self, e, domain_name):
if domain_name in str(e) and (str(e).startswith('422 Client Error: Unprocessable Entity for url:')):
return # Expected errors when zone name guess is wrong
return super(_PowerDNSLexiconClient, self)._handle_http_error(e, domain_name)
62 changes: 62 additions & 0 deletions certbot_dns_powerdns/dns_powerdns_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Tests for certbot_dns_powerdns.dns_powerdns"""

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.plugins.dns_test_common import DOMAIN

from certbot.tests import util as test_util

API_TOKEN = '00000000-0000-0000-0000-000000000000'
API_URL = 'https://127.0.0.1'


class AuthenticatorTest(test_util.TempDirTestCase,
dns_test_common_lexicon.BaseLexiconAuthenticatorTest):

def setUp(self):
super(AuthenticatorTest, self).setUp()

from certbot_dns_powerdns.dns_powerdns import Authenticator

path = os.path.join(self.tempdir, 'file.ini')
dns_test_common.write(
{"powerdns_api_url": API_URL,
"powerdns_api_key": API_TOKEN},
path
)

print("File content")
print(open(path).read())

self.config = mock.MagicMock(powerdns_credentials=path,
powerdns_propagation_seconds=0) # don't wait during tests

self.auth = Authenticator(self.config, "powerdns")

self.mock_client = mock.MagicMock()
# _get_powerdns_client | pylint: disable=protected-access
self.auth._get_powerdns_client = mock.MagicMock(return_value=self.mock_client)


class PowerDnsLexiconClientTest(unittest.TestCase,
dns_test_common_lexicon.BaseLexiconClientTest):
DOMAIN_NOT_FOUND = HTTPError('422 Client Error: Unprocessable Entity for url: {0}.'.format(DOMAIN))
LOGIN_ERROR = HTTPError('401 Client Error: Unauthorized')

def setUp(self):
from certbot_dns_powerdns.dns_powerdns import _PowerDNSLexiconClient

self.client = _PowerDNSLexiconClient(api_key=API_TOKEN, api_url=API_URL, ttl=0)

self.provider_mock = mock.MagicMock()
self.client.provider = self.provider_mock


if __name__ == "__main__":
unittest.main() # pragma: no cover
5 changes: 5 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[bdist_wheel]
# This flag says that the code is written to work on both Python 2 and Python
# 3. If at all possible, it is good practice to do this. If you cannot, you
# will need to generate wheels for each Python version that you support.
universal=1
71 changes: 71 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#! /usr/bin/env python
from os import path
from setuptools import setup
from setuptools import find_packages

version = "0.1.0"

with open('README.md') as f:
long_description = f.read()

install_requires = [
'acme>=0.31.0',
'certbot>=0.31.0',
'dns-lexicon>=2.1.23',
'dnspython',
'mock',
'setuptools',
'zope.interface',
'requests'
]

here = path.abspath(path.dirname(__file__))

setup(
name='certbot-dns-powerdns',
version=version,

description="PowerDNS DNS Authenticator plugin for Certbot",
long_description=long_description,
long_description_content_type='text/markdown',
url='https://github.com/pan-net-security/certbot-dns-powerdns',
author="DT Pan-Net s.r.o",
author_email='[email protected]',
license='Apache License 2.0',
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Plugins',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: Apache Software License',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',
'Topic :: System :: Networking',
'Topic :: System :: Systems Administration',
'Topic :: Utilities',
],

packages=find_packages(),
install_requires=install_requires,

# extras_require={
# 'docs': docs_extras,
# },

entry_points={
'certbot.plugins': [
'dns-powerdns = certbot_dns_powerdns.dns_powerdns:Authenticator',
],
},
test_suite='certbot_dns_powerdns',
)

0 comments on commit 2b41645

Please sign in to comment.