mirror of
https://github.com/darkwire/darkwire.io.git
synced 2025-07-18 10:49:02 +00:00
This reverts commit 2a8d3281db851d15f9402d0fe69291255c6b250b.
This commit is contained in:
parent
5affceb47e
commit
18065f9652
16
readme.md
16
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
|
||||
|
@ -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
|
@ -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
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
@ -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
|
@ -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;
|
Loading…
x
Reference in New Issue
Block a user