Add GET parameter to allow useres to change expiry TTL

This commit is contained in:
2026-02-07 22:34:50 +01:00
parent 5b51a8a153
commit eafe106bf1
4 changed files with 119 additions and 18 deletions

View File

@@ -110,6 +110,9 @@ path = "/var/lib/ddns-service/ddns.db" # required for sqlite
[defaults] [defaults]
# dns_ttl = 60 # default: 60 # dns_ttl = 60 # default: 60
# expiry_ttl = 3600 # default: 3600 # expiry_ttl = 3600 # default: 3600
# expiry_ttl_min = # optional, min value via HTTP
# expiry_ttl_max = # optional, max value via HTTP
# expiry_ttl_allow_zero = true # default: true, allow 0 via HTTP
[email] [email]
# enabled = false # default: false # enabled = false # default: false
@@ -182,6 +185,7 @@ ipv6 = ["myip6", "ipv6", "ip6"]
username = ["username", "user"] username = ["username", "user"]
password = ["password", "pass", "token"] password = ["password", "pass", "token"]
notify_change = ["notify_change"] notify_change = ["notify_change"]
expiry_ttl = ["expiry_ttl"]
[[endpoints]] [[endpoints]]
path = "/nic/update" path = "/nic/update"
@@ -192,6 +196,7 @@ ipv6 = ["myip6"]
username = ["username"] username = ["username"]
password = ["password"] password = ["password"]
notify_change = [] notify_change = []
expiry_ttl = []
``` ```
**Default accepted parameter names** (first match wins): **Default accepted parameter names** (first match wins):
@@ -203,6 +208,7 @@ notify_change = []
| username | username, user | | username | username, user |
| password | password, pass, token | | password | password, pass, token |
| notify_change | notify_change | | notify_change | notify_change |
| expiry_ttl | expiry_ttl |
## CLI Usage ## CLI Usage
@@ -295,7 +301,7 @@ kill -HUP $(pidof ddns-service)
### Request ### Request
``` ```
GET /update?hostname=mypc.dyn.example.com[&myip=1.2.3.4][&myip6=2001:db8::1][&notify_change=1] GET /update?hostname=mypc.dyn.example.com[&myip=1.2.3.4][&myip6=2001:db8::1][&notify_change=1][&expiry_ttl=7200]
Authorization: Basic base64(username:password) Authorization: Basic base64(username:password)
``` ```
@@ -306,6 +312,8 @@ GET /update?hostname=mypc.dyn.example.com&username=myuser&password=secret
Set `notify_change=1` to receive an email notification when the IP address changes. Requires email to be enabled and a change notification template configured. Set `notify_change=1` to receive an email notification when the IP address changes. Requires email to be enabled and a change notification template configured.
Set `expiry_ttl=N` to change the hostname's expiry TTL (in seconds). Can be sent alone without IP parameters.
### IP Detection ### IP Detection
- If `myip` and/or `myip6` provided: use those values - If `myip` and/or `myip6` provided: use those values
@@ -325,7 +333,7 @@ Set `notify_change=1` to receive an email notification when the IP address chang
**JSON (with `Accept: application/json`):** **JSON (with `Accept: application/json`):**
```json ```json
{"status": "good", "ipv4": "1.2.3.4", "ipv6": "2001:db8::1"} {"status": "good", "ipv4": "1.2.3.4", "ipv6": "2001:db8::1", "expiry_ttl": 3600}
``` ```
## Client Examples ## Client Examples
@@ -346,6 +354,11 @@ With change notification:
curl -u "username:password" "https://ddns.example.com/update?hostname=mypc.dyn.example.com&notify_change=1" curl -u "username:password" "https://ddns.example.com/update?hostname=mypc.dyn.example.com&notify_change=1"
``` ```
Change expiry TTL:
```bash
curl -u "username:password" "https://ddns.example.com/update?hostname=mypc.dyn.example.com&expiry_ttl=7200"
```
### wget ### wget
```bash ```bash

View File

@@ -42,6 +42,9 @@ path = "/var/lib/ddns-service/ddns.db" # required for sqlite
[defaults] [defaults]
# dns_ttl = 60 # default, DNS record TTL in seconds # dns_ttl = 60 # default, DNS record TTL in seconds
# expiry_ttl = 3600 # default, 0 to disable expiration # expiry_ttl = 3600 # default, 0 to disable expiration
# expiry_ttl_min = # optional, min value allowed via HTTP
# expiry_ttl_max = # optional, max value allowed via HTTP
# expiry_ttl_allow_zero = true # default, allow 0 (never expire) via HTTP
[email] [email]
# enabled = false # default # enabled = false # default
@@ -70,6 +73,7 @@ from_address = "ddns@example.com" # required if email.enabled
# username: username, user # username: username, user
# password: password, pass, token # password: password, pass, token
# notify_change: notify_change # notify_change: notify_change
# expiry_ttl: expiry_ttl
# #
# Multiple endpoints can be defined with custom parameter names # Multiple endpoints can be defined with custom parameter names
@@ -82,6 +86,7 @@ from_address = "ddns@example.com" # required if email.enabled
# username = ["username", "user"] # username = ["username", "user"]
# password = ["password", "pass", "token"] # password = ["password", "pass", "token"]
# notify_change = ["notify_change"] # notify_change = ["notify_change"]
# expiry_ttl = ["expiry_ttl"]
# [[endpoints]] # [[endpoints]]
# path = "/nic/update" # path = "/nic/update"
@@ -92,3 +97,4 @@ from_address = "ddns@example.com" # required if email.enabled
# username = ["username"] # username = ["username"]
# password = ["password"] # password = ["password"]
# notify_change = [] # notify_change = []
# expiry_ttl = []

View File

@@ -22,6 +22,7 @@ DEFAULT_ENDPOINT_PARAMS = {
"username": ["username", "user"], "username": ["username", "user"],
"password": ["password", "pass", "token"], "password": ["password", "pass", "token"],
"notify_change": ["notify_change"], "notify_change": ["notify_change"],
"expiry_ttl": ["expiry_ttl"],
} }
VALID_PARAM_KEYS = frozenset(DEFAULT_ENDPOINT_PARAMS.keys()) VALID_PARAM_KEYS = frozenset(DEFAULT_ENDPOINT_PARAMS.keys())
@@ -178,6 +179,9 @@ def load_config(config_path):
cfg.setdefault("defaults", {}) cfg.setdefault("defaults", {})
cfg["defaults"].setdefault("dns_ttl", 60) cfg["defaults"].setdefault("dns_ttl", 60)
cfg["defaults"].setdefault("expiry_ttl", 3600) cfg["defaults"].setdefault("expiry_ttl", 3600)
cfg["defaults"].setdefault("expiry_ttl_min", None)
cfg["defaults"].setdefault("expiry_ttl_max", None)
cfg["defaults"].setdefault("expiry_ttl_allow_zero", True)
cfg.setdefault("email", {}) cfg.setdefault("email", {})
cfg["email"].setdefault("enabled", False) cfg["email"].setdefault("enabled", False)

View File

@@ -377,10 +377,77 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
else: else:
ipv6 = ip ipv6 = ip
# Process expiry_ttl parameter
expiry_ttl_param = extract_param(
params, endpoint["params"]["expiry_ttl"])
expiry_ttl = None
if expiry_ttl_param:
try:
expiry_ttl = int(expiry_ttl_param)
if expiry_ttl < 0:
raise ValueError
except ValueError:
raise DDNSClientError(
"Invalid expiry_ttl",
400,
STATUS_NOHOST,
client=client_ip,
username=username,
hostname=hostname.hostname,
zone=hostname.zone,
expiry_ttl=expiry_ttl_param
)
# Validate bounds
defaults = self.app.config["defaults"]
allow_zero = defaults.get("expiry_ttl_allow_zero", True)
ttl_min = defaults.get("expiry_ttl_min")
ttl_max = defaults.get("expiry_ttl_max")
if expiry_ttl == 0:
if not allow_zero:
raise DDNSClientError(
"Zero expiry_ttl not allowed",
400,
STATUS_NOHOST,
client=client_ip,
username=username,
hostname=hostname.hostname,
zone=hostname.zone,
expiry_ttl=expiry_ttl
)
else:
if ttl_min is not None and expiry_ttl < ttl_min:
raise DDNSClientError(
"expiry_ttl below minimum",
400,
STATUS_NOHOST,
client=client_ip,
username=username,
hostname=hostname.hostname,
zone=hostname.zone,
expiry_ttl=expiry_ttl,
min=ttl_min
)
if ttl_max is not None and expiry_ttl > ttl_max:
raise DDNSClientError(
"expiry_ttl above maximum",
400,
STATUS_NOHOST,
client=client_ip,
username=username,
hostname=hostname.hostname,
zone=hostname.zone,
expiry_ttl=expiry_ttl,
max=ttl_max
)
# Process notify_change parameter # Process notify_change parameter
notify_change = extract_param(params, endpoint["params"]["notify_change"]) notify_change = extract_param(
notify_change = notify_change.lower() in ["1", "y", "yes", "on", "true"] \ params, endpoint["params"]["notify_change"])
if notify_change else False notify_change = (notify_change.lower() in
["1", "y", "yes", "on", "true"]
if notify_change else False)
# Good rate limit check # Good rate limit check
if self.app.good_limiter: if self.app.good_limiter:
@@ -406,7 +473,8 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
hostname, hostname,
ipv4, ipv4,
ipv6, ipv6,
notify_change notify_change,
expiry_ttl
) )
def _authenticate(self, client_ip, username, password): def _authenticate(self, client_ip, username, password):
@@ -466,7 +534,8 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
except Exception as e: except Exception as e:
logging.error(f"DNS rollback failed ({record_type}): {e}") logging.error(f"DNS rollback failed ({record_type}): {e}")
def _process_ip_update(self, client_ip, user, hostname, ipv4, ipv6, notify_change): def _process_ip_update(self, client_ip, user, hostname, ipv4, ipv6,
notify_change, expiry_ttl):
"""Process IP update for hostname.""" """Process IP update for hostname."""
now = now_utc() now = now_utc()
@@ -475,6 +544,12 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
ipv4_changed = False ipv4_changed = False
ipv6_changed = False ipv6_changed = False
# Apply expiry_ttl if provided
expiry_ttl_changed = False
if expiry_ttl is not None and expiry_ttl != hostname.expiry_ttl:
hostname.expiry_ttl = expiry_ttl
expiry_ttl_changed = True
if ipv4: if ipv4:
hostname.last_ipv4_update = now hostname.last_ipv4_update = now
if ipv4 != hostname.last_ipv4: if ipv4 != hostname.last_ipv4:
@@ -546,28 +621,30 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
zone=hostname.zone zone=hostname.zone
) )
changed_addrs = "" if not ipv4_changed and not ipv6_changed and not expiry_ttl_changed:
if ipv4_changed:
changed_addrs += f" ipv4={ipv4}"
if ipv6_changed:
changed_addrs += f" ipv6={ipv6}"
if not ipv4_changed and not ipv6_changed:
logging.info( logging.info(
f"No change: client={client_ip} hostname={hostname.hostname} " f"No change: client={client_ip} hostname={hostname.hostname} "
f"zone={hostname.zone}{changed_addrs} notify_change={str(notify_change).lower()}" f"zone={hostname.zone} notify_change={str(notify_change).lower()}"
) )
self.respond( self.respond(
200, 200,
STATUS_NOCHG, STATUS_NOCHG,
ipv4=hostname.last_ipv4, ipv4=hostname.last_ipv4,
ipv6=hostname.last_ipv6 ipv6=hostname.last_ipv6,
expiry_ttl=hostname.expiry_ttl
) )
return return
changed_info = ""
if ipv4_changed:
changed_info += f" ipv4={ipv4}"
if ipv6_changed:
changed_info += f" ipv6={ipv6}"
if expiry_ttl_changed:
changed_info += f" expiry_ttl={hostname.expiry_ttl}"
logging.info( logging.info(
f"Updated: client={client_ip} hostname={hostname.hostname} " f"Updated: client={client_ip} hostname={hostname.hostname} "
f"zone={hostname.zone}{changed_addrs} notify_change={str(notify_change).lower()}" f"zone={hostname.zone}{changed_info} notify_change={str(notify_change).lower()}"
) )
if notify_change: if notify_change:
@@ -585,7 +662,8 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
200, 200,
STATUS_GOOD, STATUS_GOOD,
ipv4=hostname.last_ipv4, ipv4=hostname.last_ipv4,
ipv6=hostname.last_ipv6 ipv6=hostname.last_ipv6,
expiry_ttl=hostname.expiry_ttl
) )