Switch from RabbitMQ to server-sent events

(not fully working yet)
This commit is contained in:
2021-05-03 20:58:40 +02:00
parent 533749c7c8
commit daa7209742
38 changed files with 22158 additions and 672 deletions

View File

@@ -1,9 +1,13 @@
import { Response } from "express";
import { eventManager, logger } from "../app";
import { LivebeatRequest } from "../lib/request";
import { IBeat } from "../models/beat/beat.interface";
import { Beat } from "../models/beat/beat.model.";
import { ISeverity } from "../models/notifications/notification.interface";
import { Phone } from "../models/phone/phone.model";
const timeouts: Map<string, NodeJS.Timeout> = new Map<string, NodeJS.Timeout>();
export async function GetBeatStats(req: LivebeatRequest, res: Response) {
const phones = await Phone.find({ user: req.user?._id });
const perPhone: any = {};
@@ -20,27 +24,88 @@ export async function GetBeatStats(req: LivebeatRequest, res: Response) {
}
export async function GetBeat(req: LivebeatRequest, res: Response) {
const from: number = Number(req.query.from);
const to: number = Number(req.query.to);
const from: number = Number(req.query.from || 0);
const to: number = Number(req.query.to || Date.now() / 1000);
const limit: number = Number(req.query.limit || 10000);
const sort: number = Number(req.query.sort || 1); // Either -1 or 1
const phoneId = req.query.phoneId;
// Grab default phone if non was provided.
const phone = req.query.phone === undefined ? await Phone.findOne({ user: req.user?._id }) : await Phone.findOne({ _id: phoneId, user: req.user?._id });
let beats: IBeat[] = []
let beats: IBeat[] = [];
//console.log(from, to);
//console.log(`Search from ${new Date(from).toString()} to ${new Date(to * 1000).toString()}`);
if (phone !== null) {
beats = await Beat.find(
{
phone: phone._id,
createdAt: {
$gte: new Date((from | 0) * 1000),
$lte: new Date((to | Date.now() /1000) * 1000)
$gte: new Date((from)),
$lte: new Date(to * 1000)
}
}).sort({ _id: sort }).limit(limit);
res.status(200).send(beats);
} else {
res.status(404).send({ message: 'Phone not found' });
}
}
export async function AddBeat(req: LivebeatRequest, res: Response) {
const beat = req.body as IBeat;
const androidId = req.headers.deviceId as string;
if (androidId === undefined) {
res.status(401).send({ message: 'Device id is missing' });
}
// Get phone
const phone = await Phone.findOne({ androidId });
if (phone == undefined) {
logger.warning(`Received beat from unknown device with id ${androidId}`);
return;
}
let newBeat;
if (beat.coordinate !== undefined && beat.accuracy !== undefined) {
logger.info(`New beat from ${phone.displayName} => ${beat.coordinate[0]}, ${beat.coordinate[1]} | Height: ${beat.coordinate[3]}m | Speed: ${beat.coordinate[4]} | Accuracy: ${beat.accuracy}% | Battery: ${beat.battery}%`);
newBeat = await Beat.create({
phone: phone._id,
// [latitude, longitude, altitude]
coordinate: [beat.coordinate[0], beat.coordinate[1], beat.coordinate[2]],
accuracy: beat.coordinate[3],
speed: beat.coordinate[4],
battery: beat.battery,
createdAt: Date.now()
});
}
newBeat = await Beat.create({
phone: phone._id,
battery: beat.battery,
createdAt: Date.now()
});
// Broadcast if device became active
if (timeouts.has(phone.id)) {
clearTimeout(timeouts.get(phone.id)!!);
} else {
phone.active = true;
await phone.save();
eventManager.push('phone_alive', phone.toJSON(), phone.user);
}
const timeoutTimer = setTimeout(async () => {
eventManager.push('phone_dead', phone.toJSON(), phone.user, ISeverity.WARN);
timeouts.delete(phone.id);
phone.active = false;
await phone.save();
}, 60_000);
timeouts.set(phone.id, timeoutTimer);
eventManager.push('beat', newBeat.toJSON(), phone.user);
}

View File

@@ -5,7 +5,6 @@ import { Beat } from "../models/beat/beat.model.";
import { Phone } from "../models/phone/phone.model";
export async function GetPhone(req: LivebeatRequest, res: Response) {
const phoneId: String = req.params['id'];

View File

@@ -1,7 +1,7 @@
import { Request, Response } from 'express';
import { decode, sign, verify } from 'jsonwebtoken';
import { JWT_SECRET, logger, RABBITMQ_URI } from '../app';
import { eventManager, JWT_SECRET, logger, RABBITMQ_URI } from '../app';
import * as jwt from 'jsonwebtoken';
import { config } from '../config';
import { hashPassword, randomPepper, randomString, verifyPassword } from '../lib/crypto';
@@ -51,7 +51,7 @@ export async function PostUser(req: LivebeatRequest, res: Response) {
}
const salt = randomString(config.authentification.salt_length);
const brokerToken = randomString(16);
const eventToken = randomString(16);
const hashedPassword = await hashPassword(password + salt + randomPepper()).catch(error => {
res.status(400).send({ message: 'Provided password is too weak and cannot be used.' });
return;
@@ -61,7 +61,7 @@ export async function PostUser(req: LivebeatRequest, res: Response) {
name,
password: hashedPassword,
salt,
brokerToken,
eventToken,
type,
lastLogin: new Date(0)
});
@@ -72,6 +72,27 @@ export async function PostUser(req: LivebeatRequest, res: Response) {
res.status(200).send({ setupToken });
}
export async function UserEvents(req: LivebeatRequest, res: Response) {
if (req.query.token === undefined) {
res.status(401).send({ message: 'You need to define your event token.' });
return;
}
const eventToken = req.query.token as string;
const user = await User.findOne({ eventToken });
if (user === null) {
res.status(401).send({ message: 'This event token is not valid.' });
return;
}
eventManager.join(user.id, res);
}
export async function UserSubscribeEvent(req: Request, res: Response) {
}
export async function DeleteUser(req: Request, res: Response) {
}
@@ -120,159 +141,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.