ctucx.git: dnsmasq-lease-overview

web-overview for dhcp-leases from dnsmasq

commit bfecd0b4ee84fee37cc4e8cbfdc87e4f9bf557d5
Author: Leah (ctucx) <leah@ctu.cx>
Date: Wed, 20 Jan 2021 18:00:38 +0100

init
5 files changed, 490 insertions(+), 0 deletions(-)
A
index.php
|
85
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
lib/Router.php
|
105
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
lib/Template.php
|
162
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
lib/functions.php
|
90
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
templates/overview.tpl
|
48
++++++++++++++++++++++++++++++++++++++++++++++++
diff --git a/index.php b/index.php
@@ -0,0 +1,85 @@
+<?php
+error_reporting(E_ALL);
+ini_set('display_errors', true);
+define('PATH', __DIR__);
+
+require PATH.'/lib/Template.php';
+require PATH.'/lib/Router.php';
+require PATH.'/lib/functions.php';
+
+$tpl = new Template(PATH.'/templates/', []);
+
+Router::add('GET', '/', function() {
+	header("Location: /overview");
+});
+
+Router::add('GET', '/overview', function() {
+	global $tpl;
+	
+	$leases = parseDhcpLeases('/var/lib/misc/dnsmasq.leases');
+
+	foreach ($leases['ipv4'] as $lease4) {
+		$lease6 = NULL;
+		
+		if($lease4['hostname'] !== NULL) {
+			$id = array_search($lease4['hostname'], array_column($leases['ipv6'], 'hostname'));
+
+			if (!is_bool($id) && isset($leases['ipv6'][$id])) {
+				$lease6 = $leases['ipv6'][$id];
+			}
+		}
+
+		$duration = duration($lease4['expiryTimestamp'], time());
+
+		$tpl->blockAssign('leases', [
+			'EXPIRY_TIME'   => $duration['days'].'d '.$duration['hours'].'h '.$duration['minutes'].'m',
+			'MAC_ADDRESS'   => $lease4['macAddress'],
+			'IP4_ADDRESS'   => $lease4['address'],
+			'IP6_ADDRESS'   => $lease6 !== NULL ? $lease6['address'] : '-',
+			'HOSTNAME'      => $lease4['hostname'] ? $lease4['hostname'] : '-',
+			'CLIENT_ID'     => $lease4['dhcpClientId'] ? $lease4['dhcpClientId'] : '-',
+			'HAS_HTTP'	=> isPortOpen($lease4['address'], 80),
+		]);
+	}
+
+	foreach ($leases['ipv6'] as $lease6) {		
+		if($lease6['hostname'] !== NULL) {
+			$id = array_search($lease6['hostname'], array_column($leases['ipv4'], 'hostname'));
+
+			if (!is_bool($id)) {
+				continue;
+			}
+		}
+
+		$duration = duration($lease6['expiryTimestamp'], time());
+
+		$tpl->blockAssign('leases', [
+			'EXPIRY_TIME'   => $duration['days'].'d '.$duration['hours'].'h '.$duration['minutes'].'m',
+			'MAC_ADDRESS'   => '-',
+			'IP4_ADDRESS'   => '-',
+			'IP6_ADDRESS'   => $lease6['address'],
+			'HOSTNAME'      => $lease6['hostname'] ? $lease6['hostname'] : '-',
+			'CLIENT_ID'     => $lease6['dhcpClientId'] ? $lease6['dhcpClientId'] : '-',
+			'HAS_HTTP'      => isPortOpen($lease4['address'], 80),
+		]);
+	}
+
+	$tpl->render('overview', [
+		'PAGE'     => 'DHCP Overview',
+	]);
+});
+
+Router::add('GET', '/overview/json', function() {
+	header('Content-Type: application/json');	
+	echo json_encode(parseDhcpLeases('/var/lib/misc/dnsmasq.leases'));
+});
+
+Router::pathNotFound(function() {
+	header("Loctaion: /");
+});
+
+Router::methodNotAllowed(function() {
+	header("Loctaion: /");
+});
+
+Router::run('/');
diff --git a/lib/Router.php b/lib/Router.php
@@ -0,0 +1,104 @@
+<?php
+
+class Router {
+
+  private static $routes = [];
+  private static $pathNotFound = null;
+  private static $methodNotAllowed = null;
+
+  public static function add($method, $expression, $function){
+    array_push(self::$routes, [
+      'expression' => $expression,
+      'function' => $function,
+      'method' => $method
+    ]);
+  }
+
+  public static function pathNotFound($function){
+    self::$pathNotFound = $function;
+  }
+
+  public static function methodNotAllowed($function){
+    self::$methodNotAllowed = $function;
+  }
+
+  public static function run($basepath = '/'){
+
+    // Parse current url
+    $parsed_url = parse_url($_SERVER['REQUEST_URI']);//Parse Uri
+
+    if(isset($parsed_url['path'])){
+      $path = $parsed_url['path'];
+    }else{
+      $path = '/';
+    }
+
+    // Get current request method
+    $method = $_SERVER['REQUEST_METHOD'];
+
+    $path_match_found = false;
+
+    $route_match_found = false;
+
+    foreach(self::$routes as $route){
+
+      // If the method matches check the path
+
+      // Add basepath to matching string
+      if($basepath!=''&&$basepath!='/'){
+        $route['expression'] = '('.$basepath.')'.$route['expression'];
+      }
+
+      // Add 'find string start' automatically
+      $route['expression'] = '^'.$route['expression'];
+
+      // Add 'find string end' automatically
+      $route['expression'] = $route['expression'].'$';
+
+      // echo $route['expression'].'<br/>';
+
+      // Check path match	
+      if(preg_match('#'.$route['expression'].'#',$path,$matches)){
+
+        $path_match_found = true;
+
+        // Check method match
+        if(strtolower($method) == strtolower($route['method'])){
+
+          array_shift($matches);// Always remove first element. This contains the whole string
+
+          if($basepath!=''&&$basepath!='/'){
+            array_shift($matches);// Remove basepath
+          }
+
+          call_user_func_array($route['function'], $matches);
+
+          $route_match_found = true;
+
+          // Do not check other routes
+          break;
+        }
+      }
+    }
+
+    // No matching route was found
+    if(!$route_match_found){
+
+      // But a matching path exists
+      if($path_match_found){
+        header("HTTP/1.0 405 Method Not Allowed");
+        if(self::$methodNotAllowed){
+          call_user_func_array(self::$methodNotAllowed, Array($path,$method));
+        }
+      }else{
+        header("HTTP/1.0 404 Not Found");
+        if(self::$pathNotFound){
+          call_user_func_array(self::$pathNotFound, Array($path));
+        }
+      }
+
+    }
+
+  }
+
+}+
\ No newline at end of file
diff --git a/lib/Template.php b/lib/Template.php
@@ -0,0 +1,161 @@
+<?php
+
+class Template {
+	public  $vars       = [];
+	public  $blocks     = [];
+	private $pagevars   = [];
+	private $tpl_path   = NULL;
+	private $cache_path = NULL;
+
+	public function __construct ($tpl_path, array $pagevars) {
+		if(!file_exists($tpl_path)){
+			throw new Exception('Error templates folder not found.');
+		} else {
+			$this->tpl_path   = $tpl_path;
+		}
+
+		$this->pagevars = $pagevars;
+	}
+
+	public function assign ($vars, $value = null) {
+		if (is_array($vars)) {
+			$this->vars = array_merge($this->vars, $vars);
+		} else if ($value !== null) {
+			$this->vars[$vars] = $value;
+		}
+	}
+
+	public function blockAssign ($name, $array) {
+		$this->blocks[$name][] = (array)$array;
+	}
+
+	private function compileVars ($var) {
+		$newvar = $this->compileVar($var[1]);
+		return "<?php echo isset(" . $newvar . ") ? " . $newvar . " : '{" . $var[1] . "}' ?>";
+	}
+
+	private function compileVar ($var) {
+		if (strpos($var, '.') === false) {
+			$var = '$this->vars[\'' . $var . '\']';
+		} else {
+			$vars = explode('.', $var);
+			if (!isset($this->blocks[$vars[0]]) && isset($this->vars[$var[0]]) && gettype($this->vars[$var[0]]) == 'array') {
+				$var = '$this->vars[\'' . $vars[0] . '\'][\'' . $vars[1] . '\']';
+			} else {
+				$var = preg_replace("#(.*)\.(.*)#", "\$_$1['$2']", $var);
+			}
+		}
+		return $var;
+	}
+
+	private function compileTags ($match) {
+		switch ($match[1]) {
+			case 'INCLUDE':
+				return "<?php echo \$this->compile('" . $match[2] . "'); ?>";
+				break;
+
+			case 'INCLUDEPHP':
+				return "<?php echo include(" . PATH . $match[2] . "'); ?>";
+				break;
+
+			case 'IF':
+				return $this->compileIf($match[2], false);
+				break;
+
+			case 'ELSEIF':
+				return $this->compileIf($match[2], true);
+				break;
+
+			case 'ELSE':
+				return "<?php } else { ?>";
+				break;
+
+			case 'ENDIF':
+				return "<?php } ?>";
+				break;
+
+			case 'BEGIN':
+				return "<?php if (isset(\$this->blocks['" . $match[2] . "'])) { foreach (\$this->blocks['" . $match[2] . "'] as \$_" . $match[2] . ") { ?>";
+				break;
+
+			case 'BEGINELSE':
+				return "<?php } } else { { ?>";
+				break;
+
+			case 'END':
+				return "<?php } } ?>";
+				break;
+		 }
+	}
+
+	private function compileIf ($code, $elseif) {
+		$ex = explode(' ', trim($code));
+		$code = '';
+
+		foreach ($ex as $value) {
+			$chars = strtolower($value);
+
+			switch ($chars) {
+				case 'and':
+				case '&&':
+				case 'or':
+				case '||':
+				case '==':
+				case '!=':
+				case '!==':
+				case '>':
+				case '<':
+				case '>=':
+				case '<=':
+				case '0':
+				case is_numeric($value):
+					$code .= $value;
+					break;
+
+				case 'not':
+					$code .= '!';
+					break;
+
+				default:
+					if (preg_match('/^[A-Za-z0-9_\-\.]+$/i', $value)) {
+						$var = $this->compileVar($value);
+						$code .= "(isset(" . $var . ") ? " . $var . " : '')";
+					} else {
+						$code .= '\'' . preg_replace("#(\\\\|\'|\")#", '', $value) . '\'';
+					}
+					break;
+			}
+			$code .= ' ';
+		}
+
+		return '<?php ' . (($elseif) ? '} else ' : '') . 'if (' . trim($code) . ") { ?>";
+	}
+
+	private function compile ($file) {
+		$abs_file = $this->tpl_path.'/'.$file;
+
+		$tpl = file_get_contents($abs_file);
+		$tpl = preg_replace("#<\?(.*)\?>#", '', $tpl);
+		$tpl = preg_replace_callback("#<!-- ([A-Z]+) (.*)? ?-->#U", array($this, 'compileTags'), $tpl);
+		$tpl = preg_replace_callback("#{([A-Za-z0-9_\-.]+)}#U", array($this, 'compileVars'), $tpl);
+
+		if (eval(' ?>'.$tpl.'<?php ') === false) {
+			$this->error();
+		}
+	}
+
+	public function error () {
+		exit('Fehler im Template!');
+	}
+
+	public function render ($file, $data = NULL) {
+		$this->assign($this->pagevars);
+
+		if ($data !== NULL) {
+			$this->assign($data);
+		}
+
+		$this->compile($file.'.tpl');
+		exit();
+	}
+}+
\ No newline at end of file
diff --git a/lib/functions.php b/lib/functions.php
@@ -0,0 +1,90 @@
+<?php
+
+function parseDhcpLeases ($file) {
+	$fileHandler = new SplFileObject($file);
+	$leases      = [];
+	$mode        = 'ipv4';
+
+	while(!$fileHandler->eof()) {
+		$rawLine = trim($fileHandler->fgets());
+
+		if ($rawLine == '') continue;
+
+		$rawEntry = explode(' ', $rawLine);
+
+		if ($rawEntry[0] == 'duid') {
+			$leases['duid'] = $rawEntry[1];
+			$mode           = 'ipv6';
+			continue;			
+		}
+		
+		if ($mode !== 'ipv6') {
+			$leases['ipv4'][] = [
+				'expiryTimestamp' => $rawEntry[0],
+				'macAddress'      => $rawEntry[1],
+				'address'         => $rawEntry[2],
+				'hostname'        => ($rawEntry[3] !== '*' ? $rawEntry[3] : NULL),
+				'dhcpClientId'    => ($rawEntry[4] !== '*' ? $rawEntry[4] : NULL)
+			];
+		} else {
+			$leases['ipv6'][] = [
+				'expiryTimestamp' => $rawEntry[0],
+				'iaid'            => $rawEntry[1],
+				'address'         => $rawEntry[2],
+				'hostname'        => ($rawEntry[3] !== '*' ? $rawEntry[3] : NULL),
+				'dhcpClientId'    => ($rawEntry[4] !== '*' ? $rawEntry[4] : NULL)
+			];
+		}
+	}
+
+	$fileHandler = NULL;
+
+	return $leases;
+}
+
+function walk($array, $key, $option) {
+    if( !is_array( $array)) {
+        return false;
+    }
+    foreach ($array as $k => $v) {
+        if($k == $key && is_array($v) && isset($v[$option])){
+            return $v[$option];
+        }
+        $data = walk($v, $key, $option);
+        if($data != false){
+            return $data;        
+        }
+    }
+
+    return false;
+}
+
+function duration($date1, $date2) {
+	$diff = abs($date2 - $date1);
+
+	$years     = floor($diff / (365*60*60*24));
+	$months    = floor(($diff - $years * 365*60*60*24) / (30*60*60*24));
+	$days      = floor(($diff - $years * 365*60*60*24 - $months*30*60*60*24) / (60*60*24));
+	$hours     = floor(($diff - $years * 365*60*60*24 - $months*30*60*60*24 - $days*60*60*24) / (60*60));
+	$minutes   = floor(($diff - $years * 365*60*60*24 - $months*30*60*60*24 - $days*60*60*24 - $hours*60*60) / 60);
+	$seconds   = floor(($diff - $years * 365*60*60*24 - $months*30*60*60*24 - $days*60*60*24 - $hours*60*60 - $minutes*60));
+
+	return [
+		'years'   => $years,
+		'months'  => $months,
+		'days'    => $days,
+		'hours'   => $hours,
+		'minutes' => $minutes,
+		'seconds' => $seconds,
+	];
+}
+
+function isPortOpen($ip, $port) {
+    $fp = @fsockopen($ip, $port, $errno, $errstr, 0.1);
+    if (!$fp) {
+        return false;
+    } else {
+        fclose($fp);
+        return true;
+    }
+}
diff --git a/templates/overview.tpl b/templates/overview.tpl
@@ -0,0 +1,48 @@
+<html>
+	<head>
+		<title>{PAGE}</title>
+		<!-- Google Fonts -->
+		<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
+		<!-- CSS Reset -->
+		<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.css">
+		<!-- Milligram CSS -->
+		<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/milligram/1.4.1/milligram.css">
+	</head>
+	<body>
+		<main class="wrapper">
+			<div class="container">
+				<div class="row">
+					<div class="column column-80 column-offset-10">
+						<h2>DHCP Leases</h2>
+						<table>
+							<thead>
+								<tr>
+									<th>Hostname</th>
+									<th>IPv4-Adresse</th>
+									<th>IPv6-Adresse</th>
+									<th>MAC-Adresse</th>
+									<th>Gültig bis</th>
+      							</tr>
+      						</thead>
+      						<tbody>
+      							<!-- BEGIN leases -->
+      							<tr>
+									<!-- IF leases.HAS_HTTP != true -->
+									<td><a href="http://{leases.HOSTNAME}">{leases.HOSTNAME}</a></td>
+									<!-- ELSE -->
+									<td>{leases.HOSTNAME}</td>
+									<!-- ENDIF -->
+									<td>{leases.IP4_ADDRESS}</td>
+									<td>{leases.IP6_ADDRESS}</td>
+									<td>{leases.MAC_ADDRESS}</td>
+									<td>{leases.EXPIRY_TIME}</td>
+								</tr>
+								<!-- END leases -->
+							</tbody>
+						</table>
+					</div>
+				</div>
+			</div>
+		</main>
+	</body>
+</html>