diff --git a/Anetha/index.js b/Anetha/index.js index 87664c0..83b3fc4 100644 --- a/Anetha/index.js +++ b/Anetha/index.js @@ -2,74 +2,103 @@ const WebSocket = require('ws'); const fs = require('fs'); const cookie = require('cookie') const jsonwebtoken = require('jsonwebtoken'); - +const models = require('./../models/index.model') const authenticationController = require('./../controllers/authentication'); const deviceController = require('./../controllers/devices'); +const { ws } = require('../routes/api/realtime'); let wss; - async function __server() { - wss = new WebSocket.WebSocketServer({ path: '/ws/v2/', port: 4040 }); + wss = new WebSocket.WebSocketServer({ path: '/ws/v2/', port: 4040, handshakeTimeout: 500}); + console.log("src") - + const interval = setInterval(function ping() { + wss.clients.forEach(function each(ws) { + if (ws.isAlive === false) { + _actionLogger(null, null, "ATHENA_DEVICE_TIMEOUT_FORCE_DISCONNECT", null, ws._socket.remoteAddress, null, ws.dongleId); + return ws.terminate(); + } + + ws.isAlive = false; + ws.ping(); + }); + }, 5000); wss.on('connection', manageConnection) + wss.on('close', function close() { + clearInterval(interval); + }); + } + + const authenticateDongle = async (ws, res, cookies) => { unsafeJwt = jsonwebtoken.decode(cookies.jwt); const device = await deviceController.getDeviceFromDongle(unsafeJwt.identity) - console.log(unsafeJwt) - let verifiedJWT; try { verifiedJWT = jsonwebtoken.verify(cookies.jwt, device.public_key, {ignoreNotBefore: true}); } catch (err) { - console.log("bad JWT"); + _actionLogger(null, null, "ATHENA_DEVICE_AUTHENTICATE_INVALID", null, ws._socket.remoteAddress, JSON.stringify({jwt: cookies.jwt}), null); return false; } if (verifiedJWT.identify === unsafeJwt.identify) { ws.dongleId = device.dongle_id - console.log("AUTHENTICATED DONGLE") + _actionLogger(null, device.id, "ATHENA_DEVICE_AUTHENTICATE_SUCCESS", null, ws._socket.remoteAddress, null); + console.log("AUTHENTICATED DONGLE", ws.dongleId ) return true; } else { + _actionLogger(null, device.id, "ATHENA_DEVICE_AUTHENTICATE_FAILURE", null, ws._socket.remoteAddress, JSON.stringify({jwt: cookies.jwt}), null); console.log("UNAUTHENTICATED DONGLE"); return false; } } - function commandBuilder(method, params) { - - - return { - method, - params, - "jsonrpc": "2.0", - "id": 0 - } - +function commandBuilder(method, params) { + return { method, params, "jsonrpc": "2.0", "id": 0 } } +async function heartbeat() { + this.isAlive = true; + this.heartbeat = Date.now(); +} - +async function _actionLogger(account_id, device_id, action, user_ip, device_ip, meta, dongle_id) { + models.models.athena_action_log.create({ + account_id, device_id, action, user_ip, device_ip, meta, created_at: Date.now(), dongle_id + }) +} async function manageConnection(ws, res) { ws.badMessages = 0; + ws.isAlive = true; + ws.heartbeat = Date.now(); + + ws.on('pong', heartbeat); + var cookies = cookie.parse(res.headers.cookie); ws.on('message', function incoming(message) { - if (!ws.dongleId) { return null; console.log("unauthenticated message, discarded"); } + heartbeat.call(ws) + if (!ws.dongleId) { + _actionLogger(null, null, "ATHENA_DEVICE_UNATHENTICATED_MESSAGE", null, ws._socket.remoteAddress, JSON.stringify([message]), ws.dongleId); + console.log("unauthenticated message, discarded"); + return null; + } + + _actionLogger(null, null, "ATHENA_DEVICE_MESSAGE_UNKNOWN", null, ws._socket.remoteAddress, JSON.stringify([message]), ws.dongleId); console.log("unknown message", JSON.stringify(message)) }); @@ -80,10 +109,6 @@ async function manageConnection(ws, res) { //ws.send(JSON.stringify(await commandBuilder('reboot'))) - - - - } @@ -96,12 +121,9 @@ function _findSocketFromDongle(dongleId) { let websocket = null; wss.clients.forEach((value) => { - console.log(value.dongleId) - if (value.dongleId === dongleId) { websocket = value; } - }) return websocket; @@ -110,15 +132,43 @@ function _findSocketFromDongle(dongleId) { - -function rebootDevice(dongleId) { +// TODO - This is dumb +function rebootDevice(dongleId, accountId) { const websocket = _findSocketFromDongle(dongleId); + - if (!websocket) { return false; console.log("bad")} + if (!websocket) { + return _actionLogger(accountId, null, "ATHENA_USER_INVOKE__REBOOT_FAILED_DISCONNECTED", null, null, null, ws.dongleId); + } + + _actionLogger(accountId, null, "ATHENA_USER_INVOKE__REBOOT_ISSUED", null, ws._socket.remoteAddress, null, ws.dongleId); websocket.send(JSON.stringify(commandBuilder('reboot'))) +} + +function getDetails(dongleId, accountId) { + const websocket = _findSocketFromDongle(dongleId); + + if (!websocket) { + return _actionLogger(accountId, null, "ATHENA_USER_INVOKE__GETVERSION_FAILED_DISCONNECTED", null, null, null, ws.dongleId); + } + + _actionLogger(accountId, null, "ATHENA_USER_INVOKE__GETVERSION_ISSUED", null, ws._socket.remoteAddress, null, ws.dongleId); + + websocket.send(JSON.stringify(commandBuilder('getVersion'))) + +} + + +function isDeviceConnected(dongleId, accountId) { + const websocket = _findSocketFromDongle(dongleId); + _actionLogger(null, null, "ATHENA_USER_STATUS__IS_CONNECTED", null, websocket ? websocket._socket.remoteAddress : null, JSON.stringify({connected: websocket ? true : false, heartbeat: websocket ? websocket.heartbeat : null}), dongleId); + + if (!websocket) return {connected: false} + + return {connected: true, heartbeat: websocket.heartbeat}; } @@ -126,5 +176,7 @@ function rebootDevice(dongleId) { module.exports = { - rebootDevice + rebootDevice, + isDeviceConnected, + getDetails } diff --git a/models/athena_action_log.model.js b/models/athena_action_log.model.js new file mode 100644 index 0000000..fabac5e --- /dev/null +++ b/models/athena_action_log.model.js @@ -0,0 +1,46 @@ +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + sequelize.define('athena_action_log', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: DataTypes.INTEGER + }, + account_id: { + allowNull: true, + type: DataTypes.INTEGER + }, + device_id: { + allowNull: true, + type: DataTypes.INTEGER + }, + action: { + allowNull: true, + type: DataTypes.TEXT + }, + user_ip: { + allowNull: true, + type: DataTypes.TEXT + }, + device_ip: { + allowNull: true, + type: DataTypes.TEXT + }, + meta: { + allowNull: true, + type: DataTypes.TEXT + }, + created_at: { + allowNull: true, + type: DataTypes.INTEGER + }, + dongle_id: { + allowNull: true, + type: DataTypes.TEXT + } + }, { + timestamps: false, + }); +}; \ No newline at end of file diff --git a/models/athena_returned_data.model.js b/models/athena_returned_data.model.js new file mode 100644 index 0000000..690ab2c --- /dev/null +++ b/models/athena_returned_data.model.js @@ -0,0 +1,35 @@ +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + sequelize.define('athena_returned_data', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: DataTypes.INTEGER + }, + account_id: { + allowNull: false, + type: DataTypes.INTEGER + }, + device_id: { + allowNull: true, + type: DataTypes.INTEGER + }, + type: { + allowNull: true, + type: DataTypes.TEXT + }, + data: { + allowNull: true, + type: DataTypes.BLOB + }, + created_at: { + allowNull: true, + type: DataTypes.TEXT + }, + + }, { + timestamps: false, + }); +}; \ No newline at end of file diff --git a/models/device_authorised_users.model.js b/models/device_authorised_users.model.js new file mode 100644 index 0000000..dbef38b --- /dev/null +++ b/models/device_authorised_users.model.js @@ -0,0 +1,39 @@ +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + sequelize.define('device_authorised_users', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: DataTypes.INTEGER + }, + account_id: { + allowNull: false, + type: DataTypes.INTEGER + }, + device_id: { + allowNull: true, + type: DataTypes.INTEGER + }, + athena: { + allowNull: true, + type: DataTypes.INTEGER + }, + unpair: { + allowNull: true, + type: DataTypes.INTEGER + }, + view_drives: { + allowNull: true, + type: DataTypes.INTEGER + }, + created_at: { + allowNull: true, + type: DataTypes.INTEGER + }, + + }, { + timestamps: false, + }); +}; \ No newline at end of file diff --git a/models/index.model.js b/models/index.model.js index 6b23e6f..617a2bf 100644 --- a/models/index.model.js +++ b/models/index.model.js @@ -10,6 +10,9 @@ const modelDefiners = [ require('./devices.model'), require('./drives.model'), require('./users.model'), + require('./athena_action_log.model'), + require('./athena_returned_data.model'), + require('./device_authorised_users.model'), ]; for (const modelDefiner of modelDefiners) { diff --git a/routes/api/realtime.js b/routes/api/realtime.js new file mode 100644 index 0000000..1c58767 --- /dev/null +++ b/routes/api/realtime.js @@ -0,0 +1,75 @@ +const router = require('express').Router(); + +const authenticationController = require('./../../controllers/authentication'); +const userController = require('./../../controllers/users'); +const deviceController = require('./../../controllers/devices') + +const whitelistParams = { + getMessage: false, + getVersion: true, + setNavDestination: true, + listDataDirectory: false, + reboot: true, + uploadFileToUrl: false, + listUploadQueue: true, + cancelUpload: true, + primeActivated: false, + getPublicKey: true, + getSshAuthorizedKeys: true, + getSimInfo: true, + getNetworkType: true, + getNetworks: true, + takeSnapshot: true +} + + +router.get('/dongle/:dongle_id/connected', async (req, res) => { + const account = await authenticationController.getAuthenticatedAccount(req, res); + if (account == null) { return res.status(403).json({error: true, errorMsg: 'Unauthenticated', errorObject: {authenticated: false}})} + const device = await deviceController.getDeviceFromDongle(req.params.dongle_id); + if (!device) {return res.status(400).json({error: true, errorMsg: 'no_dongle', errorObject: {authenticated: true, dongle_exists: false}})} + + // TODO support delgation of access + // TODO remove indication of dongle existing + if (device.account_id !== account.id) {return res.status(403).json({error: true, errorMsg: 'unauthorised', errorObject: {authenticated: true, dongle_exists: true, authorised_user: false}})} + + const deviceConnected = await req.athenaWebsocketTemp.isDeviceConnected(device.dongle_id); + + return res.status(200).json({success: true, dongle_id: device.dongle_id, data: deviceConnected}); +}) + +router.get('/dongle/:dongle_id/getDetails', async (req, res) => { + const account = await authenticationController.getAuthenticatedAccount(req, res); + if (account == null) { return res.status(403).json({error: true, errorMsg: 'Unauthenticated', errorObject: {authenticated: false}})} + const device = await deviceController.getDeviceFromDongle(req.params.dongle_id); + if (!device) {return res.status(400).json({error: true, errorMsg: 'no_dongle', errorObject: {authenticated: true, dongle_exists: false}})} + + // TODO support delgation of access + // TODO remove indication of dongle existing + if (device.account_id !== account.id) {return res.status(403).json({error: true, errorMsg: 'unauthorised', errorObject: {authenticated: true, dongle_exists: true, authorised_user: false}})} + + const deviceConnected = await req.athenaWebsocketTemp.getDetails(device.dongle_id); + + // return res.status(200).json({success: true, dongle_id: device.dongle_id, data: deviceConnected}); +}) + + +/*router.post('/dongle/:dongle_id/raw', bodyParser.urlencoded({extended: true}), async (req, res) => { + if (!req.body.hasOwnProperty('method') { return res.status(403).json({error: true, errorMsg: 'missing_data', errorObject: {missing: [method]}})} + const account = await authenticationController.getAuthenticatedAccount(req, res); + if (account == null) { return res.status(403).json({error: true, errorMsg: 'Unauthenticated', errorObject: {authenticated: false}})} + const device = await deviceController.getDeviceFromDongle(req.params.dongle_id); + if (!device) {return res.status(400).json({error: true, errorMsg: 'no_dongle', errorObject: {authenticated: true, dongle_exists: false}})} + + // TODO support delgation of access + // TODO remove indication of dongle existing + if (device.account_id !== account.id) {return res.status(403).json({error: true, errorMsg: 'unauthorised', errorObject: {authenticated: true, dongle_exists: true, authorised_user: false}})} + + const deviceConnected = await req.athenaWebsocketTemp.raw(req.body.method, req.body.params); + + return res.status(200).json({success: true, dongle_id: device.dongle_id, data: deviceConnected}); +})*/ + + + +module.exports = router \ No newline at end of file diff --git a/routes/index.js b/routes/index.js index d32ae17..8bac323 100644 --- a/routes/index.js +++ b/routes/index.js @@ -7,6 +7,7 @@ module.exports = (_models, _controllers, _logger) => { useradmin: require('./useradmin')(_models, _controllers, _logger), api: require('./api')(_models, _controllers, _logger), useradminapi: require('./userAdminApi')(_models, _controllers, _logger), - admin: require('./administration/adminApi')(_models, _controllers, _logger) + admin: require('./administration/adminApi')(_models, _controllers, _logger), + realtime: require('./api/realtime') } } \ No newline at end of file diff --git a/server.js b/server.js index 8f78484..a92bedf 100644 --- a/server.js +++ b/server.js @@ -70,6 +70,7 @@ const web = async () => { app.use('/admin', routers.admin); + app.use('/realtime', routers.realtime);