diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aaa587d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Config files: +/config/*.json +!/config/pb-dydns.example.json diff --git a/bin/pb-dydns.php b/bin/pb-dydns.php index f8d1e59..2612956 100755 --- a/bin/pb-dydns.php +++ b/bin/pb-dydns.php @@ -1,144 +1,13 @@ +#!/usr/bin/env php /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 __DIR__ . "/PorkbunAPI.php"; +require __DIR__ . "/Config.php"; +require __DIR__ . "/Logger.php"; +require __DIR__ . "/UpdateDnsCommand.php"; -require "PorkbunAPI.php"; +$config = new Config(__DIR__ . "/pb-dydns.json"); +$logger = new Logger(__DIR__ . "/pb-dydns.log"); -// 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: -$raw = $pbapi->ping(); -$result = json_decode($raw); - -if ($result === null) -{ - echo_to_cli("Invalid JSON returned by Porkbun API:\n$raw\n"); - exit(3); -} - -if (!isset($result->status)) -{ - echo_to_cli("Porkbun API response missing 'status' field:\n$raw\n"); - exit(3); -} - -if ($result->status !== "SUCCESS") -{ - echo_to_cli("Porkbun API returned an error:\n$raw\n"); - exit(3); -} - -if (!isset($result->yourIp)) -{ - echo_to_cli("Porkbun API did not return your public IP.\n"); - exit(3); -} - -$myIp = $result->yourIp; - -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); - } -} - -?> +$cmd = new UpdateDnsCommand($config, $logger); +$cmd->run($argv); diff --git a/config/pb-dydns.example.json b/config/pb-dydns.example.json new file mode 100644 index 0000000..a5e11d8 --- /dev/null +++ b/config/pb-dydns.example.json @@ -0,0 +1,5 @@ +{ + "url": "https://api.porkbun.com/api/json/v3/", + "apikey": "YOUR_API_KEY_HERE", + "secretapikey": "YOUR_SECRET_KEY_HERE" +} diff --git a/src/Api/PorkbunAPI.php b/src/Api/PorkbunAPI.php index 2e4fcc3..1f3e4cb 100644 --- a/src/Api/PorkbunAPI.php +++ b/src/Api/PorkbunAPI.php @@ -1,29 +1,16 @@ config = json_decode(file_get_contents($config_filename)); - $this->ch = curl_init(); - } - - function __destruct() - { - curl_close($this->ch); } // @@ -37,15 +24,17 @@ class PorkbunAPI // Create the correct endpoint based on the URL: $endpoint = $this->config->url . "ping"; + $ch = curl_init(); + // 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); + curl_setopt($ch, CURLOPT_URL, $endpoint); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($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); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); // Porkbun API wants to be passed data in JSON format: $json = array @@ -54,13 +43,13 @@ class PorkbunAPI "secretapikey" => $this->config->secretapikey ); - curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json)); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($json)); // Execute cURL and return the result: - $result = curl_exec($this->ch); - if (curl_errno($this->ch)) + $result = curl_exec($ch); + if (curl_errno($ch)) { - echo_to_cli("Error: " . curl_error($this->ch)); + Console::echo("Error: " . curl_error($ch)); exit; } @@ -78,15 +67,17 @@ class PorkbunAPI // Create the correct endpoint based on the URL: $endpoint = $this->config->url . "dns/create/" . $domain; + $ch = curl_init(); + // 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); + curl_setopt($ch, CURLOPT_URL, $endpoint); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($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); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); // Porkbun API wants to be passed data in JSON format: $json = array @@ -99,13 +90,13 @@ class PorkbunAPI "ttl" => "$ttl" ); - curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json)); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($json)); // Execute cURL and return the result: - $result = curl_exec($this->ch); - if (curl_errno($this->ch)) + $result = curl_exec($ch); + if (curl_errno($ch)) { - echo_to_cli("Error: " . curl_error($this->ch)); + Console::echo("Error: " . curl_error($ch)); exit; } @@ -124,15 +115,17 @@ class PorkbunAPI $endpoint = $this->config->url . "dns/edit/" . $domain; $endpoint .= "/" . $id; + $ch = curl_init(); + // 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); + curl_setopt($ch, CURLOPT_URL, $endpoint); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($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); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); // Porkbun API wants the API key and secret in a JSON structure: $json = array @@ -144,13 +137,13 @@ class PorkbunAPI "content" => "$content" ); - curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json)); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($json)); // Execute cURL and return the result: - $result = curl_exec($this->ch); - if (curl_errno($this->ch)) + $result = curl_exec($ch); + if (curl_errno($ch)) { - echo_to_cli("Error: " . curl_error($this->ch)); + Console::echo("Error: " . curl_error($ch)); exit; } @@ -163,21 +156,23 @@ class PorkbunAPI // https://porkbun.com/api/json/v3/documentation#DNS%20Retrieve%20Records%20by%20Domain%20or%20ID // - function retrieve(string $domain, string $id = null) + 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; + $ch = curl_init(); + // 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); + curl_setopt($ch, CURLOPT_URL, $endpoint); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($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); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); // Porkbun API wants the API key and secret in a JSON structure: $json = array @@ -186,13 +181,13 @@ class PorkbunAPI "secretapikey" => $this->config->secretapikey ); - curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json)); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($json)); // Execute cURL and return the result: - $result = curl_exec($this->ch); - if (curl_errno($this->ch)) + $result = curl_exec($ch); + if (curl_errno($ch)) { - echo_to_cli("Error: " . curl_error($this->ch)); + Console::echo("Error: " . curl_error($ch)); exit; } @@ -205,21 +200,23 @@ class PorkbunAPI // https://porkbun.com/api/json/v3/documentation#DNS%20Retrieve%20Records%20by%20Domain,%20Subdomain%20and%20Type // - function retrieveByNameType(string $domain, string $type, string $subdomain = null) + 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; + $ch = curl_init(); + // 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); + curl_setopt($ch, CURLOPT_URL, $endpoint); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($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); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); // Porkbun API wants the API key and secret in a JSON structure: $json = array @@ -228,22 +225,18 @@ class PorkbunAPI "secretapikey" => $this->config->secretapikey ); - curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json)); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($json)); // Execute cURL and return the result: - $result = curl_exec($this->ch); - if (curl_errno($this->ch)) + $result = curl_exec($ch); + if (curl_errno($ch)) { - echo_to_cli("Error: " . curl_error($this->ch)); + Console::echo("Error: " . curl_error($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/src/Command/.gitkeep b/src/Command/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/Command/UpdateDnsCommand.php b/src/Command/UpdateDnsCommand.php new file mode 100644 index 0000000..fc37ecd --- /dev/null +++ b/src/Command/UpdateDnsCommand.php @@ -0,0 +1,94 @@ +config = $config; + $this->logger = $logger; + } + + public function run(array $argv): void + { + if (count($argv) < 2) + { + echo "Usage: pb-dydns.php \n"; + exit(1); + } + + $domain = $argv[1]; + + $api = new PorkbunAPI($this->config->get('config_filename')); + + // Test connection to Porkbun API in order to get the public IP: + $raw = $api->ping(); + $result = json_decode($raw); + + if ($result === null) + { + Console::echo("Invalid JSON returned by Porkbun API:\n$raw\n"); + exit(3); + } + + if (!isset($result->status)) + { + Console::echo("Porkbun API response missing 'status' field:\n$raw\n"); + exit(3); + } + + if ($result->status !== "SUCCESS") + { + Console::echo("Porkbun API returned an error:\n$raw\n"); + exit(3); + } + + if (!isset($result->yourIp)) + { + Console::echo("Porkbun API did not return your public IP.\n"); + exit(3); + } + + $myIp = $result->yourIp; + + Console::echo("Your public IP address is $myIp" . PHP_EOL); + + // Retrieve all DNS records associated with user's domain: + $records = $api->retrieve($domain); + + // Discard those records that are not type "A": + $data = json_decode($records); + $filteredRecords = array_filter($data->records, function ($record) + { + return $record->type == "A"; + }); + + // Update the records that passed the filter with the public IP: + foreach ($filteredRecords as $record) + { + Console::echo("Porkbun's DNS for $record->name is pointing to $record->content... "); + + if ($record->content != $myIp) + { + Console::echo("Let's change that... "); + $name = rtrim(strstr($record->name, $domain, true), "."); + $result = json_decode($api->edit($domain, $record->id, $myIp, $name)); + if ($result->status == "SUCCESS") Console::echo("Done!" . PHP_EOL); + + $this->logger->log("Updated DNS on $record->name from $record->content to $myIp"); + } + + else + { + Console::echo("Nothing needs to be changed!" . PHP_EOL); + } + } + } +} diff --git a/src/Config/.gitkeep b/src/Config/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/Config/Config.php b/src/Config/Config.php new file mode 100644 index 0000000..0c14b81 --- /dev/null +++ b/src/Config/Config.php @@ -0,0 +1,27 @@ +data = json_decode($json, true); + + if ($this->data === null) + { + throw new RuntimeException("Invalid JSON in config file: $filename"); + } + } + + public function get(string $key): mixed + { + return $this->data[$key] ?? null; + } +} diff --git a/src/Util/.gitkeep b/src/Util/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/Util/Console.php b/src/Util/Console.php new file mode 100644 index 0000000..dcf667e --- /dev/null +++ b/src/Util/Console.php @@ -0,0 +1,9 @@ +filename = $filename; + } + + public function log(string $message): void + { + $timestamp = date("Y-m-d H:i:s"); + file_put_contents($this->filename, $timestamp . ' ' . $message . PHP_EOL, FILE_APPEND); + } +}