// OSSS (Open Source Surveillance and Security) // Simple Web App // // Donald Burr , 12/12/2014 var async = require('/usr/local/lib/node_modules/async'); var http = require('http') var url = require('url') var fs = require('fs') var path = require('path') var os = require("os"); var metalib = require('/usr/local/lib/node_modules/fluent-ffmpeg').Metadata var sprintf = require('/usr/local/lib/node_modules/sprintf-js').sprintf; // workaround for some node.js api changes fs.exists = fs.exists || require('path').exists; fs.existsSync = fs.existsSync || require('path').existsSync; // debug mode var DEBUG = false; // sort order defaults to forward var reverse_sort_order = 0; // get arguments var args = process.argv.slice(2); if (args.length > 0) { if (args[0] === "-d") { console.log("Debug mode enabled."); DEBUG = true; } } // port to use var PORT = 80; if (DEBUG) { console.log("Using port 8000 since we are in DEBUG mode."); PORT = 8000; } // these MUST match "width" and "height" settings in /etc/motion/motion.conf! var WIDTH = 320; var HEIGHT = 240; // number of pixels of padding to add to IFRAME (required by some browsers, // notably Firefox, to prevent scroll bars from appearing within the IFRAME) var PADDING = 20; // pretty-print # of seconds as hrs:mintes:seconds function secondsToString(seconds, shortFormat) { shortFormat = typeof shortFormat !== 'undefined' ? shortFormat : false; var str = ""; //var numyears = Math.floor(seconds / 31536000); var numdays = Math.floor((seconds % 31536000) / 86400); var numhours = Math.floor(((seconds % 31536000) % 86400) / 3600); var numminutes = Math.floor((((seconds % 31536000) % 86400) % 3600) / 60); var numseconds = (((seconds % 31536000) % 86400) % 3600) % 60; if (shortFormat) { if (numdays > 0) { str += sprintf("%02dd", numdays); } if (numhours > 0) { str += sprintf("%02dh", numhours); } if (numminutes > 0) { str += sprintf("%02dd", numminutes); } if (numseconds > 0) { str += sprintf("%02ds", numseconds); } } else { if (numdays > 0) { str += numdays + " days, "; } if (numhours > 0) { str += numhours + " hours, "; } if (numminutes > 0) { str += numminutes + " minutes, "; } if (numseconds > 0) { str += numseconds + " seconds"; } } return str; } // fun with timezone offsets function pad(value) { return value < 10 ? '0' + value : value; } function createOffset(offset_in) { var sign = (offset_in > 0) ? "-" : "+"; var offset = Math.abs(offset_in); var hours = pad(Math.floor(offset / 60)); var minutes = pad(offset % 60); return sign + hours + ":" + minutes; } function formatAMPM(date) { var hours = date.getHours(); var minutes = date.getMinutes(); var ampm = hours >= 12 ? 'pm' : 'am'; hours = hours % 12; hours = hours ? hours : 12; // the hour '0' should be '12' minutes = minutes < 10 ? '0'+minutes : minutes; var strTime = hours + ':' + minutes + ' ' + ampm; return strTime; } function humanFileSize(bytes, si) { var thresh = si ? 1000 : 1024; if(bytes < thresh) return bytes + ' B'; var units = si ? ['kB','MB','GB','TB','PB','EB','ZB','YB'] : ['KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB']; var u = -1; do { bytes /= thresh; ++u; } while(bytes >= thresh); return bytes.toFixed(1)+' '+units[u]; }; function endsWith(str, suffix) { return str.indexOf(suffix, str.length - suffix.length) !== -1; } function r(max) { return Math.floor(Math.random() * max); } function parseCookies (request) { var list = {}, rc = request.headers.cookie; rc && rc.split(';').forEach(function( cookie ) { var parts = cookie.split('='); list[parts.shift().trim()] = decodeURI(parts.join('=')); }); return list; } function respondToHttpRequest(req, res) { var hostname = os.hostname(); var host = req.headers["host"]; var hostport = host var cookies = parseCookies(req); if (cookies["reverse_sort_order"]) { reverse_sort_order = parseInt(cookies["reverse_sort_order"]); } if(host.indexOf(":") > -1) { host = host.substring(0, host.indexOf(':')); } var ua = req.headers['user-agent'], $ = {}; if (/mobile/i.test(ua)) $.Mobile = true; if (/like Mac OS X/.test(ua)) { $.iOS = /CPU( iPhone)? OS ([0-9\._]+) like Mac OS X/.exec(ua)[2].replace(/_/g, '.'); $.iPhone = /iPhone/.test(ua); $.iPad = /iPad/.test(ua); } if (/Android/.test(ua)) $.Android = /Android ([0-9\.]+)[\);]/.exec(ua)[1]; if (/webOS\//.test(ua)) $.webOS = /webOS\/([0-9\.]+)[\);]/.exec(ua)[1]; if (/(Intel|PPC) Mac OS X/.test(ua)) $.Mac = /(Intel|PPC) Mac OS X ?([0-9\._]*)[\)\;]/.exec(ua)[2].replace(/_/g, '.') || true; if (/[Ll]inux/.test(ua)) $.Linux = true; if (/Windows NT/.test(ua)) $.Windows = /Windows NT ([0-9\._]+)[\);]/.exec(ua)[1]; console.log("USER AGENT DETECTION RESULTS:"); console.log($); //console.log("host = " + host); // console.log(url); var queryData = url.parse(req.url, true, true).query; console.log("Responding to http request: " + req.url); //console.log("query data: " + queryData); if (queryData.mode) { console.log("mode = " + queryData.mode); if (queryData.mode === "live_view") { // res.writeHead(200, "Content-Type: text/html"); var rhtml = "" + "" + "OSSS @ " + hostname + " - Live View" + "" + "" + "
" + "

OSSS @ " + hostname + " - Live View

"; if ($.Android) { //rhtml += "Click here to view.
(Requires the free VLC app for Android. Download it here.)"; rhtml += ""; } else if ($.iOS) { //rhtml += "Click here to view.
(Requires the free Infuse app. Download it here.)"; rhtml += "Click here to view.
(Requires the free VLC app for iOS. Download it here.)"; } else { rhtml += ""; } rhtml += "

Back

" + "
" + "" + "" + ""; res.end(rhtml); } else if (queryData.mode === "file_list") { var headers = {"Content-Type": "text/html"}; if (queryData.set_reverse_sort_order) { reverse_sort_order = parseInt(queryData.set_reverse_sort_order); headers["Set-Cookie"] = "reverse_sort_order=" + queryData.set_reverse_sort_order; } fs.readdir("/var/spool/motion", function(err, list) { if(err) { res.writeHead(200, headers); res.end("OSSS @ " + hostname + " - Error

Error: unable to get file list: " + err + "

"); } else { res.writeHead(200, headers); res.write("OSSS @ " + hostname + " - Saved Recordings

" + hostname + " - Saved Recordings

"); var regex = new RegExp(".*\.avi"); var fileList = new Array() var done = false; console.log("start foreach"); async.forEachSeries(list, function(item) { if(regex.test(item)) { var metaobject = new metalib("/var/spool/motion/" + item); var eventNumber = item.substr(0, 2); var offset = new Date().getTimezoneOffset(); var dateString = item.substr(3, item.indexOf(".avi")-3).replace(/\./g, ':') + createOffset(offset); var dateObject = new Date(dateString); var stats = fs.statSync("/var/spool/motion/" + item) var fileSizeInBytes = stats["size"] var fileInfo = new Array(); fileInfo[0] = dateObject; fileInfo[1] = item; fileInfo[2] = eventNumber; fileInfo[3] = fileSizeInBytes; metaobject.get(function(metadata, err) { if (!err) { fileInfo[4] = metadata["durationsec"]; } else { fileInfo[4] = -1; } fileList.push(fileInfo); }); } }, function(err) { console.log("REALLY ALL DONE NOW"); done = true }); console.log("about to start while"); console.log("done = " + done); while (!done) {console.log('waiting');}; if (fileList.length > 0) { // sort by date fileList.sort(function(a,b){ a = a[0]; b = b[0]; if (reverse_sort_order) { c = a > b ? -1 : a < b ? 1 : 0; } else { c = a < b ? -1 : a > b ? 1 : 0; } return c; }); res.write("

Current Sort Order: " + (reverse_sort_order ? "Reverse" : "Forward") + " (change)

"); res.write("

Delete ALL Videos

Back

"); } else { res.write("

No Files Found

"); } } res.end("

Back

"); }); } else if (queryData.mode === "delete") { if (queryData.filename) { var filename = queryData.filename; if (queryData.confirmed) { var confirmed = queryData.confirmed; var rhtml = "" + "" + "OSSS @ " + hostname + " - Delete File(s)" + "" + "" + "

OSSS @ " + hostname + " - Delete File(s)

" + "

"; if (confirmed === "yes") { var numDeleted = 0; var numErrors = 0; var path = "/var/spool/motion/"; if (filename === "ALL") { var files = fs.readdirSync(path); var success = true files.forEach(function(file) { if (file.match("^.*avi$")) { try { fs.unlinkSync(path + file); numDeleted++; rhtml += "Deleted \"" + file + "\"
"; } catch (ex) { numErrors++; rhtml += "Could not delete \"" + file + "\": " + ex + "
"; success = false } } }); rhtml += "
"; } else { path += filename; try { fs.unlinkSync(path); numDeleted++; success = true; } catch (ex) { numErrors++; rhtml += "Could not delete \"" + filename + "\": " + ex + "

"; success = false; } } if (success) { rhtml += numDeleted + " file" + (numDeleted > 1 ? "s" : "") + " deleted successfully."; } else { rhtml += numDeleted > 0 ? numDeleted + " file" + (numDeleted > 1 ? "s" : "") + " deleted successfully." : ""; rhtml += numErrors + " delete failure" + (numErrors > 1 ? "s" : "") + "."; } } else { rhtml += "Delete cancelled."; } rhtml += "

OK

" + "" + "" + ""; res.end(rhtml); } else { var rhtml = "" + "" + "OSSS @ " + hostname + " - Delete File(s)" + "" + "" + "

OSSS @ " + hostname + " - Delete File(s)

" + "

You are about to delete " + (filename === "ALL" ? "ALL videos!" : "\"" + filename + "\"") + "

" + "Are you SURE you want to do this?

" + "YES NO

" + "" + "" + ""; res.end(rhtml); } } else { res.writeHead(200, "Content-Type: text/html"); res.end("OSSS @ " + hostname + " - Error

Error: filename not specified.

"); } } else if (queryData.mode === "view") { if (queryData.filename) { var filename = queryData.filename; var filePath = "/var/spool/motion/" + filename; fs.exists(filePath, function(exists) { if (exists) { console.log('viewing contents of local file: ' + filePath); var stat = fs.statSync(filePath); res.writeHead(200, { 'Content-Type': 'text/html' }); var rhtml = "" + "" + "OSSS @ " + hostname + " - Video Playback - " + filename + "" + "" + "
" + "

OSSS @ " + hostname + " - " + filename + "

"; if (queryData.use_vlc_plugin) { rhtml += "" + "
" + "(Note: if you don't see a video window above, then you must install the VLC browser plugin.)" + "
"; /* + "[Play] " + "[Pause] " + "[Stop] " + "[fullscreen]"; */ } else { if ($.Android) { //rhtml += "Tap here to play this video.
(Requires the free VLC app for Android. Download it here.)"; rhtml += "Tap here to download this file.
Once the file is downloaded, swipe down Notification Center, and tap on the file to open it in VLC. (Requires the free VLC app for Android. Download it here.)"; rhtml += "
Note #2: you may get an error message saying VLC encounterd an error with this media. If so, tap the \"Refresh\" (two arrows going around in circles) button in VLC, and the downloaded file should appear. Seems to be a bug with the latest VLC, they broke it. :-P"; } else if ($.iOS) { rhtml += "Tap here to play this video.
(Requires the free VLC app for iOS. Download it here.)"; //rhtml += "Tap here to play this video.
(Requires the free Infuse app. Download it here.)"; } else { rhtml += "" + "
"; // + "(Don't see any video? Try this...)"; } } rhtml += "

Back

" + "
" + "" + "" + ""; res.end(rhtml); } else { console.log('returning 404, could not find: ' + filePath); res.writeHead(404); res.end('Not found. Go away kid, you\'re bothering me.'); } }); } else { res.writeHead(200, "Content-Type: text/html"); res.end("OSSS @ " + hostname + " - Error

Error: filename not specified.

"); } } else if (queryData.mode === "download") { if (queryData.filename) { var filename = queryData.filename; var filePath = "/var/spool/motion/" + filename; fs.exists(filePath, function(exists) { if (exists) { console.log('sending contents of local file: ' + filePath); var stat = fs.statSync(filePath); var contenttype = "video/x-msvideo"; res.writeHead(200, { 'Content-Type': contenttype, 'Content-Length': stat.size, 'Content-Disposition': "attachment; filename=" + filename }); var readStream = fs.createReadStream(filePath); readStream.pipe(res); } else { console.log('returning 404, could not find: ' + filePath); res.writeHead(404); res.end('Not found. Go away kid, you\'re bothering me.'); } }); } else { res.writeHead(200, "Content-Type: text/html"); res.end("OSSS @ " + hostname + " - Error

Error: filename not specified.

"); } } } else { // send the index page console.log("Responding with index page"); res.writeHead(200, "Content-Type: text/html"); var rhtml = "" + "" + "" + "OSSS (Open Source Surveillance and Security) - " + hostname + "" + "" + "" + "

OSSS (Open Source Surveillance and Security) - " + hostname + "

" + "

" + "

" + "" + "" + ""; res.end(rhtml); } } // Set up the HTTP listener var server = http.createServer(function (req, res) { respondToHttpRequest(req, res); }).listen(PORT); server.on('connection', function(sock) { console.log('Incoming HTTP connection from ' + sock.remoteAddress); }); console.log('HTTP server running on port ' + PORT + '.');