ctucx.git: dnsmasq-lease-overview

web-overview for dhcp-leases from dnsmasq

commit 72781d18c7f591dc0d166a5b1972645a10630313
parent 84c47b17361fd3273f5d7902a407d467ae937cfd
Author: Leah (ctucx) <git@ctu.cx>
Date: Sun, 11 Dec 2022 16:15:14 +0100

rewrite in nim
13 files changed, 298 insertions(+), 490 deletions(-)
A
.gitignore
|
1
+
A
dnsmasqLeasesOverview.nimble
|
16
++++++++++++++++
A
flake.lock
|
43
+++++++++++++++++++++++++++++++++++++++++++
A
flake.nix
|
56
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
D
index.php
|
85
-------------------------------------------------------------------------------
D
lib/Router.php
|
105
-------------------------------------------------------------------------------
D
lib/Template.php
|
162
-------------------------------------------------------------------------------
D
lib/functions.php
|
90
-------------------------------------------------------------------------------
A
src/overview.nim
|
66
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/templates/overview.tpl
|
60
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/types.nim
|
15
+++++++++++++++
A
src/utils.nim
|
41
+++++++++++++++++++++++++++++++++++++++++
D
templates/overview.tpl
|
48
------------------------------------------------
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1 @@
+result
diff --git a/dnsmasqLeasesOverview.nimble b/dnsmasqLeasesOverview.nimble
@@ -0,0 +1,15 @@
+# Package
+
+version       = "0.1.0"
+author        = "Leah(ctucx)"
+description   = "web-overview for dhcp-leases from dnsmasq"
+license       = "AGPL-3.0"
+srcDir        = "./src"
+bin           = @["overview"]
+
+
+
+# Dependencies
+
+requires "nim >= 0.20.0"
+requires "mustache == 0.4.3"+
\ No newline at end of file
diff --git a/flake.lock b/flake.lock
@@ -0,0 +1,43 @@
+{
+  "nodes": {
+    "flake-utils": {
+      "locked": {
+        "lastModified": 1667395993,
+        "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1670625113,
+        "narHash": "sha256-3XuCP1b8U0/rzvQciowoM6sZjtq7nYzHOFUcNRa0WhY=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "e8ec26f41fd94805d8fbf2552d8e7a449612c08e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixos-22.11",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "flake-utils": "flake-utils",
+        "nixpkgs": "nixpkgs"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
diff --git a/flake.nix b/flake.nix
@@ -0,0 +1,55 @@
+{
+  description = "web-overview for dhcp-leases from dnsmasq";
+
+  inputs = {
+    flake-utils.url = "github:numtide/flake-utils";
+    nixpkgs.url     = "github:NixOS/nixpkgs/nixos-22.11";
+  };
+
+  outputs = { self, nixpkgs, flake-utils }: {
+
+    overlay = final: prev: {
+
+      dnsmasq-lease-overview = (
+        let
+          nim-mustache = final.fetchFromGitHub {
+            owner  = "soasme";
+            repo   = "nim-mustache";
+            rev    = "v0.4.3";
+            sha256 = "sha256-rrmKSb422YALxg0nV8rjTNgLecJAM8jvg8tnbvSa9SY";
+          };
+
+        in final.nimPackages.buildNimPackage {
+          name        = "dnsmasq-lease-overview";
+          src         = self;
+
+          buildInputs = [ nim-mustache ];
+
+          nimFlags    = [ "--showAllMismatches:on" ];
+          nimBinOnly  = true;
+          nimRelease  = true;
+        }
+      );
+
+    };
+
+  } // (flake-utils.lib.eachDefaultSystem (system:
+    let
+      pkgs = import nixpkgs {
+        inherit system;
+        overlays = [ self.overlay ];
+      };
+
+    in rec {
+
+      packages.default                = pkgs.dnsmasq-lease-overview;
+      packages.dnsmasq-lease-overview = pkgs.dnsmasq-lease-overview;
+
+      apps.default = {
+        type = "app";
+        program = "${pkgs.dnsmasq-lease-overview}/bin/overview";
+      };
+
+    }
+  ));
+}+
\ No newline at end of file
diff --git a/index.php b/index.php
@@ -1,85 +0,0 @@
-<?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/dnsmasq/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/dnsmasq/dnsmasq.leases'));
-});
-
-Router::pathNotFound(function() {
-	header("Loctaion: /");
-});
-
-Router::methodNotAllowed(function() {
-	header("Loctaion: /");
-});
-
-Router::run('/');
diff --git a/lib/Router.php b/lib/Router.php
@@ -1,104 +0,0 @@
-<?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
@@ -1,161 +0,0 @@
-<?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
@@ -1,90 +0,0 @@
-<?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/src/overview.nim b/src/overview.nim
@@ -0,0 +1,66 @@
+import std/[json, options, tables, sequtils, parseutils]
+import std/[marshal, cgi, os, httpclient, times]
+
+import mustache
+
+import types, utils
+
+const
+  templateOverview = staticRead "templates/overview.tpl"
+
+proc main() =
+  var leaseFile = "./dnsmasq.lease"
+
+  if getEnv("LEASE_PATH") != "":
+    leaseFile = getEnv("LEASE_PATH")
+
+  if not fileExists(leaseFile):
+    echo "Lease file not found!"
+    quit()
+
+  let (duid, leases) = parseLeaseFile(leaseFile)
+
+  case getRequestURI()
+    of "":
+      stdout.write("Content-type: text/plain\n\n")
+      stdout.write("Not called from cgi?")
+
+    of "/":
+      var tplLeases = newSeq[Table[string, string]]()
+
+      for lease in leases:
+        var element : Table[string, string];
+
+        if not lease.hostname.isSome:
+          element["HOSTNAME"] = "-"
+        else:
+          element["HOSTNAME"] = lease.hostname.get
+
+        if lease.type == v4:
+          element["MAC_ADDRESS"] = lease.macAddress
+          element["IP4_ADDRESS"] = lease.address
+          element["IP6_ADDRESS"] = "-"
+
+        else:
+          element["MAC_ADDRESS"] = "-"
+          element["IP4_ADDRESS"] = "-"
+          element["IP6_ADDRESS"] = lease.address
+
+        let time = (lease.expiry - now()).toParts
+        element["EXPIRY_TIME"] = $time[Days] & "d " & $time[Hours] & "h " & $time[Minutes] & "m"
+
+        tplLeases.add(element)
+
+      let context = newContext()
+
+      context["PAGE"]   = "Overview"
+      context["LEASES"] = tplLeases
+
+      stdout.write("Content-type: text/html\n\n")
+      stdout.write(templateOverview.render(context))
+
+    of "/json":
+      stdout.write("Content-type: application/json\n\n")
+      stdout.write($$leases)
+
+main()
diff --git a/src/templates/overview.tpl b/src/templates/overview.tpl
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>{{PAGE}}</title>
+		<meta charset="utf-8">
+		<style>
+			body {
+				font-family: system-ui;
+			}
+
+			main {
+				margin-left: auto;
+				margin-right: auto;
+				max-width: 55em;
+			}
+
+			table, th, td {
+				border: 1px solid #ddd;
+				border-collapse: collapse;
+				padding: 5px;
+			}
+
+			table {
+				width: 100%;
+			}
+		</style>
+	</head>
+	<body>
+		<main>
+			<h2>DHCP Leases</h2>
+			<table>
+				<thead>
+					<tr>
+						<th>Hostname</th>
+						<th>IPv4 address</th>
+						<th>IPv6 address</th>
+						<th>MAC address</th>
+						<th>Expires in</th>
+					</tr>
+				</thead>
+				<tbody>
+					{{#LEASES}}
+					<tr>
+						{{#HAS_HTTP}}
+						<td><a href="http://{{HOSTNAME}}">{{HOSTNAME}}</a></td>
+						{{/HAS_HTTP}}
+						{{^HAS_HTTP}}
+						<td>{{HOSTNAME}}</td>
+						{{/HAS_HTTP}}
+						<td>{{IP4_ADDRESS}}</td>
+						<td>{{IP6_ADDRESS}}</td>
+						<td>{{MAC_ADDRESS}}</td>
+						<td>{{EXPIRY_TIME}}</td>
+					</tr>
+					{{/LEASES}}
+				</tbody>
+			</table>
+		</main>
+	</body>
+</html>
diff --git a/src/types.nim b/src/types.nim
@@ -0,0 +1,15 @@
+import std/[options, times]
+
+type
+  Type* = enum
+    v4, v6
+  Lease* = object
+    expiry*       : DateTime
+    address*      : string
+    hostname*     : Option[string]
+    clientId*     : Option[string]
+    case `type`*  : Type
+    of v4:
+      macAddress* : string
+    of v6:
+      iaid*       : string
diff --git a/src/utils.nim b/src/utils.nim
@@ -0,0 +1,41 @@
+
+import std/[strutils, options, times]
+import types
+
+proc parseLeaseFile* (file: string): (string, seq[Lease]) = 
+  var
+    mode   : Type = v4
+    leases : seq[Lease]
+    duid   : string
+
+  for line in file.lines:
+    if line == "": continue
+
+    let fields = line.split(" ")
+
+    if fields[0] != "duid":
+      if mode != v6:
+        leases.add(Lease(
+          type       : v4,
+          expiry     : fields[0].parseInt.fromUnix.inZone(local()),
+          macAddress : fields[1],
+          address    : fields[2],
+          hostname   : if fields[3] != "*": some(fields[3]) else: none(string),
+          clientId   : if fields[4] != "*": some(fields[4]) else: none(string)
+        ))
+
+      else:
+        leases.add(Lease(
+          type      : v6,
+          expiry    : fields[0].parseInt.fromUnix.inZone(local()),
+          iaid      : fields[1],
+          address   : fields[2],
+          hostname  : if fields[3] != "*": some(fields[3]) else: none(string),
+          clientId  : if fields[4] != "*": some(fields[4]) else: none(string)
+        ))
+
+    else:
+      duid = fields[1]
+      mode = v6
+
+  return (duid, leases)
diff --git a/templates/overview.tpl b/templates/overview.tpl
@@ -1,48 +0,0 @@
-<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>