UNPKG

clexi

Version:

Node.js CLEXI is a lightweight client extension interface that enhances connected clients with functions of the underlying operating system using a duplex, realtime Websocket connection.

337 lines (306 loc) 9.14 kB
'use strict' const path = require('path'); const fs = require('fs'); const fastify = require('fastify'); const fastify_static = require('@fastify/static'); const fastify_ws = require('@fastify/websocket'); //Server const version = "0.10.0"; const settings = require('./settings.json'); //Common xtensions const commonXtensions = [ "clexi-broadcaster", "clexi-http-events", "ble-beacon-scanner", "runtime-commands", "gpio-interface" ]; var Clexi = function(customSettings){ var ClexiServer = {}; if (!customSettings) customSettings = {}; var port = customSettings.port || settings.port || 8443; var hostname = customSettings.hostname || settings.hostname || "localhost"; //default: 127.0.0.1, all: 0.0.0.0 var serverId = customSettings.id || settings.id || "clexi-123"; var idIsPassword = customSettings.idIsPassword || settings.idIsPassword || false; var sslCertPath = customSettings.sslCertPath || path.join(__dirname, "ssl"); var wwwPath = path.join(__dirname, customSettings.wwwPath || "www"); var defaultXtensionsPath = path.join(__dirname, 'xtensions'); var customXtensionsPath = customSettings.customXtensionsPath || defaultXtensionsPath; var customXtensions = customSettings.customXtensions || []; var server_options = { //http2: true, }; if (customSettings.logger){ server_options.logger = customSettings.logger; }else if (settings.logger){ server_options.logger = settings.logger; }else{ server_options.logger = { level: 'info' }; } var isLogLevelDebugOrTrace = (server_options.logger.level && (server_options.logger.level == "debug" || server_options.logger.level == "trace")); var useSsl = (customSettings.ssl != undefined)? customSettings.ssl : settings.ssl; if (useSsl){ server_options.https = { key: fs.readFileSync(path.join(sslCertPath, 'key.pem')), cert: fs.readFileSync(path.join(sslCertPath, 'certificate.pem')) }; } const server = fastify(server_options); ClexiServer.fastify = server; //Register static files plugin and path server.register(fastify_static, { root: wwwPath, redirect: false }); //Register Websocket plugin server.register(fastify_ws); //Xtensions var xtensions = {}; function loadXtensions(){ let n = 0; if (settings.xtensions && (settings.xtensions instanceof Array)){ //Load default extensions n += loadXtensionsArray(settings.xtensions, defaultXtensionsPath); } if (customXtensions && (customXtensions instanceof Array)){ //Load custom extensions n += loadXtensionsArray(customXtensions, customXtensionsPath); //NOTE: will overwrite extension if they have same name } console.log('CLEXI Xtensions loaded: ' + n); } function loadXtensionsArray(xtensionNames, xtensionsPath){ let n = 0; for (var i=0; i<xtensionNames.length; i++){ n++; let xName = xtensionNames[i]; let X = require(path.join(xtensionsPath, xName)); xtensions[xName] = new X( function(msg){ //On start msg.type = xName; server.log.info(JSON.stringify(msg, null, ' ')); //EVENT OUTPUT TO CLIENT }, function(msg){ //On event msg.type = xName; if (isLogLevelDebugOrTrace){ server.log.debug('Broadcasting event: ' + msg.type); server.log.trace(JSON.stringify(msg, null, ' ')); } broadcast(msg); }, function(error){ //On error error.type = xName; server.log.error(JSON.stringify(error, null, ' ')); broadcast(error); } ); } return n; } function getXtensionsInfo(){ var info = {}; Object.keys(xtensions).forEach(function(name) { info[name] = { active: true }; //TODO: one could add interface information here for each xtension }); return info; } //Broadcast to receiver or all function broadcast(data){ if (data.receiver){ //single receiver? var client = data.receiver; delete data.receiver; //remove object before sending sendToClient(client, data); }else{ //broadcast to all if (server.websocketServer){ server.websocketServer.clients.forEach(function each(client){ if (client.readyState === 1) { sendToClient(client, data); } }); } } } function sendToClient(client, data){ if (!idIsPassword || (idIsPassword && client.authState)){ if (typeof data === "object"){ client.send(JSON.stringify(data)); }else{ client.send(data); } }else{ //make sure all unauthorized clients are removed server.log.error("Tried to broadcast to unauthorized client. Client should be disconnected already!"); client.terminate(); } } //Server routes: //Ping server.get('/ping', function(request, reply) { var d = { v: version, id: serverId } if (idIsPassword){ d.id = "[SECRET]"; } reply.send(d); }); //CLEXI HTTP-Events server.get('/event/:name', function(request, reply){ if (idIsPassword){ if (request.headers['clexi-id'] == serverId){ sendHttpEventToXtension(request, reply, "GET"); }else{ reply.code(401); reply.send(); } }else{ sendHttpEventToXtension(request, reply, "GET"); } }); server.post('/event/:name', function(request, reply){ if (idIsPassword){ if (request.headers['clexi-id'] == serverId){ sendHttpEventToXtension(request, reply, "POST"); }else{ reply.code(401); reply.send(); } }else{ sendHttpEventToXtension(request, reply, "POST"); } }); function sendHttpEventToXtension(request, reply, method){ if (xtensions["clexi-http-events"]){ //the http-event extension is active xtensions["clexi-http-events"].event(method, request); reply.code(204); reply.send(); }else{ //no http-event extension support reply.code(404); reply.send(); } } //Websocket interface server.register(async function(fastify){ //default fastify.get('/', {websocket: true}, (socket, request) => { handleWebsocketReq(socket, request, '/'); }); //popular client path fastify.get('/clexi', {websocket: true}, (socket, request) => { handleWebsocketReq(socket, request, '/clexi'); }); //fallback for common proxy paths fastify.get('/ws', {websocket: true}, (socket, request) => { handleWebsocketReq(socket, request, '/ws'); }); }); function handleWebsocketReq(socket, request, path){ server.log.info("Client connected via WebSocket path '" + path + "'."); socket.on('message', function(msg){ try { //msg is a buffer let msgObj = JSON.parse(msg.toString()); //Handle extensions input if (msgObj.type && xtensions[msgObj.type]){ server.log.info('Calling xtensions: ' + msgObj.type); let response = xtensions[msgObj.type].input(msgObj, socket); if (response){ socket.send(JSON.stringify({ response: response, type: msgObj.type, id: msgObj.id })); } //Welcome }else if (msgObj.type == "welcome"){ //check server ID if (msgObj.data && msgObj.data.server_id == serverId){ socket.authState = true; } if (!idIsPassword){ socket.send(JSON.stringify({ type: "welcome", code: 200, info: { id: serverId, version: ("CLEXI Node.js server v" + version), xtensions: getXtensionsInfo() } })); }else if (idIsPassword && socket.authState){ socket.send(JSON.stringify({ type: "welcome", code: 200, info: { version: ("CLEXI Node.js server v" + version), xtensions: getXtensionsInfo() } })); }else{ socket.send(JSON.stringify({ type: "welcome", code: 401, info: { msg: "not authorized", version: ("CLEXI Node.js server v" + version) } })); //socket.terminate(); //the client should gracefully disconnect O_O } //undefined }else{ if (msgObj.type && commonXtensions.includes(msgObj.type)){ socket.send(JSON.stringify({ response: ("Xtension with name '" + msgObj.type + "' is not enabled. Please check your settings."), type: "undefined" })); }else{ socket.send(JSON.stringify({ response: ("Unknown message type: " + msgObj.type), type: "undefined" })); } } }catch(e){ server.log.error("Socket Message Error: " + e.message); } }); socket.on('close', function(){ server.log.info('Client disconnected.'); }); socket.on('error', function(e){ server.log.error(`Client error: ${e.message}`); }); } //Run the server ClexiServer.start = function(){ server.listen({port: port, host: hostname}, function(err, address){ if (err){ server.log.error(err); process.exit(1); } console.log(`Local date/time at start: ${new Date().toLocaleString()}`); console.log(`Server with ID '${serverId}' running at: ${address}`); console.log(`Hostname: ${hostname} - SSL: ${useSsl}`); server.log.info(`Server running at ${address}`); //Load extensions loadXtensions(); }); } ClexiServer.stop = () => server.close(); return ClexiServer; } //Run server when called directly if (require.main === module){ Clexi().start(); } module.exports = Clexi;