Socket connection now works

- Pairing a new device works

(I did a lot since the last commit)
This commit is contained in:
2021-11-14 01:42:21 +01:00
parent 28e85ea730
commit 19b7c05d75
72 changed files with 5861 additions and 23719 deletions

View File

@@ -1,63 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ChangeListManager">
<list default="true" id="19050b14-6aa3-4201-be2a-b6c9fb417ff6" name="Default Changelist" comment="">
<change beforePath="$PROJECT_DIR$/../android/app/src/main/java/de/nicolasklier/livebeat/MainActivity.kt" beforeDir="false" afterPath="$PROJECT_DIR$/../android/app/src/main/java/de/nicolasklier/livebeat/MainActivity.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app.ts" beforeDir="false" afterPath="$PROJECT_DIR$/app.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config.ts" beforeDir="false" afterPath="$PROJECT_DIR$/config.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/endpoints/beat.ts" beforeDir="false" afterPath="$PROJECT_DIR$/endpoints/beat.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/endpoints/phone.ts" beforeDir="false" afterPath="$PROJECT_DIR$/endpoints/phone.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/lib/rabbit.ts" beforeDir="false" afterPath="$PROJECT_DIR$/lib/rabbit.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../frontend/src/app/api.service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/../frontend/src/app/api.service.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../frontend/src/app/map.worker.ts" beforeDir="false" afterPath="$PROJECT_DIR$/../frontend/src/app/map.worker.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../frontend/src/app/map/map.component.html" beforeDir="false" afterPath="$PROJECT_DIR$/../frontend/src/app/map/map.component.html" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/../package-lock.json" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." />
</component>
<component name="ProjectId" id="1qRNQeCqRVVNuS6QKJoYRrUYOnM" />
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
<property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
<property name="WebServerToolWindowFactoryState" value="false" />
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="19050b14-6aa3-4201-be2a-b6c9fb417ff6" name="Default Changelist" comment="" />
<created>1617038467045</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1617038467045</updated>
<workItem from="1617038468601" duration="1438000" />
<workItem from="1617121449363" duration="83000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State />
</value>
</entry>
</map>
</option>
<option name="oldMeFiltersMigrated" value="true" />
</component>
</project>

View File

@@ -7,14 +7,15 @@ import * as figlet from 'figlet';
import * as mongoose from 'mongoose';
import { exit } from 'process';
import * as winston from 'winston';
import { createServer } from 'http';
import { config } from './config';
import { GetBeat, GetBeatStats } from './endpoints/beat';
import { getNotification } from './endpoints/notification';
import { GetPhone, PostPhone } from './endpoints/phone';
import { DeleteUser, GetUser, LoginRabbitUser, LoginUser, MW_User, PatchUser, PostUser, Resource, Topic, VHost } from './endpoints/user';
import { DeleteUser, GetUser, LoginUser, MW_User, PatchUser, PostUser } from './endpoints/user';
import { hashPassword, randomPepper, randomString } from './lib/crypto';
import { RabbitMQ } from './lib/rabbit';
import { SocketManager } from './lib/socketio';
import { UserType } from './models/user/user.interface';
import { User } from './models/user/user.model';
@@ -27,7 +28,6 @@ export const JWT_SECRET = process.env.JWT_SECRET || "";
export const IS_DEBUG = process.env.DEBUG == 'true';
export let logger: winston.Logger;
export let rabbitmq: RabbitMQ;
async function run() {
const { combine, timestamp, label, printf, prettyPrint } = winston.format;
@@ -108,10 +108,8 @@ async function run() {
await User.create({
name: 'admin',
password: await hashPassword(randomPassword + salt + randomPepper()),
brokerToken: randomString(16),
salt,
createdAt: Date.now(),
lastLogin: 0,
lastLogin: new Date(0),
type: UserType.ADMIN
});
logger.info("===================================================");
@@ -124,14 +122,23 @@ async function run() {
/**
* HTTP server
*/
logger.debug("Preparing HTTP server ...")
const app = express();
app.use(express.json());
const server = createServer(app);
app.use(cors());
app.options('*', cors());
app.use(express.json());
app.use(bodyParser.json({ limit: '5kb' }));
app.use((req, res, next) => {
res.on('finish', () => {
const done = Date.now();
// Censor any user passwords
if (req.body.password != null) {
req.body.password = "***********";
}
logger.debug(`${req.method} - ${req.url} ${JSON.stringify(req.body)} -> ${res.statusCode}`);
});
next();
@@ -141,10 +148,6 @@ async function run() {
// User authentication
app.post('/user/login', (req, res) => LoginUser(req, res));
app.get('/user/rabbitlogin', (req, res) => LoginRabbitUser(req, res));
app.get('/user/vhost', (req, res) => VHost(req, res));
app.get('/user/resource', (req, res) => Resource(req, res));
app.get('/user/topic', (req, res) => Topic(req, res));
// CRUD user
app.get('/user/notification', MW_User, (req, res) => getNotification(req, res)); // Notifications
@@ -163,16 +166,11 @@ async function run() {
app.get('/beat/', MW_User, (req, res) => GetBeat(req, res));
app.get('/beat/stats', MW_User, (req, res) => GetBeatStats(req, res));
app.listen(config.http.port, config.http.host, () => {
const socketManager = new SocketManager(server);
server.listen(config.http.port, config.http.host, () => {
logger.info(`HTTP server is running at ${config.http.host}:${config.http.port}`);
});
/**
* Message broker
*/
rabbitmq = new RabbitMQ();
await rabbitmq.init();
logger.info("Connected with message broker.");
}
run();

View File

@@ -5,12 +5,16 @@ import { Beat } from "../models/beat/beat.model.";
import { Phone } from "../models/phone/phone.model";
export async function GetBeatStats(req: LivebeatRequest, res: Response) {
const phones = await Phone.find({ user: req.user?._id });
const phones = await Phone.find({ user: req.user?._id }).exec();
const perPhone: any = {};
let totalBeats = 0;
if (phones[0] == undefined) return;
const phone = phones[0];
for (let i = 0; i < phones.length; i++) {
const beatCount = await Beat.countDocuments({ phone: phones[i] });
const beatCount = await Beat.countDocuments({ [phone.id]: phone.id });
perPhone[phones[i]._id] = {};
perPhone[phones[i]._id] = beatCount;
totalBeats += beatCount;

View File

@@ -1,5 +1,5 @@
import { Response } from "express";
import { logger, rabbitmq } from "../app";
import { logger } from "../app";
import { LivebeatRequest } from "../lib/request";
import { Beat } from "../models/beat/beat.model.";
import { Phone } from "../models/phone/phone.model";
@@ -66,7 +66,7 @@ export async function PostPhone(req: LivebeatRequest, res: Response) {
});
logger.info(`New device (${displayName}) registered for ${req.user?.name}.`);
rabbitmq.publish(req.user?.id, newPhone.toJSON(), 'phone_register')
//rabbitmq.publish(req.user?.id, newPhone.toJSON(), 'phone_register')
res.status(200).send();
}

View File

@@ -111,7 +111,7 @@ export async function LoginUser(req: Request, res: Response) {
}
// We're good. Create JWT token.
const token = sign({ user: user._id }, JWT_SECRET, { expiresIn: '30d' });
const token = sign({ user: user._id, type: 'frontend' }, JWT_SECRET, { expiresIn: '30d' });
user.lastLogin = new Date(Date.now());
await user.save();
@@ -120,159 +120,6 @@ export async function LoginUser(req: Request, res: Response) {
res.status(200).send({ token });
}
/**
* This function handles all logins to RabbitMQ since they need a differnt type of response
* then requests from frontends (web and phone).
*/
export async function LoginRabbitUser(req: Request, res: Response) {
const username = req.query.username;
const password = req.query.password;
res.status(200);
if (username === undefined || password === undefined) {
res.send('deny');
return;
}
// Check if request comes from backend. Basicly, we permitting ourself to connect with RabbitMQ.
if (username === "backend" && password === RABBITMQ_URI.split(':')[2].split('@')[0]) {
res.send('allow administrator');
return;
}
// Get user from database
const user = await User.findOne({ name: username.toString() });
// If we are here, it means we have a non-admin user.
if (user === null) {
res.send('deny');
return;
}
// Auth token for message broker is stored in plain text since it's randomly generated and only grants access to the broker.
if (user.brokerToken === password.toString()) {
if (user.type === UserType.ADMIN) {
res.send('allow administrator');
} else {
// Not an admin, grant user privilieges
res.send('allow user')
}
return;
}
res.send('deny');
}
/**
* This function basicly allows access to the root vhost if the user is known.
*/
export async function VHost(req: Request, res: Response) {
const vhost = req.query.vhost;
const username = req.query.username;
if (vhost === undefined || username === undefined) {
res.status(200).send('deny');
return;
}
if (vhost != '/') {
res.status(200).send('deny');
return;
}
// Check if user is us
if (username === 'backend') {
res.status(200).send('allow');
return;
}
const user = await User.findOne({ name: username.toString() });
if (user === null) {
// Deny if user doesn't exist.
res.status(200).send('deny');
} else {
res.status(200).send('allow');
}
}
export async function Resource(req: Request, res: Response) {
const username = req.query.username;
const vhost = req.query.vhost;
const resource = req.query.resource;
const name = req.query.name;
const permission = req.query.permission;
const tags = req.query.tags;
if (username === undefined || vhost === undefined || resource === undefined || name === undefined || permission === undefined || tags === undefined) {
res.status(200).send('deny');
return;
}
// Check if it's us
if (username.toString() == 'backend') {
res.status(200).send('allow');
return;
}
// Deny if not root vhost
if (vhost.toString() != '/') {
res.status(200).send('deny');
return;
}
// Check if user exists
const user = await User.findOne({ name: username.toString() });
if (user == null) {
res.status(200).send('deny');
return;
}
if (tags.toString() == "administrator" && user.type != UserType.ADMIN) {
res.status(200).send('deny');
return;
}
// TODO: This has to change if we want to allow users to see the realtime movement of others.
if (resource.toString().startsWith('tracker-') && resource != 'tracker-' + username) {
res.status(200).send('deny');
return;
}
res.status(200).send('allow');
}
export async function Topic(req: Request, res: Response) {
res.status(200);
const username = req.query.username;
const routingKey = req.query.routing_key;
if (routingKey === undefined || username === undefined) {
res.send('deny');
return;
}
// Check if it's us
if (username.toString() == 'backend') {
res.status(200).send('allow');
return;
}
// Check if user exists
const user = await User.findOne({ name: username.toString() });
if (user === null) {
res.send('deny');
return;
}
if (routingKey !== user.id) {
res.send('deny');
return;
}
res.status(200).send('allow');
}
/**
* This middleware validates any tokens that are required to access most of the endpoints.
* Note: This validation doesn't contain any permission checking.

View File

@@ -1,6 +1,7 @@
import { hash, verify } from 'argon2';
import { verify as jwtVerify } from 'jsonwebtoken';
import { config } from '../config';
import { IS_DEBUG, logger } from '../app';
import { IS_DEBUG, JWT_SECRET, logger } from '../app';
export async function hashPassword(input: string): Promise<string> {
const start = Date.now();
@@ -64,9 +65,19 @@ export async function verifyPassword(password: string, hashInput: string): Promi
});
}
export function randomString(length: number): string {
export async function verifyJWT(token: string): Promise<boolean> {
return new Promise<boolean>(async (resolve, reject) => {
try {
jwtVerify(token, JWT_SECRET, { algorithms: ['HS256'] });
resolve(true);
} catch {
resolve(false);
}
});
}
export function randomString(length: number, characters: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'): string {
let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const charactersLength = characters.length;
for ( let i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));

View File

@@ -1,23 +1,165 @@
import * as socketio from "socket.io";
import { Server } from 'http';
import { JWT_SECRET, logger } from "../app";
import { randomString, verifyJWT } from "./crypto";
import { decode, sign } from "jsonwebtoken";
import { User } from "../models/user/user.model";
import { IPhone } from "../models/phone/phone.interface";
import { IUser } from "../models/user/user.interface";
import { Phone } from "../models/phone/phone.model";
/**
* This class handles all SocketIO connections.
*
* *SocketIO is another layer ontop of WebSockets*
*/
class SocketManager {
export class SocketManager {
io: socketio.Server;
express: Express.Application;
constructor (express: Express.Application) {
this.io = new socketio.Server(express);
this.express = express;
/**
* Frontends have limited access to socket.io features. They just sit in connection and wait for any events.
*/
frontends: Array<string>;
/**
* A phone has some more privileges. They activly send new data and thus have write access.
*/
phones: Array<string>;
constructor(httpServer: Server) {
logger.debug("Preparing real-time communication ...");
this.frontends = [];
this.phones = [];
this.io = new socketio.Server();
this.io.listen(httpServer);
this.init();
}
getUserRoom(user: IUser) {
return `user-${user.id}`;
}
getUserFrontendRoom(user: IUser) {
return `user-${user.id}-frontend`;
}
getUserPhoneRoom(user: IUser) {
return `user-${user.id}-phone`;
}
init() {
this.io.on('connect', data => {
console.log('New connection')
this.io.on('connection', socket => {
socket.on('requestAccess', async data => {
data = JSON.parse(data);
let token: string = data.token;
let phone: IPhone = data.phone;
// If request is faulty or token invalid -> return.
if (data === undefined || phone === undefined) return;
if (await !verifyJWT(token)) return;
const id = decode(token, { json: true })!.user;
const user = await User.findById(id);
// If user doesn't exist -> return.
if (user === null) return;
const approvalCode = randomString(6, '0123456789');
// Create phone
const newPhone = await Phone.create({
...phone,
user,
approval: {
code: approvalCode
}
});
this.io.to(this.getUserRoom(user)).emit('approvePhone', newPhone);
// Respond with id so device can later submit correct code.
socket.emit('requestAccess', { phoneId: newPhone.id });
logger.info(`User ${user?.name} requests to connect new phone ${phone.displayName}`);
});
socket.on('submitPairCode', async data => {
const { phoneId, code } = JSON.parse(data);
console.log("Entry:", data, phoneId, code);
if (phoneId === undefined || code === undefined) return;
const phone = await Phone.findById(phoneId);
if (phone === null) return;
console.log(data, phoneId, code);
// If provided code isn't equal with actual code -> Emit event again.
if (phone.approval.code !== code) {
console.log(data, phoneId, code);
socket.emit('submitPairCode', '');
return;
}
phone.approval.approvedOn = new Date();
await phone.save();
// We're good. Create JWT token.
const token = sign({ user: phone.user._id, type: 'phone' }, JWT_SECRET, { expiresIn: '30d' });
socket.emit('submitPairCode', token);
});
socket.on('loginFrontend', async (token: string) => {
if (await verifyJWT(token)) {
const tokenDecoded = decode(token, { json: true });
const id = tokenDecoded!.user;
const type = tokenDecoded!.type;
const user = await User.findById(id);
if (user == null) return;
if (type != 'frontend') return;
if (this.frontends.indexOf(socket.id) != -1)
this.frontends.push(socket.id);
socket.join(this.getUserRoom(user));
socket.join(this.getUserFrontendRoom(user));
logger.info(`Socket ${socket.id} became a frontend socket.`);
}
});
socket.on('loginPhone', async (token: string) => {
if (await verifyJWT(token)) {
const tokenDecoded = decode(token, { json: true });
const id = tokenDecoded!.user;
const type = tokenDecoded!.type;
const user = await User.findById(id);
if (user == null) return;
if (type != 'phone') return;
if (this.frontends.indexOf(socket.id) != -1)
this.frontends.push(socket.id);
socket.join(this.getUserRoom(user));
socket.join(this.getUserPhoneRoom(user));
logger.info(`Socket ${socket.id} became a phone socket.`);
}
});
logger.info(`New socket connection from ${socket.handshake.address} with id ${socket.id} (total connections: ${this.io.sockets.sockets.size})`);
socket.emit('test', 'Yay, it works.');
});
}

View File

@@ -4,11 +4,14 @@ import { IUser } from '../user/user.interface';
export interface IPhone extends Document {
androidId: String,
displayName: String,
modelName: String,
modelName: string,
operatingSystem: String,
architecture: String,
user: IUser,
active: Boolean,
approval: {
approvedOn?: Date,
code: String
},
updatedAt?: Date,
createdAt?: Date
}

View File

@@ -8,7 +8,10 @@ const schemaPhone = new Schema({
operatingSystem: { type: String, required: false },
architecture: { type: String, required: false },
user: { type: SchemaTypes.ObjectId, required: true },
active: { type: Boolean, required: true }
approval: {
approvedOn: { type: Date, required: false },
code: { type: String, required: true }
}
}, {
timestamps: {
createdAt: true,

View File

@@ -12,7 +12,5 @@ export interface IUser extends Document {
salt: string,
type: UserType,
lastLogin: Date,
twoFASecret?: string,
brokerToken: string,
createdAt?: Date
twoFASecret?: string
}

View File

@@ -6,7 +6,6 @@ const schemaUser = new Schema({
salt: { type: String, required: true },
type: { type: String, required: true, default: 'user' }, // This could be user, admin, guest
twoFASecret: { type: String, required: false },
brokerToken: { type: String, required: true },
lastLogin: { type: Date, required: true, default: Date.now },
}, {
timestamps: {

3810
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,10 +17,7 @@
"author": "Mondei1",
"license": "GPL-3.0-or-later",
"dependencies": {
"@types/node": "^14.14.9",
"@types/socket.io": "^2.1.13",
"amqplib": "^0.6.0",
"argon2": "^0.27.0",
"argon2": "^0.28.2",
"body-parser": "^1.19.0",
"chalk": "^4.1.0",
"cors": "^2.8.5",
@@ -29,14 +26,13 @@
"figlet": "^1.5.0",
"jsonwebtoken": "^8.5.1",
"moment": "^2.29.1",
"mongoose": "^5.10.9",
"socket.io": "^4.0.1",
"mongoose": "^5.13.7",
"socket.io": "^4.3.2",
"ts-node": "^9.0.0",
"typescript": "^4.0.3",
"winston": "^3.3.3"
},
"devDependencies": {
"@types/amqplib": "0.5.14",
"@types/argon2": "0.15.0",
"@types/body-parser": "1.19.0",
"@types/chalk": "2.2.0",
@@ -47,6 +43,8 @@
"@types/jsonwebtoken": "8.5.0",
"@types/moment": "2.13.0",
"@types/mongoose": "5.7.36",
"@types/node": "^14.14.9",
"@types/socket.io": "^2.1.13",
"@types/typescript": "2.0.0",
"@types/winston": "2.4.4",
"concurrently": "^5.3.0",