Skip to content

Commit a5e5a75

Browse files
committed
fix edge of provider record changing without client changing.
1 parent ced3b71 commit a5e5a75

File tree

3 files changed

+133
-39
lines changed

3 files changed

+133
-39
lines changed

src/pyddns/config.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,4 @@ def get(self, section: str, option: str) -> str:
7979
if self.config.has_option(section, option):
8080
return self.config.get(section, option)
8181

82-
else:
83-
84-
raise KeyError(f"Option '{option}' not found in section '{section}'.")
82+
raise KeyError(f"Option '{option}' not found in section '{section}'.")

src/pyddns/services/cloudflare_service.py

Lines changed: 93 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
the current IP address, leveraging the Cloudflare API.
77
"""
88
import logging
9+
import socket
910
from typing import Callable, Optional, Any, Tuple
1011
from datetime import datetime
1112

@@ -90,6 +91,7 @@ def _obtain_record(self, record_name: str) -> Optional[Tuple[str, datetime, Opti
9091
record.name or 'domain': record for record in result
9192
}
9293

94+
9395
domain_record = records.get(record_name)
9496

9597
if domain_record is None:
@@ -103,46 +105,120 @@ def _obtain_record(self, record_name: str) -> Optional[Tuple[str, datetime, Opti
103105
return self.storage.retrieve_record(record_name)
104106

105107
@cf_error_handler
106-
def update_dns(self, ip_address: str, record_name: Optional[str] = None) -> None:
108+
def check_cloudflare_ip(self, record_name: str) -> Optional[str]:
109+
"""
110+
Checks the A record IP address in cloudflare utilizing the API
107111
108-
"""
109-
Updates IP address for specified record
112+
This is recomended if you have a proxied connection.
113+
"""
110114

111-
Automatically infers record_name if it is defined in the ddns.ini file.
115+
if record_name is None:
116+
logging.error("CloudFlare DNS: In _obtain_record: Record_name not provided.")
117+
raise ValueError("CloudFlare DNS: Record name cannot be None")
118+
119+
response = self.storage.retrieve_record(record_name)
120+
121+
if not response:
122+
response = self._obtain_record(record_name)
123+
124+
record_id = response[2]
125+
api_res = self.cf_client.dns.records.get(zone_id=self.zone_id, dns_record_id=record_id)
126+
127+
if not api_res:
128+
logging.error("Cloudflare DNS: API call failed.")
129+
return None
130+
131+
return api_res.content
132+
133+
def cloudflare_dns_lookup(self, record_name: str) -> str:
134+
"""
135+
Performs a DNS lookup for the record name for Cloudflare.
136+
137+
This will not return the correct IP if you have a proxied connection.
112138
"""
113-
record_name = record_name or self.config.get(self.service_name,'record_name')
139+
logging.debug("CloudFlare DNS: Performing DNS lookup for %s", record_name)
140+
return socket.gethostbyname(record_name)
141+
142+
def check_and_update_dns(self, record_name: Optional[str] = None) -> None:
143+
"""
144+
Compares the actual Cloudflare A record with the local database record.
145+
If they are different, updates Cloudflare with the current IP address.
146+
"""
147+
record_name = record_name or self.config.get(self.service_name, 'record_name')
114148

115149
if not record_name:
116150
raise ValueError("CloudFlare DNS: Record name cannot be None")
117151

118-
logging.debug("CloudFlare DNS: Preparing to uppdate %s with IP:", record_name)
152+
logging.debug("CloudFlare DNS: Checking current IP for %s", record_name)
153+
cloudflare_ip = self.check_cloudflare_ip(record_name)
154+
logging.debug("CloudFlare DNS: Current IP for %s is %s", record_name, cloudflare_ip)
119155

120156
record: Optional[Tuple[str, datetime, str]] = self._obtain_record(record_name)
121157

122158
if not record:
123159
logging.error("CloudFlare DNS: No record found for %s.", record_name)
124160
return
125161

126-
current_ip: str = record[0]
127-
record_id: str = record[2]
162+
current_ip = self.get_ipv4()
163+
db_ip = record[0]
164+
logging.debug("CloudFlare DNS: IP from database is %s", db_ip)
128165

129-
if current_ip == ip_address:
166+
if current_ip != db_ip:
130167
logging.info(
131-
"CloudFlare DNS: No update needed for %s - IP is already %s.",
132-
record_name, ip_address
168+
"CloudFlare DNS: Local IP has changed from %s to %s, updating Cloudflare.",
169+
db_ip, current_ip
133170
)
171+
self.update_dns(current_ip, record_name)
134172
return
135173

174+
if db_ip != cloudflare_ip:
175+
logging.info(
176+
"CloudFlare DNS: A record has changed from %s to %s for %s, updating Cloudflare",
177+
db_ip, cloudflare_ip, record_name
178+
)
179+
self.update_dns(current_ip, record_name)
180+
return
181+
182+
logging.info(
183+
"CloudFlare DNS: No update needed for %s - IP is still %s",
184+
record_name, db_ip
185+
)
186+
return
187+
188+
@cf_error_handler
189+
def update_dns(self, ip_address: str, record_name: Optional[str] = None) -> None:
190+
"""
191+
Updates IP address for specified record
192+
Automatically infers record_name if it is defined in the ddns.ini file.
193+
"""
194+
record_name = record_name or self.config.get(self.service_name, 'record_name')
195+
196+
if not record_name:
197+
raise ValueError("CloudFlare DNS: Record name cannot be None")
198+
199+
logging.info("CloudFlare DNS: Preparing to update %s with IP: %s", record_name, ip_address)
200+
201+
record: Optional[Tuple[str, datetime, str]] = self._obtain_record(record_name)
202+
203+
if not record:
204+
logging.error("CloudFlare DNS: No record found for %s.", record_name)
205+
return
206+
207+
record_id: str = record[2]
208+
209+
comment = f"Updated on {datetime.now()} by py_ddns."
136210
response = self.cf_client.dns.records.update(
137-
content = ip_address,
138-
zone_id = self.zone_id,
139-
name = record_name or NOT_GIVEN,
140-
dns_record_id = record_id,
141-
comment= f"Updated on {datetime.now()} by py_ddns.",
211+
content=ip_address,
212+
zone_id=self.zone_id,
213+
type="A",
214+
proxied=True,
215+
name=record_name or NOT_GIVEN,
216+
dns_record_id=record_id,
217+
comment=comment,
142218
)
143219

144220
if response is None:
145-
logging.error("CloudFlare DNS: No response recienved from cloudflare.")
221+
logging.error("CloudFlare DNS: No response received from Cloudflare.")
146222
return
147223

148224
self.storage.update_ip(self.service_name, record_name, response.content)

src/pyddns/services/duckdns_service.py

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ def _parse_domain_name(self, record_name: str) -> str:
7474

7575
if record_name.endswith('.duckdns.org'):
7676
return record_name[:-len('.duckdns.org')]
77-
else:
78-
return record_name
77+
78+
return record_name
7979

8080
def _parse_api_response(self, response: str) -> Tuple[str, Optional[str], Optional[str], str]:
8181
"""
@@ -96,58 +96,79 @@ def _parse_api_response(self, response: str) -> Tuple[str, Optional[str], Option
9696

9797
def check_duckdns_ip(self, record_name: str) -> str:
9898
"""
99-
Checks the current IP address for DuckDNS
99+
Performs a DNS lookup for record name for DuckDNS
100100
"""
101101
logging.debug("DuckDNS: Performing DNS lookup for %s", record_name)
102102
return socket.gethostbyname(f"{self._parse_domain_name(record_name)}.duckdns.org")
103103

104-
def update_dns(self, ip_address: str, record_name: Optional[str] = None) -> None:
104+
def check_and_update_dns(self, record_name: Optional[str] = None) -> None:
105105
"""
106-
Updates the IP address for DuckDNS, and then updates the IP adress in the database.
106+
Compares the actual DuckDNS A record with the local database record.
107+
If they are different, updates DuckDNS with the current IP address.
107108
"""
108-
109109
record_name = record_name or self.config.get(self.service_name, 'domains')
110110
record_name = self._parse_domain_name(record_name)
111111

112112
if not record_name:
113113
raise ValueError("DuckDNS: Record name cannot be None")
114114

115-
logging.debug(
116-
"DuckDNS: Preparing to update DuckDNS for %s.duckdns.org with IP: %s",
117-
record_name, ip_address
118-
)
115+
logging.debug("DuckDNS: Checking current IP for %s", record_name)
116+
duck_ip = self.check_duckdns_ip(record_name)
117+
logging.debug("DuckDNS: Current IP for %s is %s", record_name, duck_ip)
118+
119119
record = self._obtain_record(record_name)
120120

121121
if not record:
122122
logging.error("DuckDNS: No record found for %s.", record_name)
123123
return
124124

125-
current_ip = record[0]
125+
current_ip = self.get_ipv4()
126+
db_ip = record[0]
127+
logging.debug("DuckDNS: IP from database is %s", db_ip)
128+
129+
if current_ip != db_ip:
130+
logging.info("IP has changed from %s to %s, updating DuckDNS.", current_ip, db_ip)
131+
self.update_dns(current_ip, record_name)
132+
return
126133

127-
if current_ip == ip_address:
134+
if db_ip != duck_ip:
128135
logging.info(
129-
"DuckDNS: No update needed for %s.duckdns.org - Current IP is already %s.",
130-
record_name, ip_address
136+
"DuckDNS: A record has changed from %s to %s for %s, updating DuckDNS",
137+
db_ip, duck_ip, record_name
131138
)
139+
self.update_dns(current_ip, record_name)
132140
return
133141

142+
logging.info("DuckDNS: No update needed for %s - IP is still %s", record_name, db_ip)
143+
144+
def update_dns(self, ip_address: str, record_name: Optional[str] = None) -> None:
145+
"""
146+
Updates the IP address for DuckDNS in the database.
147+
This method assumes that the IP address has already been verified to be different.
148+
"""
149+
record_name = record_name or self.config.get(self.service_name, 'domains')
150+
record_name = self._parse_domain_name(record_name)
151+
152+
if not record_name:
153+
raise ValueError("DuckDNS: Record name cannot be None")
154+
134155
payload = {
135156
"domains": f"{record_name}.duckdns.org",
136157
"token": self.token,
137158
"ip": ip_address,
138159
"verbose": 'true'
139160
}
140-
try:
141161

162+
try:
142163
logging.debug("DuckDNS: Making API call to %s with parameters %s", self.url, payload)
143-
response = requests.get(self.url, params = payload, timeout=10)
164+
response = requests.get(self.url, params=payload, timeout=10)
144165
response.raise_for_status()
145166

146167
ipv4 = self._parse_api_response(response.text)[1]
147-
logging.debug("DuckDNS: Recieved %s from DuckDNS API.", response.text)
168+
logging.debug("DuckDNS: Received %s from DuckDNS API.", response.text)
148169

149170
self.storage.update_ip(self.service_name, self._parse_domain_name(record_name), ipv4)
150-
logging.info("DuckDNS: Updated %s to %s.", ip_address, ipv4)
171+
logging.info("DuckDNS: Updated %s to %s.", ip_address, ipv4)
151172

152173
except requests.HTTPError as err:
153174
logging.error("DuckDNS: API Call %s", err)
@@ -158,4 +179,3 @@ def update_dns(self, ip_address: str, record_name: Optional[str] = None) -> None
158179
except Exception as err:
159180
logging.error("DuckDNS: API Call %s", err)
160181
raise
161-

0 commit comments

Comments
 (0)