PairDrop/index.js
2022-12-31 18:52:16 +01:00

392 lines
11 KiB
JavaScript

var process = require('process')
var {spawn} = require('child_process')
var net = require('net')
// 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)
})
// Handle APP ERRORS
process.on('uncaughtException', (error, origin) => {
console.log('----- Uncaught exception -----')
console.log(error)
console.log('----- Exception origin -----')
console.log(origin)
})
process.on('unhandledRejection', (reason, promise) => {
console.log('----- Unhandled Rejection at -----')
console.log(promise)
console.log('----- Reason -----')
console.log(reason)
})
process.on(
'uncaughtException',
() => {
process.once(
'exit',
() => spawn(
process.argv.shift(),
process.argv,
{
cwd: process.cwd(),
detached: true,
stdio: 'inherit'
}
)
);
process.exit();
}
);
const express = require('express');
const RateLimit = require('express-rate-limit');
const http = require('http');
const limiter = RateLimit({
windowMs: 5 * 60 * 1000, // 5 minutes
max: 1000, // Limit each IP to 100 requests per `window` (here, per 5 minutes)
message: 'Too many requests from this IP Address, please try again after 5 minutes.',
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
})
const app = express();
const port = process.env.PORT || 3000;
app.use(limiter);
app.use(express.static('public'));
app.use(function(req, res) {
res.redirect('/');
});
app.get('/', (req, res) => {
res.sendFile('index.html');
});
const server = http.createServer(app);
server.listen(port);
const parser = require('ua-parser-js');
const { uniqueNamesGenerator, animals, colors } = require('unique-names-generator');
class SnapdropServer {
constructor() {
const WebSocket = require('ws');
this._wss = new WebSocket.Server({ server });
this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request)));
this._rooms = {};
console.log('Snapdrop is running on port', port);
}
_onConnection(peer) {
this._joinRoom(peer);
peer.socket.on('message', message => this._onMessage(peer, message));
peer.socket.on('error', console.error);
this._keepAlive(peer);
// send displayName
this._send(peer, {
type: 'display-name',
message: {
displayName: peer.name.displayName,
deviceName: peer.name.deviceName
}
});
}
_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]) {
if (otherPeerId === peer.id) continue;
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]) {
if (otherPeerId === peer.id) continue;
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 = 500;
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()
// is WebRTC supported ?
this.rtcSupported = request.url.indexOf('webrtc') > -1;
// set name
this._setName(request);
// for keepalive
this.timerId = 0;
this.lastBeat = Date.now();
console.debug(this.name.displayName)
}
_setIP(request) {
if (request.headers['cf-connecting-ip']) {
this.ip = request.headers['cf-connecting-ip'].split(/\s*,\s*/)[0];
} else 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
// put all peers on the same network as the server into the same room as well
if (this.ip === '::1' || this.ip === '::ffff:127.0.0.1' || this.ip === '::1' || this.ipIsPrivate(this.ip)) {
this.ip = '127.0.0.1';
}
console.debug(this.ip)
}
ipIsPrivate(ip) {
if (ip.substring(0,7) === "::ffff:")
ip = ip.substring(7);
if (net.isIPv4(ip)) {
// 10.0.0.0 - 10.255.255.255 || 172.16.0.0 - 172.31.255.255 || 192.168.0.0 - 192.168.255.255
return /^(10)\.(.*)\.(.*)\.(.*)$/.test(ip) || /^(172)\.(1[6-9]|2[0-9]|3[0-1])\.(.*)\.(.*)$/.test(ip) || /^(192)\.(168)\.(.*)\.(.*)$/.test(ip)
}
// else: ip is IPv6
const firstWord = ip.split(":").find(el => !!el); //get first not empty word
// The original IPv6 Site Local addresses (fec0::/10) are deprecated. Range: fec0 - feff
if (/^fe[c-f][0-f]$/.test(firstWord))
return true;
// These days Unique Local Addresses (ULA) are used in place of Site Local.
// Range: fc00 - fcff
else if (/^fc[0-f]{2}$/.test(firstWord))
return true;
// Range: fd00 - fcff
else if (/^fd[0-f]{2}$/.test(firstWord))
return true;
// Link local addresses (prefixed with fe80) are not routable
else if (firstWord === "fe80")
return true;
// Discard Prefix
else if (firstWord === "100")
return true;
// Any other IP address is not Unique Local Address (ULA)
return false;
}
_setPeerId() {
this.id = Peer.uuid();
}
toString() {
return `<Peer id=${this.id} ip=${this.ip} rtcSupported=${this.rtcSupported}>`
}
_setName(req) {
let ua = parser(req.headers['user-agent']);
let deviceName = '';
if (ua.os && ua.os.name) {
deviceName = ua.os.name.replace('Mac OS', 'Mac') + ' ';
}
if (ua.device.model) {
deviceName += ua.device.model;
} else {
deviceName += ua.browser.name;
}
if(!deviceName)
deviceName = 'Unknown Device';
const displayName = uniqueNamesGenerator({
length: 2,
separator: ' ',
dictionaries: [colors, animals],
style: 'capital',
seed: this.id.hashCode()
})
this.name = {
model: ua.device.model,
os: ua.os.name,
browser: ua.browser.name,
type: ua.device.type,
deviceName,
displayName
};
}
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;
};
}
Object.defineProperty(String.prototype, 'hashCode', {
value: function() {
var hash = 0, i, chr;
for (i = 0; i < this.length; i++) {
chr = this.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
}
});
new SnapdropServer();