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.