commit 72781d18c7f591dc0d166a5b1972645a10630313
parent 84c47b17361fd3273f5d7902a407d467ae937cfd
Author: Leah (ctucx) <git@ctu.cx>
Date: Sun, 11 Dec 2022 16:15:14 +0100
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(-)
D
|
162
-------------------------------------------------------------------------------
D
|
90
-------------------------------------------------------------------------------
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>