Compare commits
No commits in common. "0feb341664cf2fb0cf0b564204dfad8911aa2160" and "ab2dca6c27ab05b78d58c653b6b3e53aad5a3b05" have entirely different histories.
0feb341664
...
ab2dca6c27
|
|
@ -1,9 +0,0 @@
|
|||
# Config files:
|
||||
/config/*.json
|
||||
!/config/pb-dydns.example.json
|
||||
|
||||
# Composer files:
|
||||
/vendor/
|
||||
|
||||
# PHPUnit files:
|
||||
.phpunit.result.cache
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Api;
|
||||
//
|
||||
// Outputs a message to CLI, if this script is run from CLI.
|
||||
//
|
||||
|
||||
use stdClass;
|
||||
use App\Util\Console;
|
||||
function echo_to_cli(string $message)
|
||||
{
|
||||
if (php_sapi_name() === 'cli') echo $message;
|
||||
}
|
||||
|
||||
//
|
||||
// Wrapper class to communicate with Porkbun API.
|
||||
|
|
@ -11,11 +15,15 @@ use App\Util\Console;
|
|||
|
||||
class PorkbunAPI
|
||||
{
|
||||
private stdClass $config;
|
||||
|
||||
function __construct(string $config_filename)
|
||||
{
|
||||
$this->config = json_decode(file_get_contents($config_filename));
|
||||
$this->ch = curl_init();
|
||||
}
|
||||
|
||||
function __destruct()
|
||||
{
|
||||
curl_close($this->ch);
|
||||
}
|
||||
|
||||
//
|
||||
|
|
@ -29,17 +37,15 @@ class PorkbunAPI
|
|||
// Create the correct endpoint based on the URL:
|
||||
$endpoint = $this->config->url . "ping";
|
||||
|
||||
$ch = curl_init();
|
||||
|
||||
// Set some cURL options:
|
||||
curl_setopt($ch, CURLOPT_URL, $endpoint);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
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($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers);
|
||||
|
||||
// Porkbun API wants to be passed data in JSON format:
|
||||
$json = array
|
||||
|
|
@ -48,13 +54,13 @@ class PorkbunAPI
|
|||
"secretapikey" => $this->config->secretapikey
|
||||
);
|
||||
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($json));
|
||||
curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json));
|
||||
|
||||
// Execute cURL and return the result:
|
||||
$result = curl_exec($ch);
|
||||
if (curl_errno($ch))
|
||||
$result = curl_exec($this->ch);
|
||||
if (curl_errno($this->ch))
|
||||
{
|
||||
Console::echo("Error: " . curl_error($ch));
|
||||
echo_to_cli("Error: " . curl_error($this->ch));
|
||||
exit;
|
||||
}
|
||||
|
||||
|
|
@ -72,17 +78,15 @@ 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($ch, CURLOPT_URL, $endpoint);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
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($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers);
|
||||
|
||||
// Porkbun API wants to be passed data in JSON format:
|
||||
$json = array
|
||||
|
|
@ -95,13 +99,13 @@ class PorkbunAPI
|
|||
"ttl" => "$ttl"
|
||||
);
|
||||
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($json));
|
||||
curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json));
|
||||
|
||||
// Execute cURL and return the result:
|
||||
$result = curl_exec($ch);
|
||||
if (curl_errno($ch))
|
||||
$result = curl_exec($this->ch);
|
||||
if (curl_errno($this->ch))
|
||||
{
|
||||
Console::echo("Error: " . curl_error($ch));
|
||||
echo_to_cli("Error: " . curl_error($this->ch));
|
||||
exit;
|
||||
}
|
||||
|
||||
|
|
@ -120,17 +124,15 @@ class PorkbunAPI
|
|||
$endpoint = $this->config->url . "dns/edit/" . $domain;
|
||||
$endpoint .= "/" . $id;
|
||||
|
||||
$ch = curl_init();
|
||||
|
||||
// Set some cURL options:
|
||||
curl_setopt($ch, CURLOPT_URL, $endpoint);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
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($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers);
|
||||
|
||||
// Porkbun API wants the API key and secret in a JSON structure:
|
||||
$json = array
|
||||
|
|
@ -142,13 +144,13 @@ class PorkbunAPI
|
|||
"content" => "$content"
|
||||
);
|
||||
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($json));
|
||||
curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json));
|
||||
|
||||
// Execute cURL and return the result:
|
||||
$result = curl_exec($ch);
|
||||
if (curl_errno($ch))
|
||||
$result = curl_exec($this->ch);
|
||||
if (curl_errno($this->ch))
|
||||
{
|
||||
Console::echo("Error: " . curl_error($ch));
|
||||
echo_to_cli("Error: " . curl_error($this->ch));
|
||||
exit;
|
||||
}
|
||||
|
||||
|
|
@ -161,23 +163,21 @@ 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($ch, CURLOPT_URL, $endpoint);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
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($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers);
|
||||
|
||||
// Porkbun API wants the API key and secret in a JSON structure:
|
||||
$json = array
|
||||
|
|
@ -186,13 +186,13 @@ class PorkbunAPI
|
|||
"secretapikey" => $this->config->secretapikey
|
||||
);
|
||||
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($json));
|
||||
curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json));
|
||||
|
||||
// Execute cURL and return the result:
|
||||
$result = curl_exec($ch);
|
||||
if (curl_errno($ch))
|
||||
$result = curl_exec($this->ch);
|
||||
if (curl_errno($this->ch))
|
||||
{
|
||||
Console::echo("Error: " . curl_error($ch));
|
||||
echo_to_cli("Error: " . curl_error($this->ch));
|
||||
exit;
|
||||
}
|
||||
|
||||
|
|
@ -205,23 +205,21 @@ 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($ch, CURLOPT_URL, $endpoint);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
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($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers);
|
||||
|
||||
// Porkbun API wants the API key and secret in a JSON structure:
|
||||
$json = array
|
||||
|
|
@ -230,18 +228,22 @@ class PorkbunAPI
|
|||
"secretapikey" => $this->config->secretapikey
|
||||
);
|
||||
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($json));
|
||||
curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json));
|
||||
|
||||
// Execute cURL and return the result:
|
||||
$result = curl_exec($ch);
|
||||
if (curl_errno($ch))
|
||||
$result = curl_exec($this->ch);
|
||||
if (curl_errno($this->ch))
|
||||
{
|
||||
Console::echo("Error: " . curl_error($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;
|
||||
};
|
||||
|
||||
?>
|
||||
83
README.md
83
README.md
|
|
@ -2,82 +2,43 @@
|
|||
|
||||
Script in PHP to update DNS type A records on porkbun.com using their API.
|
||||
|
||||
This can easily be adapted to other registrars or scripting languages.
|
||||
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 the Porkbun API (API key + secret key).
|
||||
|
||||
## Installation & Deployment
|
||||
|
||||
### 1. Clone the repository
|
||||
|
||||
```bash
|
||||
git clone https://gitea.ramoncaballero.dev/mon/pb-dydns.git
|
||||
cd pb-dydns
|
||||
```
|
||||
|
||||
### 2. Deploy the script
|
||||
|
||||
Run the provided deployment script:
|
||||
|
||||
```bash
|
||||
./deploy.sh
|
||||
```
|
||||
|
||||
This creates a directory:
|
||||
|
||||
```
|
||||
$HOME/pb-dydns-live
|
||||
```
|
||||
|
||||
Inside it you will find:
|
||||
|
||||
- pb-dydns.php (the script you run)
|
||||
- PorkbunAPI.php
|
||||
- UpdateDnsCommand.php
|
||||
- Config.php
|
||||
- Logger.php
|
||||
- pb-dydns.json (your configuration file)
|
||||
- pb-dydns.log (log file)
|
||||
|
||||
### 3. Configure it
|
||||
|
||||
Edit the configuration file `$HOME/pb-dydns-live/pb-dydns.json`
|
||||
|
||||
Fill in:
|
||||
|
||||
- the Porkbun API endpoint (usually https://api.porkbun.com/api/json/v3/)
|
||||
- your Porkbun API key
|
||||
- your secret key
|
||||
- Access to Porkbun API.
|
||||
|
||||
## How to use it
|
||||
|
||||
### From the command line
|
||||
### from command line
|
||||
|
||||
```
|
||||
$ php /path/to/pb-dydns-live/pb-dydns.php yourdomain.com
|
||||
$ php /path/to/pb-dydns.php domain_name
|
||||
```
|
||||
|
||||
### As a cron job
|
||||
|
||||
Edit your crontab:
|
||||
|
||||
```bash
|
||||
crontab -e
|
||||
```
|
||||
|
||||
Add a line like:
|
||||
### as a cron job
|
||||
|
||||
```
|
||||
*/10 * * * * php /path/to/pb-dydns-live/pb-dydns.php yourdomain.com > /dev/null
|
||||
$ crontab -e
|
||||
```
|
||||
|
||||
This runs the updater every 10 minutes.
|
||||
Modify this line to fit your needs, and add it as many times as domains you want to automatically update:
|
||||
|
||||
## Logs
|
||||
```
|
||||
*/10 * * * * php /path/to/pb-dydns.php domain_name > /dev/null
|
||||
```
|
||||
|
||||
DNS updates are written to `/path/to/pb-dydns-live/pb-dydns.log`
|
||||
That will run the script every 10 minutes.
|
||||
|
||||
Runs where nothing changes do not produce log entries.
|
||||
Then restart cron (I'm not sure if this is necessary):
|
||||
|
||||
```
|
||||
$ sudo systemctl restart cron
|
||||
```
|
||||
|
||||
Entries to `pb-dydns.log` will be added, to view it you can:
|
||||
|
||||
```
|
||||
$ cat /path/to/pb-dydns.log
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use App\Config\Config;
|
||||
use App\Util\Logger;
|
||||
use App\Command\UpdateDnsCommand;
|
||||
|
||||
$config = new Config(__DIR__ . "/../config/pb-dydns.json");
|
||||
$logger = new Logger(__DIR__ . "/../logs/pb-dydns.log");
|
||||
|
||||
$cmd = new UpdateDnsCommand($config, $logger);
|
||||
exit($cmd->run($argv));
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"name": "ramoncaballero.dev/pb-dydns",
|
||||
"description": "Dynamic DNS updater for Porkbun",
|
||||
"type": "project",
|
||||
"autoload":
|
||||
{
|
||||
"psr-4":
|
||||
{
|
||||
"App\\": "src/"
|
||||
}
|
||||
},
|
||||
"require-dev":
|
||||
{
|
||||
"phpunit/phpunit": "^12.5"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"url": "https://api.porkbun.com/api/json/v3/",
|
||||
"apikey": "YOUR_API_KEY_HERE",
|
||||
"secretapikey": "YOUR_SECRET_KEY_HERE"
|
||||
}
|
||||
35
deploy.sh
35
deploy.sh
|
|
@ -1,35 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
DEPLOY_DIR="$HOME/pb-dydns-live"
|
||||
|
||||
echo "Deploying to $DEPLOY_DIR"
|
||||
|
||||
mkdir -p "$DEPLOY_DIR"
|
||||
|
||||
# Copy main script:
|
||||
cp bin/pb-dydns.php "$DEPLOY_DIR/"
|
||||
|
||||
# Copy all PHP source files into a flat structure:
|
||||
cp src/Api/PorkbunAPI.php "$DEPLOY_DIR/"
|
||||
cp src/Command/UpdateDnsCommand.php "$DEPLOY_DIR/"
|
||||
cp src/Config/Config.php "$DEPLOY_DIR/"
|
||||
cp src/Util/Logger.php "$DEPLOY_DIR/"
|
||||
|
||||
# Copy example config only if user doesn't already have a real one:
|
||||
if [ ! -f "$DEPLOY_DIR/pb-dydns.json" ]; then
|
||||
cp config/pb-dydns.example.json "$DEPLOY_DIR/pb-dydns.json"
|
||||
echo "Created default configuration file: pb-dydns.json"
|
||||
else
|
||||
echo "Existing pb-dydns.json preserved"
|
||||
fi
|
||||
|
||||
# Ensure log file exists:
|
||||
touch "$DEPLOY_DIR/pb-dydns.log"
|
||||
|
||||
# Make script executable:
|
||||
chmod +x "$DEPLOY_DIR/pb-dydns.php"
|
||||
|
||||
echo "Deployment complete."
|
||||
echo "Remember to edit $DEPLOY_DIR/pb-dydns.json with your Porkbun API keys."
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
<?php
|
||||
|
||||
//
|
||||
// This script updates the DNS type A on porkbun.com so it is always pointing to your public IP address.
|
||||
//
|
||||
// Dependencies:
|
||||
//
|
||||
// - PorkbunAPI.php
|
||||
// - pb-dydns.json (it will be created automatically the 1st time)
|
||||
//
|
||||
// How to use it:
|
||||
//
|
||||
// - from command line:
|
||||
//
|
||||
// $ php /path/to/pb-dydns.php domain_name
|
||||
//
|
||||
// It will retrieve all DNS records for the specificied domain_name and update the A type ones
|
||||
// with your public IP address.
|
||||
//
|
||||
//- as a cron job, to add it to:
|
||||
//
|
||||
// $ crontab -e
|
||||
//
|
||||
// Append this line as many times as domains you want to automatically update:
|
||||
//
|
||||
// */10 * * * * php /path/to/pb-dydns.php domain_name > /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:
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit bootstrap="vendor/autoload.php" colors="true" verbose="true" backupGlobals="false" backupStaticAttributes="false">
|
||||
<testsuites>
|
||||
<testsuite name="pb-dydns Test Suite">
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Api\PorkbunAPI;
|
||||
use App\Config\Config;
|
||||
use App\Util\Logger;
|
||||
use App\Util\Console;
|
||||
|
||||
class UpdateDnsCommand
|
||||
{
|
||||
public const STATUS_NOTHING_TO_CHANGE = 0;
|
||||
public const STATUS_UPDATED = 1;
|
||||
public const STATUS_ERROR = 2;
|
||||
|
||||
private Config $config;
|
||||
private Logger $logger;
|
||||
|
||||
public function __construct(Config $config, Logger $logger)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function run(array $argv, ?PorkbunAPI $api = null, ?string $myIp = null): int
|
||||
{
|
||||
if (count($argv) < 2)
|
||||
{
|
||||
echo "Usage: pb-dydns.php <domain>\n";
|
||||
return self::STATUS_ERROR;
|
||||
}
|
||||
|
||||
$domain = $argv[1];
|
||||
|
||||
// Allow API injection for tests:
|
||||
$api = $api ?? new PorkbunAPI($this->config->get('config_filename'));
|
||||
|
||||
// 1. Test connection to Porkbun API in order to get the public IP:
|
||||
$raw = $api->ping();
|
||||
$result = json_decode($raw);
|
||||
|
||||
if ($result === null)
|
||||
{
|
||||
return $this->fail("Invalid JSON returned by Porkbun API: " . $raw);
|
||||
}
|
||||
|
||||
if (!isset($result->status))
|
||||
{
|
||||
return $this->fail("Porkbun API response missing 'status' field: " . $raw);
|
||||
}
|
||||
|
||||
if ($result->status !== "SUCCESS")
|
||||
{
|
||||
return $this->fail("Porkbun API returned an error: " . $raw);
|
||||
}
|
||||
|
||||
if (!isset($result->yourIp))
|
||||
{
|
||||
return $this->fail("Porkbun API did not return your public IP.");
|
||||
}
|
||||
|
||||
$myIp = $myIp ?? $result->yourIp;
|
||||
|
||||
Console::echo("Your public IP address is $myIp" . PHP_EOL);
|
||||
|
||||
// 2. Retrieve all DNS records associated with user's domain:
|
||||
$records = $api->retrieve($domain);
|
||||
$data = json_decode($records);
|
||||
|
||||
if ($data === null || !isset($data->records))
|
||||
{
|
||||
return $this->fail("Invalid DNS records returned by Porkbun API: " . $records);
|
||||
}
|
||||
|
||||
// Discard those records that are not type "A":
|
||||
$filteredRecords = array_filter($data->records, function ($record)
|
||||
{
|
||||
return isset($record->type) && $record->type == "A";
|
||||
});
|
||||
|
||||
// 3. Update the records that passed the filter with the public IP:
|
||||
$updated = false;
|
||||
foreach ($filteredRecords as $record)
|
||||
{
|
||||
if (!isset($record->id, $record->name, $record->content, $record->type))
|
||||
{
|
||||
return $this->fail("Porkbun API returned a malformed DNS 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 && isset($result->status) && $result->status === "SUCCESS")
|
||||
{
|
||||
Console::echo("Done!\n");
|
||||
$this->logger->log("Updated DNS on $record->name from $record->content to $myIp");
|
||||
$updated = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Console::echo("Failed!\n");
|
||||
return self::STATUS_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
Console::echo("Nothing needs to be changed!" . PHP_EOL);
|
||||
}
|
||||
|
||||
Console::echo("");
|
||||
}
|
||||
|
||||
return $updated ? self::STATUS_UPDATED : self::STATUS_NOTHING_TO_CHANGE;
|
||||
}
|
||||
|
||||
private function fail(string $message): int
|
||||
{
|
||||
Console::echo($message);
|
||||
$this->logger->log("ERROR: " . $message);
|
||||
return self::STATUS_ERROR;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Config;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class Config
|
||||
{
|
||||
private array $data;
|
||||
|
||||
public function __construct(string $filename)
|
||||
{
|
||||
if (!file_exists($filename))
|
||||
{
|
||||
throw new RuntimeException("Config file not found: $filename");
|
||||
}
|
||||
|
||||
$json = file_get_contents($filename);
|
||||
$data = json_decode($json, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE)
|
||||
{
|
||||
throw new \RuntimeException("Invalid JSON in config file: " . json_last_error_msg());
|
||||
}
|
||||
|
||||
if ($data === null)
|
||||
{
|
||||
throw new RuntimeException("Invalid JSON in config file: $filename");
|
||||
}
|
||||
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
public function get(string $key): mixed
|
||||
{
|
||||
return $this->data[$key] ?? null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util;
|
||||
|
||||
class Console
|
||||
{
|
||||
public static function echo(string $message): void
|
||||
{
|
||||
fwrite(STDOUT, $message . PHP_EOL);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util;
|
||||
|
||||
class Logger
|
||||
{
|
||||
private string $filename;
|
||||
|
||||
public function __construct(string $filename)
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use App\Api\PorkbunAPI;
|
||||
|
||||
class PorkbunAPITest extends TestCase
|
||||
{
|
||||
private string $tmpConfig;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->tmpConfig = tempnam(sys_get_temp_dir(), 'cfg_');
|
||||
file_put_contents($this->tmpConfig, json_encode([
|
||||
"url" => "https://example.com/api/",
|
||||
"apikey" => "TESTKEY",
|
||||
"secretapikey" => "TESTSECRET"
|
||||
]));
|
||||
}
|
||||
|
||||
public function testPingBuildsCorrectEndpointAndReturnsValue()
|
||||
{
|
||||
$api = $this->getMockBuilder(PorkbunAPI::class)
|
||||
->setConstructorArgs([$this->tmpConfig])
|
||||
->onlyMethods(['ping'])
|
||||
->getMock();
|
||||
|
||||
$api->expects($this->once())
|
||||
->method('ping')
|
||||
->willReturn('{"status":"SUCCESS"}');
|
||||
|
||||
$this->assertSame('{"status":"SUCCESS"}', $api->ping());
|
||||
}
|
||||
|
||||
public function testCreateReturnsExpectedValue()
|
||||
{
|
||||
$api = $this->getMockBuilder(PorkbunAPI::class)
|
||||
->setConstructorArgs([$this->tmpConfig])
|
||||
->onlyMethods(['create'])
|
||||
->getMock();
|
||||
|
||||
$api->expects($this->once())
|
||||
->method('create')
|
||||
->with("example.com", "www", "A", "1.2.3.4", 600)
|
||||
->willReturn('{"status":"CREATED"}');
|
||||
|
||||
$result = $api->create("example.com", "www", "A", "1.2.3.4", 600);
|
||||
|
||||
$this->assertSame('{"status":"CREATED"}', $result);
|
||||
}
|
||||
|
||||
public function testEditReturnsExpectedValue()
|
||||
{
|
||||
$api = $this->getMockBuilder(PorkbunAPI::class)
|
||||
->setConstructorArgs([$this->tmpConfig])
|
||||
->onlyMethods(['edit'])
|
||||
->getMock();
|
||||
|
||||
$api->expects($this->once())
|
||||
->method('edit')
|
||||
->with("example.com", "123", "5.6.7.8", "www")
|
||||
->willReturn('{"status":"EDITED"}');
|
||||
|
||||
$result = $api->edit("example.com", "123", "5.6.7.8", "www");
|
||||
|
||||
$this->assertSame('{"status":"EDITED"}', $result);
|
||||
}
|
||||
|
||||
public function testRetrieveReturnsExpectedValue()
|
||||
{
|
||||
$api = $this->getMockBuilder(PorkbunAPI::class)
|
||||
->setConstructorArgs([$this->tmpConfig])
|
||||
->onlyMethods(['retrieve'])
|
||||
->getMock();
|
||||
|
||||
$api->expects($this->once())
|
||||
->method('retrieve')
|
||||
->with("example.com", null)
|
||||
->willReturn('{"records":[1,2,3]}');
|
||||
|
||||
$result = $api->retrieve("example.com");
|
||||
|
||||
$this->assertSame('{"records":[1,2,3]}', $result);
|
||||
}
|
||||
|
||||
public function testRetrieveByNameTypeReturnsExpectedValue()
|
||||
{
|
||||
$api = $this->getMockBuilder(PorkbunAPI::class)
|
||||
->setConstructorArgs([$this->tmpConfig])
|
||||
->onlyMethods(['retrieveByNameType'])
|
||||
->getMock();
|
||||
|
||||
$api->expects($this->once())
|
||||
->method('retrieveByNameType')
|
||||
->with("example.com", "A", "www")
|
||||
->willReturn('{"records":["A","B"]}');
|
||||
|
||||
$result = $api->retrieveByNameType("example.com", "A", "www");
|
||||
|
||||
$this->assertSame('{"records":["A","B"]}', $result);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use App\Api\PorkbunAPI;
|
||||
use App\Command\UpdateDnsCommand;
|
||||
use App\Config\Config;
|
||||
use App\Util\Logger;
|
||||
|
||||
class UpdateDnsCommandTest extends TestCase
|
||||
{
|
||||
private $config;
|
||||
private $logger;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->config = $this->createMock(Config::class);
|
||||
$this->logger = $this->createMock(Logger::class);
|
||||
}
|
||||
|
||||
// Scenario: DNS already matches public IP, so no update should occur:
|
||||
public function testNoChangesNeeded()
|
||||
{
|
||||
$api = $this->createMock(PorkbunAPI::class);
|
||||
|
||||
// ping() must return yourIp or the command returns STATUS_ERROR immediately:
|
||||
$api->method('ping')->willReturn('{"status":"SUCCESS","yourIp":"1.2.3.4"}');
|
||||
|
||||
// retrieve() must return JSON with a records array, otherwise json_decode() fails:
|
||||
$api->method('retrieve')->willReturn(json_encode(
|
||||
[
|
||||
"records" => [
|
||||
[
|
||||
"name" => "test.example.com",
|
||||
"type" => "A",
|
||||
"content" => "1.2.3.4",
|
||||
"id" => "123"
|
||||
]
|
||||
]
|
||||
]));
|
||||
|
||||
$cmd = new UpdateDnsCommand($this->config, $this->logger);
|
||||
$status = $cmd->run(["script.php", "example.com"], $api, "1.2.3.4");
|
||||
|
||||
$this->assertSame(UpdateDnsCommand::STATUS_NOTHING_TO_CHANGE, $status);
|
||||
}
|
||||
|
||||
// Scenario: DNS is outdated, so edit() should be called and STATUS_UPDATED returned:
|
||||
public function testDnsIsUpdated()
|
||||
{
|
||||
$api = $this->createMock(PorkbunAPI::class);
|
||||
|
||||
// ping() must return yourIp or the command returns STATUS_ERROR immediately:
|
||||
$api->method('ping')->willReturn('{"status":"SUCCESS","yourIp":"1.2.3.4"}');
|
||||
|
||||
// retrieve() must return JSON with a records array, otherwise json_decode() fails:
|
||||
$api->method('retrieve')->willReturn(json_encode(
|
||||
[
|
||||
"records" => [
|
||||
[
|
||||
"name" => "test.example.com",
|
||||
"type" => "A",
|
||||
"content" => "5.6.7.8",
|
||||
"id" => "123"
|
||||
]
|
||||
]
|
||||
]));
|
||||
|
||||
// edit() must return a SUCCESS status or the command returns STATUS_ERROR:
|
||||
$api->method('edit')->willReturn(json_encode(["status" => "SUCCESS"]));
|
||||
|
||||
// When DNS is updated, the command must record the change in the log exactly once:
|
||||
$this->logger->expects($this->once())->method('log')->with($this->stringContains("Updated DNS"));
|
||||
|
||||
$cmd = new UpdateDnsCommand($this->config, $this->logger);
|
||||
$status = $cmd->run(["script.php", "example.com"], $api, "1.2.3.4");
|
||||
|
||||
$this->assertSame(UpdateDnsCommand::STATUS_UPDATED, $status);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use App\Config\Config;
|
||||
|
||||
class ConfigTest extends TestCase
|
||||
{
|
||||
public function testLoadsValidJson()
|
||||
{
|
||||
$tmp = tempnam(sys_get_temp_dir(), 'cfg_');
|
||||
file_put_contents($tmp, json_encode(["foo" => "bar"]));
|
||||
|
||||
$config = new Config($tmp);
|
||||
|
||||
$this->assertSame("bar", $config->get("foo"));
|
||||
}
|
||||
|
||||
public function testThrowsOnMissingFile()
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
new Config("/nonexistent/file.json");
|
||||
}
|
||||
|
||||
public function testThrowsOnInvalidJson()
|
||||
{
|
||||
$tmp = tempnam(sys_get_temp_dir(), 'cfg_');
|
||||
file_put_contents($tmp, "{invalid json");
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
new Config($tmp);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use App\Util\Logger;
|
||||
|
||||
class LoggerTest extends TestCase
|
||||
{
|
||||
public function testLogWritesMessage()
|
||||
{
|
||||
$tmp = tempnam(sys_get_temp_dir(), 'log_');
|
||||
$logger = new Logger($tmp);
|
||||
|
||||
$logger->log("Hello world");
|
||||
|
||||
$contents = file_get_contents($tmp);
|
||||
|
||||
$this->assertStringContainsString("Hello world", $contents);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue