f9bace9a16
Currently, server run by `node index.js` is not able to handle SIGTERM or SIGINT properly. The only fate is being killed. This change adds basic handling logic for these two signals, helping server to behave more properly as expected by many daemons and users.
247 lines
6.9 KiB
JavaScript
247 lines
6.9 KiB
JavaScript
var process = require('process')
|
|
// Handle SIGINT
|
|
process.on('SIGINT', () => {
|
|
console.info("SIGINT Received, exiting...")
|
|
process.exit(0)
|
|
})
|
|
|
|
// Handle SIGTERM
|
|
process.on('SIGTERM', () => {
|
|
console.info("SIGTERM Received, exiting...")
|
|
process.exit(0)
|
|
})
|
|
|
|
const parser = require('ua-parser-js');
|
|
const { uniqueNamesGenerator, animals, colors } = require('unique-names-generator');
|
|
|
|
class SnapdropServer {
|
|
|
|
constructor(port) {
|
|
const WebSocket = require('ws');
|
|
this._wss = new WebSocket.Server({ port: port });
|
|
this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request)));
|
|
this._wss.on('headers', (headers, response) => this._onHeaders(headers, response));
|
|
|
|
this._rooms = {};
|
|
|
|
console.log('Snapdrop is running on port', port);
|
|
}
|
|
|
|
_onConnection(peer) {
|
|
this._joinRoom(peer);
|
|
peer.socket.on('message', message => this._onMessage(peer, message));
|
|
this._keepAlive(peer);
|
|
|
|
// send displayName
|
|
this._send(peer, { type: 'displayName', message: peer.name.displayName });
|
|
}
|
|
|
|
_onHeaders(headers, response) {
|
|
if (response.headers.cookie && response.headers.cookie.indexOf('peerid=') > -1) return;
|
|
response.peerId = Peer.uuid();
|
|
headers.push('Set-Cookie: peerid=' + response.peerId + "; SameSite=Strict; Secure");
|
|
}
|
|
|
|
_onMessage(sender, message) {
|
|
// Try to parse message
|
|
try {
|
|
message = JSON.parse(message);
|
|
} catch (e) {
|
|
return; // TODO: handle malformed JSON
|
|
}
|
|
|
|
switch (message.type) {
|
|
case 'disconnect':
|
|
this._leaveRoom(sender);
|
|
break;
|
|
case 'pong':
|
|
sender.lastBeat = Date.now();
|
|
break;
|
|
}
|
|
|
|
// relay message to recipient
|
|
if (message.to && this._rooms[sender.ip]) {
|
|
const recipientId = message.to; // TODO: sanitize
|
|
const recipient = this._rooms[sender.ip][recipientId];
|
|
delete message.to;
|
|
// add sender id
|
|
message.sender = sender.id;
|
|
this._send(recipient, message);
|
|
return;
|
|
}
|
|
}
|
|
|
|
_joinRoom(peer) {
|
|
// if room doesn't exist, create it
|
|
if (!this._rooms[peer.ip]) {
|
|
this._rooms[peer.ip] = {};
|
|
}
|
|
|
|
// notify all other peers
|
|
for (const otherPeerId in this._rooms[peer.ip]) {
|
|
const otherPeer = this._rooms[peer.ip][otherPeerId];
|
|
this._send(otherPeer, {
|
|
type: 'peer-joined',
|
|
peer: peer.getInfo()
|
|
});
|
|
}
|
|
|
|
// notify peer about the other peers
|
|
const otherPeers = [];
|
|
for (const otherPeerId in this._rooms[peer.ip]) {
|
|
otherPeers.push(this._rooms[peer.ip][otherPeerId].getInfo());
|
|
}
|
|
|
|
this._send(peer, {
|
|
type: 'peers',
|
|
peers: otherPeers
|
|
});
|
|
|
|
// add peer to room
|
|
this._rooms[peer.ip][peer.id] = peer;
|
|
}
|
|
|
|
_leaveRoom(peer) {
|
|
if (!this._rooms[peer.ip] || !this._rooms[peer.ip][peer.id]) return;
|
|
this._cancelKeepAlive(this._rooms[peer.ip][peer.id]);
|
|
|
|
// delete the peer
|
|
delete this._rooms[peer.ip][peer.id];
|
|
|
|
peer.socket.terminate();
|
|
//if room is empty, delete the room
|
|
if (!Object.keys(this._rooms[peer.ip]).length) {
|
|
delete this._rooms[peer.ip];
|
|
} else {
|
|
// notify all other peers
|
|
for (const otherPeerId in this._rooms[peer.ip]) {
|
|
const otherPeer = this._rooms[peer.ip][otherPeerId];
|
|
this._send(otherPeer, { type: 'peer-left', peerId: peer.id });
|
|
}
|
|
}
|
|
}
|
|
|
|
_send(peer, message) {
|
|
if (!peer) return;
|
|
if (this._wss.readyState !== this._wss.OPEN) return;
|
|
message = JSON.stringify(message);
|
|
peer.socket.send(message, error => '');
|
|
}
|
|
|
|
_keepAlive(peer) {
|
|
this._cancelKeepAlive(peer);
|
|
var timeout = 30000;
|
|
if (!peer.lastBeat) {
|
|
peer.lastBeat = Date.now();
|
|
}
|
|
if (Date.now() - peer.lastBeat > 2 * timeout) {
|
|
this._leaveRoom(peer);
|
|
return;
|
|
}
|
|
|
|
this._send(peer, { type: 'ping' });
|
|
|
|
peer.timerId = setTimeout(() => this._keepAlive(peer), timeout);
|
|
}
|
|
|
|
_cancelKeepAlive(peer) {
|
|
if (peer && peer.timerId) {
|
|
clearTimeout(peer.timerId);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
class Peer {
|
|
|
|
constructor(socket, request) {
|
|
// set socket
|
|
this.socket = socket;
|
|
|
|
|
|
// set remote ip
|
|
this._setIP(request);
|
|
|
|
// set peer id
|
|
this._setPeerId(request)
|
|
// is WebRTC supported ?
|
|
this.rtcSupported = request.url.indexOf('webrtc') > -1;
|
|
// set name
|
|
this._setName(request);
|
|
// for keepalive
|
|
this.timerId = 0;
|
|
this.lastBeat = Date.now();
|
|
}
|
|
|
|
_setIP(request) {
|
|
if (request.headers['x-forwarded-for']) {
|
|
this.ip = request.headers['x-forwarded-for'].split(/\s*,\s*/)[0];
|
|
} else {
|
|
this.ip = request.connection.remoteAddress;
|
|
}
|
|
// IPv4 and IPv6 use different values to refer to localhost
|
|
if (this.ip == '::1' || this.ip == '::ffff:127.0.0.1') {
|
|
this.ip = '127.0.0.1';
|
|
}
|
|
}
|
|
|
|
_setPeerId(request) {
|
|
if (request.peerId) {
|
|
this.id = request.peerId;
|
|
} else {
|
|
this.id = request.headers.cookie.replace('peerid=', '');
|
|
}
|
|
}
|
|
|
|
toString() {
|
|
return `<Peer id=${this.id} ip=${this.ip} rtcSupported=${this.rtcSupported}>`
|
|
}
|
|
|
|
_setName(req) {
|
|
var ua = parser(req.headers['user-agent']);
|
|
this.name = {
|
|
model: ua.device.model,
|
|
os: ua.os.name,
|
|
browser: ua.browser.name,
|
|
type: ua.device.type,
|
|
displayName: uniqueNamesGenerator({ length: 2, separator: ' ', dictionaries: [colors, animals], style: 'capital' })
|
|
};
|
|
}
|
|
|
|
getInfo() {
|
|
return {
|
|
id: this.id,
|
|
name: this.name,
|
|
rtcSupported: this.rtcSupported
|
|
}
|
|
}
|
|
|
|
// return uuid of form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
|
static uuid() {
|
|
let uuid = '',
|
|
ii;
|
|
for (ii = 0; ii < 32; ii += 1) {
|
|
switch (ii) {
|
|
case 8:
|
|
case 20:
|
|
uuid += '-';
|
|
uuid += (Math.random() * 16 | 0).toString(16);
|
|
break;
|
|
case 12:
|
|
uuid += '-';
|
|
uuid += '4';
|
|
break;
|
|
case 16:
|
|
uuid += '-';
|
|
uuid += (Math.random() * 4 | 8).toString(16);
|
|
break;
|
|
default:
|
|
uuid += (Math.random() * 16 | 0).toString(16);
|
|
}
|
|
}
|
|
return uuid;
|
|
};
|
|
}
|
|
|
|
const server = new SnapdropServer(process.env.PORT || 3000);
|