From ed96875322835e1265fceee2023a221029675c4e Mon Sep 17 00:00:00 2001 From: Akkadius Date: Sat, 2 Sep 2017 04:44:39 -0500 Subject: [PATCH] Realtime dhcp log watching via websockets --- app.js | 180 ++++++++++++++++++++++++++--- bin/www | 68 +++++------ lib/lease_parser.js | 2 +- lib/render_template.js | 2 +- public/assets/js/file-saver.js | 2 + public/assets/js/page-dhcp-logs.js | 82 +++++++++++++ public/templates/dhcp_log.html | 39 +++++++ public/templates/index.html | 12 +- routes/dhcp_log.js | 29 +++++ routes/glass_settings.js | 3 + 10 files changed, 364 insertions(+), 55 deletions(-) create mode 100644 public/assets/js/file-saver.js create mode 100644 public/assets/js/page-dhcp-logs.js create mode 100644 public/templates/dhcp_log.html create mode 100644 routes/dhcp_log.js diff --git a/app.js b/app.js index 42dc590..2d02593 100644 --- a/app.js +++ b/app.js @@ -9,7 +9,7 @@ var app = express(); /* Read Config */ var json_file = require('jsonfile'); -glass_config = json_file.readFileSync('config/glass_config.json'); +var glass_config = json_file.readFileSync('config/glass_config.json'); // uncomment after placing your favicon in /public //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); @@ -25,6 +25,7 @@ app.use('/users', require('./routes/users')); app.use('/get_dashboard', require('./routes/dashboard')); app.use('/get_stats', require('./routes/get_stats')); app.use('/dhcp_leases', require('./routes/dhcp_leases')); +app.use('/dhcp_log', require('./routes/dhcp_log')); app.use('/api_examples', require('./routes/api_examples')); app.use('/glass_settings', require('./routes/glass_settings')); app.use('/glass_settings_save', require('./routes/glass_settings_save')); @@ -37,33 +38,43 @@ app.set('view engine', 'html'); // catch 404 and forward to error handler app.use(function(req, res, next) { - var err = new Error('Not Found'); - err.status = 404; - next(err); + var err = new Error('Not Found'); + err.status = 404; + next(err); }); // error handler app.use(function(err, req, res, next) { - // set locals, only providing error in development - res.locals.message = err.message; - res.locals.error = req.app.get('env') === 'development' ? err : {}; + // set locals, only providing error in development + res.locals.message = err.message; + res.locals.error = req.app.get('env') === 'development' ? err : {}; - // render the error page - res.status(err.status || 500); - res.send(err.message); + // render the error page + res.status(err.status || 500); + res.send(err.message); }); module.exports = app; -/* Tail leases file */ +/** + * Global Variables + */ +cpu_utilization = 0; +total_leases = 0; + current_time = 0; leases_per_second = 0; current_leases_per_second = 0; leases_last_update_time = 0; +listening_to_log_file = 0; + options = {}; options.interval = 1000; +/** + * Ingest Current Lease File + */ var lease_parser = require('./lib/lease_parser.js'); dhcp_lease_data = {}; lease_read_buffer = ""; @@ -73,11 +84,14 @@ fs.readFile(glass_config.leases_file, 'utf8', function (err,data) { if (err) { return console.log(err); } - - lease_parser.parse(data); - // console.log(JSON.stringify(dhcp_lease_data, null, 2)); + else { + lease_parser.parse(data); + } }); +/** + * Leases File Listener + */ var tail_module = require('always-tail'); tail = new tail_module( glass_config.leases_file, @@ -109,9 +123,15 @@ tail.on("line", function(data) { } }); -/* Globals */ -cpu_utilization = 0; -total_leases = 0; +/** + * Watch DHCP Log File + */ + +var json_file = require('jsonfile'); +var glass_config = json_file.readFileSync('config/glass_config.json'); + +var options = {}; +options.interval = 1000; dashboard_timer = setInterval(function(){ // console.log("Checking timers..."); @@ -126,4 +146,128 @@ dashboard_timer = setInterval(function(){ lease_clean_timer = setInterval(function(){ lease_parser.clean(); -}, (60 * 1000)); \ No newline at end of file +}, (60 * 1000)); + + + +function get_socket_clients_connected_count() { + wss.clients.forEach(function each(client) { + if (client.readyState === WebSocket.OPEN) { + socket_clients++; + } + }); + return socket_clients; +} + +/** + * Websocker Server + */ + +const WebSocket = require('ws'); +const wss = new WebSocket.Server({ port: 8080 }); + + +options.interval = 100; +var tail_dhcp_log = new tail_module( + glass_config.log_file, + "\n", + options +); + +tail_dhcp_log.on("line", function(data) { + if(listening_to_log_file) { + // console.log(data); + wss.broadcast_event(data, 'dhcp_log_subscription'); + } +}); + +wss.on('connection', function connection(ws) { + socket_clients++; + console.log("[WS] CLIENT_CONNECT: Socket clients (" + socket_clients + ")"); + + if (!listening_to_log_file) { + /* Watch log file for new information */ + var tail_module = require('always-tail'); + + listening_to_log_file = 1; + } + +}); + +wss.on('close', function close() { + socket_clients--; + console.log("[WS] CLIENT_DISCONNECT: Socket clients (" + socket_clients + ")"); +}); + +function heartbeat() { + this.isAlive = true; +} + +function isJson(str) { + try { + JSON.parse(str); + } catch (e) { + return false; + } + return true; +} + +wss.on('connection', function connection(ws) { + ws.isAlive = true; + ws.on('pong', heartbeat); + ws.event_subscription = []; + + ws.on('message', function incoming(data) { + if(data != "" && isJson(data)) { + var json = JSON.parse(data); + if(typeof json["event_subscription"] !== "undefined"){ + console.log("[WS] Incoming Subscription '%s'", json['event_subscription']); + ws.event_subscription[json["event_subscription"]] = 1; + } + if(typeof json["event_unsubscribe"] !== "undefined"){ + console.log("[WS] event_unsubscribe '%s'", json['event_unsubscribe']); + delete ws.event_subscription[json["event_unsubscribe"]]; + } + } + }); + + stale_connections_audit(); +}); + +wss.broadcast = function broadcast(data) { + wss.clients.forEach(function each(client) { + if (client.readyState === WebSocket.OPEN) { + client.send(data); + } + }); +}; + +wss.broadcast_event = function broadcast(data, event) { + wss.clients.forEach(function each(client) { + if (client.readyState === WebSocket.OPEN) { + if(client.event_subscription[event]) + client.send(data); + } + }); +}; + +function stale_connections_audit() { + socket_clients = 0; + wss.clients.forEach(function each(ws) { + if (ws.isAlive === false) return ws.terminate(); + + ws.isAlive = false; + ws.ping('', false, true); + + socket_clients++; + }); + + console.log("[WS] STATUS: Socket clients (" + socket_clients + ")"); +} + +/* Keepalive - kill stale connections (30s poll) */ +const interval = setInterval(function ping() { + stale_connections_audit(); +}, 30000); + +var socket_clients = 0; \ No newline at end of file diff --git a/bin/www b/bin/www index 6967d4f..398b171 100644 --- a/bin/www +++ b/bin/www @@ -34,19 +34,19 @@ server.on('listening', onListening); */ function normalizePort(val) { - var port = parseInt(val, 10); + var port = parseInt(val, 10); - if (isNaN(port)) { - // named pipe - return val; - } + if (isNaN(port)) { + // named pipe + return val; + } - if (port >= 0) { - // port number - return port; - } + if (port >= 0) { + // port number + return port; + } - return false; + return false; } /** @@ -54,27 +54,27 @@ function normalizePort(val) { */ function onError(error) { - if (error.syscall !== 'listen') { - throw error; - } + if (error.syscall !== 'listen') { + throw error; + } - var bind = typeof port === 'string' - ? 'Pipe ' + port - : 'Port ' + port; + var bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port; - // handle specific listen errors with friendly messages - switch (error.code) { - case 'EACCES': - console.error(bind + ' requires elevated privileges'); - process.exit(1); - break; - case 'EADDRINUSE': - console.error(bind + ' is already in use'); - process.exit(1); - break; - default: - throw error; - } + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } } /** @@ -82,9 +82,9 @@ function onError(error) { */ function onListening() { - var addr = server.address(); - var bind = typeof addr === 'string' - ? 'pipe ' + addr - : 'port ' + addr.port; - debug('Listening on ' + bind); + var addr = server.address(); + var bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + debug('Listening on ' + bind); } diff --git a/lib/lease_parser.js b/lib/lease_parser.js index a1cce83..3eeb2f1 100644 --- a/lib/lease_parser.js +++ b/lib/lease_parser.js @@ -107,7 +107,7 @@ module.exports = { end_unix_time = dhcp_lease_data[key].end; if((now_unix_time >= end_unix_time)) { - console.log("element " + key + " has expired - clearing"); + console.log("[DHCP Lease Data] Lease " + key + " has expired - clearing"); delete dhcp_lease_data[key]; } } diff --git a/lib/render_template.js b/lib/render_template.js index bba535a..f26f460 100644 --- a/lib/render_template.js +++ b/lib/render_template.js @@ -13,7 +13,7 @@ module.exports = { core = fs.readFileSync('./public/templates/index.html', 'utf8'); core = core.replace(/\[application_name\]/, 'Glass - isc dhcp server utility'); core = core.replace(/\[body_content\]/, body_content); - core = core.replace(/\[(.*?)\]/, ""); + // core = core.replace(/\[(.*?)\]/, ""); return core; } }, diff --git a/public/assets/js/file-saver.js b/public/assets/js/file-saver.js new file mode 100644 index 0000000..0e22ad6 --- /dev/null +++ b/public/assets/js/file-saver.js @@ -0,0 +1,2 @@ +/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ +var saveAs=saveAs||function(e){"use strict";if(typeof e==="undefined"||typeof navigator!=="undefined"&&/MSIE [1-9]\./.test(navigator.userAgent)){return}var t=e.document,n=function(){return e.URL||e.webkitURL||e},r=t.createElementNS("http://www.w3.org/1999/xhtml","a"),o="download"in r,a=function(e){var t=new MouseEvent("click");e.dispatchEvent(t)},i=/constructor/i.test(e.HTMLElement)||e.safari,f=/CriOS\/[\d]+/.test(navigator.userAgent),u=function(t){(e.setImmediate||e.setTimeout)(function(){throw t},0)},s="application/octet-stream",d=1e3*40,c=function(e){var t=function(){if(typeof e==="string"){n().revokeObjectURL(e)}else{e.remove()}};setTimeout(t,d)},l=function(e,t,n){t=[].concat(t);var r=t.length;while(r--){var o=e["on"+t[r]];if(typeof o==="function"){try{o.call(e,n||e)}catch(a){u(a)}}}},p=function(e){if(/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(e.type)){return new Blob([String.fromCharCode(65279),e],{type:e.type})}return e},v=function(t,u,d){if(!d){t=p(t)}var v=this,w=t.type,m=w===s,y,h=function(){l(v,"writestart progress write writeend".split(" "))},S=function(){if((f||m&&i)&&e.FileReader){var r=new FileReader;r.onloadend=function(){var t=f?r.result:r.result.replace(/^data:[^;]*;/,"data:attachment/file;");var n=e.open(t,"_blank");if(!n)e.location.href=t;t=undefined;v.readyState=v.DONE;h()};r.readAsDataURL(t);v.readyState=v.INIT;return}if(!y){y=n().createObjectURL(t)}if(m){e.location.href=y}else{var o=e.open(y,"_blank");if(!o){e.location.href=y}}v.readyState=v.DONE;h();c(y)};v.readyState=v.INIT;if(o){y=n().createObjectURL(t);setTimeout(function(){r.href=y;r.download=u;a(r);h();c(y);v.readyState=v.DONE});return}S()},w=v.prototype,m=function(e,t,n){return new v(e,t||e.name||"download",n)};if(typeof navigator!=="undefined"&&navigator.msSaveOrOpenBlob){return function(e,t,n){t=t||e.name||"download";if(!n){e=p(e)}return navigator.msSaveOrOpenBlob(e,t)}}w.abort=function(){};w.readyState=w.INIT=0;w.WRITING=1;w.DONE=2;w.error=w.onwritestart=w.onprogress=w.onwrite=w.onabort=w.onerror=w.onwriteend=null;return m}(typeof self!=="undefined"&&self||typeof window!=="undefined"&&window||this.content);if(typeof module!=="undefined"&&module.exports){module.exports.saveAs=saveAs}else if(typeof define!=="undefined"&&define!==null&&define.amd!==null){define("FileSaver.js",function(){return saveAs})} \ No newline at end of file diff --git a/public/assets/js/page-dhcp-logs.js b/public/assets/js/page-dhcp-logs.js new file mode 100644 index 0000000..df7270c --- /dev/null +++ b/public/assets/js/page-dhcp-logs.js @@ -0,0 +1,82 @@ +function log_action (action){ + switch(action) { + case "stop": + socket.send(JSON.stringify({"event_unsubscribe": "dhcp_log_subscription"})); + break; + case "start": + socket.send(JSON.stringify({"event_subscription": "dhcp_log_subscription"})); + break; + case "clear": + editor.setValue(""); + break; + case "download_logs": + var d = new Date(); + var am_pm = format_am_pm(d); + var df = d.getMonth() + '-' + d.getDate() + '-' + d.getFullYear() + '_' + (d.getHours()) + '-' + d.getMinutes() + ' ' + am_pm; + var filename = "dhcp_logs_" + df; + var text = editor.getValue(); + var blob = new Blob([text], {type: "text/plain;charset=utf-8"}); + saveAs(blob, filename + ".txt"); + + break; + default: + break; + } +} + +function format_am_pm(date) { + var hours = date.getHours(); + var minutes = date.getMinutes(); + var am_pm = hours >= 12 ? 'PM' : 'AM'; + return am_pm; +} + +var killed_connection = 0; + +if(typeof socket === "undefined") { + var socket = new WebSocket("ws://" + window.location.hostname + ":8080"); + + socket.onopen = function (event) { + console.log("socket is opened"); + console.log("[Subscription] subscribing to dhcp log listen "); + }; + + socket.onmessage = function (event) { + if(killed_connection) + return false; + + if(!document.getElementById("dhcp_log")){ + console.log("[WS] DHCP Log unsubscribed"); + socket.send(JSON.stringify({"event_unsubscribe": "dhcp_log_subscription"})); + killed_connection = 1; + return false; + } + + if(document.getElementById("grep_fitler").value){ + var matcher = new RegExp(document.getElementById("grep_fitler").value, "i"); + var found = matcher.test(event.data); + if(!found){ + return false; + } + } + + var session = editor.session; + session.insert({ + row: session.getLength(), + column: 0 + }, "\n" + event.data); + + if(session.getLength() >= 50000){ + /* If we get over 500,000 lines lets clear the editor */ + editor.setValue(""); + } + + var row = editor.session.getLength() - 1 + var column = editor.session.getLine(row).length // or simply Infinity + editor.gotoLine(row + 1, column); + }; +} + +var editor = ace.edit("dhcp_log"); +editor.setTheme("ace/theme/terminal"); +editor.$blockScrolling = Infinity; \ No newline at end of file diff --git a/public/templates/dhcp_log.html b/public/templates/dhcp_log.html new file mode 100644 index 0000000..1450137 --- /dev/null +++ b/public/templates/dhcp_log.html @@ -0,0 +1,39 @@ +
+
+
+
+

+ [title] +

+
+
+ [log_content] +
+ +
+
+ + + + +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+
+
+ + + + diff --git a/public/templates/index.html b/public/templates/index.html index eac283c..ec11fab 100644 --- a/public/templates/index.html +++ b/public/templates/index.html @@ -85,7 +85,7 @@
  • - + view_stream Logs @@ -99,6 +99,14 @@
  • +
  • Admin
  • +
  • + + mode_edit + DHCP Config + +
  • +
  • Glass API
  • @@ -166,6 +174,8 @@ + + diff --git a/routes/dhcp_log.js b/routes/dhcp_log.js new file mode 100644 index 0000000..9ef516b --- /dev/null +++ b/routes/dhcp_log.js @@ -0,0 +1,29 @@ +var express = require('express'); +var router = express.Router(); +var fs = require('fs'); +var template_render = require('../lib/render_template.js'); + +function human_time (time){ + var time = new Date(time); + var year = time.getFullYear(); + var month = time.getMonth()+1; + var date1 = time.getDate(); + var hour = time.getHours(); + var minutes = time.getMinutes(); + var seconds = time.getSeconds(); + + return year + "-" + month+"-"+date1+" "+hour+":"+minutes+":"+seconds; +} + +router.get('/', function(req, res, next) { + + var content = ""; + + content = template_render.get_template("dhcp_log"); + content = template_render.set_template_variable(content, "title", "DHCP Log"); + content = template_render.set_template_variable(content, "log_content", ""); + + res.send(template_render.get_index_template(content, req.url)); +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/glass_settings.js b/routes/glass_settings.js index 8985633..c0a1571 100644 --- a/routes/glass_settings.js +++ b/routes/glass_settings.js @@ -22,6 +22,9 @@ router.get('/', function(req, res, next) { /* Config File */ input = input + template_render.form_input('Config File', ''); + /* Log File */ + input = input + template_render.form_input('Log File', ''); + /* Admin User */ input = input + template_render.form_input('Admin User', ''); input = input + template_render.form_input('Admin Password', '');