Browse Source

init

develop
Robin Thoni 7 years ago
commit
092d8ac15e

+ 136
- 0
.gitignore View File

@@ -0,0 +1,136 @@
1
+# Byte-compiled / optimized / DLL files
2
+__pycache__/
3
+*.py[cod]
4
+*$py.class
5
+
6
+# C extensions
7
+*.so
8
+
9
+# Distribution / packaging
10
+.Python
11
+env/
12
+build/
13
+develop-eggs/
14
+dist/
15
+downloads/
16
+eggs/
17
+.eggs/
18
+lib/
19
+lib64/
20
+parts/
21
+sdist/
22
+var/
23
+wheels/
24
+*.egg-info/
25
+.installed.cfg
26
+*.egg
27
+
28
+# PyInstaller
29
+#  Usually these files are written by a python script from a template
30
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
31
+*.manifest
32
+*.spec
33
+
34
+# Installer logs
35
+pip-log.txt
36
+pip-delete-this-directory.txt
37
+
38
+# Unit test / coverage reports
39
+htmlcov/
40
+.tox/
41
+.coverage
42
+.coverage.*
43
+.cache
44
+nosetests.xml
45
+coverage.xml
46
+*,cover
47
+.hypothesis/
48
+
49
+# Translations
50
+*.mo
51
+*.pot
52
+
53
+# Django stuff:
54
+*.log
55
+local_settings.py
56
+
57
+# Flask stuff:
58
+instance/
59
+.webassets-cache
60
+
61
+# Scrapy stuff:
62
+.scrapy
63
+
64
+# Sphinx documentation
65
+docs/_build/
66
+
67
+# PyBuilder
68
+target/
69
+
70
+# Jupyter Notebook
71
+.ipynb_checkpoints
72
+
73
+# pyenv
74
+.python-version
75
+
76
+# celery beat schedule file
77
+celerybeat-schedule
78
+
79
+# dotenv
80
+.env
81
+
82
+# virtualenv
83
+.venv/
84
+venv/
85
+ENV/
86
+
87
+# Spyder project settings
88
+.spyderproject
89
+
90
+# Rope project settings
91
+.ropeproject
92
+
93
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
94
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
95
+
96
+# User-specific stuff:
97
+.idea/workspace.xml
98
+.idea/tasks.xml
99
+
100
+# Sensitive or high-churn files:
101
+.idea/dataSources/
102
+.idea/dataSources.ids
103
+.idea/dataSources.xml
104
+.idea/dataSources.local.xml
105
+.idea/sqlDataSources.xml
106
+.idea/dynamic.xml
107
+.idea/uiDesigner.xml
108
+
109
+# Gradle:
110
+.idea/gradle.xml
111
+.idea/libraries
112
+
113
+# Mongo Explorer plugin:
114
+.idea/mongoSettings.xml
115
+
116
+## File-based project format:
117
+*.iws
118
+
119
+## Plugin-specific files:
120
+
121
+# IntelliJ
122
+/out/
123
+
124
+# mpeltonen/sbt-idea plugin
125
+.idea_modules/
126
+
127
+# JIRA plugin
128
+atlassian-ide-plugin.xml
129
+
130
+# Crashlytics plugin (for Android Studio and IntelliJ)
131
+com_crashlytics_export_strings.xml
132
+crashlytics.properties
133
+crashlytics-build.properties
134
+fabric.properties
135
+
136
+/config.json

+ 11
- 0
.idea/certbot-dns.iml View File

@@ -0,0 +1,11 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<module type="PYTHON_MODULE" version="4">
3
+  <component name="NewModuleRootManager">
4
+    <content url="file://$MODULE_DIR$" />
5
+    <orderEntry type="jdk" jdkName="Python 2.7.6 (/usr/bin/python2.7)" jdkType="Python SDK" />
6
+    <orderEntry type="sourceFolder" forTests="false" />
7
+  </component>
8
+  <component name="TestRunnerService">
9
+    <option name="PROJECT_TEST_RUNNER" value="Unittests" />
10
+  </component>
11
+</module>

+ 4
- 0
.idea/misc.xml View File

@@ -0,0 +1,4 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<project version="4">
3
+  <component name="ProjectRootManager" version="2" project-jdk-name="Python 2.7.6 (/usr/bin/python2.7)" project-jdk-type="Python SDK" />
4
+</project>

+ 8
- 0
.idea/modules.xml View File

@@ -0,0 +1,8 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<project version="4">
3
+  <component name="ProjectModuleManager">
4
+    <modules>
5
+      <module fileurl="file://$PROJECT_DIR$/.idea/certbot-dns.iml" filepath="$PROJECT_DIR$/.idea/certbot-dns.iml" />
6
+    </modules>
7
+  </component>
8
+</project>

+ 6
- 0
.idea/vcs.xml View File

@@ -0,0 +1,6 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<project version="4">
3
+  <component name="VcsDirectoryMappings">
4
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
+  </component>
6
+</project>

+ 100
- 0
certbot_dns/PdnsApiAuthenticator.py View File

@@ -0,0 +1,100 @@
1
+import json
2
+
3
+import logging
4
+
5
+import time
6
+from certbot import errors
7
+
8
+from certbot_dns.pdnsapi import PdnsApi
9
+
10
+logger = logging.getLogger(__name__)
11
+
12
+
13
+class PdnsApiAuthenticator:
14
+    api = None
15
+    zones = None
16
+    axfr_time = None
17
+
18
+    def find_best_matching_zone(self, domain):
19
+        if domain is None or domain == "":
20
+            return None
21
+        for zone in self.zones:
22
+            if zone['name'] == domain + ".":
23
+                return zone
24
+        return self.find_best_matching_zone(domain[domain.index(".") + 1:]) if "." in domain else None
25
+
26
+    def find_soa(self, zone):
27
+        for rrset in zone["rrsets"]:
28
+            if rrset["type"] == "SOA":
29
+                return rrset, rrset["records"][0]
30
+        return None
31
+
32
+    def flush_zone(self, zone_name):
33
+        res = self.api.flush_zone_cache(zone_name)
34
+        if res is None or "result" not in res or res["result"] != "Flushed cache.":
35
+            raise errors.PluginError("Bad return from PDNS API when flushing cache: %s" % res)
36
+
37
+    def notify_zone(self, zone_name):
38
+        res = self.api.notify_zone(zone_name)
39
+        if res is None or "result" not in res or res["result"] != "Notification queued":
40
+            raise errors.PluginError("Bad return from PDNS API when notifying: %s" % res)
41
+
42
+    def update_soa(self, zone_name):
43
+        zone = self.api.get_zone(zone_name)
44
+        if zone is None or "error" in zone:
45
+            raise errors.PluginError("Bad return from PDNS API when getting zone %s: %s" % (zone_name, zone))
46
+        rrset, soa = self.find_soa(zone)
47
+        split = soa["content"].split(" ")
48
+        split[2] = str(int(split[2]) + 1)
49
+        soa["content"] = ' '.join(split)
50
+        res = self.api.replace_record(zone_name, zone_name, rrset["type"], rrset["ttl"], soa["content"],
51
+                                      soa["disabled"], False)
52
+        if res is not None:
53
+            raise errors.PluginError("Bad return from PDNS API when updating SOA: %s" % res)
54
+
55
+    def prepare(self, conf_path):
56
+        self.api = PdnsApi()
57
+        with open(conf_path) as f:
58
+            config = json.load(f)
59
+        self.api.set_api_key(config["api-key"])
60
+        self.api.set_base_url(config["base-url"])
61
+        self.axfr_time = config["axfr-time"]
62
+        self.zones = self.api.list_zones()
63
+        # print(self.zones)
64
+        # raw_input('Press <ENTER> to continue')
65
+        if self.zones is None or "error" in self.zones:
66
+            raise errors.PluginError("Could not list zones %s" % self.zones)
67
+
68
+    def perform_single(self, achall, response, validation):
69
+        domain = achall.domain
70
+        token = validation.encode()
71
+        zone = self.find_best_matching_zone(domain)
72
+        if zone is None:
73
+            raise errors.PluginError("Could not find zone for %s" % domain)
74
+
75
+        logger.debug("Found zone %s for domain %s" % (zone["name"], domain))
76
+
77
+        res = self.api.replace_record(zone["name"], "_acme-challenge." + domain + ".", "TXT", 1, "\"" + token + "\"", False, False)
78
+        if res is not None:
79
+            raise errors.PluginError("Bad return from PDNS API when adding record: %s" % res)
80
+        self.update_soa(zone["name"])
81
+        self.flush_zone(zone["name"])
82
+        self.notify_zone(zone["name"])
83
+
84
+        # raw_input('Press <ENTER> to continue')
85
+        logger.info("Waiting %i seconds..." % self.axfr_time)
86
+        time.sleep(self.axfr_time)
87
+
88
+        return response
89
+
90
+    def cleanup(self, achall):
91
+        domain = achall.domain
92
+        zone = self.find_best_matching_zone(domain)
93
+        if zone is None:
94
+            return
95
+        res = self.api.delete_record(zone["name"], "_acme-challenge." + domain + ".", "TXT", 1, None, False, False)
96
+        if res is not None:
97
+            raise errors.PluginError("Bad return from PDNS API when deleting record: %s" % res)
98
+        self.update_soa(zone["name"])
99
+        self.flush_zone(zone["name"])
100
+        self.notify_zone(zone["name"])

+ 1
- 0
certbot_dns/__init__.py View File

@@ -0,0 +1 @@
1
+"""Let's Encrypt DNS plugin"""

+ 62
- 0
certbot_dns/authenticator.py View File

@@ -0,0 +1,62 @@
1
+"""DNS plugin."""
2
+import collections
3
+import logging
4
+
5
+import zope.interface
6
+from acme import challenges
7
+from certbot import interfaces
8
+from certbot.plugins import common
9
+
10
+from certbot_dns.PdnsApiAuthenticator import PdnsApiAuthenticator
11
+
12
+logger = logging.getLogger(__name__)
13
+
14
+
15
+@zope.interface.implementer(interfaces.IAuthenticator)
16
+@zope.interface.provider(interfaces.IPluginFactory)
17
+class Authenticator(common.Plugin):
18
+    """DNS Authenticator."""
19
+
20
+    description = "Place challenges in DNS records"
21
+
22
+    MORE_INFO = """\
23
+Authenticator plugin that performs dns-01 challenge by saving
24
+necessary validation resources to appropriate records in DNS server.
25
+It expects that there is some other DNS server configured
26
+to serve all records."""
27
+
28
+    backend = None
29
+
30
+    def more_info(self):  # pylint: disable=missing-docstring,no-self-use
31
+        return self.MORE_INFO
32
+
33
+    @classmethod
34
+    def add_parser_arguments(cls, add):
35
+        add("certbot-dns-config", "-f", default="/etc/letsencrypt/certbot-dns.json",
36
+            help="Path to certbot-dns configuration file")
37
+
38
+    def get_chall_pref(self, domain):  # pragma: no cover
39
+        # pylint: disable=missing-docstring,no-self-use,unused-argument
40
+        return [challenges.DNS01]
41
+
42
+    def __init__(self, *args, **kwargs):
43
+        super(Authenticator, self).__init__(*args, **kwargs)
44
+        self.full_roots = {}
45
+        self.performed = collections.defaultdict(set)
46
+
47
+    def prepare(self):  # pylint: disable=missing-docstring
48
+        self.backend = PdnsApiAuthenticator()
49
+        conf_path = self.conf("certbot-dns-config")
50
+        self.backend.prepare(conf_path)
51
+        pass
52
+
53
+    def perform(self, achalls):  # pylint: disable=missing-docstring
54
+        return [self._perform_single(achall) for achall in achalls]
55
+
56
+    def _perform_single(self, achall):
57
+        response, validation = achall.response_and_validation()
58
+        return self.backend.perform_single(achall, response, validation)
59
+
60
+    def cleanup(self, achalls):  # pylint: disable=missing-docstring
61
+        for achall in achalls:
62
+            self.backend.cleanup(achall)

+ 87
- 0
certbot_dns/pdnsapi.py View File

@@ -0,0 +1,87 @@
1
+import json
2
+
3
+import requests
4
+
5
+
6
+class PdnsApi:
7
+    api_key = None
8
+    base_url = None
9
+
10
+    def set_api_key(self, api_key):
11
+        self.api_key = api_key
12
+
13
+    def set_base_url(self, base_url):
14
+        self.base_url = base_url
15
+
16
+    def _query(self, uri, method, kwargs=None):
17
+        headers = {
18
+            'X-API-Key': self.api_key,
19
+            'Accept': 'application/json',
20
+            'Content-Type': 'application/json'
21
+        }
22
+
23
+        data = json.dumps(kwargs)
24
+
25
+        if method == "GET":
26
+            request = requests.get(self.base_url + uri, headers=headers)
27
+        elif method == "POST":
28
+            request = requests.post(self.base_url + uri, headers=headers, data=data)
29
+        elif method == "PUT":
30
+            request = requests.put(self.base_url + uri, headers=headers, data=data)
31
+        elif method == "PATCH":
32
+            request = requests.patch(self.base_url + uri, headers=headers, data=data)
33
+        elif method == "DELETE":
34
+            request = requests.delete(self.base_url + uri, headers=headers)
35
+        else:
36
+            raise ValueError("Invalid method '%s'" % method)
37
+
38
+        return None if request.status_code == 204 else request.json()
39
+
40
+    def list_zones(self):
41
+        return self._query("/servers/localhost/zones", "GET")
42
+
43
+    def get_zone(self, zone_name):
44
+        return self._query("/servers/localhost/zones/%s" % zone_name, "GET")
45
+
46
+    def update_zone(self, zone_name, data):
47
+        return self._query("/servers/localhost/zones/%s" % zone_name, "PUT", data)
48
+
49
+    def replace_record(self, zone_name, name, type, ttl, content, disabled, set_ptr):
50
+        return self._query("/servers/localhost/zones/%s" % zone_name, "PATCH", {"rrsets": [
51
+            {
52
+                "name": name,
53
+                "type": type,
54
+                "ttl": ttl,
55
+                "changetype": "REPLACE",
56
+                "records": [
57
+                    {
58
+                        "content": content,
59
+                        "disabled": disabled,
60
+                        "set-prt": set_ptr
61
+                    }
62
+                ]
63
+            }
64
+        ]})
65
+
66
+    def delete_record(self, zone_name, name, type, ttl, content, disabled, set_ptr):
67
+        return self._query("/servers/localhost/zones/%s" % zone_name, "PATCH", {"rrsets": [
68
+            {
69
+                "name": name,
70
+                "type": type,
71
+                "ttl": ttl,
72
+                "changetype": "DELETE",
73
+                "records": [
74
+                    {
75
+                        "content": content,
76
+                        "disabled": disabled,
77
+                        "set-prt": set_ptr
78
+                    }
79
+                ]
80
+            }
81
+        ]})
82
+
83
+    def notify_zone(self, zone_name):
84
+        return self._query("/servers/localhost/zones/%s/notify" % zone_name, "PUT")
85
+
86
+    def flush_zone_cache(self, zone_name):
87
+        return self._query("/servers/localhost/cache/flush?domain=%s" % zone_name, "PUT")

+ 26
- 0
setup.py View File

@@ -0,0 +1,26 @@
1
+#! /usr/bin/env python
2
+
3
+from setuptools import setup
4
+from setuptools import find_packages
5
+
6
+install_requires = [
7
+    'acme',
8
+    'certbot',
9
+    'zope.interface',
10
+]
11
+
12
+setup(
13
+    name='certbot-dns',
14
+    description="Certbot DNS authenticator",
15
+    url='https://git.rthoni.com/robin.thoni/certbot-dns',
16
+    author="Robin Thoni",
17
+    author_email='robin@rthoni.com',
18
+    license='MIT',
19
+    install_requires=install_requires,
20
+    packages=find_packages(),
21
+    entry_points={
22
+        'certbot.plugins': [
23
+            'auth = certbot_dns.authenticator:Authenticator',
24
+        ],
25
+    },
26
+)

Loading…
Cancel
Save