#!/bin/sh
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# DDNS update script for the netcup DNS API
# https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON
#
# For use with the OpenWrt ddns-scripts package.
# Sourced by dynamic_dns_updater.sh — do NOT call directly.
#
# Configuration mapping (set in /etc/config/ddns):
#   username  = netcup customer number
#   password  = netcup API password
#   param_enc = netcup API key (generated in the CCP)
#   domain    = fully qualified subdomain to update  (e.g. home.example.de)
#   param_opt = (optional) root/zone domain override (e.g. example.de)
#               When omitted the root domain is derived by stripping the
#               leftmost label from 'domain'. This only works correctly for
#               a single subdomain level (e.g. "home.example.de").
#               param_opt MUST be set explicitly in two cases:
#               1. Deep subdomains: domain=test.internal.example.org
#                  → param_opt=example.org  (hostname becomes "test.internal")
#               2. ccSLD apex domains: domain=example.co.nz
#                  → param_opt=example.co.nz  (hostname becomes "@")
#                  Note: a subdomain of a ccSLD works without param_opt:
#                  domain=home.example.co.nz → zone "example.co.nz" is
#                  derived correctly by stripping the leftmost label.

. /usr/share/libubox/jshn.sh

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

__NETCUP_ENDPOINT="https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON"

# ---------------------------------------------------------------------------
# Validate required configuration variables
# ---------------------------------------------------------------------------

[ -z "$username" ]      && write_log 14 "netcup DDNS: 'username' (customer number) not set"
[ -z "$password" ]      && write_log 14 "netcup DDNS: 'password' (API password) not set"
[ -z "$param_enc" ]     && write_log 14 "netcup DDNS: 'param_enc' (API key) not set"
[ -z "$domain" ]        && write_log 14 "netcup DDNS: 'domain' (subdomain to update) not set"
[ -z "$__IP" ]          && write_log 14 "netcup DDNS: __IP (current IP) not set by the framework"
[ -z "$REGISTERED_IP" ] && write_log 14 "netcup DDNS: REGISTERED_IP not set by the framework"

# Require an HTTPS-capable client — the netcup endpoint is HTTPS only.
[ -z "$CURL_SSL" ] && [ -z "$WGET_SSL" ] && \
	write_log 14 "netcup DDNS: neither curl nor wget with HTTPS support is available"

# ---------------------------------------------------------------------------
# Derive DNS zone and record hostname from configuration
# ---------------------------------------------------------------------------

# Use param_opt as an explicit zone override; otherwise strip the leftmost
# DNS label to obtain the root domain (e.g. "home.example.de" → "example.de").
# This automatic derivation only works for a single subdomain level — set
# param_opt explicitly for deep subdomains or ccSLD apex domains (see header).
if [ -n "$param_opt" ]; then
	__ZONE="$param_opt"
else
	__ZONE="${domain#*.}"
	# If the result contains no dot the input was already a root domain.
	case "$__ZONE" in
		*.*) : ;;
		*)   __ZONE="$domain" ;;
	esac
fi

# The record hostname is everything left of the zone name.
# For the zone apex itself use "@".
[ "$domain" = "$__ZONE" ] \
	&& __REC_HOSTNAME="@" \
	|| __REC_HOSTNAME="${domain%.${__ZONE}}"

# DNS record type derived from the ip-version setting.
[ "${use_ipv6:-0}" -ne 0 ] && __RRTYPE="AAAA" || __RRTYPE="A"

write_log 7 "netcup DDNS: zone='$__ZONE' hostname='$__REC_HOSTNAME' type=$__RRTYPE target=$__IP"

# ---------------------------------------------------------------------------
# netcup_post()
#
# POST the JSON object currently held in jshn state to the netcup endpoint.
# The response body is written to the framework's $DATFILE.
# Stderr of the HTTP client goes to $ERRFILE.
#
# Returns the exit code of the HTTP client (0 = transport OK).
# Response status is not validated here; use netcup_check_response().
# ---------------------------------------------------------------------------

netcup_post() {
	local __payload
	__payload="$(json_dump)"
	write_log 7 "netcup DDNS: POST payload: $__payload"

	if [ -n "$CURL_SSL" ]; then
		$CURL -Ss \
			-H "Content-Type: application/json" \
			-d "$__payload" \
			-o "$DATFILE" 2>"$ERRFILE" \
			"$__NETCUP_ENDPOINT"
	else
		# WGET_SSL is always GNU Wget, which supports --header and --post-data.
		$WGET_SSL -q \
			--header="Content-Type: application/json" \
			--post-data="$__payload" \
			-O "$DATFILE" \
			"$__NETCUP_ENDPOINT" 2>"$ERRFILE"
	fi
}

# ---------------------------------------------------------------------------
# netcup_check_response()
#
# Load $DATFILE as JSON and assert the API returned status "success".
# On failure the jshn state is cleared and the script terminates.
#
# $1 — human-readable context string for the error log (e.g. "login")
#
# On success the jshn JSON state remains loaded so the caller can continue
# reading fields. The caller is responsible for calling json_cleanup().
# ---------------------------------------------------------------------------

netcup_check_response() {
	local __context="$1"
	local __status __statuscode __shortmsg

	json_load_file "$DATFILE"
	json_get_var __status     "status"
	json_get_var __statuscode "statuscode"
	json_get_var __shortmsg   "shortmessage"

	if [ "$__status" != "success" ]; then
		json_cleanup
		write_log 3 "netcup DDNS: $__context failed (status='$__status' code=$__statuscode): $__shortmsg"
		return 1
	fi
	return 0
}

# ---------------------------------------------------------------------------
# Main update procedure
# ---------------------------------------------------------------------------

write_log 6 "netcup DDNS: starting update — '$domain' → $__IP"

# --- Step 1: Authenticate and obtain a session ID --------------------------

json_init
json_add_string "action" "login"
json_add_object "param"
	json_add_string "customernumber" "$username"
	json_add_string "apikey"         "$param_enc"
	json_add_string "apipassword"    "$password"
json_close_object

if ! netcup_post; then
	write_log 3 "netcup DDNS: HTTP request failed during login"
	return 1
fi
netcup_check_response "login" || return 1

json_select "responsedata"
json_get_var __SESSION_ID "apisessionid"
json_select ".."
json_cleanup

if [ -z "$__SESSION_ID" ]; then
	write_log 3 "netcup DDNS: login succeeded but no session ID was returned"
	return 1
fi

write_log 6 "netcup DDNS: login successful"

# --- Step 2: Fetch all DNS records for the zone ----------------------------

json_init
json_add_string "action" "infoDnsRecords"
json_add_object "param"
	json_add_string "domainname"     "$__ZONE"
	json_add_string "customernumber" "$username"
	json_add_string "apikey"         "$param_enc"
	json_add_string "apisessionid"   "$__SESSION_ID"
json_close_object

if ! netcup_post; then
	write_log 3 "netcup DDNS: HTTP request failed during infoDnsRecords"
	return 1
fi
netcup_check_response "infoDnsRecords" || return 1

# --- Step 3: Find the record matching our hostname and type ----------------
#
# The API returns ALL records of the zone (A, AAAA, MX, TXT, …).
# We iterate and look for the record where both hostname and type match
# the values derived from the 'domain' configuration option.
#
# The record ID is required by updateDnsRecords to address the exact record.

__MATCH_ID=""

json_select "responsedata"
json_select "dnsrecords"
json_get_keys __RECORD_KEYS

for __key in $__RECORD_KEYS; do
	json_select "$__key"
	json_get_var __rec_id          "id"
	json_get_var __rec_name        "hostname"
	json_get_var __rec_type        "type"
	json_get_var __rec_destination "destination"
	json_select ".."

	write_log 7 "netcup DDNS: examining record id=$__rec_id '$__rec_name' [$__rec_type] = '$__rec_destination'"

	if [ "$__rec_type" = "$__RRTYPE" ] \
	&& [ "$__rec_name" = "$__REC_HOSTNAME" ] \
	&& [ "$__rec_destination" = "$REGISTERED_IP" ]; then
		__MATCH_ID="$__rec_id"
		write_log 7 "netcup DDNS: matched record id=$__MATCH_ID"
		break
	fi
done

json_cleanup

if [ -z "$__MATCH_ID" ]; then
	write_log 3 "netcup DDNS: no [$__RRTYPE] record found for hostname '$__REC_HOSTNAME' in zone '$__ZONE'"
	return 1
fi

# --- Step 4: Update the matched record with the new IP ---------------------

json_init
json_add_string "action" "updateDnsRecords"
json_add_object "param"
	json_add_string "domainname"      "$__ZONE"
	json_add_string "customernumber"  "$username"
	json_add_string "apikey"          "$param_enc"
	json_add_string "apisessionid"    "$__SESSION_ID"
	json_add_object "dnsrecordset"
		json_add_array "dnsrecords"
			json_add_object
				json_add_string "id"           "$__MATCH_ID"
				json_add_string "hostname"     "$__REC_HOSTNAME"
				json_add_string "type"         "$__RRTYPE"
				json_add_string "priority"     ""
				json_add_string "destination"  "$__IP"
				json_add_string "deleterecord" "false"
				json_close_object
		json_close_array
	json_close_object
json_close_object

if ! netcup_post; then
	write_log 3 "netcup DDNS: HTTP request failed during updateDnsRecords"
	return 1
fi

netcup_check_response "updateDnsRecords" || return 1
json_cleanup

write_log 6 "netcup DDNS: '$__REC_HOSTNAME.$__ZONE' [$__RRTYPE] updated to $__IP"

return 0
