From 18065f965215c32b4b66e13246a6a54977088518 Mon Sep 17 00:00:00 2001 From: Alan Friedman Date: Tue, 5 May 2020 10:03:50 -0400 Subject: [PATCH] =?UTF-8?q?Revert=20"Allow=20to=20launch=20server=20withou?= =?UTF-8?q?t=C2=A0Redis=20(#119)"=20(#133)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 2a8d3281db851d15f9402d0fe69291255c6b250b. --- .nvmrc | 1 - client/{.env.dist => .env.example} | 0 readme.md | 20 +---- server/{.env.dist => .env.sample} | 11 +-- server/src/inactive_rooms.js | 22 +++-- server/src/index.js | 95 +++++++++++----------- server/src/socket.js | 125 ++++++++++++----------------- server/src/store/Memory.js | 48 ----------- server/src/store/Redis.js | 42 ---------- server/src/store/index.js | 19 ----- 10 files changed, 113 insertions(+), 270 deletions(-) delete mode 100644 .nvmrc rename client/{.env.dist => .env.example} (100%) rename server/{.env.dist => .env.sample} (61%) delete mode 100644 server/src/store/Memory.js delete mode 100644 server/src/store/Redis.js delete mode 100644 server/src/store/index.js diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index b009dfb..0000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -lts/* diff --git a/client/.env.dist b/client/.env.example similarity index 100% rename from client/.env.dist rename to client/.env.example diff --git a/readme.md b/readme.md index 63cc5e8..4b423ff 100644 --- a/readme.md +++ b/readme.md @@ -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. diff --git a/server/.env.dist b/server/.env.sample similarity index 61% rename from server/.env.dist rename to server/.env.sample index ce2c9cd..0986f8e 100644 --- a/server/.env.dist +++ b/server/.env.sample @@ -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 - +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 \ No newline at end of file diff --git a/server/src/inactive_rooms.js b/server/src/inactive_rooms.js index bcb2a94..5783c5d 100644 --- a/server/src/inactive_rooms.js +++ b/server/src/inactive_rooms.js @@ -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 } diff --git a/server/src/index.js b/server/src/index.js index 277900d..e170718 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -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(); diff --git a/server/src/socket.js b/server/src/socket.js index e5cadb6..9570753 100644 --- a/server/src/socket.js +++ b/server/src/socket.js @@ -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); } + } diff --git a/server/src/store/Memory.js b/server/src/store/Memory.js deleted file mode 100644 index e5c463c..0000000 --- a/server/src/store/Memory.js +++ /dev/null @@ -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 \ No newline at end of file diff --git a/server/src/store/Redis.js b/server/src/store/Redis.js deleted file mode 100644 index d9dc8d5..0000000 --- a/server/src/store/Redis.js +++ /dev/null @@ -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 \ No newline at end of file diff --git a/server/src/store/index.js b/server/src/store/index.js deleted file mode 100644 index 5a74a16..0000000 --- a/server/src/store/index.js +++ /dev/null @@ -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;