ddns-scripts: add support for Porkbun
authorAnsel Horn <dev@cahorn.net>
Wed, 17 Jul 2024 19:12:35 +0000 (15:12 -0400)
committerFlorian Eckert <Eckert.Florian@googlemail.com>
Thu, 22 Aug 2024 14:02:59 +0000 (16:02 +0200)
Extends DDNS support for the Porkbun v3 JSON API with a custom update
script and service configuration.

See: https://porkbun.com/api/json/v3/documentation

Depends on cURL (with SSL) for transport. Porkbun authentication API keys
and secret keys are passed through the ddns-scripts "username" and
"password" variables, respectively. As Porkbun DNS is currently backed by
Cloudflare, also support ddns-scripts "rec_id" variable for specific
record targeting.

Signed-off-by: Ansel Horn <dev@cahorn.net>
net/ddns-scripts/Makefile
net/ddns-scripts/files/usr/lib/ddns/update_porkbun_v3.sh [new file with mode: 0644]
net/ddns-scripts/files/usr/share/ddns/default/porkbun.com-v3.json [new file with mode: 0644]

index 6233377d0ec98705a57065ac84e2bf9dadceabb0..103130558d3305924962a2c1d34082ec42bbf306 100644 (file)
@@ -292,6 +292,22 @@ define Package/ddns-scrtips-one/description
 endef
 
 
+define Package/ddns-scripts-porkbun
+       $(call Package/ddns-scripts/Default)
+       TITLE:=Extension for porkbun.com API v3
+       DEPENDS:=ddns-scripts +curl
+       PROVIDES:=ddns-scripts_porkbun.com-v3
+endef
+
+define Package/ddns-scripts-porkbun/description
+       Dynamic DNS Client scripts extension for porkbun.com API v3 (require curl)
+       It requires:
+       "option username" to be a Porkbun API key
+       "option password" to be the corresponding Porkbun API secret key
+       "option domain" to be the FQDN for which to configure DDNS
+endef
+
+
 define Build/Configure
 endef
 
@@ -369,6 +385,7 @@ define Package/ddns-scripts-services/install
        rm $(1)/usr/share/ddns/default/transip.nl.json
        rm $(1)/usr/share/ddns/default/ns1.com.json
        rm $(1)/usr/share/ddns/default/one.com.json
+       rm $(1)/usr/share/ddns/default/porkbun.com-v3.json
 endef
 
 
@@ -685,6 +702,25 @@ exit 0
 endef
 
 
+define Package/ddns-scripts-porkbun/install
+       $(INSTALL_DIR) $(1)/usr/lib/ddns
+       $(INSTALL_BIN) ./files/usr/lib/ddns/update_porkbun_v3.sh \
+               $(1)/usr/lib/ddns
+
+       $(INSTALL_DIR) $(1)/usr/share/ddns/default
+       $(INSTALL_DATA) ./files/usr/share/ddns/default/porkbun.com-v3.json \
+               $(1)/usr/share/ddns/default/
+endef
+
+define Package/ddns-scripts-porkbun/prerm
+#!/bin/sh
+if [ -z "$${IPKG_INSTROOT}" ]; then
+       /etc/init.d/ddns stop
+fi
+exit 0
+endef
+
+
 $(eval $(call BuildPackage,ddns-scripts))
 $(eval $(call BuildPackage,ddns-scripts-services))
 $(eval $(call BuildPackage,ddns-scripts-utils))
@@ -704,3 +740,4 @@ $(eval $(call BuildPackage,ddns-scripts-pdns))
 $(eval $(call BuildPackage,ddns-scripts-transip))
 $(eval $(call BuildPackage,ddns-scripts-ns1))
 $(eval $(call BuildPackage,ddns-scripts-one))
+$(eval $(call BuildPackage,ddns-scripts-porkbun))
diff --git a/net/ddns-scripts/files/usr/lib/ddns/update_porkbun_v3.sh b/net/ddns-scripts/files/usr/lib/ddns/update_porkbun_v3.sh
new file mode 100644 (file)
index 0000000..037a4aa
--- /dev/null
@@ -0,0 +1,162 @@
+#
+# Distributed under the terms of the GNU General Public License (GPL) version 2.0
+# 2024 Ansel Horn <dev@cahorn.net>
+#
+# Script for DDNS support via Porkbun's v3 API for the OpenWRT ddns-scripts package.
+#
+# Will attempt to create a new or edit an existing A or AAAA record for the
+# given domain and subdomain. Existing CNAME and ALIAS records WILL NOT BE
+# EDITED OR DELETED!  "username" and "password" configurations should be set to
+# Porkbun API key and secret key, respectively.
+#
+# Porkbun API documentation:
+# https://porkbun.com/api/json/v3/documentation#DNS%20Create%20Record
+#
+
+# Source JSON parser
+. /usr/share/libubox/jshn.sh
+
+# Set API base URL
+# Porkbun has warned it may change API hostname in the future:
+# https://porkbun.com/api/json/v3/documentation#apiHost
+__API="https://api.porkbun.com/api/json/v3"
+
+# Check availability of cURL with SSL
+[ -z "$CURL" ] && [ -z "$CURL_SSL" ] && write_log 14 "cURL with SSL support required! Please install"
+
+# Validate configuration
+[ -z "$domain" ] && write_log 14 "Service section not configured correctly! Missing 'domain'"
+[ -z "$username" ] && write_log 14 "Service section not configured correctly! Missing 'username'"
+[ -z "$password" ] && write_log 14 "Service section not configured correctly! Missing 'password'"
+
+# Split FQDN into domain and subdomain(s)
+__DOMAIN_REGEX='^\(\(.*\)\.\)\?\([^.]\+\.[^.]\+\)$'
+echo $domain | grep "$__DOMAIN_REGEX" > /dev/null || write_log 14 "Invalid domain! Check 'domain' config"
+__DOMAIN=$(echo $domain | sed -e "s/$__DOMAIN_REGEX/\3/")
+__SUBDOMAIN=$(echo $domain | sed -e "s/$__DOMAIN_REGEX/\2/")
+
+# Determine IPv4 or IPv6 address and record type
+if [ "$use_ipv6" -eq 1 ]; then
+       expand_ipv6 "$__IP" __ADDR
+       __TYPE="AAAA"
+else
+       __ADDR="$__IP"
+       __TYPE="A"
+fi
+
+
+# Inject authentication into API request JSON payload
+function json_authenticate() {
+       json_add_string "apikey" "$username"
+       json_add_string "secretapikey" "$password"
+}
+
+# Make Porkbun API call
+# $1 - Porkbun API endpoint
+# $2 - request JSON payload
+function api_call() {
+       local response url
+       url="$__API/$1"
+       write_log 7 "API endpoint URL: $url"
+       write_log 7 "API request JSON payload: $2"
+       response=$($CURL --data "$2" "$url")
+       write_log 7 "API response JSON payload: $response"
+       echo "$response"
+
+
+# Check Porkbun API response status
+function json_check_status() {
+       local status
+       json_get_var status "status"
+       [ "$status" == "SUCCESS" ] || write_log 14 "API request failed!"
+}
+
+# Review DNS record and, if it is the record we're looking for, get its id
+function callback_review_record() {
+       local id name type
+       json_select "$2"
+       json_get_var id "id"
+       json_get_var name "name"
+       json_get_var type "type"
+       [ "$name" == "$domain" -a "$type" == "$__TYPE" ] && echo "$id"
+       json_select ..
+}
+
+# Retrieve all DNS records, find the first appropriate A/AAAA record, and get its id
+function find_existing_record_id() {
+       local request response
+       json_init
+       json_authenticate
+       request=$(json_dump)
+       response=$(api_call "/dns/retrieve/$__DOMAIN" "$request")
+       json_load "$response"
+       json_check_status
+       json_for_each_item callback_review_record "records"
+}
+
+# Create a new A/AAAA record
+function create_record() {
+       local request response
+       json_init
+       json_authenticate
+       json_add_string "name" "$__SUBDOMAIN"
+       json_add_string "type" "$__TYPE"
+       json_add_string "content" "$__ADDR"
+       request=$(json_dump)
+       response=$(api_call "/dns/create/$__DOMAIN" "$request")
+       json_load "$response"
+       json_check_status
+}
+
+# Retrieve an existing record and get its content
+# $1 - record id to retrieve
+function retrieve_record_content() {
+       local content request response
+       json_init
+       json_authenticate
+       request=$(json_dump)
+       response=$(api_call "/dns/retrieve/$__DOMAIN/$1" "$request")
+       json_load "$response"
+       json_check_status
+       json_select "records"
+       json_select 1
+       json_get_var content "content"
+       echo "$content"
+}
+
+# Edit an existing A/AAAA record
+# $1 - record id to edit
+function edit_record() {
+       local request response
+       json_init
+       json_authenticate
+       json_add_string "type" "$__TYPE"
+       json_add_string "content" "$__ADDR"
+       request=$(json_dump)
+       response=$(api_call "/dns/edit/$__DOMAIN/$1" "$request")
+       json_load "$response"
+       json_check_status
+}
+
+
+# Try to identify an appropriate existing DNS record to update
+if [ -z $rec_id]; then
+       write_log 7 "Retrieving DNS $__TYPE record"
+       __ID=$(find_existing_record_id)
+else
+       write_log 7 "Using user-supplied DNS record id: $rec_id"
+       __ID=$rec_id
+fi
+
+# Create or update DNS record with current IP address
+if [ -z "$__ID" ]; then
+       write_log 7 "Creating new DNS $__TYPE record"
+       create_record
+else
+       write_log 7 "Updating existing DNS $__TYPE record"
+       if [ "$(retrieve_record_content $__ID)" == "$__ADDR" ]; then
+               write_log 7 "Skipping Porkbun-unsupported forced noop update"
+       else
+               edit_record "$__ID"
+       fi
+fi
diff --git a/net/ddns-scripts/files/usr/share/ddns/default/porkbun.com-v3.json b/net/ddns-scripts/files/usr/share/ddns/default/porkbun.com-v3.json
new file mode 100644 (file)
index 0000000..69509c1
--- /dev/null
@@ -0,0 +1,9 @@
+{
+       "name": "porkbun.com-v3",
+       "ipv4": {
+               "url": "update_porkbun_v3.sh"
+       },
+       "ipv6": {
+               "url": "update_porkbun_v3.sh"
+       }
+}