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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) => {

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) => {
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) => {

View File

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

View File

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