Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
bikram990 committed May 11, 2021
1 parent b1fa17e commit 850829d
Show file tree
Hide file tree
Showing 5 changed files with 339 additions and 0 deletions.
112 changes: 112 additions & 0 deletions README.md
@@ -0,0 +1,112 @@
certbot-dns-dynu
============

Dynu 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-dynu
```

Verify:

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

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

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

```
certbot_dns_dynu:dns_dynu_auth_token = AbCbASsd!@34
```

Usage
-----


```
certbot ... \
--authenticator certbot-dns-dynu:dns-dynu \
--certbot-dns-dynu:dns-dynu-credentials ~/dynu-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

Development
-----------

Create a virtualenv, install the plugin (`editable` mode),
spawn the environment and run the test:

```
virtualenv -p python3 .venv
. .venv/bin/activate
pip install -e .
docker-compose up -d
./test/run_certonly.sh test/dynu-credentials.ini
```

License
--------

Copyright (c) 2021 [Bikramjeet Singh](https://github.com/bikram990)

Credits
--------
[PowerDNS](https://github.com/pan-net-security/certbot-dns-powerdns)

[dns-lexicon](https://github.com/AnalogJ/lexicon)

Helpful links
--------

[DNS Plugin list](https://certbot.eff.org/docs/using.html?highlight=dns#dns-plugins)

[acme.sh](https://github.com/acmesh-official/acme.sh)

[dynu with acme.sh](https://gist.github.com/tavinus/15ea64c50ac5fb7cea918e7786c94a95)

[dynu api](https://www.dynu.com/Support/API)






1 change: 1 addition & 0 deletions certbot_dns_dynu/__init__.py
@@ -0,0 +1 @@
"""Let's Encrypt Dynu DNS plugin"""
93 changes: 93 additions & 0 deletions certbot_dns_dynu/dns_dynu.py
@@ -0,0 +1,93 @@
"""DNS Authenticator for Dynu."""

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 dynu

logger = logging.getLogger(__name__)


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

description = 'Obtain certificates using a DNS TXT record ' + \
'(if you are using Dynu 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="Dynu 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 ' + \
'Dynu API'

def _setup_credentials(self):
self._configure_file('credentials',
'Absolute path to Dynu credentials file')
dns_common.validate_file_permissions(self.conf('credentials'))
self.credentials = self._configure_credentials(
'credentials',
'Dynu credentials file',
{
'auth-token': 'Dynu-compatible API key (API-Key)',
}
)

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

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

def _get_dynu_client(self):
return _DynuLexiconClient(
self.credentials.conf('auth-token'),
self.ttl
)


class _DynuLexiconClient(dns_common_lexicon.LexiconClient):
"""
Encapsulates all communication with the Dynu via Lexicon.
"""

def __init__(self, auth_token, ttl):
super(_DynuLexiconClient, self).__init__()

config = dns_common_lexicon.build_lexicon_config('dynu', {
'ttl': ttl,
}, {
'auth_token': auth_token,
})

self.provider = dynu.Provider(config)

def _handle_http_error(self, e, domain_name):
if domain_name in str(e) and (
# 4.0 and 4.1 compatibility
str(e).startswith('422 Client Error: Unprocessable Entity for url:') or
# 4.2
str(e).startswith('404 Client Error: Not Found for url:')
):
return # Expected errors when zone name guess is wrong
return super(_DynuLexiconClient, self)._handle_http_error(e, domain_name)

63 changes: 63 additions & 0 deletions certbot_dns_dynu/dns_dynu_test.py
@@ -0,0 +1,63 @@
"""Tests for certbot_dns_dynu.dns_dynu"""

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

AUTH_TOKEN = '00000000-0000-0000-0000-000000000000'


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

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

from certbot_dns_dynu.dns_dynu import Authenticator

path = os.path.join(self.tempdir, 'file.ini')
dns_test_common.write(
{"dynu_auth_token": AUTH_TOKEN},
path
)

print("File content: ")
# print(open(path).read())
with open(path) as f:
print(f.read())

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

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

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



class DynuLexiconClientTest(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_dynu.dns_dynu import _DynuLexiconClient

self.client = _DynuLexiconClient(auth_token=AUTH_TOKEN, ttl=0)

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


if __name__ == "__main__":
unittest.main() # pragma: no cover
70 changes: 70 additions & 0 deletions setup.py
@@ -0,0 +1,70 @@
#! /usr/bin/env python
from os import path
from setuptools import setup
from setuptools import find_packages

version = "0.0.1"

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

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

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

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

description="Dynu DNS Authenticator plugin for Certbot",
long_description=long_description,
long_description_content_type='text/markdown',
url='https://github.com/bikram990/certbot-dns-dynu',
download_url='https://github.com/bikram990/certbot-dns-dynu/archive/refs/tags/0.0.1.tar.gz',
author="Bikramjeet Singh",
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.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-dynu = certbot_dns_dynu.dns_dynu:Authenticator',
],
},
test_suite='certbot_dns_dynu',
)

0 comments on commit 850829d

Please sign in to comment.