#!/usr/bin/php
<?php
	error_reporting(0);
	$starttime = explode(' ', microtime());
	$starttime =  $starttime[1] + $starttime[0];

	$rpckey = "yourrpckeyhere";
	$rpcurl = "https://dronebl.org/RPC2";

	define('DR_VERSION', '1.7.7');

	function showhelp()
	{
		echo 	'-----------------------------------------------------------------'.PHP_EOL.
				' DroneBL reporter v'.DR_VERSION.' <outsider@scarynet.org>'.PHP_EOL.
				'-----------------------------------------------------------------'.PHP_EOL.
				' Usage: '.$_SERVER['argv'][0].' <options> [<ip[:port]> ...]'.PHP_EOL.PHP_EOL.
				' <options> are:'.PHP_EOL.PHP_EOL.
				'  -i <file>		: get data from specified file.'.PHP_EOL.
				'			: by default data is read from stdin.'.PHP_EOL.
				'  -t <type>		: report as certain type (default: 6).'.PHP_EOL.
				'			: if no type specified after -t, show list.'.PHP_EOL.
				'  -c <comment>		: specify a quoted comment. ex: -c "the reason"'.PHP_EOL.
				'  -v			: also show statistics.'.PHP_EOL.
				'  -q			: turn quiet operation on.'.PHP_EOL.
				'  --help		: show this help and exit.'.PHP_EOL.
				'  --nofilter		: skip existing entry checking.'.PHP_EOL.
				'  --noreport		: do not report result to dronebl (test mode).'.PHP_EOL.
				'  --bs <number>	: force batchsize to <number> (min: 1, max: 1000, default: 50).'.PHP_EOL.
				'  --debug		: show return data and debug information.'.PHP_EOL;
	}

	function getArgs()
	{
		$args = $_SERVER['argv'];
		$validchars = 'a-zA-Z0-9\-_';
		$out = [];
		$last_arg = null;
		$counter = 0;
		$singlequote = false;
		$doublequote = false;
		for($i=1,$il=sizeof($args); $i < $il; $i++)
		{
			if(preg_match('/^[-]{1,2}(['.$validchars.']+)/', $args[$i], $match) && $last_arg === null)
			{
				$parts = explode('=', $match[1]);
				$key = preg_replace('/[^'.$validchars.']+/', '', $parts[0]);
				$counter++;
				switch($key)
				{
					case 'v':
					case 'q':
					case 'help':
					case 'nofilter':
					case 'noreport':
					case 'debug':
						$out[$key] = true;
						$last_arg = null;
						continue;
					case 'i':
					case 'c':
					case 'bs':
						if (!isset($parts[1]) && preg_match('/^[-]{1,2}['.$validchars.']+/', $args[$i+1]))
							continue;
					case 't':
						if (isset($parts[1]))
							$out[$key] = $parts[1];
						else if (!preg_match('/^[-]{1,2}['.$validchars.']+/', $args[$i+1]) && isset($args[$i +1]))
							$last_arg = $key;
						else
							$out[$key] = true;
						break;	
				}
			}
			else if($last_arg !== null)
			{
				$out[$last_arg] = $args[$i];
				$last_arg = null;
			}
			else
				break;
		}
		$out['iplist'] = array_slice($args, $i);

		return $out;
	}

	function parseXML($data)
	{
		$out = [];
		$error = false;

		preg_match_all('/<([^>]+)>/', $data, $matches);

		foreach($matches[1] as $line)
		{	
			if (preg_match('/^\?/', $line) || preg_match('!^/!', $line))
				continue;

			list($tag, $attr) = explode(' ', $line, 2);
			if (substr($attr, -1) == '/')
				$attr = trim(substr($attr, 0, -1));

			switch($tag)
			{
				case 'response':
					if (stristr('error', $attr))
						$error = true;
					break;
				default:
					if ($error)
						preg_match_all('/([.^>]+)>([.^<]+)</', $attr, $split, PREG_SET_ORDER);
					else
						preg_match_all('/([a-z^\=]+)\="([^"]+)/', $attr, $split, PREG_SET_ORDER);
					if ($split == array([],[]))
						continue;

					$tmp = [];
					foreach($split as $item)
						$tmp[$item[1]] = $item[2];
					$out[$tag][] = $tmp;
			}
		}
		return $out;
	}

	function validpair($ip, $port)
	{
		global $quiet;
		if (!filter_var($ip, FILTER_VALIDATE_IP))
		{
			echo (!$quiet ? 'Invalid ip: '.$ip.PHP_EOL : '');
			return false;
		}

		if (intval($port) < 1 || intval($port) > 65535)
		{
			echo (!$quiet ? 'Invalid port: '.$port.PHP_EOL : '');
			return false;
		}
		return true;
	}

	function postData($data)
	{
		global $rpcurl, $debug;
		$opts = [
			'http' => [
				'method'  	=> 'POST',
				'header'  	=> [
					'Content-type: application/x-www-form-urlencoded',
					'Connection: close'
				],
				'user-agent'	=> 'dronereport v'.DR_VERSION,
				'content' 	=> DR_XMLHEADER.$data.DR_XMLFOOTER
			]
		];

		$context  = stream_context_create($opts);
		if ($debug) echo '[DEBUG] Post request: '.PHP_EOL.DR_XMLHEADER.$data.DR_XMLFOOTER.PHP_EOL;
		$result = file_get_contents($rpcurl, false, $context);
		if ($debug) echo '[DEBUG] Post result: '.PHP_EOL.$result.PHP_EOL;
		return $result;
	}

	function getTypes()
	{
		global $rpctypes;
		if (!empty($rpctypes))
			return;
		$types = parseXML(postData(PHP_TAB.'<typelist />'.PHP_EOL));
		foreach ($types['typelist'] AS $type)
			$rpctypes[(int)$type['type']] = (string)$type['description'];
	}

	function parseResult($result)
	{
		global $quiet, $verbose;
		if (!$quiet || !$verbose)
			return;
		$data = parseXML($result);
		foreach ($data['warning'] AS $line)
			echo ' [W] '.$line['data'].PHP_EOL;
	}

	define('PHP_TAB', "\t");
	$rpctypes = array();
	$errors = 0;

	$comms = getArgs();
	$comment = isset($comms['c']) ? strip_tags($comms['c']) : false;
	$debug = isset($comms['debug']);
	$filter = !isset($comms['nofilter']);
	$noreport = isset($comms['noreport']);
	$quiet = isset($comms['q']);
	$verbose = isset($comms['v']);

	define('DR_XMLHEADER', '<?xml version="1.0" encoding="UTF-8" ?>'.PHP_EOL.'<request key="'.$rpckey.'"'.($noreport?' staging="1"':'').($debug?' debug="1"':'').'>'.PHP_EOL);
	define('DR_XMLFOOTER', '</request>'.PHP_EOL);

	if (isset($comms['help']))
	{
		showhelp();
		exit;
	}

	if (isset($comms['t']))
	{
		getTypes();
		if (is_numeric($comms['t']))
		{
			$type = intval($comms['t']);
			if (isset($rpctypes[$type]))
				$reportas = intval($comms['t']);
			else
				die('Invalid type entered');
		}
		else
		{
			echo 'Possible types are:'.PHP_EOL.PHP_EOL;
			echo '  Type:'.PHP_TAB.PHP_TAB.'Description:'.PHP_EOL.'  -----'.PHP_TAB.PHP_TAB.'------------'.PHP_EOL;
			foreach (array_keys($rpctypes) AS $types)
				echo '  '.$types.PHP_TAB.PHP_TAB.$rpctypes[$types].PHP_EOL;
			exit;
		}
	}
	else
		$reportas = 6;

	if (isset($comms['bs']))
	{
		$bs = intval($comms['bs']);
		if ($bs < 1)
		{
			echo (!$quiet ? 'Min batchsize is 1'.PHP_EOL : '');
			$bs = 1;
		}
		else if ($bs > 1000)
		{
			echo (!$quiet ? 'Max batchsize is 1000'.PHP_EOL : '');
			$bs = 1000;
		}
	}
	else
		$bs = 50;

	if (isset($comms['i']))
	{
		if (file_exists($comms['i']))
			$iplist = file_get_contents($comms['i']);
		else
		{
			echo '-i requires a valid input file to be specified.'.PHP_EOL;
			$errors++;
		}
	}
	else if (isset($comms['iplist']) && !empty($comms['iplist']))
		$iplist = implode(PHP_EOL, $comms['iplist']);
	else
	{
		$fd = fopen('php://stdin', 'rb'); 
		while(!feof($fd)) 
			$iplist .= fread($fd, 4096);
		fclose($fd);
	}

	if ($errors > 0)
	{
		echo (!$quiet ? 'Errors detected in commandline. Please fix and try again.'.PHP_EOL : '');
		exit;
	}

	/*
		We do support the following formats:
		- ipv4: 				1.2.3.4
		- ipv4 with port: 		1.2.3.4:1234
		- ipv6: 				1:2:3:4:5:6:7:8
		- ipv6 with port: 		[1:2:3:4:5:6:7:8]:1234
		- hostname:				some.host.net
		- hostname with port:	some.host.net:1234
	*/

	$buffer = array();
	$resolve = 0;
	foreach(explode(PHP_EOL, $iplist) AS $line)
	{
		$line = trim($line);

		if (empty($line))
			continue;

		if (filter_var($line, FILTER_VALIDATE_IP))
		{
			if (!isset($buffer[$line]))
				$buffer[$line] = array();
		}
		else if (preg_match('/^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}:[0-9]{1,5})$/', $line, $match))
		{
			list($ip, $port) = explode(':', $match[0]);
			if (!validpair($ip, $port))
				continue;
			if (!isset($buffer[$ip]))
				$buffer[$ip] = array($port);
			else if(!in_array($port, $buffer[$ip]))
				$buffer[$ip][] = $port;
		}
		else if (preg_match('/^\[([0-9a-f:]+)\]:([0-9]{1,5})$/', $line, $match))
		{
			$ip = $match[0];
			$port = $match[1];
			if (!validpair($ip, $port))
				continue;
			if (!isset($buffer[$ip]))
				$buffer[$ip] = array($port);
			else if(!in_array($port, $buffer[$ip]))
				$buffer[$ip][] = $port;
		}
		else
		{
			if (stristr($line, ':'))
			{
				list($host, $port) = explode(':', $line, 2);
				if (!validpair('127.0.0.1', $port))
					continue;
			}
			else
				$host = $line;

			$resolve++;
			$result = dns_get_record($host, DNS_A + DNS_AAAA);
			if (!result)
			{
				echo (!$quiet ? 'Unresolvable host: '.$host.PHP_EOL : '');
				continue;
			}

			foreach ($result AS $dnsreply)
			{
				$returntypes = ['ip', 'ipv6'];
				foreach ($returntypes AS $rt)
				{
			 		if (isset($dnsreply[$rt]))
					{
						$ip = $dnsreply[$rt];
						if (!isset($buffer[$ip]))
							$buffer[$ip] = isset($port) ? array($port) : array();
						else if(isset($port) && !in_array($port, $buffer[$ip]))
							$buffer[$ip][] = $port;
					}
				}
			}
		}
	}

	if (!empty($buffer) && $filter)
	{
		echo (!$quiet && $verbose ? 'Filtering '.count($buffer).' results...'.PHP_EOL : '');
		$counter = 0;

		$data = '';
		$existing = 0;
		foreach (array_keys($buffer) AS $ip)
		{
			$data .= PHP_TAB.'<lookup ip="'.$ip.'" listed="1" limit="1" />'. PHP_EOL;
			$counter++;
			if ($counter == $bs)
			{
				$result = postData($data);
				$data = parseXML($result);
				foreach ($data['result'] AS $line)
				{
					$existing++;
					echo (!$quiet && $verbose ? '[LISTED] '.(string)$line['ip'].PHP_EOL : '');
					unset($buffer[(string)$line['ip']]);
					$counter = 0;
					$data = '';
				}
			}
		}
		if (!empty($data))
		{
			$result = postData($data);
			$data = parseXML($result);
			foreach ($data['result'] AS $line)
			{
				$existing++;
				echo (!$quiet && $verbose ? '[LISTED] '.(string)$line['ip'].PHP_EOL : '');
				unset($buffer[(string)$line['ip']]);
			}
		}
		echo (!$quiet && $verbose ? PHP_EOL : '');
	}

	echo (!$quiet && $verbose ?
		'[RESULTS]'.PHP_EOL.
		'Count of lines in input : '.count(explode(PHP_EOL, $iplist)).PHP_EOL.
		'Host DNS Lookups        : '.$resolve.PHP_EOL.
		($filter ? 'Count of existing ips   : '.$existing.PHP_EOL : '').
		'Count of ips to report  : '.count($buffer).PHP_EOL.PHP_EOL : '');

	if (empty($buffer))
		echo (!$quiet ? 'No entries found needing to report.'.PHP_EOL : '');
	else
	{
		echo (!$quiet && $verbose ? 'Reporting results...'.PHP_EOL : '');
		$counter = 0;
		$reported = 0;

		$data = '';
		foreach (array_keys($buffer) AS $ip)
		{
			if (empty($buffer[$ip]))
			{
				$data .= PHP_TAB.'<add ip="'.$ip.'" type="'.$reportas.'"'.($comment ? ' comment="'.$comment.'"' : '').' />'.PHP_EOL;
				$reported++;
			}
			else
			{
				foreach ($buffer[$ip] AS $port)
				{
					$data .= PHP_TAB.'<add ip="'.$ip.'" port="'.$port.'" type="'.$reportas.'"'.($comment ? ' comment="'.$comment.'"' : '').' />'.PHP_EOL;
					$reported++;
				}
			}

			if (++$counter == $bs)
			{
				parseResult(postData($data));
				$counter = 0;
				$data = '';
			}
		}
		if (!empty($data))
			parseResult(postData($data));

		echo (!$quiet ? 'Reported '.$reported.' entr'.($reported === 1 ? 'y' : 'ies').' ('.count($buffer).' ip'.(count($buffer) !== 1 ? '\'s' : '').' to DroneBL.'.PHP_EOL : '');
	}
	$mtime = explode(' ', microtime());
	echo (!$quiet && $verbose ? 'Total processing time: '.sprintf('%.3f', $mtime[0]+$mtime[1]-$starttime).' seconds.'.PHP_EOL : '');
?>
