public inbox for ddns@lists.ipfire.org
 help / color / mirror / Atom feed
* Add Cloudflare to DDNS Providers List
@ 2025-02-16 19:46 Bernhard B.
  0 siblings, 0 replies; only message in thread
From: Bernhard B. @ 2025-02-16 19:46 UTC (permalink / raw)
  To: ddns

[-- Attachment #1: Type: text/plain, Size: 311 bytes --]

Hello,

please find attached a patch for adding Cloudflare to the list of the 
DDNS Providers. I've tested the changeset with a fresh IpFire 
installation inside QEMU and a custom domain that I own. Everything 
seems to be working fine.

Please let me know if you want anything changed.

Best Regards,
Bernhard

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Added-Cloudflare-to-DDNS-providers.patch --]
[-- Type: text/x-patch, Size: 6052 bytes --]

From f24f37560d4f538d70af69231cabaa0d0d730ee7 Mon Sep 17 00:00:00 2001
From: Bernhard B <bernhard@liftingcoder.com>
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


^ permalink raw reply	[flat|nested] only message in thread

only message in thread, other threads:[~2025-02-16 19:46 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-02-16 19:46 Add Cloudflare to DDNS Providers List Bernhard B.

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox