234 lines
6.6 KiB
JavaScript
234 lines
6.6 KiB
JavaScript
const parser = require('ua-parser-js');
|
|
const { uniqueNamesGenerator, animals, adjectives } = 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);
|
|
}
|
|
|
|
_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 = 10000;
|
|
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: [adjectives, 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);
|