Kirjautuminen

Haku

Tehtävät

Keskustelu: Koodit: PHP: *.dy.fi-osoitteen päivitys (IPv4, IPv6, MX)

Metabolix [13.10.2019 20:56:29]

#

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;
	}
}

Metabolix [21.01.2024 19:11:17]

#

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.

Vastaus

Aihe on jo aika vanha, joten et voi enää vastata siihen.

Tietoa sivustosta