From 9361043d836e821499fade16537a7091169ce5e3 Mon Sep 17 00:00:00 2001 From: AdamSBlack Date: Mon, 25 Oct 2021 22:56:40 +0100 Subject: [PATCH] Added proof of concept for Athena --- Anetha/index.js | 130 ++++++++++++++++++++++++++++++ controllers/authentication.js | 10 +-- controllers/devices.js | 43 +++++++--- controllers/index.js | 2 +- controllers/users.js | 10 +-- package.json | 6 +- routes/administration/adminApi.js | 27 +++++-- routes/api/authentication.js | 47 +++++++++++ routes/api/registration.js | 38 +++++++++ routes/userAdminApi.js | 65 ++------------- routes/useradmin.js | 2 +- server.js | 21 +++-- 12 files changed, 300 insertions(+), 101 deletions(-) create mode 100644 Anetha/index.js create mode 100644 routes/api/authentication.js create mode 100644 routes/api/registration.js diff --git a/Anetha/index.js b/Anetha/index.js new file mode 100644 index 0000000..87664c0 --- /dev/null +++ b/Anetha/index.js @@ -0,0 +1,130 @@ +const WebSocket = require('ws'); +const fs = require('fs'); +const cookie = require('cookie') +const jsonwebtoken = require('jsonwebtoken'); + + +const authenticationController = require('./../controllers/authentication'); +const deviceController = require('./../controllers/devices'); + +let wss; + +async function __server() { + wss = new WebSocket.WebSocketServer({ path: '/ws/v2/', port: 4040 }); + console.log("src") + + + + wss.on('connection', manageConnection) + +} + +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"); + return false; + } + + + if (verifiedJWT.identify === unsafeJwt.identify) { + ws.dongleId = device.dongle_id + console.log("AUTHENTICATED DONGLE") + return true; + } else { + console.log("UNAUTHENTICATED DONGLE"); + return false; + } +} + + + function commandBuilder(method, params) { + + + return { + method, + params, + "jsonrpc": "2.0", + "id": 0 + } + +} + + + + + +async function manageConnection(ws, res) { + ws.badMessages = 0; + var cookies = cookie.parse(res.headers.cookie); + + + ws.on('message', function incoming(message) { + if (!ws.dongleId) { return null; console.log("unauthenticated message, discarded"); } + + console.log("unknown message", JSON.stringify(message)) + }); + + + if (await authenticateDongle(ws, res, cookies) === false) { + ws.close(); + } + + //ws.send(JSON.stringify(await commandBuilder('reboot'))) + + + + + +} + + +__server(); + + + + +function _findSocketFromDongle(dongleId) { + + let websocket = null; + wss.clients.forEach((value) => { + console.log(value.dongleId) + + if (value.dongleId === dongleId) { + websocket = value; + } + + }) + + return websocket; +} + + + + + +function rebootDevice(dongleId) { + const websocket = _findSocketFromDongle(dongleId); + + if (!websocket) { return false; console.log("bad")} + + websocket.send(JSON.stringify(commandBuilder('reboot'))) + + +} + + + + + +module.exports = { + rebootDevice +} diff --git a/controllers/authentication.js b/controllers/authentication.js index 0ecba51..f876659 100644 --- a/controllers/authentication.js +++ b/controllers/authentication.js @@ -8,7 +8,7 @@ async function validateJWT(token, key) { try { return jwt.verify(token.replace("JWT ", ""), key, {algorithms: ['RS256'], ignoreNotBefore: true}); } catch (exception) { - logger.warn(`failed to validate JWT ${exception}`) + console.log(`failed to validate JWT ${exception}`) } return null; } @@ -26,12 +26,9 @@ async function signIn(email, password) { let account = await models_orm.models.accounts.findOne({where: {email: email}}); - if (account.dataValues) { account = account.dataValues; - const inputPassword = crypto.createHash('sha256').update(password + config.applicationSalt).digest('hex'); - if (account.password === inputPassword) { const token = jwt.sign({accountId: account.id}, config.applicationSalt) return {success: true, jwt: token}; @@ -44,6 +41,8 @@ async function signIn(email, password) { } + + async function changePassword(account, newPassword, oldPassword) { if (!account || !newPassword || !oldPassword) { return {success: false, error: 'MISSING_DATA'}} const oldPasswordHash = crypto.createHash('sha256').update(oldPassword + config.applicationSalt).digest('hex') @@ -101,5 +100,6 @@ module.exports = { validateJWT: validateJWT, getAuthenticatedAccount: getAuthenticatedAccount, changePassword: changePassword, - signIn: signIn + signIn: signIn, + readJWT: readJWT } \ No newline at end of file diff --git a/controllers/devices.js b/controllers/devices.js index 6f34b43..0256138 100644 --- a/controllers/devices.js +++ b/controllers/devices.js @@ -13,7 +13,7 @@ async function pairDevice(account, qr_string) { let deviceQuery; let pairJWT; if (qrCodeParts.length > 0) { - deviceQuery = await models_orm.models.device.findOne({ where: { imei: qrCodeParts[0], serial: qrCodeParts[1] }}); + deviceQuery = await models_orm.models.device.findOne({ where: { serial: qrCodeParts[1] }}); pairJWT = qrCodeParts[2]; } else { pairJWT = qr_string; @@ -21,25 +21,42 @@ async function pairDevice(account, qr_string) { deviceQuery = await models_orm.models.device.findOne({ where: { dongle_id: data.identiy }}); } - if (deviceQuery.dataValues == null) { + + if (deviceQuery == null) { return {success: false, registered: false} } const device = deviceQuery.dataValues; - var decoded = controllers.authentication.validateJWT(pairJWT, device.public_key); + var decoded = await authenticationController.validateJWT(pairJWT, device.public_key); if (decoded == null || decoded.pair == undefined) { return {success: false, badToken: true} } + + if (device.account_id != 0) { return {success: false, alreadyPaired: true, dongle_id: device.dongle_id} } + return await pairDeviceToAccountId(device.dongle_id, account.id ) + +} - const update = models_orm.models.accounts.update( - { account_id: account.id }, - { where: { dongle_id: device.dongle_id } } +async function pairDeviceToAccountId(dongle_id, account_id) { + console.log("input", account_id, dongle_id) + const update = await models_orm.models.device.update( + { account_id: account_id }, + { where: { dongle_id: dongle_id } } ) - return {success: true, paired: true, dongle_id: device.dongle_id, account_id: account.id} + console.log("update:" , update) + + const check = await models_orm.models.device.findOne({where: {dongle_id: dongle_id, account_id: account_id}}) + console.log(check); + if (check.dataValues) { + return {success: true, paired: true, dongle_id: dongle_id, account_id: account_id} + } else { + return {success: false, paired: false} + } + } async function unpairDevice(account, dongleId) { @@ -68,13 +85,15 @@ async function setDeviceNickname(account, dongleId, nickname) { } async function getDevices(accountId) { - const devices = await models_orm.models.device.getOne({where: {account_id: accountId}}); + const devices = await models_orm.models.device.findAll(); - return devices.dataValues || null + console.log("kkk", devices); + + return devices.dataValues } async function getDeviceFromDongle(dongleId) { - const devices = await models_orm.models.device.getOne({where: {dongle_id: dongleId}}); + const devices = await models_orm.models.device.findOne({where: {dongle_id: dongleId}}); return devices.dataValues || null } @@ -91,10 +110,9 @@ async function setIgnoredUploads(dongleId, isIgnored) { } async function getAllDevicesFiltered() { - console.log(models_orm.models.device) const devices = await models_orm.models.device.findAll(); - return devices.dataValues || null + return devices } @@ -106,4 +124,5 @@ module.exports = { getDeviceFromDongle, setIgnoredUploads, getAllDevicesFiltered, + pairDeviceToAccountId, } diff --git a/controllers/index.js b/controllers/index.js index d7fca58..76034cf 100644 --- a/controllers/index.js +++ b/controllers/index.js @@ -9,7 +9,7 @@ module.exports = async (models, logger, models_sqli) => { helpers: require('./helpers')(models, logger), storage: require('./storage')(models, logger), mailing: require('./mailing')(models, logger), - users: require('./users')(models, logger), + users: require('./users'), admin: require('./admin'), devices: require('./devices') } diff --git a/controllers/users.js b/controllers/users.js index 652967f..dd72427 100644 --- a/controllers/users.js +++ b/controllers/users.js @@ -2,8 +2,7 @@ const config = require('./../config'); const crypto = require('crypto'); const models_orm = require('./../models/index.model'); const authentication = require('./authentication'); -let models; -let logger; + async function getAccountFromId(id) { @@ -70,14 +69,9 @@ async function getAllUsers() { -module.exports = (_models, _logger) => { - models = _models; - logger = _logger; - - return { +module.exports = { createAccount, verifyEmailToken, getAccountFromId, getAllUsers } -} diff --git a/package.json b/package.json index 4cd904c..b748c47 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@sendgrid/client": "^7.4.3", "chai": "^4.3.4", "chai-http": "^4.3.0", + "cookie": "^0.4.1", "cookie-parser": "^1.4.5", "cors": "^2.8.5", "crypto": "^1.0.1", @@ -21,6 +22,7 @@ "esm": "^3.2.25", "express": "^4.17.1", "express-fileupload": "^1.2.1", + "express-ws": "^5.0.2", "fast-folder-size": "^1.3.0", "ffprobe": "^1.1.2", "ffprobe-static": "^3.0.0", @@ -36,6 +38,8 @@ "sequelize": "^6.6.5", "sqlite": "^4.0.22", "sqlite3": "^5.0.2", - "supertest": "^6.1.3" + "supertest": "^6.1.3", + "websocket": "^1.0.34", + "ws": "^8.2.3" } } diff --git a/routes/administration/adminApi.js b/routes/administration/adminApi.js index 017e752..c0d9ee2 100644 --- a/routes/administration/adminApi.js +++ b/routes/administration/adminApi.js @@ -3,7 +3,7 @@ const bodyParser = require('body-parser'); const crypto = require('crypto'); const { route } = require('../../server'); const config = require('./../../config'); - +const deviceController = require('./../../controllers/devices') function runAsyncWrapper(callback) { return function (req, res, next) { @@ -66,20 +66,25 @@ router.get('/device/:dongle_id', runAsyncWrapper(async (req, res) => { })); -router.get('/device', runAsyncWrapper(async (req, res) => { +router.get('/device/:dongle_id/pair/:user_id', runAsyncWrapper(async (req, res) => { + if (!req.params.dongle_id || !req.params.user_id) { return req.status(400).json({error: true, msg: 'MISSING DATA', status: 400})} + + const pairDeviceToAccountId = await controllers.devices.pairDeviceToAccountId(req.params.dongle_id, req.params.user_id) - return res.status(200).json({success: true, data: await controllers.devices.getAllDevicesFiltered()}) + return res.status(200).json(pairDeviceToAccountId) + +})); + +router.get('/device', runAsyncWrapper(async (req, res) => { + const filteredDevices = await controllers.devices.getAllDevicesFiltered(); + console.log("fil", filteredDevices) + return res.status(200).json({success: true, data: filteredDevices}) })); router.get('/device/:dongle_id/ignore/:ignore_uploads', runAsyncWrapper(async (req, res) => { if (!req.params.dongle_id || !req.params.ignore_uploads) { return req.status(400).json({error: true, msg: 'MISSING DATA', status: 400})} - - - - - })); router.get('/admin/device/:dongle_id/ignore/:ignore_uploads', runAsyncWrapper(async (req, res) => { @@ -107,6 +112,12 @@ router.get('/admin/device/:dongle_id/ignore/:ignore_uploads', runAsyncWrapper(as })); +router.get('/device/:dongle_id/athena/reboot', runAsyncWrapper(async (req, res) => { + + req.athenaWebsocketTemp.rebootDevice(req.params.dongle_id) + res.send("ok"); + +})); module.exports = (_models, _controllers, _logger) => { diff --git a/routes/api/authentication.js b/routes/api/authentication.js new file mode 100644 index 0000000..aa74929 --- /dev/null +++ b/routes/api/authentication.js @@ -0,0 +1,47 @@ +const router = require('express').Router(); +const config = require('./../config'); + +const authenticationController = require('./../../controllers/authentication'); +const userController = require('./../../controllers/users'); + + + + +function runAsyncWrapper(callback) { + return function (req, res, next) { + callback(req, res, next) + .catch(next) + } +} + + +router.post('/retropilot/0/useradmin/auth', bodyParser.urlencoded({extended: true}), runAsyncWrapper(async (req, res) => { + const signIn = await authentication.signIn(req.body.email, req.body.password) + + if (signIn.success) { + res.cookie('jwt', signIn.jwt, {signed: true}); + res.redirect('/useradmin/overview'); + } else { + res.redirect('/useradmin?status=' + encodeURIComponent('Invalid credentials or banned account')); + } +})) + + +router.get('/retropilot/0/useradmin/signout', runAsyncWrapper(async (req, res) => { + res.clearCookie('session'); + return res.json({success: true}); +})) + +router.get('/session/get', runAsyncWrapper(async (req, res) => { + const account = await controllers.authentication.getAuthenticatedAccount(req, res); + + if (!account) { + res.json({success: true, hasSession: false, session: {}}) + } else { + res.json({success: true, hasSession: false, session: account}) + + } + +})) + +module.exports = router \ No newline at end of file diff --git a/routes/api/registration.js b/routes/api/registration.js new file mode 100644 index 0000000..a588f90 --- /dev/null +++ b/routes/api/registration.js @@ -0,0 +1,38 @@ +const router = require('express').Router(); +const config = require('./../../config'); + +const userController = require('./../../controllers/users') + +router.post('/retropilot/0/register/email', bodyParser.urlencoded({extended: true}), runAsyncWrapper(async (req, res) => { + if (!req.body.hasOwnProperty('email') || req.body.email === "" || !req.body.hasOwnProperty('password') || req.body.password === "") { + res.json({success: false, msg: 'malformed request'}).status(400); + logger.warn("/useradmin/register/token - Malformed Request!") + return; + } + + const accountStatus = await controllers.users.createAccount(req.body.email, req.body.password); + + if (accountStatus && accountStatus.status) { + return res.json(accountStatus).status(accountStatus.status) + } else { + return res.json({success: false, msg: 'contact server admin'}).status(500); + } +})); + + +router.get('/retropilot/0/register/verify/:token', bodyParser.urlencoded({extended: true}), runAsyncWrapper(async (req, res) => { + if (!req.params.token) { + res.json({success: false, status: 400, data: {missingToken: true}}).status(400); + } + + const verified = await userController.verifyEmailToken(req.params.token) + + if (verified && verified.status) { + return res.json(verified).status(verified.status) + } else { + return res.json({success: false, msg: 'contact server admin'}).status(500); + } +})); + + +module.exports = router; \ No newline at end of file diff --git a/routes/userAdminApi.js b/routes/userAdminApi.js index c486b9a..abeae04 100644 --- a/routes/userAdminApi.js +++ b/routes/userAdminApi.js @@ -22,13 +22,14 @@ let logger; router.post('/retropilot/0/useradmin/auth', bodyParser.urlencoded({extended: true}), runAsyncWrapper(async (req, res) => { - const account = await models.__db.get('SELECT * FROM accounts WHERE email = ? AND password = ?', req.body.email, crypto.createHash('sha256').update(req.body.password + config.applicationSalt).digest('hex')); + const signIn = await controllers.authentication.signIn(req.body.email, req.body.password) - if (!account || account.banned) { - return res.json({success: false, msg: account ? 'BANNED' : 'INVALID ACCOUNT'}).status(401); + if (signIn.success) { + res.cookie('jwt', signIn.jwt, {signed: true}); + res.redirect('/useradmin/overview'); + } else { + res.redirect('/useradmin?status=' + encodeURIComponent('Invalid credentials or banned account')); } - res.cookie('session', {account: account.email, expires: Date.now() + 1000 * 3600 * 24 * 365}, {signed: true}); - res.json({success: true}) })) @@ -65,60 +66,6 @@ router.get('/retropilot/0/useradmin', runAsyncWrapper(async (req, res) => { Requires username and password to register */ -router.post('/retropilot/0/register/email', bodyParser.urlencoded({extended: true}), runAsyncWrapper(async (req, res) => { - if (!req.body.hasOwnProperty('email') || req.body.email === "" || !req.body.hasOwnProperty('password') || req.body.password === "") { - res.json({success: false, msg: 'malformed request'}).status(400); - logger.warn("/useradmin/register/token - Malformed Request!") - return; - } - - const accountStatus = await controllers.users.createAccount(req.body.email, req.body.password); - - if (accountStatus && accountStatus.status) { - return res.json(accountStatus).status(accountStatus.status) - } else { - return res.json({success: false, msg: 'contact server admin'}).status(500); - } -})); - - -router.get('/retropilot/0/register/verify/:token', bodyParser.urlencoded({extended: true}), runAsyncWrapper(async (req, res) => { - if (!req.params.token) { - res.json({success: false, status: 400, data: {missingToken: true}}).status(400); - } - - const verified = await controllers.users.verifyEmailToken(req.params.token) - - console.log(verified); - - if (verified && verified.status) { - return res.json(verified).status(verified.status) - } else { - return res.json({success: false, msg: 'contact server admin'}).status(500); - } -})); - - - - - - -router.get('/retropilot/0/dongle/:dongle_id/nickname/:nickname', bodyParser.urlencoded({extended: true}), runAsyncWrapper(async (req, res) => { - if (!req.params.nickname || !req.params.dongle_id) { - return res.json({success: false, status: 400, msg: 'MISSING PRAMS'}).status(400); - } - - const account = await controllers.authentication.getAuthenticatedAccount(req, res); - if (account == null) { - return res.redirect('/useradmin?status=' + encodeURIComponent('Invalid or expired session')); - - } - - const setNickname = await controllers.devices.setDeviceNickname(account, req.params.dongle_id, req.params.nickname) - if (setNickname.status === true) { - res.json({success: true, data: {nickname: setNickname.data.nickname}}) - } -})); /* router.post('/useradmin/register/token', bodyParser.urlencoded({extended: true}), runAsyncWrapper(async (req, res) => { diff --git a/routes/useradmin.js b/routes/useradmin.js index 5d92320..435b1a0 100644 --- a/routes/useradmin.js +++ b/routes/useradmin.js @@ -241,7 +241,7 @@ router.get('/useradmin/unpair_device/:dongleId', runAsyncWrapper(async (req, res return; } - const pairDevice = await controllers.devices.pairDevice(req.body.qr_string); + const pairDevice = await controllers.devices.pairDevice(account, req.body.qr_string); if (pairDevice.success === true) { res.redirect('/useradmin/overview'); diff --git a/server.js b/server.js index 20907ac..8f78484 100644 --- a/server.js +++ b/server.js @@ -40,6 +40,11 @@ function runAsyncWrapper(callback) { const app = express(); + + +const athena = require('./Anetha/index'); + + const web = async () => { // TODO clean up const _models = await models(logger); @@ -57,10 +62,16 @@ const web = async () => { app.use(routers.api); app.use(routers.useradmin); + app.use((req, res, next) => { + + req.athenaWebsocketTemp = athena; + return next(); + }); + + app.use('/admin', routers.admin); - //if (config.flags.useUserAdminApi) app.use(routers.useradminapi); - //app.use(routers.useradminapi) + app.use(cors()); @@ -84,14 +95,14 @@ const web = async () => { app.get('*', runAsyncWrapper(async (req, res) => { logger.error("HTTP.GET unhandled request: " + controllers.helpers.simpleStringify(req) + ", " + controllers.helpers.simpleStringify(res) + "") - res.status(400); + res.status(404); res.send('Not Implemented'); })) app.post('*', runAsyncWrapper(async (req, res) => { logger.error("HTTP.POST unhandled request: " + controllers.helpers.simpleStringify(req) + ", " + controllers.helpers.simpleStringify(res) + "") - res.status(400); + res.status(404); res.send('Not Implemented'); })); @@ -113,8 +124,6 @@ lockfile.lock('retropilot_server.lock', {realpath: false, stale: 30000, update: var httpsServer = https.createServer(sslCredentials, app); - - httpServer.listen(config.httpPort, config.httpInterface, () => { logger.info(`Retropilot Server listening at http://` + config.httpInterface + `:` + config.httpPort) });