Version 2 initial commit

This commit is contained in:
RobinLinus 2018-09-21 16:05:03 +02:00
parent 81252d301c
commit 663db5cbb3
99 changed files with 2448 additions and 27650 deletions

217
server/index.js Normal file
View 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
View 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
View 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"
}
}

View file

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