initial setup
This commit is contained in:
parent
7c62833cb3
commit
0c7ac77127
|
@ -1 +1,2 @@
|
||||||
*.secret
|
*.secret
|
||||||
|
**/__pycache__
|
||||||
|
|
|
@ -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()
|
|
@ -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")
|
24
records.json
24
records.json
|
@ -86,8 +86,30 @@
|
||||||
{
|
{
|
||||||
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
requests==2.31.0
|
37
script.py
37
script.py
|
@ -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)
|
|
|
@ -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)
|
|
@ -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)
|
Loading…
Reference in New Issue