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

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

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;