PairDrop/server/index.js
Ying Kanyang (Harry Ying) f9bace9a16
Add support for SIGINT and SIGTERM handling
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.
2020-08-28 22:07:25 +08:00

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);