mirror of
https://github.com/schlagmichdoch/PairDrop.git
synced 2025-04-21 07:16:18 -04:00
Version 2 initial commit
This commit is contained in:
parent
81252d301c
commit
663db5cbb3
99 changed files with 2448 additions and 27650 deletions
217
server/index.js
Normal file
217
server/index.js
Normal file
|
@ -0,0 +1,217 @@
|
|||
const parser = require('ua-parser-js');
|
||||
|
||||
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 = {};
|
||||
this._timerID = 0;
|
||||
|
||||
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) {
|
||||
message = JSON.parse(message);
|
||||
|
||||
switch (message.type) {
|
||||
case 'disconnect':
|
||||
this._leaveRoom(sender);
|
||||
case 'pong':
|
||||
sender.lastBeat = Date.now();
|
||||
}
|
||||
|
||||
// relay message to recipient
|
||||
if (message.to) {
|
||||
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] = {};
|
||||
}
|
||||
|
||||
// console.log(peer.id, ' joined the room', 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) {
|
||||
// delete the peer
|
||||
this._cancelKeepAlive(peer);
|
||||
if (!this._rooms[peer.ip]) return;
|
||||
|
||||
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 console.error('undefined peer');
|
||||
message = JSON.stringify(message);
|
||||
peer.socket.send(message, error => {
|
||||
if (error) this._leaveRoom(peer);
|
||||
});
|
||||
}
|
||||
|
||||
_keepAlive(peer) {
|
||||
var timeout = 10000;
|
||||
// console.log(Date.now() - peer.lastBeat);
|
||||
if (Date.now() - peer.lastBeat > 2 * timeout) {
|
||||
this._leaveRoom(peer);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._wss.readyState == this._wss.OPEN) {
|
||||
this._send(peer, {
|
||||
type: 'ping'
|
||||
});
|
||||
}
|
||||
peer.timerId = setTimeout(() => this._keepAlive(peer), timeout);
|
||||
}
|
||||
|
||||
_cancelKeepAlive(peer) {
|
||||
if (peer.timerId) {
|
||||
clearTimeout(peer.timerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class Peer {
|
||||
|
||||
constructor(socket, request) {
|
||||
// set socket
|
||||
this.socket = socket;
|
||||
|
||||
|
||||
// set remote ip
|
||||
if (request.headers['x-forwarded-for'])
|
||||
this.ip = request.headers['x-forwarded-for'].split(/\s*,\s*/)[0];
|
||||
else
|
||||
this.ip = request.connection.remoteAddress;
|
||||
|
||||
if (request.peerId) {
|
||||
this.id = request.peerId;
|
||||
} else {
|
||||
this.id = request.headers.cookie.replace('peerid=', '');
|
||||
}
|
||||
// set peer id
|
||||
// is WebRTC supported ?
|
||||
this.rtcSupported = request.url.indexOf('webrtc') > -1;
|
||||
// set name
|
||||
this.setName(request);
|
||||
// for keepalive
|
||||
this.timerId = 0;
|
||||
this.lastBeat = Date.now();
|
||||
}
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
getInfo() {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
rtcSupported: this.rtcSupported
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const server = new SnapdropServer(process.env.PORT || 3000);
|
26
server/package-lock.json
generated
Normal file
26
server/package-lock.json
generated
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "snapdrop",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"async-limiter": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
|
||||
"integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg=="
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "0.7.18",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.18.tgz",
|
||||
"integrity": "sha512-LtzwHlVHwFGTptfNSgezHp7WUlwiqb0gA9AALRbKaERfxwJoiX0A73QbTToxteIAuIaFshhgIZfqK8s7clqgnA=="
|
||||
},
|
||||
"ws": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-6.0.0.tgz",
|
||||
"integrity": "sha512-c2UlYcAZp1VS8AORtpq6y4RJIkJ9dQz18W32SpR/qXGfLDZ2jU4y4wKvvZwqbi7U6gxFQTeE+urMbXU/tsDy4w==",
|
||||
"requires": {
|
||||
"async-limiter": "~1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
server/package.json
Normal file
15
server/package.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "snapdrop",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"ua-parser-js": "^0.7.18",
|
||||
"ws": "^6.0.0"
|
||||
}
|
||||
}
|
|
@ -1,154 +0,0 @@
|
|||
'use strict';
|
||||
var parser = require('ua-parser-js');
|
||||
|
||||
// Start Binary.js server
|
||||
var BinaryServer = require('binaryjs').BinaryServer;
|
||||
|
||||
exports.create = function(server) {
|
||||
|
||||
// link it to express
|
||||
var bs = BinaryServer({
|
||||
server: server,
|
||||
path: '/binary'
|
||||
});
|
||||
|
||||
function guid() {
|
||||
function s4() {
|
||||
return Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
}
|
||||
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
|
||||
s4() + '-' + s4() + s4() + s4();
|
||||
}
|
||||
|
||||
function getDeviceName(req) {
|
||||
var ua = parser(req.headers['user-agent']);
|
||||
return {
|
||||
model: ua.device.model,
|
||||
os: ua.os.name,
|
||||
browser: ua.browser.name,
|
||||
type: ua.device.type
|
||||
};
|
||||
}
|
||||
|
||||
function hash(text) {
|
||||
// A string hashing function based on Daniel J. Bernstein's popular 'times 33' hash algorithm.
|
||||
var h = 5381,
|
||||
index = text.length;
|
||||
while (index) {
|
||||
h = (h * 33) ^ text.charCodeAt(--index);
|
||||
}
|
||||
return h >>> 0;
|
||||
}
|
||||
|
||||
function getIP(socket) {
|
||||
return socket.upgradeReq.headers['x-forwarded-for'] || socket.upgradeReq.connection.remoteAddress;
|
||||
}
|
||||
// Wait for new user connections
|
||||
bs.on('connection', function(client) {
|
||||
|
||||
client.uuidRaw = guid();
|
||||
//ip is hashed to prevent injections by spoofing the 'x-forwarded-for' header
|
||||
// client.hashedIp = 1; //use this to test locally
|
||||
client.hashedIp = hash(getIP(client._socket));
|
||||
|
||||
client.deviceName = getDeviceName(client._socket.upgradeReq);
|
||||
|
||||
// Incoming stream from browsers
|
||||
client.on('stream', function(stream, meta) {
|
||||
if (meta && meta.serverMsg === 'rtc-support') {
|
||||
client.uuid = (meta.rtc ? 'rtc_' : '') + client.uuidRaw;
|
||||
client.send({
|
||||
isSystemEvent: true,
|
||||
type: 'handshake',
|
||||
name: client.deviceName,
|
||||
uuid: client.uuid
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (meta && meta.serverMsg === 'device-name') {
|
||||
//max name length = 40
|
||||
if (meta.name && meta.name.length > 40) {
|
||||
return;
|
||||
}
|
||||
client.name = meta.name;
|
||||
return;
|
||||
}
|
||||
|
||||
meta.from = client.uuid;
|
||||
|
||||
// broadcast to the other client
|
||||
for (var id in bs.clients) {
|
||||
if (bs.clients.hasOwnProperty(id)) {
|
||||
var otherClient = bs.clients[id];
|
||||
if (otherClient !== client && meta.toPeer === otherClient.uuid) {
|
||||
var send = otherClient.createStream(meta);
|
||||
stream.pipe(send, meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function forEachClient(fn) {
|
||||
for (var id in bs.clients) {
|
||||
if (bs.clients.hasOwnProperty(id)) {
|
||||
var client = bs.clients[id];
|
||||
fn(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function notifyBuddies() {
|
||||
var locations = {};
|
||||
//group all clients by location (by public ip address)
|
||||
forEachClient(function(client) {
|
||||
var ip = client.hashedIp;
|
||||
locations[ip] = locations[ip] || [];
|
||||
locations[ip].push({
|
||||
socket: client,
|
||||
contact: {
|
||||
peerId: client.uuid,
|
||||
name: client.name || client.deviceName,
|
||||
device: client.name ? client.deviceName : undefined
|
||||
}
|
||||
});
|
||||
});
|
||||
//notify every location
|
||||
Object.keys(locations).forEach(function(locationKey) {
|
||||
//notify every client of all other clients in this location
|
||||
var location = locations[locationKey];
|
||||
location.forEach(function(client) {
|
||||
//all other clients
|
||||
var buddies = location.reduce(function(result, otherClient) {
|
||||
if (otherClient !== client) {
|
||||
result.push(otherClient.contact);
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
var currState = hash(JSON.stringify(buddies));
|
||||
console.log(currState);
|
||||
var socket = client.socket;
|
||||
//protocol
|
||||
var msg = {
|
||||
buddies: buddies,
|
||||
isSystemEvent: true,
|
||||
type: 'buddies'
|
||||
};
|
||||
//send only if state changed
|
||||
if (currState !== socket.lastState) {
|
||||
socket.send(msg);
|
||||
socket.lastState = currState;
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setInterval(notifyBuddies, 3000);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue