From f24f37560d4f538d70af69231cabaa0d0d730ee7 Mon Sep 17 00:00:00 2001 From: Bernhard B Date: Sun, 16 Feb 2025 19:20:21 +0100 Subject: [PATCH] Added Cloudflare to DDNS providers This changesets adds Cloudflare to the list of DDNS providers. DNS Record Update Workflow: * first, the zone id for the given domain name is fetched -> if no zone id can be obtained, then either the provided token is wrong or doesn't have the necessary permissions. * next, all existing records in that zone are fetched -> if a type A record with the hostname is found, that one is updated. -> if no type A record is found, a new DNS record is created. In order to implement the necessary changes, the 'send_request method' in system.py was extended. In particular: * PATCH support was added * support for token based authentication was added --- src/ddns/providers.py | 85 +++++++++++++++++++++++++++++++++++++++++++ src/ddns/system.py | 10 +++-- 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/src/ddns/providers.py b/src/ddns/providers.py index 59f9665..4d28685 100644 --- a/src/ddns/providers.py +++ b/src/ddns/providers.py @@ -2017,3 +2017,88 @@ class DDNSProviderInfomaniak(DDNSProtocolDynDNS2, DDNSProvider): # https://www.infomaniak.com/de/support/faq/2376/dyndns-aktualisieren-eines-dynamischen-dns-uber-die-api url = "https://infomaniak.com/nic/update" + +class DDNSProviderCloudflare(DDNSProvider): + handle = "cloudflare.com" + name = "cloudflare.com" + website = "https://cloudflare.com/" + protocols = ("ipv4",) + + url = "https://api.cloudflare.com/client/v4/zones" + can_remove_records = False + supports_token_auth = True + + # https://developers.cloudflare.com/api/resources/zones/methods/list/ + def _get_zone_id(self): + url = "https://api.cloudflare.com/client/v4/zones" + result = self.send_request(url, method="GET", token=self.token) + if result.code == 200: + try: + data = json.loads(result.read().decode(result.info().get_param('charset') or 'utf-8')) + for elem in data["result"]: + if self.hostname.endswith(elem["name"]): + return elem["id"] + except KeyError: + return None + return None + + # https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/list/ + def _get_dns_record_id(self, zone_id): + url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records" + result = self.send_request(url, method="GET", token=self.token) + if result.code == 200: + data = json.loads(result.read().decode(result.info().get_param('charset') or 'utf-8')) + for elem in data["result"]: + if elem["name"] == self.hostname and elem["type"] == "A": + return elem["id"] + return None + + # https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/edit/ + def _update_dns_record(self, zone_id, dns_record_id, ip_address): + payload = { + "content": ip_address + } + url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records/{dns_record_id}" + result = self.send_request(url, data=json.dumps(payload).encode("utf-8"), method="PATCH", token=self.token) + if result.code == 200: + data = json.loads(result.read().decode(result.info().get_param('charset') or 'utf-8')) + if data["success"]: + return True + return False + + # https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/create/ + def _create_dns_record(self, zone_id, ip_address): + url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records" + payload = { + "comment": "DNS Record created by IpFire", + "proxied": False, + "type": "A", + "ttl": 60 + } + + payload["content"] = f"{ip_address}" + payload["name"] = f"{self.hostname}" + result = self.send_request(url, data=json.dumps(payload).encode("utf-8"), method="POST", token=self.token) + if result.code == 200: + data = json.loads(result.read().decode(result.info().get_param('charset') or 'utf-8')) + if data["success"]: + return True + return False + + def update_protocol(self, proto): + # retrieve ip + ip_address = self.get_address(proto) + + zone_id = self._get_zone_id() + if zone_id is not None: + # Check if there already exists a DNS Record. If so, update the existing one. + # Otherwise, create a new one. + dns_record_id = self._get_dns_record_id(zone_id) + if dns_record_id is not None: + if not self._update_dns_record(zone_id, dns_record_id, ip_address): + raise DDNSUpdateError + else: + if not self._create_dns_record(zone_id, ip_address): + raise DDNSUpdateError + else: + raise DDNSAuthenticationError diff --git a/src/ddns/system.py b/src/ddns/system.py index 48c9a8f..3ca95b9 100644 --- a/src/ddns/system.py +++ b/src/ddns/system.py @@ -121,8 +121,10 @@ class DDNSSystem(object): return self._guess_external_ip_address(url, **kwargs) - def send_request(self, url, method="GET", data=None, username=None, password=None, timeout=30): - assert method in ("GET", "POST") + def send_request(self, url, method="GET", data=None, username=None, password=None, timeout=30, token=None): + assert method in ("GET", "POST", "PATCH") + if token is not None and ((username is not None) or (password is not None)): + raise AsssertionError("Please use either password based authentication or token based authentication - but not both!") # Add all arguments in the data dict to the URL and escape them properly. if method == "GET" and data: @@ -138,11 +140,13 @@ class DDNSSystem(object): if data: logger.debug(" data: %s" % data) - req = urllib.request.Request(url, data=data) + req = urllib.request.Request(url, data=data, method=method) if username and password: basic_auth_header = self._make_basic_auth_header(username, password) req.add_header("Authorization", "Basic %s" % basic_auth_header.decode()) + elif token is not None: + req.add_header("Authorization", f"Bearer {token}") # Set the user agent. req.add_header("User-Agent", self.USER_AGENT) -- 2.39.5