Revert "Allow to launch server without Redis (#119)" (#133)

This reverts commit 2a8d3281db851d15f9402d0fe69291255c6b250b.
This commit is contained in:
Alan Friedman 2020-05-05 10:03:50 -04:00 committed by GitHub
parent 5affceb47e
commit 18065f9652
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 113 additions and 270 deletions

1
.nvmrc
View File

@ -1 +0,0 @@
lts/*

View File

@ -16,22 +16,6 @@ The Darkwire.io [web client](/client) is written in JavaScript with React JS and
### Development
#### Prerequisites
Copy `.env.dist` files in `server/` and `client/` directories without the `.dist`
extensions and adapt them to your needs.
You need [Redis](https://redis.io/) in order to make the server works.
A simple way to achieve this, if you have docker, is to execute the following
command:
```
docker run --name darkwire-redis --rm -p 6379:6379 -d redis redis-server --appendonly yes
```
Alternatively, you can select the _memory_ `STORE_BACKEND` instead of _redis_
in your server `.env` file to avoid Redis use.
#### Setup
Install dependencies
@ -74,7 +58,7 @@ Here's an overview of a chat between Alice and Bob (also applies to group chats)
1. Bob creates a room and immediately creates a public/private key pair (RSA-OAEP).
2. Alice joins the room and also creates a public/private key pair. She is sent Bob's public key and she sends Bob her public key.
3. When Bob goes to send a message, three things are created: a session key (AES-CBC), a signing key (HMAC SHA-256) and an initialization vector (used in the encryption process).
4. Bob's message is encrypted with the session key and initialization vector, and a signature is created using the signing key.
4. Bob's message is encrypted with the session key and initialization vector, and a signature is created using the signing key.
5. The session key and signing key are encrypted with each recipient's public key (in this case only Alice, but in a group chat multiple).
6. The encrypted message, initialization vector, signature, encrypted session key and encrypted signing key are sent to all recipients (in this case just Alice) as a package.
7. Alice receives the package and decrypts the session key and signing key using her private key. She decrypts the message with the decrypted session key and vector, and verifies the signature with the decrypted signing key.
@ -87,7 +71,7 @@ Darkwire does not provide any guarantee that the person you're communicating wit
## File Transfer
Darkwire encodes documents (up to 1MB) into base64 using [btoa](https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/btoa) and is encrypted the same way chat messages are.
Darkwire encodes documents (up to 1MB) into base64 using [btoa](https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/btoa) and is encrypted the same way chat messages are.
1. When a file is "uploaded", the document is encoded on the client and the server recieves the encrypted base64 string.
2. The server sends the encrypted base64 string to clients in the same chat room.

View File

@ -1,15 +1,8 @@
# Abuse mail configuration
MAILGUN_API_KEY=api-key
MAILGUN_DOMAIN=darkwire.io
ABUSE_TO_EMAIL_ADDRESS=abuse@darkwire.io
ABUSE_FROM_EMAIL_ADDRESS=Darkwire <no-reply@darkwire.io>
REDIS_URL=redis://localhost:6379
CLIENT_DIST_DIRECTORY='client/dist/path'
ROOM_HASH_SECRET='some-uuid'
SITE_URL=https://darkwire.io
# Store configuration
STORE_BACKEND=redis
STORE_HOST=redis://localhost:6379
SITE_URL=https://darkwire.io

View File

@ -1,25 +1,21 @@
import getStore from './store';
import { getRedis } from './index'
export async function pollForInactiveRooms() {
const store = getStore();
const redis = getRedis();
console.log('Checking for inactive rooms...');
const rooms = (await store.getAll('rooms')) || {};
const rooms = await redis.hgetallAsync('rooms') || {};
console.log(`${Object.keys(rooms).length} rooms found`);
Object.keys(rooms).forEach(async (roomId) => {
Object.keys(rooms).forEach(async roomId => {
const room = JSON.parse(rooms[roomId]);
const timeSinceUpdatedInSeconds = (Date.now() - room.updatedAt) / 1000;
const timeSinceUpdatedInDays = Math.round(
timeSinceUpdatedInSeconds / 60 / 60 / 24
);
const timeSinceUpdatedInDays = Math.round(timeSinceUpdatedInSeconds / 60 / 60 / 24);
if (timeSinceUpdatedInDays > 7) {
console.log(
`Deleting roomId ${roomId} which hasn't been used in ${timeSinceUpdatedInDays} days`
);
await store.del('rooms', roomId);
console.log(`Deleting roomId ${roomId} which hasn't been used in ${timeSinceUpdatedInDays} days`);
await redis.hdelAsync('rooms', roomId);
}
});
})
setTimeout(pollForInactiveRooms, 1000 * 60 * 60 * 12); // every 12 hours
setTimeout(pollForInactiveRooms, (1000 * 60 * 60 * 12)); // every 12 hours
}

View File

@ -1,4 +1,4 @@
require('dotenv').config();
require('dotenv').config()
import http from 'http';
import https from 'https';
import Koa from 'koa';
@ -6,13 +6,22 @@ import Io from 'socket.io';
import KoaBody from 'koa-body';
import cors from 'kcors';
import Router from 'koa-router';
import bluebird from 'bluebird';
import Redis from 'redis';
import socketRedis from 'socket.io-redis';
import Socket from './socket';
import crypto from 'crypto';
import crypto from 'crypto'
import mailer from './utils/mailer';
import koaStatic from 'koa-static';
import koaSend from 'koa-send';
import { pollForInactiveRooms } from './inactive_rooms';
import getStore from './store';
import {pollForInactiveRooms} from './inactive_rooms';
bluebird.promisifyAll(Redis.RedisClient.prototype);
bluebird.promisifyAll(Redis.Multi.prototype);
const redis = Redis.createClient(process.env.REDIS_URL)
export const getRedis = () => redis
const env = process.env.NODE_ENV || 'development';
@ -26,16 +35,12 @@ const appName = process.env.HEROKU_APP_NAME;
const isReviewApp = /-pr-/.test(appName);
const siteURL = process.env.SITE_URL;
const store = getStore();
if ((siteURL || env === 'development') && !isReviewApp) {
app.use(
cors({
origin: env === 'development' ? '*' : siteURL,
allowMethods: ['GET', 'HEAD', 'POST'],
credentials: true,
})
);
app.use(cors({
origin: env === 'development' ? '*' : siteURL,
allowMethods: ['GET','HEAD','POST'],
credentials: true,
}));
}
router.post('/abuse/:roomId', koaBody, async (ctx) => {
@ -43,22 +48,19 @@ router.post('/abuse/:roomId', koaBody, async (ctx) => {
roomId = roomId.trim();
if (
process.env.ABUSE_FROM_EMAIL_ADDRESS &&
process.env.ABUSE_TO_EMAIL_ADDRESS
) {
const abuseForRoomExists = await store.get('abuse', roomId);
if (process.env.ABUSE_FROM_EMAIL_ADDRESS && process.env.ABUSE_TO_EMAIL_ADDRESS) {
const abuseForRoomExists = await redis.hgetAsync('abuse', roomId);
if (!abuseForRoomExists) {
mailer.send({
from: process.env.ABUSE_FROM_EMAIL_ADDRESS,
to: process.env.ABUSE_TO_EMAIL_ADDRESS,
subject: 'Darkwire Abuse Notification',
text: `Room ID: ${roomId}`,
text: `Room ID: ${roomId}`
});
}
}
await store.inc('abuse', roomId);
await redis.hincrbyAsync('abuse', roomId, 1);
ctx.status = 200;
});
@ -66,9 +68,7 @@ router.post('/abuse/:roomId', koaBody, async (ctx) => {
app.use(router.routes());
const apiHost = process.env.API_HOST;
const cspDefaultSrc = `'self'${
apiHost ? ` https://${apiHost} wss://${apiHost}` : ''
}`;
const cspDefaultSrc = `'self'${apiHost ? ` https://${apiHost} wss://${apiHost}` : ''}`
function setStaticFileHeaders(ctx) {
ctx.set({
@ -78,8 +78,7 @@ function setStaticFileHeaders(ctx) {
'X-XSS-Protection': '1; mode=block',
'X-Content-Type-Options': 'nosniff',
'Referrer-Policy': 'no-referrer',
'Feature-Policy':
"geolocation 'none'; vr 'none'; payment 'none'; microphone 'none'",
'Feature-Policy': "geolocation 'none'; vr 'none'; payment 'none'; microphone 'none'",
});
}
@ -88,16 +87,16 @@ if (clientDistDirectory) {
app.use(async (ctx, next) => {
setStaticFileHeaders(ctx);
await koaStatic(clientDistDirectory, {
maxage: ctx.req.url === '/' ? 60 * 1000 : 365 * 24 * 60 * 60 * 1000, // one minute in ms for html doc, one year for css, js, etc
maxage: ctx.req.url === '/' ? 60 * 1000 : 365 * 24 * 60 * 60 * 1000 // one minute in ms for html doc, one year for css, js, etc
})(ctx, next);
});
app.use(async (ctx) => {
setStaticFileHeaders(ctx);
await koaSend(ctx, 'index.html', { root: clientDistDirectory });
});
})
} else {
app.use(async (ctx) => {
app.use(async ctx => {
ctx.body = { ready: true };
});
}
@ -107,52 +106,52 @@ const protocol = (process.env.PROTOCOL || 'http') === 'http' ? http : https;
const server = protocol.createServer(app.callback());
const io = Io(server, {
pingInterval: 20000,
pingTimeout: 5000,
pingTimeout: 5000
});
// Only use socket adapter if store has one
if (store.hasSocketAdapter) {
io.adapter(store.getSocketAdapter());
}
io.adapter(socketRedis(process.env.REDIS_URL));
const roomHashSecret = process.env.ROOM_HASH_SECRET;
const getRoomIdHash = (id) => {
if (env === 'development') {
return id;
return id
}
if (roomHashSecret) {
return crypto.createHmac('sha256', roomHashSecret).update(id).digest('hex');
return crypto
.createHmac('sha256', roomHashSecret)
.update(id)
.digest('hex')
}
return crypto.createHash('sha256').update(id).digest('hex');
};
}
export const getIO = () => io;
export const getIO = () => io
io.on('connection', async (socket) => {
const roomId = socket.handshake.query.roomId;
const roomId = socket.handshake.query.roomId
const roomIdHash = getRoomIdHash(roomId);
const roomIdHash = getRoomIdHash(roomId)
let room = await store.get('rooms', roomIdHash);
room = JSON.parse(room || '{}');
let room = await redis.hgetAsync('rooms', roomIdHash)
room = JSON.parse(room || '{}')
new Socket({
roomIdOriginal: roomId,
roomId: roomIdHash,
socket,
room,
});
});
})
})
const init = async () => {
server.listen(PORT, () => {
console.log(`Darkwire is online at port ${PORT}`);
});
})
pollForInactiveRooms();
};
}
init()
init();

View File

@ -1,27 +1,27 @@
import _ from 'lodash';
import { getIO } from './index';
import getStore from './store';
import uuid from 'uuid/v4';
import { getIO, getRedis } from './index'
export default class Socket {
constructor(opts) {
const { roomId, socket, room, roomIdOriginal } = opts;
const { roomId, socket, room, roomIdOriginal } = opts
this._roomId = roomId;
this._roomId = roomId
this.socket = socket;
this.roomIdOriginal = roomIdOriginal;
this.room = room;
if (room.isLocked) {
this.sendRoomLocked();
return;
return
}
this.init(opts);
}
this.init(opts)
}
async init(opts) {
const { roomId, socket, room } = opts;
await this.joinRoom(roomId, socket);
this.handleSocket(socket);
const { roomId, socket, room } = opts
await this.joinRoom(roomId, socket.id)
this.handleSocket(socket)
}
sendRoomLocked() {
@ -32,41 +32,30 @@ export default class Socket {
const json = {
...room,
updatedAt: Date.now(),
};
}
return getStore().set('rooms', this._roomId, JSON.stringify(json));
return getRedis().hsetAsync('rooms', this._roomId, JSON.stringify(json))
}
async destroyRoom() {
return getStore().del('rooms', this._roomId);
return getRedis().hdel('rooms', this._roomId)
}
fetchRoom() {
return new Promise(async (resolve, reject) => {
const res = await getStore().get('rooms', this._roomId);
resolve(JSON.parse(res || '{}'));
});
const res = await getRedis().hgetAsync('rooms', this._roomId)
resolve(JSON.parse(res || '{}'))
})
}
joinRoom(roomId, socket) {
joinRoom(roomId, socketId) {
return new Promise((resolve, reject) => {
if (getStore().hasSocketAdapter) {
getIO()
.of('/')
.adapter.remoteJoin(socket.id, roomId, (err) => {
if (err) {
reject();
}
resolve();
});
} else {
socket.join(roomId, (err) => {
if (err) {
reject();
}
resolve();
});
}
getIO().of('/').adapter.remoteJoin(socketId, roomId, (err) => {
if (err) {
reject()
}
resolve()
});
});
}
@ -76,63 +65,56 @@ export default class Socket {
});
socket.on('USER_ENTER', async (payload) => {
let room = await this.fetchRoom();
let room = await this.fetchRoom()
if (_.isEmpty(room)) {
room = {
id: this._roomId,
users: [],
isLocked: false,
createdAt: Date.now(),
};
}
}
const newRoom = {
...room,
users: [
...(room.users || []),
{
socketId: socket.id,
publicKey: payload.publicKey,
isOwner: (room.users || []).length === 0,
},
],
};
await this.saveRoom(newRoom);
users: [...(room.users || []), {
socketId: socket.id,
publicKey: payload.publicKey,
isOwner: (room.users || []).length === 0,
}]
}
await this.saveRoom(newRoom)
getIO()
.to(this._roomId)
.emit('USER_ENTER', {
...newRoom,
id: this.roomIdOriginal,
});
});
getIO().to(this._roomId).emit('USER_ENTER', {
...newRoom,
id: this.roomIdOriginal
});
})
socket.on('TOGGLE_LOCK_ROOM', async (data, callback) => {
const room = await this.fetchRoom();
const user = (room.users || []).find(
(u) => u.socketId === socket.id && u.isOwner
);
const room = await this.fetchRoom()
const user = (room.users || []).find(u => u.socketId === socket.id && u.isOwner)
if (!user) {
callback({
isLocked: room.isLocked,
});
return;
})
return
}
await this.saveRoom({
...room,
isLocked: !room.isLocked,
});
})
socket.to(this._roomId).emit('TOGGLE_LOCK_ROOM', {
locked: !room.isLocked,
publicKey: user && user.publicKey,
publicKey: user && user.publicKey
});
callback({
isLocked: !room.isLocked,
});
})
});
socket.on('disconnect', () => this.handleDisconnect(socket));
@ -141,26 +123,25 @@ export default class Socket {
}
async handleDisconnect(socket) {
let room = await this.fetchRoom();
let room = await this.fetchRoom()
const newRoom = {
...room,
users: (room.users || [])
.filter((u) => u.socketId !== socket.id)
.map((u, index) => ({
...u,
isOwner: index === 0,
})),
};
users: (room.users || []).filter(u => u.socketId !== socket.id).map((u, index) => ({
...u,
isOwner: index === 0,
}))
}
await this.saveRoom(newRoom);
await this.saveRoom(newRoom)
getIO().to(this._roomId).emit('USER_EXIT', newRoom.users);
if (newRoom.users && newRoom.users.length === 0) {
await this.destroyRoom();
await this.destroyRoom()
}
socket.disconnect(true);
}
}

View File

@ -1,48 +0,0 @@
/**
* Memory store more for testing purpose than production use.
*/
export class MemoryStore {
constructor() {
this.store = {};
this.hasSocketAdapter = false;
}
async get(key, field) {
if (this.store[key] === undefined || this.store[key][field] === undefined) {
return null;
}
return this.store[key][field];
}
async getAll(key) {
if (this.store[key] === undefined) {
return [];
}
return this.store[key];
}
async set(key, field, value) {
if (this.store[key] === undefined) {
this.store[key] = {};
}
this.store[key][field] = value;
return 1;
}
async del(key, field) {
if (this.store[key] === undefined || this.store[key][field] === undefined) {
return 0;
}
delete this.store[key][field];
return 1;
}
async inc(key, field, inc = 1) {
this.store[key][field] += inc;
return this.store[key][field];
}
}
export default MemoryStore

View File

@ -1,42 +0,0 @@
import Redis from 'redis';
import bluebird from 'bluebird';
import socketRedis from 'socket.io-redis';
/**
* Redis store.
*/
export class RedisStore {
constructor(redisUrl) {
bluebird.promisifyAll(Redis.RedisClient.prototype);
bluebird.promisifyAll(Redis.Multi.prototype);
this.redisUrl = redisUrl;
this.redis = Redis.createClient(redisUrl);
this.hasSocketAdapter = true;
}
get(key, field) {
return this.redis.hgetAsync(key, field);
}
getAll(key) {
return this.redis.hgetallAsync(key);
}
set(key, field, value) {
return this.redis.hsetAsync(key, field, value);
}
del(key, field) {
return this.hdelAsync(key, field);
}
inc(key, field, inc = 1) {
return this.redis.incrbyAsync(key, field, inc);
}
getSocketAdapter() {
return socketRedis(this.redisUrl);
}
}
export default RedisStore

View File

@ -1,19 +0,0 @@
import MemoryStore from './Memory';
import RedisStore from './Redis';
const storeBackend = process.env.STORE_BACKEND || 'redis';
let store;
switch (storeBackend) {
case 'memory':
store = new MemoryStore();
break;
case 'redis':
default:
store = new RedisStore(process.env.STORE_HOST);
break;
}
const getStore = () => store;
export default getStore;