Omalle palvelimelle yleensä halutaan verkkotunnus eli domain. Eräs ilmaisia ja helposti päivitettäviä verkkotunnuksia tarjoava palvelu on dy.fi. Palvelusta hankitun nimen voi ohjata omalle koneelle dy.fi:n hallintapaneelin kautta viikoksi kerrallaan, mutta pidemmän päälle on kätevämpää käyttää automaattista skriptiä.
Valitettavasti dy.fi:n konekäyttöinen rajapinta päivittää vain IPv4-osoitteen, kuten vanhassa koodissani näkyy. Tässä esiteltävä koodi kirjautuukin palveluun selaimen tavoin ja päivittää sitä kautta myös IPv6-osoitteen ja MX-osoitteen. Koodi tukee myös useaa verkkotunnusta ja jopa useaa käyttäjätiliä.
Omat IP-osoitteet yritetään hakea iproute2-paketin ip-ohjelmalla. Jos tämä ei onnistu, osoitteet tarkastetaan netistä.
Verkkotunnus täytyy tietenkin ensin varata dy.fi-palvelusta. Päivittämiseen tarvitaan sitten vain yksi kutsu funktiolle dyfi\update. Funktiokutsuun laitetaan tiedostonimi tilanteen muistamista varten (esimerkiksi dyfi.status), päivitettävät tiedot, oma tunnus (email), salasana (password) ja verkkotunnukset (hostname).
Esimerkki käytöstä:
<?php chdir(__DIR__); require_once "dyfi.php"; // Yksinkertainen käyttö yhdellä tilillä: dyfi\update("dyfi.status", [ "ipv4" => true, "ipv6" => true, "mx" => "offline.dy.fi", "email" => "mail@example.com", "password" => "foo-bar-baz", ["hostname" => "alpha-example.dy.fi"], ["hostname" => "bravo-example.dy.fi"], ]); // Monta tiliä ja myös oma MX-palvelin: dyfi\update("dyfi.status", [ "ipv4" => true, "ipv6" => true, "mx" => "mailserver-example.dy.fi", [ "email" => "mail-1@example.com", "password" => "foo-bar-baz", ["hostname" => "alpha-example.dy.fi"], ["hostname" => "bravo-example.dy.fi"], ], [ "email" => "mail-2@example.com", "password" => "foo-bar-baz", ["hostname" => "charlie-example.dy.fi"], ["hostname" => "delta-example.dy.fi"], ], ]);
Itse koodin voi tallentaa tiedostoon dyfi.php. Koodi ei ole välttämättä kaunista tai viimeisteltyä, mutta se on toiminut jo vuosia ongelmitta.
<?php
namespace dyfi;
const AUTHOR = "Metabolix";
const VERSION = "2019-10-13";
const DEBUG = 0;
function update($status_file, $data) {
$u = new Updater;
$u->run($status_file, $data);
}
class Exception extends \Exception {
}
class IP {
public static function fix($ip) {
return inet_ntop(inet_pton($ip));
}
public static function ipv4_fallback() {
$ip = file_get_contents("http://ip4only.me/api/");
return self::fix(explode(",", $ip)[1] ?? null);
}
public static function ipv6_fallback() {
$ip = file_get_contents("http://ip6only.me/api/");
return self::fix(explode(",", $ip)[1] ?? null);
}
public static function iproute2() {
$s = @shell_exec("ip -o addr 2>/dev/null");
preg_match_all('#inet (?!127\\.|10\\.|192\\.168\\.|172\\.(?:1[6789]|2[0-9]|3[01])\\.)([0-9.]+)(/[0-9]+)? .*scope global#', $s, $m);
$ipv4 = array_map([self::class, "fix"], $m[1])[0] ?? null;
preg_match_all('#inet6 (?!fe[89a-f]|f[cd]|ff[0:]|[0:]+1[ /])([0-9a-f:]+)(/[0-9]+)? scope global#', $s, $m);
$ipv6 = array_map([self::class, "fix"], $m[1])[0] ?? null;
return [$ipv4, $ipv6];
}
public static function find() {
list($ipv4, $ipv6) = self::iproute2();
return [$ipv4 ?: self::ipv4_fallback(), $ipv6 ?: self::ipv6_fallback()];
}
}
class Account {
private $curl;
private $email, $password;
private $status = [];
private function parse($html) {
$this->status = [];
if (preg_match_all('#<.*td-ht-(un)?pointed.*hostid=(\\d+).*>#', $html, $m, PREG_SET_ORDER)) {
foreach ($m as $row) {
$status = [];
$txt = preg_replace('/^|(&[^;]{1,6};|\\s|<[^<>]*>)+|$/', ' ', $row[0]);
$status["hostid"] = (int) $row[2];
$status["unpointed"] = (bool) @$row[1];
$status["hostname"] = preg_match('# (\\S+) #', $txt, $m) ? $m[1] : null;
$status["ipv4"] = preg_match('# (\\d+\\.\\d+\\.\\d+\\.\\d+) #', $txt, $m) ? IP::fix($m[1]) : null;
$status["ipv6"] = preg_match('#IPv6: ([0-9a-f:]+) #', $txt, $m) ? IP::fix($m[1]) : null;
$status["mx"] = preg_match('#MX: (\\S+) #', $txt, $m) ? $m[1] : null;
$status["expires"] = preg_match('#released in: ([0-9dmh ]+) #', $txt, $m) ? self::timeToSeconds($m[1]) : 0;
$status["email"] = $this->email;
$this->status[$status["hostname"]] = $status;
}
}
if (DEBUG) {
echo json_encode($this->status, JSON_PRETTY_PRINT), "\n";
}
}
private static function curl() {
$curl = curl_init();
curl_setopt($curl, CURLOPT_USERAGENT, "PHP");
curl_setopt($curl, CURLOPT_FAILONERROR, 1);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($curl, CURLOPT_FAILONERROR, 1);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_ENCODING, "");
curl_setopt($curl, CURLOPT_COOKIEFILE, "");
return $curl;
}
private function login() {
if ($this->curl) {
return;
}
if (DEBUG) {
echo "login {$this->email}\n";
}
$this->curl = self::curl();
curl_setopt($this->curl, CURLOPT_POST, 1);
curl_setopt($this->curl, CURLOPT_POSTFIELDS, http_build_query(["c" => "login", "submit" => "login", "lang" => "en", "email" => $this->email, "password" => $this->password]));
curl_setopt($this->curl, CURLOPT_URL, "https://www.dy.fi/?");
$this->parse(curl_exec($this->curl));
}
public function refresh() {
$this->login();
curl_setopt($this->curl, CURLOPT_POST, 0);
curl_setopt($this->curl, CURLOPT_URL, "https://www.dy.fi/");
$this->parse(curl_exec($this->curl));
}
private static function timeToSeconds($s) {
$t = 0;
if (preg_match_all('#([0-9]+)(d|h|m)#', $s, $m)) {
foreach ($m[1] as $i => $j) {
$t += $j * ["d" => 86400, "h" => 3600, "m" => 60][$m[2][$i]];
}
}
return $t;
}
public function __construct($email, $password) {
$this->email = $email;
$this->password = $password;
}
public function getStatus() {
$this->login();
return $this->status;
}
private function check($hostname) {
$this->login();
if (empty($this->status[$hostname])) {
throw new Exception("Error: $hostname: No such hostname!");
}
}
public function updateIPv4($hostname, $ipv4 = null, &$result_ipv4) {
$this->check($hostname);
if (DEBUG) {
echo "update $hostname ipv4\n";
$this->status[$hostname]["ipv4"] = $ipv4;
$this->status[$hostname]["expires"] = 7 * 86400;
return;
}
$curl = self::curl();
curl_setopt($curl, CURLOPT_HTTPHEADER, ["Authorization: Basic ".base64_encode($this->email.":".$this->password)]);
curl_setopt($curl, CURLOPT_URL, "https://www.dy.fi/nic/update?hostname=".urlencode($hostname));
$str = curl_exec($curl);
if (!preg_match('/^nochg|^good/', $str)) {
throw new Exception("Error: $hostname: $str");
}
if (preg_match('/^good (\\d+\\.\\d+\\.\\d+\\.\\d+)/', $str, $m) && $ipv4 != $m[1]) {
$result_ipv4 = $m[1];
$this->status[$hostname]["ipv4"] = $result_ipv4;
$this->status[$hostname]["expires"] = 7 * 86400;
}
return true;
}
private function postUpdate($hostname) {
$status = $this->status[$hostname];
curl_setopt($this->curl, CURLOPT_POST, 1);
curl_setopt($this->curl, CURLOPT_POSTFIELDS, http_build_query(["c" => "hopt", "hostid" => $status["hostid"], "aaaa" => $status["ipv6"], "mx" => $status["mx"], "url" => "", "title" => "", "framed" => "", "submit" => "1"]));
curl_setopt($this->curl, CURLOPT_URL, "https://www.dy.fi/?");
$str = curl_exec($this->curl);
if (!preg_match('/updated successfully/', $str)) {
$str = substr(strip_tags($str), 0, 100);
throw new Exception("Error: $hostname: Update failed! $str");
}
$this->parse($str);
return true;
}
public function updateIPv6($hostname, $ipv6) {
$this->check($hostname);
$this->status[$hostname]["ipv6"] = $ipv6;
if (DEBUG) {
echo "update $hostname ipv6\n";
return;
}
return $this->postUpdate($hostname);
}
public function updateMX($hostname, $mx) {
$this->check($hostname);
$this->status[$hostname]["mx"] = $mx;
if (DEBUG) {
echo "update $hostname mx\n";
return;
}
return $this->postUpdate($hostname);
}
}
class Updater {
const EXPIRATION_MARGIN = 12347;
private $accounts;
public function run($status_file, $data) {
// Sanity check: time can't be less than file modification time.
if (time() < filemtime(__FILE__)) {
return false;
}
// Find current IP.
list($ipv4, $ipv6) = IP::find();
if (!$ipv4) {
throw new Exception("IPv4 address is missing!");
}
// Parse targets.
$targets = self::collect($data);
// Get old data.
$old = (object) ["ipv4" => 0, "ipv4_local" => 0, "ipv6" => 0, "expires" => 0, "hosts" => []];
if ($tmp = @json_decode(file_get_contents($status_file), true)) {
foreach ($tmp as $a => $b) if (property_exists($old, $a)) {
$old->$a = $b;
}
}
// Create new status.
$new = (object) ["ipv4" => 0, "ipv4_local" => 0, "ipv6" => 0, "expires" => 0, "hosts" => []];
$new->ipv4_local = $new->ipv4 = $ipv4;
// If local IPv4 has not changed, assume that the external has not changed either.
if ($new->ipv4_local == $old->ipv4_local) {
$new->ipv4 = $old->ipv4;
}
$new->ipv6 = $ipv6;
$new->expires = $old->expires;
// Which hosts will be updated?
$tmp = array_keys($targets);
sort($tmp);
$new->hosts = $tmp;
// Update needed?
if ($old->expires > time() && $old->ipv4 == $new->ipv4 && $old->ipv6 == $ipv6 && $old->hosts == $new->hosts) {
return false;
}
foreach ($targets as $key => $entry) {
$targets[$key]["ipv4"] = $targets[$key]["ipv4"] ? $new->ipv4 : null;
$targets[$key]["ipv6"] = $targets[$key]["ipv6"] ? $new->ipv6 : null;
}
$accounts = [];
foreach ($targets as $entry) {
$accounts[$entry["email"]] = $entry["password"];
}
$status = [];
foreach ($accounts as $email => $password) {
$accounts[$email] = $a = new Account($email, $password);
foreach ($a->getStatus() as $hostname => $entry) {
if (isset($targets[$hostname])) {
$status[$hostname] = $entry;
}
}
}
$update_ipv4 = $update_ipv6 = $update_mx = false;
foreach (array_keys($targets) as $hostname) {
if (empty($status[$hostname])) {
if (DEBUG) {
echo "warning: $hostname not found!\n";
}
unset($targets[$hostname]);
continue;
}
if ($status[$hostname]["ipv4"] != $targets[$hostname]["ipv4"] || $status[$hostname]["expires"] < self::EXPIRATION_MARGIN) {
$update_ipv4 = true;
}
}
foreach (array_keys($targets) as $hostname) {
$account = $accounts[$status[$hostname]["email"]];
if ($update_ipv4) {
$account->updateIPv4($hostname, $targets[$hostname]["ipv4"], $new->ipv4);
}
if ($status[$hostname]["ipv6"] != $targets[$hostname]["ipv6"]) {
$account->updateIPv6($hostname, $targets[$hostname]["ipv6"]);
}
if ($status[$hostname]["mx"] != $targets[$hostname]["mx"]) {
$account->updateMX($hostname, $targets[$hostname]["mx"]);
}
}
// Which hosts were updated?
$tmp = array_keys($targets);
sort($tmp);
$new->hosts = $tmp;
// Get next expiration date.
$expires = 7 * 86400;
foreach ($accounts as $account) {
$account->refresh();
foreach ($account->getStatus() as $entry) {
if (isset($targets[$entry["hostname"]])) {
$expires = min($expires, $entry["expires"]);
}
}
}
$new->expires = time() + $expires - self::EXPIRATION_MARGIN;
$new->expires_str = date("Y-m-d H:i:s", $new->expires);
file_put_contents($status_file, json_encode($new, JSON_PRETTY_PRINT)."\n");
}
private static function collect($data, $targets = [], $entry = []) {
// Find each ["hostname" => "x"] and imbue any upper level properties.
// from: ["a" => 1, [..., ["hostname" => "example.com"]]]
// to: ["example.com" => ["hostname" => "example.com", "a" => 1, ...]]
foreach ($data as $key => $value) if (!is_array($value)) {
$entry[$key] = $value;
}
foreach ($data as $key => $value) if (is_array($value)) {
$targets = self::collect($value, $targets, $entry);
}
if (isset($entry["hostname"])) {
$targets[$entry["hostname"]] = $entry;
}
return $targets;
}
}Mikäli nyt dy.fi vielä jotakuta kiinnostaa, vähän päivitetty versio koodista löytyy GitHubista, päivityksenä tuki komentoriviparametreille ja erilliselle asetustiedostolle, mukana valmiit systemd-tiedostot.
Aihe on jo aika vanha, joten et voi enää vastata siihen.