Added proof of concept for Athena

pull/4/head
AdamSBlack 2021-10-25 22:56:40 +01:00
parent 4075e5730a
commit 9361043d83
12 changed files with 300 additions and 101 deletions

130
Anetha/index.js 100644
View File

@ -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
}

View File

@ -8,7 +8,7 @@ async function validateJWT(token, key) {
try { try {
return jwt.verify(token.replace("JWT ", ""), key, {algorithms: ['RS256'], ignoreNotBefore: true}); return jwt.verify(token.replace("JWT ", ""), key, {algorithms: ['RS256'], ignoreNotBefore: true});
} catch (exception) { } catch (exception) {
logger.warn(`failed to validate JWT ${exception}`) console.log(`failed to validate JWT ${exception}`)
} }
return null; return null;
} }
@ -26,12 +26,9 @@ async function signIn(email, password) {
let account = await models_orm.models.accounts.findOne({where: {email: email}}); let account = await models_orm.models.accounts.findOne({where: {email: email}});
if (account.dataValues) { if (account.dataValues) {
account = account.dataValues; account = account.dataValues;
const inputPassword = crypto.createHash('sha256').update(password + config.applicationSalt).digest('hex'); const inputPassword = crypto.createHash('sha256').update(password + config.applicationSalt).digest('hex');
if (account.password === inputPassword) { if (account.password === inputPassword) {
const token = jwt.sign({accountId: account.id}, config.applicationSalt) const token = jwt.sign({accountId: account.id}, config.applicationSalt)
return {success: true, jwt: token}; return {success: true, jwt: token};
@ -44,6 +41,8 @@ async function signIn(email, password) {
} }
async function changePassword(account, newPassword, oldPassword) { async function changePassword(account, newPassword, oldPassword) {
if (!account || !newPassword || !oldPassword) { return {success: false, error: 'MISSING_DATA'}} if (!account || !newPassword || !oldPassword) { return {success: false, error: 'MISSING_DATA'}}
const oldPasswordHash = crypto.createHash('sha256').update(oldPassword + config.applicationSalt).digest('hex') const oldPasswordHash = crypto.createHash('sha256').update(oldPassword + config.applicationSalt).digest('hex')
@ -101,5 +100,6 @@ module.exports = {
validateJWT: validateJWT, validateJWT: validateJWT,
getAuthenticatedAccount: getAuthenticatedAccount, getAuthenticatedAccount: getAuthenticatedAccount,
changePassword: changePassword, changePassword: changePassword,
signIn: signIn signIn: signIn,
readJWT: readJWT
} }

View File

@ -13,7 +13,7 @@ async function pairDevice(account, qr_string) {
let deviceQuery; let deviceQuery;
let pairJWT; let pairJWT;
if (qrCodeParts.length > 0) { 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]; pairJWT = qrCodeParts[2];
} else { } else {
pairJWT = qr_string; 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 }}); deviceQuery = await models_orm.models.device.findOne({ where: { dongle_id: data.identiy }});
} }
if (deviceQuery.dataValues == null) {
if (deviceQuery == null) {
return {success: false, registered: false} return {success: false, registered: false}
} }
const device = deviceQuery.dataValues; 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) { if (decoded == null || decoded.pair == undefined) {
return {success: false, badToken: true} return {success: false, badToken: true}
} }
if (device.account_id != 0) { if (device.account_id != 0) {
return {success: false, alreadyPaired: true, dongle_id: device.dongle_id} 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( async function pairDeviceToAccountId(dongle_id, account_id) {
{ account_id: account.id }, console.log("input", account_id, dongle_id)
{ where: { dongle_id: device.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) { async function unpairDevice(account, dongleId) {
@ -68,13 +85,15 @@ async function setDeviceNickname(account, dongleId, nickname) {
} }
async function getDevices(accountId) { 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) { 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 return devices.dataValues || null
} }
@ -91,10 +110,9 @@ async function setIgnoredUploads(dongleId, isIgnored) {
} }
async function getAllDevicesFiltered() { async function getAllDevicesFiltered() {
console.log(models_orm.models.device)
const devices = await models_orm.models.device.findAll(); const devices = await models_orm.models.device.findAll();
return devices.dataValues || null return devices
} }
@ -106,4 +124,5 @@ module.exports = {
getDeviceFromDongle, getDeviceFromDongle,
setIgnoredUploads, setIgnoredUploads,
getAllDevicesFiltered, getAllDevicesFiltered,
pairDeviceToAccountId,
} }

View File

@ -9,7 +9,7 @@ module.exports = async (models, logger, models_sqli) => {
helpers: require('./helpers')(models, logger), helpers: require('./helpers')(models, logger),
storage: require('./storage')(models, logger), storage: require('./storage')(models, logger),
mailing: require('./mailing')(models, logger), mailing: require('./mailing')(models, logger),
users: require('./users')(models, logger), users: require('./users'),
admin: require('./admin'), admin: require('./admin'),
devices: require('./devices') devices: require('./devices')
} }

View File

@ -2,8 +2,7 @@ const config = require('./../config');
const crypto = require('crypto'); const crypto = require('crypto');
const models_orm = require('./../models/index.model'); const models_orm = require('./../models/index.model');
const authentication = require('./authentication'); const authentication = require('./authentication');
let models;
let logger;
async function getAccountFromId(id) { async function getAccountFromId(id) {
@ -70,14 +69,9 @@ async function getAllUsers() {
module.exports = (_models, _logger) => { module.exports = {
models = _models;
logger = _logger;
return {
createAccount, createAccount,
verifyEmailToken, verifyEmailToken,
getAccountFromId, getAccountFromId,
getAllUsers getAllUsers
} }
}

View File

@ -14,6 +14,7 @@
"@sendgrid/client": "^7.4.3", "@sendgrid/client": "^7.4.3",
"chai": "^4.3.4", "chai": "^4.3.4",
"chai-http": "^4.3.0", "chai-http": "^4.3.0",
"cookie": "^0.4.1",
"cookie-parser": "^1.4.5", "cookie-parser": "^1.4.5",
"cors": "^2.8.5", "cors": "^2.8.5",
"crypto": "^1.0.1", "crypto": "^1.0.1",
@ -21,6 +22,7 @@
"esm": "^3.2.25", "esm": "^3.2.25",
"express": "^4.17.1", "express": "^4.17.1",
"express-fileupload": "^1.2.1", "express-fileupload": "^1.2.1",
"express-ws": "^5.0.2",
"fast-folder-size": "^1.3.0", "fast-folder-size": "^1.3.0",
"ffprobe": "^1.1.2", "ffprobe": "^1.1.2",
"ffprobe-static": "^3.0.0", "ffprobe-static": "^3.0.0",
@ -36,6 +38,8 @@
"sequelize": "^6.6.5", "sequelize": "^6.6.5",
"sqlite": "^4.0.22", "sqlite": "^4.0.22",
"sqlite3": "^5.0.2", "sqlite3": "^5.0.2",
"supertest": "^6.1.3" "supertest": "^6.1.3",
"websocket": "^1.0.34",
"ws": "^8.2.3"
} }
} }

View File

@ -3,7 +3,7 @@ const bodyParser = require('body-parser');
const crypto = require('crypto'); const crypto = require('crypto');
const { route } = require('../../server'); const { route } = require('../../server');
const config = require('./../../config'); const config = require('./../../config');
const deviceController = require('./../../controllers/devices')
function runAsyncWrapper(callback) { function runAsyncWrapper(callback) {
return function (req, res, next) { 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) => { 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})} 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) => { 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) => { module.exports = (_models, _controllers, _logger) => {

View File

@ -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

View File

@ -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;

View File

@ -22,13 +22,14 @@ let logger;
router.post('/retropilot/0/useradmin/auth', bodyParser.urlencoded({extended: true}), runAsyncWrapper(async (req, res) => { 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) { if (signIn.success) {
return res.json({success: false, msg: account ? 'BANNED' : 'INVALID ACCOUNT'}).status(401); 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 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) => { router.post('/useradmin/register/token', bodyParser.urlencoded({extended: true}), runAsyncWrapper(async (req, res) => {

View File

@ -241,7 +241,7 @@ router.get('/useradmin/unpair_device/:dongleId', runAsyncWrapper(async (req, res
return; 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) { if (pairDevice.success === true) {
res.redirect('/useradmin/overview'); res.redirect('/useradmin/overview');

View File

@ -40,6 +40,11 @@ function runAsyncWrapper(callback) {
const app = express(); const app = express();
const athena = require('./Anetha/index');
const web = async () => { const web = async () => {
// TODO clean up // TODO clean up
const _models = await models(logger); const _models = await models(logger);
@ -57,10 +62,16 @@ const web = async () => {
app.use(routers.api); app.use(routers.api);
app.use(routers.useradmin); app.use(routers.useradmin);
app.use((req, res, next) => {
req.athenaWebsocketTemp = athena;
return next();
});
app.use('/admin', routers.admin); app.use('/admin', routers.admin);
//if (config.flags.useUserAdminApi) app.use(routers.useradminapi);
//app.use(routers.useradminapi)
app.use(cors()); app.use(cors());
@ -84,14 +95,14 @@ const web = async () => {
app.get('*', runAsyncWrapper(async (req, res) => { app.get('*', runAsyncWrapper(async (req, res) => {
logger.error("HTTP.GET unhandled request: " + controllers.helpers.simpleStringify(req) + ", " + controllers.helpers.simpleStringify(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'); res.send('Not Implemented');
})) }))
app.post('*', runAsyncWrapper(async (req, res) => { app.post('*', runAsyncWrapper(async (req, res) => {
logger.error("HTTP.POST unhandled request: " + controllers.helpers.simpleStringify(req) + ", " + controllers.helpers.simpleStringify(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'); res.send('Not Implemented');
})); }));
@ -113,8 +124,6 @@ lockfile.lock('retropilot_server.lock', {realpath: false, stale: 30000, update:
var httpsServer = https.createServer(sslCredentials, app); var httpsServer = https.createServer(sslCredentials, app);
httpServer.listen(config.httpPort, config.httpInterface, () => { httpServer.listen(config.httpPort, config.httpInterface, () => {
logger.info(`Retropilot Server listening at http://` + config.httpInterface + `:` + config.httpPort) logger.info(`Retropilot Server listening at http://` + config.httpInterface + `:` + config.httpPort)
}); });