diff --git a/cloudflare_ddns/app.py b/cloudflare_ddns/app.py index 32af2ab..fc9d3cc 100644 --- a/cloudflare_ddns/app.py +++ b/cloudflare_ddns/app.py @@ -1,11 +1,11 @@ import logging import threading from dataclasses import dataclass -from typing import Tuple +from typing import Tuple, List, Dict, NoReturn import requests -from cloudflare_ddns.constants import VERIFY_TOKEN +from cloudflare_ddns.constants import VERIFY_TOKEN, LIST_ZONES, LIST_DNS, ACCEPTED_RECORDS from cloudflare_ddns.utils import parse_duration, BearerAuth log = logging.getLogger("ddns") @@ -15,21 +15,99 @@ log = logging.getLogger("ddns") class Domain: domain: str record_type: str + zone: str + id: str class ApplicationJob(threading.Thread): - def __init__(self, delay: str, token: str, raw_domains: Tuple[str], default_ipv6: bool): + def __init__(self, raw_delay: str, token: str, raw_domains: Tuple[str], default_ipv6: bool): super().__init__() self.stop_signal = threading.Event() - self.delay = delay + self.delay = None + self.domains: List[Domain] = [] + self.found_domains: Dict[str, Domain] = {} + self.auth = BearerAuth(token) - self.raw_domains = raw_domains self.default_ipv6 = default_ipv6 + self.raw_domains = raw_domains + self.raw_delay = raw_delay + def launch(self) -> None: self.validate_arguments() + log.debug("Starting job.") + self.start() + + def run(self) -> None: + log.debug("Parsing domains.") + self.parse_domains() + log.debug(f"Using domains: {', '.join(f'{domain.record_type}:{domain.domain}' for domain in self.domains)}") + + log.info("Starting app.") + self.update_records() + while not self.stop_signal.wait(self.delay): + self.update_records() + + def update_records(self): + ... + + def parse_domains(self): + type_1 = "A" if not self.default_ipv6 else "AAAA" + type_2 = "A" if self.default_ipv6 else "AAAA" + + for zone_json in requests.get(LIST_ZONES, auth=self.auth).json()["result"]: + for record_json in requests.get( + LIST_DNS.format(zone_identifier=zone_json["id"]), + auth=self.auth + ).json()["result"]: + if record_json["type"] in ACCEPTED_RECORDS: + domain = Domain( + record_json["name"], + record_json["type"], + record_json["zone_id"], + record_json["id"] + ) + self.found_domains[f'{record_json["name"]}-{record_json["type"]}'] = domain + + log.debug( + f"Found domains: " + f"{', '.join(f'{domain.record_type}:{domain.domain}' for domain in self.found_domains.values())}" + ) + for domain in self.raw_domains: + if ':' in domain: + type_, domain = domain.split(':', maxsplit=1) + + if type_ not in ACCEPTED_RECORDS: + log.error(f"Invalid record type {type_}. Must be one of {', '.join(ACCEPTED_RECORDS)}.") + log.info(f"Exiting with code 65.") + exit(65) + + if f"{domain}-{type_}" not in self.found_domains: + log.error( + f"Cannot find an {type_} record for the domain {domain} in your Cloudflare settings. " + f"Have you defined this record yet?" + ) + log.info(f"Exiting with code 65.") + exit(65) + else: + if f"{domain}-{type_1}" in self.found_domains: + type_ = type_1 + elif f"{domain}-{type_2}" in self.found_domains: + type_ = type_2 + else: + log.error( + f"Cannot find the domain {domain} in your Cloudflare settings. " + f"Have you defined this record yet?" + ) + log.info(f"Exiting with code 65.") + exit(65) + + self.domains.append(self.found_domains[f"{domain}-{type_}"]) + + + def validate_arguments(self): failed = False @@ -39,20 +117,21 @@ class ApplicationJob(threading.Thread): failed = True try: - self.delay = parse_duration(self.delay) + self.delay = parse_duration(self.raw_delay) except ValueError as e: log.error(f"Failed to parse delay: {e}") failed = True - try: - log.debug("Validating bearer token.") + if not failed: + try: + log.debug("Validating bearer token.") - self.validate_bearer() - except ValueError as e: - log.error(f"Failed to validate bearer token: {e}") - failed = True - else: - log.info("Successfully validated the bearer token.") + self.validate_bearer() + except ValueError as e: + log.error(f"Failed to validate bearer token: {e}") + failed = True + else: + log.info("Successfully validated the bearer token.") if failed: log.info("Exiting with code 64.") diff --git a/cloudflare_ddns/constants.py b/cloudflare_ddns/constants.py index 03d2fcd..cf2d5d0 100644 --- a/cloudflare_ddns/constants.py +++ b/cloudflare_ddns/constants.py @@ -5,3 +5,9 @@ DEFAULT_DELAY = "5 minutes" BASE_ENDPOINT = "https://api.cloudflare.com/client/v4/" VERIFY_TOKEN = BASE_ENDPOINT + "user/tokens/verify" + +LIST_ZONES = BASE_ENDPOINT + "zones" +LIST_DNS = BASE_ENDPOINT + "zones/{zone_identifier}/dns_records" + +# Utilities +ACCEPTED_RECORDS = ('A', 'AAAA')