#!/usr/bin/env php
<?php
# rdt -- recursive DNS lookup tool
#
# (c) 2009-2021 Mantas Mikulėnas <grawity@gmail.com>
# Released under the MIT License <https://spdx.org/licenses/MIT>

error_reporting(-1); // use strict;

function usage() {
	$usage = <<<EOM
Usage: rdt [options] <address>...

Displays forward and reverse DNS of a given address, recursively. The starting
point can be either a domain name or an IPv4 or IPv6 address.

In SRV mode, the address should be "service@domain", e.g. "imaps@example.com",
returning a list of hosts which provide that service (for both TCP and UDP).

Options:
  --[no]-color                  colorize output
  -s, --srv                     perform SRV lookup instead of PTR

EOM;
	fwrite(STDERR, $usage);
}

function warn($msg) {
	fwrite(STDERR, "rdt: $msg\n");
}

function is_inetaddr($a) {
	return @inet_pton($a) !== false;
}

function is_inet4addr($a) {
	return ($p = @inet_pton($a)) !== false and strlen($p) == 4;
}

function is_inet6addr($a) {
	return ($p = @inet_pton($a)) !== false and strlen($p) == 16;
}

# cmp() for IP addresses
function ipcmp($a, $b) {
	$ap = @inet_pton($a);
	$bp = @inet_pton($b);
	if ($ap === false || $bp === false) {
		return 0;
	} else {
		# Sort IPv4 before IPv6, then lexicographically.
		return (strlen($ap) - strlen($bp))
			?: strcmp($ap, $bp);
	}
}

# Convert IP address to .arpa domain for PTR
function to_ptr($ip) {
	$packed = @inet_pton($ip);
	if ($packed === false) {
		return false;
	} elseif (strlen($packed) == 4) {
		$ip = unpack("C*", $packed);
		$suffix = ".in-addr.arpa.";
	} elseif (strlen($packed) == 16) {
		$ip = unpack("H*", $packed);
		$ip = str_split($ip[1]);
		$suffix = ".ip6.arpa.";
	} else {
		return false;
	}
	return implode(".", array_reverse($ip)) . $suffix;
}

# Determine the system's domain name
function getdomain() {
	$dom = getenv("LOCALDOMAIN");
	if ($dom) {
		return $dom;
	}
	$host = gethostname();
	if (!preg_match('/\./', $host)) {
		$addr = gethostbyname($host);
		$host = gethostbyaddr($addr);
		if ($host === $addr) {
			$host = null;
		}
	}
	if ($host && preg_match('/^[^.]+\.(.+)$/', $host, $m)) {
		return $m[1];
	}
	warn("could not determine the system's local domain");
	exit(1);
}

# Do DNS requests.
# - Always return IP addresses sorted by their binary value.
function resolve($addr) {
	$sortfunc = "strcmp";

	if (is_inetaddr($addr)) {
		$addr = to_ptr($addr);
		$rr = @dns_get_record($addr, DNS_PTR);
	} else {
		$rr = @dns_get_record($addr, DNS_CNAME);
		if (empty($rr)) {
			$rr4 = @dns_get_record($addr, DNS_A);
			$rr6 = @dns_get_record($addr, DNS_AAAA);
			$rr = array_merge($rr4 ?: [],
			                  $rr6 ?: []);
			$sortfunc = "ipcmp";
		}
	}

	$addresses = [];
	if (empty($rr))
		return $addresses;
	foreach ($rr as $record) {
		switch ($record["type"]) {
		case "A":
			$addresses[] = $record["ip"];
			break;
		case "AAAA":
			$addresses[] = $record["ipv6"];
			break;
		case "CNAME":
		case "PTR":
			$addresses[] = $record["target"];
			break;
		}
	}
	usort($addresses, $sortfunc);
	return $addresses;
}

function colorize($addr) {
	global $use_color;

	$addr = "$addr";

	if (!$use_color)
		return $addr;

	if ($addr === "(none)")
		$c = "38;5;9";
	elseif ($addr[0] == "_") // SRV name
		$c = "1";
	elseif (preg_match('/^[0-9]+$/', $addr)) // port number
		$c = "38;5;112";
	elseif (is_inet4addr($addr))
		$c = "38;5;175";
	elseif (is_inet6addr($addr))
		$c = "38;5;110";
	else
		$c = "38;5;215";

	return "\033[{$c}m{$addr}\033[m";
}

function go($from, $depth = 0, $skip = [], &$visited = []) {
	$visited[] = $from;

	print str_repeat(" ", $depth*3) . colorize($from) . " = ";

	$addresses = resolve($from);

	if (empty($addresses))
		print colorize("(none)");
	else
		print implode(", ", $addresses);
	print "\n";

	# recursively look up the results
	foreach ($addresses as $addr) {
		$addr = strtolower($addr);
		if (in_array($addr, $visited) or in_array($addr, $skip))
			continue;
		$visited[] = $addr;
		go($addr, $depth+1, $skip+$addresses, $visited);
	}
}

function gosrv($from) {
	if (preg_match('/^(.*)@(.*)$/', $from, $m)) {
		list(, $service, $domain) = $m;
		if (preg_match('/^[^.]+$/', $domain)) {
			$domain .= "." . getdomain();
		}
	} else {
		$service = $from;
		$domain = getdomain();
	}

	if (!strlen($domain)) {
		warn("domain name cannot be empty");
		return false;
	}
	if (!strlen($service)) {
		warn("service name cannot be empty");
		return false;
	}
	if (!preg_match('/^[a-z0-9-]+$/', $service)) {
		warn("service '$service' does not look valid");
		return false;
	}
	switch ($service) {
		case "afsdb": $service = "afs3-vlserver"; break;
		case "kadmin": $service = "kerberos-adm"; break;
	}

	$rrsets = [];
	if ($service == "smtp") {
		warn("service '$service' uses MX instead of SRV");
		foreach ([$domain] as $name) {
			$rrset = dns_get_record($name, DNS_MX);
			if (!$rrset)
				continue;
			foreach ($rrset as &$rr) {
				$rr["weight"] = 0;
				$rr["port"] = 25;
			}
			$rrsets[] = ["Mail exchange on ", $name, $rrset];
		}
	} else {
		foreach (["tcp", "udp", "sctp"] as $proto) {
			$name = "_$service._$proto.$domain";
			$rrset = dns_get_record($name, DNS_SRV);
			if (!$rrset)
				continue;
			$rrsets[] = ["", $name, $rrset];
		}
	}

	$found = 0;
	foreach ($rrsets as [$vis_prefix, $name, $rrset]) {
		usort($rrset, function ($a, $b) {
			return ($a["pri"] - $b["pri"])
				?: -($a["weight"] - $b["weight"])
				?: strcmp($a["target"], $b["target"])
				?: ($a["port"] - $b["port"]);
		});
		if ($found > 0)
			print "\n";
		print $vis_prefix . colorize($name) . " is provided by:\n";
		foreach ($rrset as $rr) {
			if ($rr["target"] == "" && $rr["port"] == 0) {
				print str_repeat(" ", 1*3)
					. colorize("(none)")
					. " (null record found)\n";
				continue;
			}

			print str_repeat(" ", 1*3)
					. "(" . $rr["pri"] . ":"
					. $rr["weight"] . ") "
					. colorize($rr["target"])
					. " port "
					. colorize($rr["port"])
					. " = ";
			$addresses = resolve($rr["target"]);
			if (empty($addresses))
				print colorize("(none)");
			else
				print implode(", ", $addresses);
			print "\n";
		}
		$found++;
	}
	if (!$found) {
		warn("service '$service' on '$domain' not found");
		if ($service == "afs3-vlserver") {
			warn("invoking 'host -t AFSDB' as fallback");
			system("host -t AFSDB ".escapeshellarg($domain));
		}
		return false;
	}
	return true;
}

# on Windows, %TERM% will most likely be unset
# on Unix, posix.so might be disabled, if it is -- check $TERM
if (function_exists("posix_isatty")) {
	$use_color = posix_isatty(STDOUT);
} else {
	$TERM = getenv("TERM");
	$use_color = !($TERM === false or $TERM === "dumb");
}

$do_srv = (basename($argv[0]) == "srv");

$optind = 0;

foreach (getopt("cChs", ["color", "no-color", "help", "srv"], $optind) as $opt => $arg) {
	switch ($opt) {
		case "c":
		case "color":
			$use_color = true; break;
		case "C":
		case "no-color":
			$use_color = false; break;
		case "s":
		case "srv":
			$do_srv = true; break;
		case "h":
		case "help":
			usage();
			exit(0);
		default:
			// Unfortunately getopt() as of 8.0 doesn't indicate anything
			// at all for unknown options, so this is not reached.
			warn("unknown option '$opt'");
			exit(2);
	}
}

$addresses = array_slice($argv, $optind);

if (!count($addresses)) {
	warn("no addresses specified");
	exit(2);
}

foreach ($addresses as $i => $start_addr) {
	if ($i > 0)
		print "\n";

	if ($do_srv)
		gosrv($start_addr);
	else
		go($start_addr);
}
