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 ### 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 #### Setup
Install dependencies Install dependencies

View File

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

View File

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

View File

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