mirror of
https://github.com/schlagmichdoch/PairDrop.git
synced 2025-04-20 07:05:05 -04:00

use unique-names-generator to generate peer names instead of relying on UA. This prevents confusion when there are multiple clients on the network with same UA.
231 lines
6.4 KiB
JavaScript
231 lines
6.4 KiB
JavaScript
const parser = require('ua-parser-js');
|
|
const { uniqueNamesGenerator } = 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);
|
|
}
|
|
|
|
_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 })
|
|
};
|
|
}
|
|
|
|
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);
|