aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--args.py22
-rw-r--r--main.py26
-rw-r--r--records.json204
-rw-r--r--requirements.txt1
-rw-r--r--script.py37
-rw-r--r--updater/acmedns.py164
-rw-r--r--updater/adapter.py24
8 files changed, 351 insertions, 128 deletions
diff --git a/.gitignore b/.gitignore
index bceb371..1cec515 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
*.secret
+**/__pycache__
diff --git a/args.py b/args.py
new file mode 100644
index 0000000..69599e2
--- /dev/null
+++ b/args.py
@@ -0,0 +1,22 @@
+import argparse
+
+
+def get_args():
+ parser = argparse.ArgumentParser()
+
+ parser.add_argument("--endpoint", default="https://hatecomputers.club")
+ parser.add_argument("--api-key-file", default="apikey.secret")
+ parser.add_argument("--records-file", default="records.json")
+ parser.add_argument(
+ "--log-level",
+ default="INFO",
+ choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
+ )
+
+ parser.add_argument("--certbot", action="store_true", default=False)
+ parser.add_argument("--acme-url", required=True)
+ parser.add_argument("--acme-storage", default="acme.json")
+
+ parser.add_argument("--sync", action="store_true", default=False)
+
+ return parser.parse_args()
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..98cf488
--- /dev/null
+++ b/main.py
@@ -0,0 +1,26 @@
+import json
+import logging
+
+from updater.adapter import HatecomputersDNSAdapter
+from args import get_args
+
+
+def sync_records(adapter, records_path):
+ records_file = open(records_path, "r")
+ dns_records = json.load(records_file)
+ adapter.post_records(dns_records)
+
+
+if __name__ == "__main__":
+ args = get_args()
+ logging.basicConfig()
+ logging.root.setLevel(args.log_level)
+
+ api_key = open(args.api_key_file, "r").read().strip()
+ adapter = HatecomputersDNSAdapter(args.endpoint, api_key)
+
+ if args.sync:
+ sync_records(adapter, args.records_file)
+
+ if args.certbot:
+ logging.info("certbot mode")
diff --git a/records.json b/records.json
index 1629eda..f46db30 100644
--- a/records.json
+++ b/records.json
@@ -1,93 +1,115 @@
[
- {
- "type": "A",
- "name": "johan.internal.simponic.xyz.",
- "content": "100.64.0.5",
- "ttl": "43200",
- "internal": "on"
- },
- {
- "type": "A",
- "name": "europa.internal.simponic.xyz.",
- "content": "100.64.0.8",
- "ttl": "43200",
- "internal": "on"
- },
- {
- "type": "CNAME",
- "name": "vaultwarden.internal.simponic.xyz.",
- "content": "johan.internal.simponic.xyz.",
- "ttl": "43200",
- "internal": "on"
- },
- {
- "type": "CNAME",
- "name": "lldap.internal.simponic.xyz.",
- "content": "johan.internal.simponic.xyz.",
- "ttl": "43200",
- "internal": "on"
- },
- {
- "type": "CNAME",
- "name": "ca.internal.simponic.xyz.",
- "content": "johan.internal.simponic.xyz.",
- "ttl": "43200",
- "internal": "on"
- },
- {
- "type": "CNAME",
- "name": "pihole.internal.simponic.xyz.",
- "content": "johan.internal.simponic.xyz.",
- "ttl": "43200",
- "internal": "on"
- },
- {
- "type": "CNAME",
- "name": "owncloud.internal.simponic.xyz.",
- "content": "europa.internal.simponic.xyz.",
- "ttl": "43200",
- "internal": "on"
- },
- {
- "type": "CNAME",
- "name": "jellyfin.internal.simponic.xyz.",
- "content": "europa.internal.simponic.xyz.",
- "ttl": "43200",
- "internal": "on"
- },
- {
- "type": "CNAME",
- "name": "drone.internal.simponic.xyz.",
- "content": "europa.internal.simponic.xyz.",
- "ttl": "43200",
- "internal": "on"
- },
- {
- "type": "CNAME",
- "name": "scurvy.internal.simponic.xyz.",
- "content": "europa.internal.simponic.xyz.",
- "ttl": "43200",
- "internal": "on"
- },
- {
- "type": "CNAME",
- "name": "roundcube.internal.simponic.xyz.",
- "content": "europa.internal.simponic.xyz.",
- "ttl": "43200",
- "internal": "on"
- },
- {
- "type": "CNAME",
- "name": "simponic.endpoints",
- "content": "levi.simponic.xyz.",
- "ttl": "43200",
- "internal": "off"
- },
- {
- "type": "CNAME",
- "name": "simponic",
- "content": "simponic.xyz",
- "ttl": "43200",
- "internal": "off"
- }
+ {
+ "type": "A",
+ "name": "johan.internal.simponic.xyz.",
+ "content": "100.64.0.5",
+ "ttl": "43200",
+ "internal": "on"
+ },
+ {
+ "type": "A",
+ "name": "europa.internal.simponic.xyz.",
+ "content": "100.64.0.8",
+ "ttl": "43200",
+ "internal": "on"
+ },
+ {
+ "type": "CNAME",
+ "name": "vaultwarden.internal.simponic.xyz.",
+ "content": "johan.internal.simponic.xyz.",
+ "ttl": "43200",
+ "internal": "on"
+ },
+ {
+ "type": "CNAME",
+ "name": "lldap.internal.simponic.xyz.",
+ "content": "johan.internal.simponic.xyz.",
+ "ttl": "43200",
+ "internal": "on"
+ },
+ {
+ "type": "CNAME",
+ "name": "ca.internal.simponic.xyz.",
+ "content": "johan.internal.simponic.xyz.",
+ "ttl": "43200",
+ "internal": "on"
+ },
+ {
+ "type": "CNAME",
+ "name": "pihole.internal.simponic.xyz.",
+ "content": "johan.internal.simponic.xyz.",
+ "ttl": "43200",
+ "internal": "on"
+ },
+ {
+ "type": "CNAME",
+ "name": "owncloud.internal.simponic.xyz.",
+ "content": "europa.internal.simponic.xyz.",
+ "ttl": "43200",
+ "internal": "on"
+ },
+ {
+ "type": "CNAME",
+ "name": "jellyfin.internal.simponic.xyz.",
+ "content": "europa.internal.simponic.xyz.",
+ "ttl": "43200",
+ "internal": "on"
+ },
+ {
+ "type": "CNAME",
+ "name": "drone.internal.simponic.xyz.",
+ "content": "europa.internal.simponic.xyz.",
+ "ttl": "43200",
+ "internal": "on"
+ },
+ {
+ "type": "CNAME",
+ "name": "scurvy.internal.simponic.xyz.",
+ "content": "europa.internal.simponic.xyz.",
+ "ttl": "43200",
+ "internal": "on"
+ },
+ {
+ "type": "CNAME",
+ "name": "roundcube.internal.simponic.xyz.",
+ "content": "europa.internal.simponic.xyz.",
+ "ttl": "43200",
+ "internal": "on"
+ },
+ {
+ "type": "CNAME",
+ "name": "simponic.endpoints",
+ "content": "levi.simponic.xyz.",
+ "ttl": "43200",
+ "internal": "off"
+ },
+ {
+ "type": "CNAME",
+ "name": "simponic",
+ "content": "simponic.xyz.",
+ "ttl": "43200",
+ "internal": "off"
+ },
+
+ {
+ "type": "A",
+ "name": "armin.internal.simponic.xyz",
+ "content": "100.64.0.6",
+ "ttl": "43200",
+ "internal": "on"
+ },
+ {
+ "type": "CNAME",
+ "name": "dev.armin.internal.simponic.xyz",
+ "content": "armin.internal.simponic.xyz",
+ "ttl": "43200",
+ "internal": "on"
+ },
+ {
+ "type": "CNAME",
+ "name": "traefik.armin.internal.simponic.xyz.",
+ "content": "armin.internal.simponic.xyz.",
+ "ttl": "43200",
+ "internal": "on"
+ }
]
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..2c24336
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1 @@
+requests==2.31.0
diff --git a/script.py b/script.py
deleted file mode 100644
index 4315de9..0000000
--- a/script.py
+++ /dev/null
@@ -1,37 +0,0 @@
-import json
-import requests
-import time
-import logging
-
-RECORDS_FILE = "records.json"
-ENDPOINT = "https://hatecomputers.club"
-API_KEY = open('apikey.secret', 'r').read().strip()
-
-class HatecomputersDNSAdapter:
- def __init__(self, endpoint, api_key):
- self.endpoint = endpoint
- self.session = requests.Session()
- self.headers = {'Authorization': 'Bearer ' + api_key}
-
- def post_record(self, record):
- endpoint = self.endpoint + "/dns"
- logging.info(f"adding {record} to {endpoint}")
-
- self.session.post(endpoint, headers=self.headers, data=record)
-
- def post_records(self, dns_entries, eepy_time=0.25):
- for record in dns_entries:
- self.post_record(record)
-
- logging.info(f"sleeping for {eepy_time}")
- time.sleep(eepy_time)
-
-if __name__ == "__main__":
- logging.basicConfig()
- logging.root.setLevel(logging.NOTSET)
-
- records_file = open(RECORDS_FILE, 'r')
- dns_records = json.load(records_file)
-
- adapter = HatecomputersDNSAdapter(ENDPOINT, API_KEY)
- adapter.post_records(dns_records)
diff --git a/updater/acmedns.py b/updater/acmedns.py
new file mode 100644
index 0000000..400f91e
--- /dev/null
+++ b/updater/acmedns.py
@@ -0,0 +1,164 @@
+#!/usr/bin/env python
+import json
+import os
+import requests
+import sys
+
+### EDIT THESE: Configuration values ###
+
+# URL to acme-dns instance
+ACMEDNS_URL = "https://auth.acme-dns.io"
+# Path for acme-dns credential storage
+STORAGE_PATH = "/etc/letsencrypt/acmedns.json"
+# Whitelist for address ranges to allow the updates from
+# Example: ALLOW_FROM = ["192.168.10.0/24", "::1/128"]
+ALLOW_FROM = []
+# Force re-registration. Overwrites the already existing acme-dns accounts.
+FORCE_REGISTER = False
+
+### DO NOT EDIT BELOW THIS POINT ###
+### HERE BE DRAGONS ###
+
+DOMAIN = os.environ["CERTBOT_DOMAIN"]
+if DOMAIN.startswith("*."):
+ DOMAIN = DOMAIN[2:]
+VALIDATION_DOMAIN = "_acme-challenge." + DOMAIN
+VALIDATION_TOKEN = os.environ["CERTBOT_VALIDATION"]
+
+
+class AcmeDnsClient(object):
+ """
+ Handles the communication with ACME-DNS API
+ """
+
+ def __init__(self, acmedns_url):
+ self.acmedns_url = acmedns_url
+
+ def register_account(self, allowfrom):
+ """Registers a new ACME-DNS account"""
+
+ if allowfrom:
+ # Include whitelisted networks to the registration call
+ reg_data = {"allowfrom": allowfrom}
+ res = requests.post(
+ self.acmedns_url + "/register", data=json.dumps(reg_data)
+ )
+ else:
+ res = requests.post(self.acmedns_url + "/register")
+ if res.status_code == 201:
+ # The request was successful
+ return res.json()
+ else:
+ # Encountered an error
+ msg = (
+ "Encountered an error while trying to register a new acme-dns "
+ "account. HTTP status {}, Response body: {}"
+ )
+ print(msg.format(res.status_code, res.text))
+ sys.exit(1)
+
+ def update_txt_record(self, account, txt):
+ """Updates the TXT challenge record to ACME-DNS subdomain."""
+ update = {"subdomain": account["subdomain"], "txt": txt}
+ headers = {
+ "X-Api-User": account["username"],
+ "X-Api-Key": account["password"],
+ "Content-Type": "application/json",
+ }
+ res = requests.post(
+ self.acmedns_url + "/update", headers=headers, data=json.dumps(update)
+ )
+ if res.status_code == 200:
+ # Successful update
+ return
+ else:
+ msg = (
+ "Encountered an error while trying to update TXT record in "
+ "acme-dns. \n"
+ "------- Request headers:\n{}\n"
+ "------- Request body:\n{}\n"
+ "------- Response HTTP status: {}\n"
+ "------- Response body: {}"
+ )
+ s_headers = json.dumps(headers, indent=2, sort_keys=True)
+ s_update = json.dumps(update, indent=2, sort_keys=True)
+ s_body = json.dumps(res.json(), indent=2, sort_keys=True)
+ print(msg.format(s_headers, s_update, res.status_code, s_body))
+ sys.exit(1)
+
+
+class Storage(object):
+ def __init__(self, storagepath):
+ self.storagepath = storagepath
+ self._data = self.load()
+
+ def load(self):
+ """Reads the storage content from the disk to a dict structure"""
+ data = dict()
+ filedata = ""
+ try:
+ with open(self.storagepath, "r") as fh:
+ filedata = fh.read()
+ except IOError as e:
+ if os.path.isfile(self.storagepath):
+ # Only error out if file exists, but cannot be read
+ print("ERROR: Storage file exists but cannot be read")
+ sys.exit(1)
+ try:
+ data = json.loads(filedata)
+ except ValueError:
+ if len(filedata) > 0:
+ # Storage file is corrupted
+ print("ERROR: Storage JSON is corrupted")
+ sys.exit(1)
+ return data
+
+ def save(self):
+ """Saves the storage content to disk"""
+ serialized = json.dumps(self._data)
+ try:
+ with os.fdopen(
+ os.open(self.storagepath, os.O_WRONLY | os.O_CREAT, 0o600), "w"
+ ) as fh:
+ fh.truncate()
+ fh.write(serialized)
+ except IOError as e:
+ print("ERROR: Could not write storage file.")
+ sys.exit(1)
+
+ def put(self, key, value):
+ """Puts the configuration value to storage and sanitize it"""
+ # If wildcard domain, remove the wildcard part as this will use the
+ # same validation record name as the base domain
+ if key.startswith("*."):
+ key = key[2:]
+ self._data[key] = value
+
+ def fetch(self, key):
+ """Gets configuration value from storage"""
+ try:
+ return self._data[key]
+ except KeyError:
+ return None
+
+
+if __name__ == "__main__":
+ # Init
+ client = AcmeDnsClient(ACMEDNS_URL)
+ storage = Storage(STORAGE_PATH)
+
+ # Check if an account already exists in storage
+ account = storage.fetch(DOMAIN)
+ if FORCE_REGISTER or not account:
+ # Create and save the new account
+ account = client.register_account(ALLOW_FROM)
+ storage.put(DOMAIN, account)
+ storage.save()
+
+ # Display the notification for the user to update the main zone
+ msg = "Please add the following CNAME record to your main DNS zone:\n{}"
+ cname = "{} CNAME {}.".format(VALIDATION_DOMAIN, account["fulldomain"])
+ print(msg.format(cname))
+
+ # Update the TXT record in acme-dns instance
+ client.update_txt_record(account, VALIDATION_TOKEN)
diff --git a/updater/adapter.py b/updater/adapter.py
new file mode 100644
index 0000000..b0980a3
--- /dev/null
+++ b/updater/adapter.py
@@ -0,0 +1,24 @@
+import requests
+import time
+import logging
+
+
+class HatecomputersDNSAdapter:
+ def __init__(self, endpoint, api_key, logger=None):
+ self.endpoint = endpoint
+ self.session = requests.Session()
+ self.headers = {"Authorization": "Bearer " + api_key}
+ self.logger = logger or logging.getLogger(__name__)
+
+ def post_record(self, record):
+ endpoint = self.endpoint + "/dns"
+ self.logger.info(f"adding {record} to {endpoint}")
+
+ self.session.post(endpoint, headers=self.headers, data=record)
+
+ def post_records(self, dns_entries, eepy_time=0.25):
+ for record in dns_entries:
+ self.post_record(record)
+
+ self.logger.info(f"sleeping for {eepy_time}")
+ time.sleep(eepy_time)