From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mail02.haj.ipfire.org (localhost [IPv6:::1]) by mail02.haj.ipfire.org (Postfix) with ESMTP id 4cCvRf1B3zz30Qc for ; Fri, 29 Aug 2025 10:20:46 +0000 (UTC) Received: from mail01.ipfire.org (mail01.haj.ipfire.org [IPv6:2001:678:b28::25]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange x25519) (Client CN "mail01.haj.ipfire.org", Issuer "R13" (verified OK)) by mail02.haj.ipfire.org (Postfix) with ESMTPS id 4cCvRd2tcKz2xQc for ; Fri, 29 Aug 2025 10:20:45 +0000 (UTC) Received: from hosted.mailcow.de (hosted.mailcow.de [5.1.76.202]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange x25519 server-signature RSA-PSS (4096 bits) server-digest SHA256) (Client did not present a certificate) by mail01.ipfire.org (Postfix) with ESMTPS id 4cCvRb2NRSz2F for ; Fri, 29 Aug 2025 10:20:43 +0000 (UTC) Authentication-Results: mail01.ipfire.org; dkim=pass header.d=liftingcoder.com header.s=default header.b=YG1c0Y6q; dmarc=none; spf=none (mail01.ipfire.org: domain of bernhard@liftingcoder.com has no SPF policy when checking 5.1.76.202) smtp.mailfrom=bernhard@liftingcoder.com ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=lists.ipfire.org; s=202003rsa; t=1756462843; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: in-reply-to:in-reply-to:references:references:dkim-signature; bh=cjsQXcCJdvVXmYsdoUd8stHW40ASII7zK1uwBP9A3s8=; b=SzaViRnmv1MOu4o+hzUaT+iyNOEUbqIQBJ03YAjojxm3Ks01zLcgseaJtHZCOURozGkJrM iWUKx+rGHf7Ut7PSZv8Js3At5exnCUYVYT8wdV7ul8bRN06K2dzVPxVHi5teKYBrHYs90T EOk2wgk3tE2B/Q/Bop1ouRvQG2gO3pur+l4yjb6uWQfxj3tJTeqSGd8g3UPgAVNYrHph/a EU9Q+vmizlZ+xkyCT/ogkQQlD9fIYlg1TzhWxQH2jwSe2pviu453Ge+GKryXhi4STPclU8 23YS393W9dMAVg5/OJrAwbsqXM3WrQhBgERByO5TSjkQDu5Zz4rU1FgOE3NggQ== ARC-Authentication-Results: i=1; mail01.ipfire.org; dkim=pass header.d=liftingcoder.com header.s=default header.b=YG1c0Y6q; dmarc=none; spf=none (mail01.ipfire.org: domain of bernhard@liftingcoder.com has no SPF policy when checking 5.1.76.202) smtp.mailfrom=bernhard@liftingcoder.com ARC-Seal: i=1; s=202003rsa; d=lists.ipfire.org; t=1756462843; a=rsa-sha256; cv=none; b=Yei2aMAUQjVtVaIyG6eek/T/o6iwxAxM9aRqsfU5hbEm5Bg8RYS8zMU3SWVlHrEPVULkm8 7bVGvfpICHjgTorhM9NZHq/hdFueYIhIREHw+wTs4NwZyMCBd5z9UlOGC8Pvx6AeM7IGjo zMrySBKOE4jsQNxGkO3XB6pQyE+UowUWrt8G2RhmQE8chrpBxC3rLj3jLcsw8lBaZlMhTG FDHIRUhCDba+krrZQ6UmPHLAQpJZewKsFEAcPxGb9TTgLgsEykHfSWTBMKc1Q4UhSZMfgw SgrcsVkTt/3X7NolrPOeCRrnYW408tnQg25CQZjcJBbJzzP3Hc3AAP/FLnz8dw== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=liftingcoder.com; s=default; t=1756462842; h=from:subject:date:message-id:to:cc:mime-version:content-type: in-reply-to:references; bh=cjsQXcCJdvVXmYsdoUd8stHW40ASII7zK1uwBP9A3s8=; b=YG1c0Y6q+6GMX9bDcI7XAzqYPgNjkTv8dVPEUtYXsht5JIr0NkiStetllGh0qxkL1yxtjq f1xWqanY0/XC0q8W8bdG93WhbgRDUrFmT+OvLPea+cfZ08JhMI/UU17hXpE2cKrPDvqNyx 26B1623cB+z2qcmM8FD1gDtPyxeIk8Q= Received: from hosted.mailcow.de (hosted.mailcow.de [5.1.76.202]) (Authenticated sender: bernhard@beroun.io) by hosted.mailcow.de (Postcow) with ESMTPA id 51ECE5C71C4; Fri, 29 Aug 2025 12:20:42 +0200 (CEST) From: "Bernhard Beroun" In-Reply-To: Content-Type: multipart/alternative; boundary="----=_=-_OpenGroupware_org_NGMime-2177527-1756462842.182486-1------" References: Date: Fri, 29 Aug 2025 12:20:42 +0200 Cc: "Chris Anton" , ddns@lists.ipfire.org To: "Michael Tremer" Precedence: list List-Id: List-Subscribe: , List-Unsubscribe: , List-Post: List-Help: Sender: Mail-Followup-To: MIME-Version: 1.0 Message-ID: <2139f7-68b17f00-3-48fa3400@237050521> Subject: =?utf-8?q?Re=3A?= [PATCH] =?utf-8?q?ddns=3A?= add Cloudflare (v4) provider using API token X-Rspamd-Server: mail01.haj.ipfire.org X-Rspamd-Queue-Id: 4cCvRb2NRSz2F X-Rspamd-Action: no action X-Spamd-Result: default: False [-2.26 / 11.00]; BAYES_HAM(-2.55)[97.98%]; SUBJ_EXCESS_QP(1.20)[]; NEURAL_HAM(-1.00)[-1.000]; MID_RHS_NOT_FQDN(0.50)[]; RCVD_IN_DNSWL_LOW(-0.20)[5.1.76.202:received,5.1.76.202:from]; R_DKIM_ALLOW(-0.20)[liftingcoder.com:s=default]; RCVD_NO_TLS_LAST(0.10)[]; MIME_GOOD(-0.10)[multipart/alternative,text/plain]; MX_GOOD(-0.01)[]; MIME_TRACE(0.00)[0:+,1:+,2:~]; FREEMAIL_CC(0.00)[gmail.com,lists.ipfire.org]; DMARC_NA(0.00)[liftingcoder.com]; ARC_NA(0.00)[]; DKIM_TRACE(0.00)[liftingcoder.com:+]; FUZZY_RATELIMITED(0.00)[rspamd.com]; RCPT_COUNT_THREE(0.00)[3]; R_SPF_NA(0.00)[no SPF record]; TO_DN_SOME(0.00)[]; TO_MATCH_ENVRCPT_SOME(0.00)[]; FROM_EQ_ENVFROM(0.00)[]; FROM_HAS_DN(0.00)[]; MISSING_XM_UA(0.00)[]; TAGGED_RCPT(0.00)[]; RCVD_VIA_SMTP_AUTH(0.00)[]; DKIM_REPUTATION(0.00)[0]; RCVD_COUNT_ONE(0.00)[1]; ASN(0.00)[asn:34549, ipnet:5.1.76.0/24, country:DE]; ARC_SIGNED(0.00)[lists.ipfire.org:s=202003rsa:i=1] ------=_=-_OpenGroupware_org_NGMime-2177527-1756462842.182486-1------ Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable Content-Length: 9134 Hi everybody, I don't want to hijack this mail thread here, but just wanted to let yo= u know that I've also submitted a patch a few months ago. See https://l= ists.ipfire.org/ddns/19993045-2374-4537-96a2-5e2b1900a750@liftingcoder.= com/T/#u Unfortunately I never received any feedback, so I assumed Cloudflare su= pport wasn't desired. Again, sorry for hijacking this email chain here,= just wanted to quickly drop it here, since it matches the topic. Have a nice day, Bernhard Am Freitag, August 29, 2025 11:50 CEST, schrieb Michael Tremer : =C2=A0 Hello Chris, You don=E2=80=99t need to submit any patches more than once. We will ge= t back to you as soon as there is time. So let=E2=80=99s get into this... > On 29 Aug 2025, at 09:16, Chris Anton wrote= : >=C2=A0 > From: faithinchaos21 <45313722+faithinchaos21@users.noreply.github.co= m> I assume that you are faithinchaos21 and this code did not come from so= meone else? > Date: Wed, 27 Aug 2025 01:22:46 -0500 > Subject: [PATCH] ddns: add Cloudflare (v4) provider using API token > MIME-Version: 1.0 > Content-Type: text/plain; charset=3DUTF-8 > Content-Transfer-Encoding: 8bit >=C2=A0 > This adds a provider =E2=80=9Ccloudflare.com-v4=E2=80=9D that updates= an A record > via Cloudflare=E2=80=99s v4 API using a Bearer token. The token is ac= cepted > from either =E2=80=98token=E2=80=99 or legacy =E2=80=98password=E2=80= =99 for UI compatibility. >=C2=A0 > Tested on IPFire 2.29 / Core 196: > - no-op if A already matches WAN IP > - successful update when WAN IP changes > - logs include CFv4 breadcrumbs for troubleshooting To make it short, this patch is sadly very far away from what is accept= able. Before we get into the actual implementation, there are many design iss= ues here that simply cannot be accepted into DDNS: * This patch only adds support for IPv4. As far as I am aware, Cloudfla= re is capable of IPv6, too. Why would this be limited to IPv4 only? * You are not using the scaffolding and tools that DDNS is providing. A= lot of code is being written with plain Python and throwing away all t= he features that DDNS provides (catching Exceptions, proxy support and = many, many more=E2=80=A6) * This all looks very AI-generated to me and is implemented on a green = field. You are even importing all the modules that you need and ignore = everything else from DDNS. Why not use this snippet as a standalone scr= ipt then? There was no consideration for what code existed already. > Signed-off-by: Chris Anton > --- > 563f089d0820bd61ad4aecac248d5cc1f2adfc81 > src/ddns/providers.py | 121 +++++++++++++++++++++++++++++++++++++++++= + > 1 file changed, 121 insertions(+) >=C2=A0 > diff --git a/src/ddns/providers.py b/src/ddns/providers.py > index 59f9665..df0f3a9 100644 > --- a/src/ddns/providers.py > +++ b/src/ddns/providers.py > @@ -341,6 +341,127 @@ def have=5Faddress(self, proto): >=C2=A0 > return False >=C2=A0 > +class DDNSProviderCloudflareV4(DDNSProvider): > + """ > + Cloudflare v4 API using a Bearer Token. > + Put the API Token in the 'token' OR 'password' field of the DDNS en= try. > + Optional in ddns.conf: > + proxied =3D false|true (default false; keep false for WireGuard) > + ttl =3D 1|60|120... (default 1 =3D 'automatic') > + """ > + handle =3D "cloudflare.com-v4" > + name =3D "Cloudflare (v4)" > + website =3D "https://www.cloudflare.com/" > + protocols =3D ("ipv4",) > + supports=5Ftoken=5Fauth =3D True > + holdoff=5Ffailure=5Fdays =3D 0 > + > + def =5Fbool(self, key, default=3DFalse): > + v =3D str(self.get(key, default)).strip().lower() > + return v in ("1", "true", "yes", "on") > + This is a good example of a function which totally goes against the cod= ing style of the rest of the code base. It uses str(), chains a lot of = functions to gather to make the code look shorter, but very difficult t= o read. > + def update(self): > + import json, urllib.request, urllib.error Just no. We don=E2=80=99t import anything further down the line. DDNS p= rovides a toolkit of what to use and you should stay within it. If some= functionally is missing, DDNS=E2=80=99 functionality should be extende= d so that other providers can re-use the same well-maintained and teste= d code base. > + > + tok =3D self.get("token") or self.get("password") > + if not tok: > + raise DDNSConfigurationError("API Token (password/token) > is missing.") > + > + proxied =3D self.=5Fbool("proxied", False) > + try: > + ttl =3D int(self.get("ttl", 1)) > + except Exception: > + ttl =3D 1 A TTL of one second is never a good idea. > + > + headers =3D { > + "Authorization": "Bearer {0}".format(tok), > + "Content-Type": "application/json", > + "User-Agent": "IPFireDDNSUpdater/CFv4", > + } Since you are not using the internal request handler, you are creating = your own headers and a completely nonsensical user agent. Why? > + > + # --- find zone --- > + parts =3D self.hostname.split(".") > + if len(parts) < 2: > + raise DDNSRequestError("Hostname '{0}' is not a valid > domain.".format(self.hostname)) > + > + zone=5Fid =3D None > + zone=5Fname =3D None > + for i in range(len(parts) - 1): > + candidate =3D ".".join(parts[i:]) > + url =3D > f"https://api.cloudflare.com/client/v4/zones?name=3D{candidate}" > + try: > + req =3D urllib.request.Request(url, headers=3Dheaders, > method=3D"GET") > + with urllib.request.urlopen(req, timeout=3D20) as r: > + data =3D json.loads(r.read().decode()) > + except Exception as e: > + raise DDNSUpdateError(f"Failed to query Cloudflare > zones API: {e}") > + > + if data.get("success") and data.get("result"): > + zone=5Fid =3D data["result"][0]["id"] > + zone=5Fname =3D candidate > + break It is not acceptable to build anything custom that sends a request. You= are removing all other kinds of features that I have mentioned above. To just =E2=80=9Cguess=E2=80=9D the name of the zone is something that = I would not consider good style. > + > + if not zone=5Fid: > + raise DDNSRequestError(f"Could not find a Cloudflare Zone > for '{self.hostname}'.") > + > + logger.info("CFv4: zone=3D%s id=3D%s", zone=5Fname, zone=5Fid) > + > + # --- get record --- > + rec=5Furl =3D > f"https://api.cloudflare.com/client/v4/zones/{zone=5Fid}/dns=5Frecord= s?type=3DA&name=3D{self.hostname}" > + try: > + req =3D urllib.request.Request(rec=5Furl, headers=3Dheaders, > method=3D"GET") > + with urllib.request.urlopen(req, timeout=3D20) as r: > + rec=5Fdata =3D json.loads(r.read().decode()) > + except Exception as e: > + raise DDNSUpdateError(f"Failed to query Cloudflare DNS > records API: {e}") > + > + if not rec=5Fdata.get("success"): > + errs =3D rec=5Fdata.get("errors") or [] > + if any("Authentication error" in (e.get("message", "") or > "") for e in errs): > + raise DDNSAuthenticationError("Invalid API Token.") > + raise DDNSUpdateError(f"Cloudflare API error finding > record: {errs}") Same as above, hardcoded defaults like the timeout. Spaghetti code. > + > + results =3D rec=5Fdata.get("result") or [] > + if not results: > + raise DDNSRequestError(f"No A record found for > '{self.hostname}' in zone '{zone=5Fname}'.") > + > + record=5Fid =3D results[0]["id"] > + stored=5Fip =3D results[0]["content"] > + logger.info("CFv4: record=5Fid=3D%s stored=5Fip=3D%s", record=5Fid,= stored=5Fip) > + > + # --- compare IPs --- > + current=5Fip =3D self.get=5Faddress("ipv4") > + logger.info("CFv4: current=5Fip=3D%s vs stored=5Fip=3D%s", > current=5Fip, stored=5Fip) > + if current=5Fip =3D=3D stored=5Fip: > + logger.info("CFv4: no update needed") > + return Why is this done using the API at all? We have functionality to use DNS= for this which creates a lot less load than performing several API req= uests every couple of minutes. > + > + # --- update --- > + upd=5Furl =3D > f"https://api.cloudflare.com/client/v4/zones/{zone=5Fid}/dns=5Frecord= s/{record=5Fid}" > + payload =3D { > + "type": "A", > + "name": self.hostname, > + "content": current=5Fip, > + "ttl": ttl, > + "proxied": proxied, > + } > + logger.info("CFv4: updating %s -> %s (proxied=3D%s ttl=3D%s)", > self.hostname, current=5Fip, proxied, ttl) > + > + try: > + req =3D urllib.request.Request( > + upd=5Furl, data=3Djson.dumps(payload).encode(), > headers=3Dheaders, method=3D"PUT" > + ) > + with urllib.request.urlopen(req, timeout=3D20) as r: > + upd =3D json.loads(r.read().decode()) > + except Exception as e: > + raise DDNSUpdateError(f"Failed to send update request to > Cloudflare: {e}=E2=80=9D) >=C2=A0 Once again the same custom request logic. > + if not upd.get("success"): > + raise DDNSUpdateError(f"Cloudflare API error on update: > {upd.get('errors')}") > + > + logger.info("CFv4: update ok for %s -> %s", self.hostname, current=5F= ip) > + return > + >=C2=A0 > class DDNSProtocolDynDNS2(object): > """ >=C2=A0 I would really like to have support for Cloudflare in DDNS, but this is= sadly not the way. Please study my comments thoroughly and then lets come up with a plan t= ogether how to implement this properly. Best, -Michael =C2=A0 =C2=A0 ------=_=-_OpenGroupware_org_NGMime-2177527-1756462842.182486-1------ Content-Type: text/html; charset=utf-8 Content-Transfer-Encoding: quoted-printable Content-Length: 10864

Hi everybody,

I don't want to hijack this mail thread he= re, but just wanted to let you know that I've also submitted a patch a = few months ago. See https://lists.ipfire.org/ddns/19993045-2374-4537-96= a2-5e2b1900a750@liftingcoder.com/T/#u

Unfortunately I never recei= ved any feedback, so I assumed Cloudflare support wasn't desired. Again= , sorry for hijacking this email chain here, just wanted to quickly dro= p it here, since it matches the topic.

Have a nice day,

Ber= nhard

Am Freitag, August 29, 2025 11:50 CEST, schrieb Michael Tr= emer <michael.tremer@ipfire.org>:

 

Hello Chris,

You don=E2=80=99t need to submit any patches more = than once. We will get back to you as soon as there is time.

So = let=E2=80=99s get into this...

> On 29 Aug 2025, at 09:16, Ch= ris Anton <chris.v.anton@gmail.com> wrote:

> = From: faithinchaos21 <45313722+faithinchaos21@users.noreply.github.c= om>

I assume that you are faithinchaos21 and this code did no= t come from someone else?

> Date: Wed, 27 Aug 2025 01:22:46 -= 0500
> Subject: [PATCH] ddns: add Cloudflare (v4) provider using = API token
> MIME-Version: 1.0
> Content-Type: text/plain; c= harset=3DUTF-8
> Content-Transfer-Encoding: 8bit
> This adds a provider =E2=80=9Ccloudflare.com-v4=E2=80=9D that upd= ates an A record
> via Cloudflare=E2=80=99s v4 API using a Bearer= token. The token is accepted
> from either =E2=80=98token=E2=80=99= or legacy =E2=80=98password=E2=80=99 for UI compatibility.
>&nbs= p;
> Tested on IPFire 2.29 / Core 196:
> - no-op if A alrea= dy matches WAN IP
> - successful update when WAN IP changes
&g= t; - logs include CFv4 breadcrumbs for troubleshooting

To make i= t short, this patch is sadly very far away from what is acceptable.
=
Before we get into the actual implementation, there are many design= issues here that simply cannot be accepted into DDNS:

* This pa= tch only adds support for IPv4. As far as I am aware, Cloudflare is cap= able of IPv6, too. Why would this be limited to IPv4 only?

* You= are not using the scaffolding and tools that DDNS is providing. A lot = of code is being written with plain Python and throwing away all the fe= atures that DDNS provides (catching Exceptions, proxy support and many,= many more=E2=80=A6)

* This all looks very AI-generated to me an= d is implemented on a green field. You are even importing all the modul= es that you need and ignore everything else from DDNS. Why not use this= snippet as a standalone script then? There was no consideration for wh= at code existed already.

> Signed-off-by: Chris Anton <chr= is.v.anton@gmail.com>
> ---
> 563f089d0820bd61ad4aecac24= 8d5cc1f2adfc81
> src/ddns/providers.py | 121 ++++++++++++++++++++= ++++++++++++++++++++++
> 1 file changed, 121 insertions(+)
>= ; 
> diff --git a/src/ddns/providers.py b/src/ddns/providers= .py
> index 59f9665..df0f3a9 100644
> --- a/src/ddns/provid= ers.py
> +++ b/src/ddns/providers.py
> @@ -341,6 +341,127 @= @ def have=5Faddress(self, proto):

> return False
> +class DDNSProviderCloudflareV4(DDNSProvider):
= > + """
> + Cloudflare v4 API using a Bearer Token.
> + = Put the API Token in the 'token' OR 'password' field of the DDNS entry.=
> + Optional in ddns.conf:
> + proxied =3D false|true (def= ault false; keep false for WireGuard)
> + ttl =3D 1|60|120... (de= fault 1 =3D 'automatic')
> + """
> + handle =3D "cloudflare= .com-v4"
> + name =3D "Cloudflare (v4)"
> + website =3D "ht= tps://www.cloudflare.com/"
> + protocols =3D ("ipv4",)
> + = supports=5Ftoken=5Fauth =3D True
> + holdoff=5Ffailure=5Fdays =3D= 0
> +
> + def =5Fbool(self, key, default=3DFalse):
>= + v =3D str(self.get(key, default)).strip().lower()
> + return v= in ("1", "true", "yes", "on")
> +

This is a good example = of a function which totally goes against the coding style of the rest o= f the code base. It uses str(), chains a lot of functions to gather to = make the code look shorter, but very difficult to read.

> + d= ef update(self):
> + import json, urllib.request, urllib.error
Just no. We don=E2=80=99t import anything further down the line. D= DNS provides a toolkit of what to use and you should stay within it. If= some functionally is missing, DDNS=E2=80=99 functionality should be ex= tended so that other providers can re-use the same well-maintained and = tested code base.

> +
> + tok =3D self.get("token") or = self.get("password")
> + if not tok:
> + raise DDNSConfigur= ationError("API Token (password/token)
> is missing.")
> +<= br>> + proxied =3D self.=5Fbool("proxied", False)
> + try:
= > + ttl =3D int(self.get("ttl", 1))
> + except Exception:
&= gt; + ttl =3D 1

A TTL of one second is never a good idea.
> +
> + headers =3D {
> + "Authorization": "Bearer {0}"= .format(tok),
> + "Content-Type": "application/json",
> + "= User-Agent": "IPFireDDNSUpdater/CFv4",
> + }

Since you are= not using the internal request handler, you are creating your own head= ers and a completely nonsensical user agent. Why?

> +
>= + # --- find zone ---
> + parts =3D self.hostname.split(".")
= > + if len(parts) < 2:
> + raise DDNSRequestError("Hostname= '{0}' is not a valid
> domain.".format(self.hostname))
> +=
> + zone=5Fid =3D None
> + zone=5Fname =3D None
> + = for i in range(len(parts) - 1):
> + candidate =3D ".".join(parts[= i:])
> + url =3D
> f"https://api.cloudflare.com/client/v4/z= ones?name=3D{candidate}"
> + try:
> + req =3D urllib.reques= t.Request(url, headers=3Dheaders,
> method=3D"GET")
> + wit= h urllib.request.urlopen(req, timeout=3D20) as r:
> + data =3D js= on.loads(r.read().decode())
> + except Exception as e:
> + = raise DDNSUpdateError(f"Failed to query Cloudflare
> zones API: {= e}")
> +
> + if data.get("success") and data.get("result"):=
> + zone=5Fid =3D data["result"][0]["id"]
> + zone=5Fname = =3D candidate
> + break

It is not acceptable to build anyt= hing custom that sends a request. You are removing all other kinds of f= eatures that I have mentioned above.

To just =E2=80=9Cguess=E2=80= =9D the name of the zone is something that I would not consider good st= yle.

> +
> + if not zone=5Fid:
> + raise DDNSRequ= estError(f"Could not find a Cloudflare Zone
> for '{self.hostname= }'.")
> +
> + logger.info("CFv4: zone=3D%s id=3D%s", zone=5F= name, zone=5Fid)
> +
> + # --- get record ---
> + rec= =5Furl =3D
> f"https://api.cloudflare.com/client/v4/zones/{zone=5F= id}/dns=5Frecords?type=3DA&name=3D{self.hostname}"
> + try:> + req =3D urllib.request.Request(rec=5Furl, headers=3Dheaders,> method=3D"GET")
> + with urllib.request.urlopen(req, timeo= ut=3D20) as r:
> + rec=5Fdata =3D json.loads(r.read().decode())> + except Exception as e:
> + raise DDNSUpdateError(f"Faile= d to query Cloudflare DNS
> records API: {e}")
> +
> = + if not rec=5Fdata.get("success"):
> + errs =3D rec=5Fdata.get("= errors") or []
> + if any("Authentication error" in (e.get("messa= ge", "") or
> "") for e in errs):
> + raise DDNSAuthenticat= ionError("Invalid API Token.")
> + raise DDNSUpdateError(f"Cloudf= lare API error finding
> record: {errs}")

Same as above, h= ardcoded defaults like the timeout. Spaghetti code.

> +
&g= t; + results =3D rec=5Fdata.get("result") or []
> + if not result= s:
> + raise DDNSRequestError(f"No A record found for
> '{s= elf.hostname}' in zone '{zone=5Fname}'.")
> +
> + record=5F= id =3D results[0]["id"]
> + stored=5Fip =3D results[0]["content"]=
> + logger.info("CFv4: record=5Fid=3D%s stored=5Fip=3D%s", recor= d=5Fid, stored=5Fip)
> +
> + # --- compare IPs ---
> = + current=5Fip =3D self.get=5Faddress("ipv4")
> + logger.info("CF= v4: current=5Fip=3D%s vs stored=5Fip=3D%s",
> current=5Fip, store= d=5Fip)
> + if current=5Fip =3D=3D stored=5Fip:
> + logger.= info("CFv4: no update needed")
> + return

Why is this done= using the API at all? We have functionality to use DNS for this which = creates a lot less load than performing several API requests every coup= le of minutes.

> +
> + # --- update ---
> + upd=5F= url =3D
> f"https://api.cloudflare.com/client/v4/zones/{zone=5Fid= }/dns=5Frecords/{record=5Fid}"
> + payload =3D {
> + "type"= : "A",
> + "name": self.hostname,
> + "content": current=5F= ip,
> + "ttl": ttl,
> + "proxied": proxied,
> + }
= > + logger.info("CFv4: updating %s -> %s (proxied=3D%s ttl=3D%s)"= ,
> self.hostname, current=5Fip, proxied, ttl)
> +
> = + try:
> + req =3D urllib.request.Request(
> + upd=5Furl, d= ata=3Djson.dumps(payload).encode(),
> headers=3Dheaders, method=3D= "PUT"
> + )
> + with urllib.request.urlopen(req, timeout=3D= 20) as r:
> + upd =3D json.loads(r.read().decode())
> + exc= ept Exception as e:
> + raise DDNSUpdateError(f"Failed to send up= date request to
> Cloudflare: {e}=E2=80=9D)


= Once again the same custom request logic.

> + if not upd.get(= "success"):
> + raise DDNSUpdateError(f"Cloudflare API error on u= pdate:
> {upd.get('errors')}")
> +
> + logger.info("C= Fv4: update ok for %s -> %s", self.hostname, current=5Fip)
> += return
> +

> class DDNSProtocolDynDNS2(objec= t):
> """


I would really like to have suppor= t for Cloudflare in DDNS, but this is sadly not the way.

Please = study my comments thoroughly and then lets come up with a plan together= how to implement this properly.

Best,
-Michael


&n= bsp;




 

------=_=-_OpenGroupware_org_NGMime-2177527-1756462842.182486-1--------