initial setup

This commit is contained in:
Elizabeth Hunt 2024-04-10 16:13:16 -06:00
parent 7c62833cb3
commit 0c7ac77127
Signed by untrusted user who does not match committer: simponic
GPG Key ID: 2909B9A7FF6213EE
8 changed files with 351 additions and 128 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
*.secret *.secret
**/__pycache__

22
args.py Normal file
View File

@ -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()

26
main.py Normal file
View File

@ -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")

View File

@ -1,93 +1,115 @@
[ [
{ {
"type": "A", "type": "A",
"name": "johan.internal.simponic.xyz.", "name": "johan.internal.simponic.xyz.",
"content": "100.64.0.5", "content": "100.64.0.5",
"ttl": "43200", "ttl": "43200",
"internal": "on" "internal": "on"
}, },
{ {
"type": "A", "type": "A",
"name": "europa.internal.simponic.xyz.", "name": "europa.internal.simponic.xyz.",
"content": "100.64.0.8", "content": "100.64.0.8",
"ttl": "43200", "ttl": "43200",
"internal": "on" "internal": "on"
}, },
{ {
"type": "CNAME", "type": "CNAME",
"name": "vaultwarden.internal.simponic.xyz.", "name": "vaultwarden.internal.simponic.xyz.",
"content": "johan.internal.simponic.xyz.", "content": "johan.internal.simponic.xyz.",
"ttl": "43200", "ttl": "43200",
"internal": "on" "internal": "on"
}, },
{ {
"type": "CNAME", "type": "CNAME",
"name": "lldap.internal.simponic.xyz.", "name": "lldap.internal.simponic.xyz.",
"content": "johan.internal.simponic.xyz.", "content": "johan.internal.simponic.xyz.",
"ttl": "43200", "ttl": "43200",
"internal": "on" "internal": "on"
}, },
{ {
"type": "CNAME", "type": "CNAME",
"name": "ca.internal.simponic.xyz.", "name": "ca.internal.simponic.xyz.",
"content": "johan.internal.simponic.xyz.", "content": "johan.internal.simponic.xyz.",
"ttl": "43200", "ttl": "43200",
"internal": "on" "internal": "on"
}, },
{ {
"type": "CNAME", "type": "CNAME",
"name": "pihole.internal.simponic.xyz.", "name": "pihole.internal.simponic.xyz.",
"content": "johan.internal.simponic.xyz.", "content": "johan.internal.simponic.xyz.",
"ttl": "43200", "ttl": "43200",
"internal": "on" "internal": "on"
}, },
{ {
"type": "CNAME", "type": "CNAME",
"name": "owncloud.internal.simponic.xyz.", "name": "owncloud.internal.simponic.xyz.",
"content": "europa.internal.simponic.xyz.", "content": "europa.internal.simponic.xyz.",
"ttl": "43200", "ttl": "43200",
"internal": "on" "internal": "on"
}, },
{ {
"type": "CNAME", "type": "CNAME",
"name": "jellyfin.internal.simponic.xyz.", "name": "jellyfin.internal.simponic.xyz.",
"content": "europa.internal.simponic.xyz.", "content": "europa.internal.simponic.xyz.",
"ttl": "43200", "ttl": "43200",
"internal": "on" "internal": "on"
}, },
{ {
"type": "CNAME", "type": "CNAME",
"name": "drone.internal.simponic.xyz.", "name": "drone.internal.simponic.xyz.",
"content": "europa.internal.simponic.xyz.", "content": "europa.internal.simponic.xyz.",
"ttl": "43200", "ttl": "43200",
"internal": "on" "internal": "on"
}, },
{ {
"type": "CNAME", "type": "CNAME",
"name": "scurvy.internal.simponic.xyz.", "name": "scurvy.internal.simponic.xyz.",
"content": "europa.internal.simponic.xyz.", "content": "europa.internal.simponic.xyz.",
"ttl": "43200", "ttl": "43200",
"internal": "on" "internal": "on"
}, },
{ {
"type": "CNAME", "type": "CNAME",
"name": "roundcube.internal.simponic.xyz.", "name": "roundcube.internal.simponic.xyz.",
"content": "europa.internal.simponic.xyz.", "content": "europa.internal.simponic.xyz.",
"ttl": "43200", "ttl": "43200",
"internal": "on" "internal": "on"
}, },
{ {
"type": "CNAME", "type": "CNAME",
"name": "simponic.endpoints", "name": "simponic.endpoints",
"content": "levi.simponic.xyz.", "content": "levi.simponic.xyz.",
"ttl": "43200", "ttl": "43200",
"internal": "off" "internal": "off"
}, },
{ {
"type": "CNAME", "type": "CNAME",
"name": "simponic", "name": "simponic",
"content": "simponic.xyz", "content": "simponic.xyz.",
"ttl": "43200", "ttl": "43200",
"internal": "off" "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"
}
] ]

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
requests==2.31.0

View File

@ -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)

164
updater/acmedns.py Normal file
View File

@ -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)

24
updater/adapter.py Normal file
View File

@ -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)