From ec0905e5f443712922ca56fdac780ff0bf140d15 Mon Sep 17 00:00:00 2001 From: Ramon Caballero Date: Wed, 31 Jul 2024 10:49:26 +0100 Subject: [PATCH] Script in PHP to update DNS type A records on porkbun.com using their API --- PorkbunAPI.php | 249 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 44 +++++++++ pb-dydns.php | 124 ++++++++++++++++++++++++ 3 files changed, 417 insertions(+) create mode 100644 PorkbunAPI.php create mode 100644 README.md create mode 100755 pb-dydns.php diff --git a/PorkbunAPI.php b/PorkbunAPI.php new file mode 100644 index 0000000..2e4fcc3 --- /dev/null +++ b/PorkbunAPI.php @@ -0,0 +1,249 @@ +config = json_decode(file_get_contents($config_filename)); + $this->ch = curl_init(); + } + + function __destruct() + { + curl_close($this->ch); + } + + // + // Test communication with Porkbun API. + // + // https://porkbun.com/api/json/v3/documentation#Authentication + // + + function ping() + { + // Create the correct endpoint based on the URL: + $endpoint = $this->config->url . "ping"; + + // Set some cURL options: + curl_setopt($this->ch, CURLOPT_URL, $endpoint); + curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($this->ch, CURLOPT_POST, 1); + + // Tell the server that the request body contains JSON data: + $headers = array(); + $headers[] = "Content-Type: application/json"; + curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers); + + // Porkbun API wants to be passed data in JSON format: + $json = array + ( + "apikey" => $this->config->apikey, + "secretapikey" => $this->config->secretapikey + ); + + curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json)); + + // Execute cURL and return the result: + $result = curl_exec($this->ch); + if (curl_errno($this->ch)) + { + echo_to_cli("Error: " . curl_error($this->ch)); + exit; + } + + return $result; + } + + // + // Create a DNS record. + // + // https://porkbun.com/api/json/v3/documentation#DNS%20Create%20Record + // + + function create(string $domain, string $name = "", string $type = "A", string $content = "1.1.1.1", int $ttl = 600) + { + // Create the correct endpoint based on the URL: + $endpoint = $this->config->url . "dns/create/" . $domain; + + // Set some cURL options: + curl_setopt($this->ch, CURLOPT_URL, $endpoint); + curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($this->ch, CURLOPT_POST, 1); + + // Tell the server that the request body contains JSON data: + $headers = array(); + $headers[] = "Content-Type: application/json"; + curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers); + + // Porkbun API wants to be passed data in JSON format: + $json = array + ( + "apikey" => $this->config->apikey, + "secretapikey" => $this->config->secretapikey, + "name" => "$name", + "type" => "$type", + "content" => "$content", + "ttl" => "$ttl" + ); + + curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json)); + + // Execute cURL and return the result: + $result = curl_exec($this->ch); + if (curl_errno($this->ch)) + { + echo_to_cli("Error: " . curl_error($this->ch)); + exit; + } + + return $result; + } + + // + // Edit a DNS record. + // + // https://porkbun.com/api/json/v3/documentation#DNS%20Edit%20Record%20by%20Domain%20and%20ID + // + + function edit(string $domain, string $id, string $content, string $name = "") + { + // Create the correct endpoint based on the URL: + $endpoint = $this->config->url . "dns/edit/" . $domain; + $endpoint .= "/" . $id; + + // Set some cURL options: + curl_setopt($this->ch, CURLOPT_URL, $endpoint); + curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($this->ch, CURLOPT_POST, 1); + + // Tell the server that the request body contains JSON data: + $headers = array(); + $headers[] = "Content-Type: application/json"; + curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers); + + // Porkbun API wants the API key and secret in a JSON structure: + $json = array + ( + "apikey" => $this->config->apikey, + "secretapikey" => $this->config->secretapikey, + "name" => "$name", + "type" => "A", + "content" => "$content" + ); + + curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json)); + + // Execute cURL and return the result: + $result = curl_exec($this->ch); + if (curl_errno($this->ch)) + { + echo_to_cli("Error: " . curl_error($this->ch)); + exit; + } + + return $result; + } + + // + // Retrieve all editable DNS records associated with a domain or a single record for a particular record ID. + // + // https://porkbun.com/api/json/v3/documentation#DNS%20Retrieve%20Records%20by%20Domain%20or%20ID + // + + function retrieve(string $domain, string $id = null) + { + // Create the correct endpoint based on the URL: + $endpoint = $this->config->url . "dns/retrieve/" . $domain; + if (!is_null($id)) $endpoint .= "/" . $id; + + // Set some cURL options: + curl_setopt($this->ch, CURLOPT_URL, $endpoint); + curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($this->ch, CURLOPT_POST, 1); + + // Tell the server that the request body contains JSON data: + $headers = array(); + $headers[] = "Content-Type: application/json"; + curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers); + + // Porkbun API wants the API key and secret in a JSON structure: + $json = array + ( + "apikey" => $this->config->apikey, + "secretapikey" => $this->config->secretapikey + ); + + curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json)); + + // Execute cURL and return the result: + $result = curl_exec($this->ch); + if (curl_errno($this->ch)) + { + echo_to_cli("Error: " . curl_error($this->ch)); + exit; + } + + return $result; + } + + // + // Retrieve all editable DNS records associated with a domain, subdomain and type.v + // + // https://porkbun.com/api/json/v3/documentation#DNS%20Retrieve%20Records%20by%20Domain,%20Subdomain%20and%20Type + // + + function retrieveByNameType(string $domain, string $type, string $subdomain = null) + { + // Create the correct endpoint based on the URL: + $endpoint = $this->config->url . "dns/retrieveByNameType/" . $domain . "/" . $type; + if (!is_null($subdomain)) $endpoint .= "/" . $subdomain; + + // Set some cURL options: + curl_setopt($this->ch, CURLOPT_URL, $endpoint); + curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($this->ch, CURLOPT_POST, 1); + + // Tell the server that the request body contains JSON data: + $headers = array(); + $headers[] = "Content-Type: application/json"; + curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers); + + // Porkbun API wants the API key and secret in a JSON structure: + $json = array + ( + "apikey" => $this->config->apikey, + "secretapikey" => $this->config->secretapikey + ); + + curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json)); + + // Execute cURL and return the result: + $result = curl_exec($this->ch); + if (curl_errno($this->ch)) + { + echo_to_cli("Error: " . curl_error($this->ch)); + exit; + } + + return $result; + } + + // These properties don't need to be declared as they would be dynamically created when assigned a value: + private stdClass $config; + private CurlHandle $ch; +}; + +?> diff --git a/README.md b/README.md new file mode 100644 index 0000000..06923ef --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# pb-dydns + +Script in PHP to update DNS type A records on porkbun.com using their API. + +This can easily be modified to a different programming or scripting language and domain registrar. + +## Prerequisites + +- At least one domain in porkbun.com with DNS type A records already pointing to your dynamic IP address. +- Access to Porkbun API. + +## How to use it + +### from command line + +``` +$ php /path/to/pb-dydns.php domain_name +``` + +### as a cron job + +``` +$ crontab -e +``` + +Modify this line to fit your needs, and add it as many times as domains you want to automatically update: + +``` +*/10 * * * * php /path/to/pb-dydns.php domain_name > /dev/null +``` + +That will run the script every 10 minutes. + +Then restart cron (I'm not sure if this is necessary): + +``` +$ sudo systemctl restart cron +``` + +Entries to a log file called "pb-dydns.log" will be added, to view it you can: + +``` +$ cat /path/to/pb-dydns.log +``` diff --git a/pb-dydns.php b/pb-dydns.php new file mode 100755 index 0000000..c400e6d --- /dev/null +++ b/pb-dydns.php @@ -0,0 +1,124 @@ + /dev/null +// +// And restart cron (I'm not sure if this is necessary): +// +// $ sudo systemctl restart cron +// +// Entries to a log file called "pb-dydns.log" will be added, to view it you can: +// +// $ cat /path/to/pb-dydns.log +// + +require "PorkbunAPI.php"; + +// Make sure that pb-dydns.json exists: +$config_filename = __DIR__ . "/pb-dydns.json"; + +if (!file_exists($config_filename)) +{ + echo "The config file $config_filename does not exist." . PHP_EOL; + + $config = json_encode(array + ( + "url" => "https://porkbun.com/api/json/v3/", + "apikey" => "", + "secretapikey" => "" + ), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + + file_put_contents($config_filename, $config); + + echo "It has been created but you must edit it with your API key and secret." . PHP_EOL; + echo "After that you can try again." . PHP_EOL; + + exit(1); +} + +// Domain name must be provided as argument. +// There should be 2 arguments as $argc includes the script name itself: +if ($argc != 2) +{ + echo_to_cli("This script requires a domain name as argument:" . PHP_EOL); + echo_to_cli("php $argv[0] domain_name" . PHP_EOL); + + exit(2); +} + +// Take the arguments in individual variables: +$myDomain = $argv[1]; + +// Create an instance of PorkbunAPI: +$pbapi = new PorkbunAPI($config_filename); + +// Test connection to Porkbun API in order to get the public IP: +$result = json_decode($pbapi->ping()); +if ($result->status == "SUCCESS") $myIp = $result->yourIp; +else +{ + echo_to_cli("There was an error trying to ping Porkbun." . PHP_EOL); + var_dump($result); + exit(3); +} + +echo_to_cli("Your public IP address is $myIp" . PHP_EOL); + +// Retrieve all DNS records associated with user's domain: +$records = $pbapi->retrieve($myDomain); + +// Discard those records that are not type "A": +$data = json_decode($records); +$filteredRecords = array_filter($data->records, function ($record) +{ + return $record->type == "A"; +}); + +$log_filename = __DIR__ . "/pb-dydns.log"; + +// Update the records that passed the filter with the public IP: +foreach ($filteredRecords as $record) +{ + echo_to_cli("Porkbun's DNS for $record->name is pointing to $record->content... "); + + if ($record->content != $myIp) + { + echo_to_cli("Let's change that... "); + $name = rtrim(strstr($record->name, $myDomain, true), "."); + $result = json_decode($pbapi->edit($myDomain, $record->id, $myIp, $name)); + if ($result->status == "SUCCESS") echo_to_cli("Done!" . PHP_EOL); + + $now = date('Y-m-d H:i:s'); + $message = "$now: Updated DNS on $record->name from $record->content to $myIp" . PHP_EOL; + file_put_contents($log_filename, $message, FILE_APPEND); + } + + else + { + echo_to_cli("Nothing needs to be changed!" . PHP_EOL); + } +} + +?>