mirror of
https://github.com/schlagmichdoch/PairDrop.git
synced 2025-04-20 15:06:15 -04:00
486 lines
14 KiB
JavaScript
486 lines
14 KiB
JavaScript
import {WebSocketServer} from "ws";
|
|
import crypto from "crypto"
|
|
|
|
import Peer from "./peer.js";
|
|
import {hasher, randomizer} from "./helper.js";
|
|
|
|
export default class PairDropWsServer {
|
|
|
|
constructor(server, conf) {
|
|
this._conf = conf
|
|
|
|
this._rooms = {}; // { roomId: peers[] }
|
|
|
|
this._roomSecrets = {}; // { pairKey: roomSecret }
|
|
this._keepAliveTimers = {};
|
|
|
|
this._wss = new WebSocketServer({ server });
|
|
this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request, conf)));
|
|
}
|
|
|
|
_onConnection(peer) {
|
|
peer.socket.on('message', message => this._onMessage(peer, message));
|
|
peer.socket.onerror = e => console.error(e);
|
|
|
|
this._keepAlive(peer);
|
|
|
|
this._send(peer, {
|
|
type: 'ws-config',
|
|
wsConfig: {
|
|
rtcConfig: this._conf.rtcConfig,
|
|
wsFallback: this._conf.wsFallback
|
|
}
|
|
});
|
|
|
|
// send displayName
|
|
this._send(peer, {
|
|
type: 'display-name',
|
|
displayName: peer.name.displayName,
|
|
deviceName: peer.name.deviceName,
|
|
peerId: peer.id,
|
|
peerIdHash: hasher.hashCodeSalted(peer.id)
|
|
});
|
|
}
|
|
|
|
_onMessage(sender, message) {
|
|
// Try to parse message
|
|
try {
|
|
message = JSON.parse(message);
|
|
} catch (e) {
|
|
console.warn("WS: Received JSON is malformed");
|
|
return;
|
|
}
|
|
|
|
switch (message.type) {
|
|
case 'disconnect':
|
|
this._onDisconnect(sender);
|
|
break;
|
|
case 'pong':
|
|
this._setKeepAliveTimerToNow(sender);
|
|
break;
|
|
case 'join-ip-room':
|
|
this._joinIpRoom(sender);
|
|
break;
|
|
case 'room-secrets':
|
|
this._onRoomSecrets(sender, message);
|
|
break;
|
|
case 'room-secrets-deleted':
|
|
this._onRoomSecretsDeleted(sender, message);
|
|
break;
|
|
case 'pair-device-initiate':
|
|
this._onPairDeviceInitiate(sender);
|
|
break;
|
|
case 'pair-device-join':
|
|
this._onPairDeviceJoin(sender, message);
|
|
break;
|
|
case 'pair-device-cancel':
|
|
this._onPairDeviceCancel(sender);
|
|
break;
|
|
case 'regenerate-room-secret':
|
|
this._onRegenerateRoomSecret(sender, message);
|
|
break;
|
|
case 'create-public-room':
|
|
this._onCreatePublicRoom(sender);
|
|
break;
|
|
case 'join-public-room':
|
|
this._onJoinPublicRoom(sender, message);
|
|
break;
|
|
case 'leave-public-room':
|
|
this._onLeavePublicRoom(sender);
|
|
break;
|
|
case 'signal':
|
|
this._signalAndRelay(sender, message);
|
|
break;
|
|
case 'request':
|
|
case 'header':
|
|
case 'partition':
|
|
case 'partition-received':
|
|
case 'progress':
|
|
case 'files-transfer-response':
|
|
case 'file-transfer-complete':
|
|
case 'message-transfer-complete':
|
|
case 'text':
|
|
case 'display-name-changed':
|
|
case 'ws-chunk':
|
|
// relay ws-fallback
|
|
if (this._conf.wsFallback) {
|
|
this._signalAndRelay(sender, message);
|
|
}
|
|
else {
|
|
console.log("Websocket fallback is not activated on this instance.")
|
|
}
|
|
}
|
|
}
|
|
|
|
_signalAndRelay(sender, message) {
|
|
const room = message.roomType === 'ip'
|
|
? sender.ip
|
|
: message.roomId;
|
|
|
|
// relay message to recipient
|
|
if (message.to && Peer.isValidUuid(message.to) && this._rooms[room]) {
|
|
const recipient = this._rooms[room][message.to];
|
|
delete message.to;
|
|
// add sender
|
|
message.sender = {
|
|
id: sender.id,
|
|
rtcSupported: sender.rtcSupported
|
|
};
|
|
this._send(recipient, message);
|
|
}
|
|
}
|
|
|
|
_onDisconnect(sender) {
|
|
this._disconnect(sender);
|
|
}
|
|
|
|
_disconnect(sender) {
|
|
this._removePairKey(sender.pairKey);
|
|
sender.pairKey = null;
|
|
|
|
this._cancelKeepAlive(sender);
|
|
delete this._keepAliveTimers[sender.id];
|
|
|
|
this._leaveIpRoom(sender, true);
|
|
this._leaveAllSecretRooms(sender, true);
|
|
this._leavePublicRoom(sender, true);
|
|
|
|
sender.socket.terminate();
|
|
}
|
|
|
|
_onRoomSecrets(sender, message) {
|
|
if (!message.roomSecrets) return;
|
|
|
|
const roomSecrets = message.roomSecrets.filter(roomSecret => {
|
|
return /^[\x00-\x7F]{64,256}$/.test(roomSecret);
|
|
})
|
|
|
|
if (!roomSecrets) return;
|
|
|
|
this._joinSecretRooms(sender, roomSecrets);
|
|
}
|
|
|
|
_onRoomSecretsDeleted(sender, message) {
|
|
for (let i = 0; i<message.roomSecrets.length; i++) {
|
|
this._deleteSecretRoom(message.roomSecrets[i]);
|
|
}
|
|
}
|
|
|
|
_deleteSecretRoom(roomSecret) {
|
|
const room = this._rooms[roomSecret];
|
|
if (!room) return;
|
|
|
|
for (const peerId in room) {
|
|
const peer = room[peerId];
|
|
|
|
this._leaveSecretRoom(peer, roomSecret, true);
|
|
|
|
this._send(peer, {
|
|
type: 'secret-room-deleted',
|
|
roomSecret: roomSecret,
|
|
});
|
|
}
|
|
}
|
|
|
|
_onPairDeviceInitiate(sender) {
|
|
let roomSecret = randomizer.getRandomString(256);
|
|
let pairKey = this._createPairKey(sender, roomSecret);
|
|
|
|
if (sender.pairKey) {
|
|
this._removePairKey(sender.pairKey);
|
|
}
|
|
sender.pairKey = pairKey;
|
|
|
|
this._send(sender, {
|
|
type: 'pair-device-initiated',
|
|
roomSecret: roomSecret,
|
|
pairKey: pairKey
|
|
});
|
|
this._joinSecretRoom(sender, roomSecret);
|
|
}
|
|
|
|
_onPairDeviceJoin(sender, message) {
|
|
if (sender.rateLimitReached()) {
|
|
this._send(sender, { type: 'join-key-rate-limit' });
|
|
return;
|
|
}
|
|
|
|
if (!this._roomSecrets[message.pairKey] || sender.id === this._roomSecrets[message.pairKey].creator.id) {
|
|
this._send(sender, { type: 'pair-device-join-key-invalid' });
|
|
return;
|
|
}
|
|
|
|
const roomSecret = this._roomSecrets[message.pairKey].roomSecret;
|
|
const creator = this._roomSecrets[message.pairKey].creator;
|
|
this._removePairKey(message.pairKey);
|
|
this._send(sender, {
|
|
type: 'pair-device-joined',
|
|
roomSecret: roomSecret,
|
|
peerId: creator.id
|
|
});
|
|
this._send(creator, {
|
|
type: 'pair-device-joined',
|
|
roomSecret: roomSecret,
|
|
peerId: sender.id
|
|
});
|
|
this._joinSecretRoom(sender, roomSecret);
|
|
this._removePairKey(sender.pairKey);
|
|
}
|
|
|
|
_onPairDeviceCancel(sender) {
|
|
const pairKey = sender.pairKey
|
|
|
|
if (!pairKey) return;
|
|
|
|
this._removePairKey(pairKey);
|
|
this._send(sender, {
|
|
type: 'pair-device-canceled',
|
|
pairKey: pairKey,
|
|
});
|
|
}
|
|
|
|
_onCreatePublicRoom(sender) {
|
|
let publicRoomId = randomizer.getRandomString(5, true).toLowerCase();
|
|
|
|
this._send(sender, {
|
|
type: 'public-room-created',
|
|
roomId: publicRoomId
|
|
});
|
|
|
|
this._joinPublicRoom(sender, publicRoomId);
|
|
}
|
|
|
|
_onJoinPublicRoom(sender, message) {
|
|
if (sender.rateLimitReached()) {
|
|
this._send(sender, { type: 'join-key-rate-limit' });
|
|
return;
|
|
}
|
|
|
|
if (!this._rooms[message.publicRoomId] && !message.createIfInvalid) {
|
|
this._send(sender, { type: 'public-room-id-invalid', publicRoomId: message.publicRoomId });
|
|
return;
|
|
}
|
|
|
|
this._leavePublicRoom(sender);
|
|
this._joinPublicRoom(sender, message.publicRoomId);
|
|
}
|
|
|
|
_onLeavePublicRoom(sender) {
|
|
this._leavePublicRoom(sender, true);
|
|
this._send(sender, { type: 'public-room-left' });
|
|
}
|
|
|
|
_onRegenerateRoomSecret(sender, message) {
|
|
const oldRoomSecret = message.roomSecret;
|
|
const newRoomSecret = randomizer.getRandomString(256);
|
|
|
|
// notify all other peers
|
|
for (const peerId in this._rooms[oldRoomSecret]) {
|
|
const peer = this._rooms[oldRoomSecret][peerId];
|
|
this._send(peer, {
|
|
type: 'room-secret-regenerated',
|
|
oldRoomSecret: oldRoomSecret,
|
|
newRoomSecret: newRoomSecret,
|
|
});
|
|
peer.removeRoomSecret(oldRoomSecret);
|
|
}
|
|
delete this._rooms[oldRoomSecret];
|
|
}
|
|
|
|
_createPairKey(creator, roomSecret) {
|
|
let pairKey;
|
|
do {
|
|
// get randomInt until keyRoom not occupied
|
|
pairKey = crypto.randomInt(1000000, 1999999).toString().substring(1); // include numbers with leading 0s
|
|
} while (pairKey in this._roomSecrets)
|
|
|
|
this._roomSecrets[pairKey] = {
|
|
roomSecret: roomSecret,
|
|
creator: creator
|
|
}
|
|
|
|
return pairKey;
|
|
}
|
|
|
|
_removePairKey(pairKey) {
|
|
if (pairKey in this._roomSecrets) {
|
|
this._roomSecrets[pairKey].creator.pairKey = null
|
|
delete this._roomSecrets[pairKey];
|
|
}
|
|
}
|
|
|
|
_joinIpRoom(peer) {
|
|
this._joinRoom(peer, 'ip', peer.ip);
|
|
}
|
|
|
|
_joinSecretRoom(peer, roomSecret) {
|
|
this._joinRoom(peer, 'secret', roomSecret);
|
|
|
|
// add secret to peer
|
|
peer.addRoomSecret(roomSecret);
|
|
}
|
|
|
|
_joinPublicRoom(peer, publicRoomId) {
|
|
// prevent joining of 2 public rooms simultaneously
|
|
this._leavePublicRoom(peer);
|
|
|
|
this._joinRoom(peer, 'public-id', publicRoomId);
|
|
|
|
peer.publicRoomId = publicRoomId;
|
|
}
|
|
|
|
_joinRoom(peer, roomType, roomId) {
|
|
// roomType: 'ip', 'secret' or 'public-id'
|
|
if (this._rooms[roomId] && this._rooms[roomId][peer.id]) {
|
|
// ensures that otherPeers never receive `peer-left` after `peer-joined` on reconnect.
|
|
this._leaveRoom(peer, roomType, roomId);
|
|
}
|
|
|
|
// if room doesn't exist, create it
|
|
if (!this._rooms[roomId]) {
|
|
this._rooms[roomId] = {};
|
|
}
|
|
|
|
this._notifyPeers(peer, roomType, roomId);
|
|
|
|
// add peer to room
|
|
this._rooms[roomId][peer.id] = peer;
|
|
}
|
|
|
|
|
|
_leaveIpRoom(peer, disconnect = false) {
|
|
this._leaveRoom(peer, 'ip', peer.ip, disconnect);
|
|
}
|
|
|
|
_leaveSecretRoom(peer, roomSecret, disconnect = false) {
|
|
this._leaveRoom(peer, 'secret', roomSecret, disconnect)
|
|
|
|
//remove secret from peer
|
|
peer.removeRoomSecret(roomSecret);
|
|
}
|
|
|
|
_leavePublicRoom(peer, disconnect = false) {
|
|
if (!peer.publicRoomId) return;
|
|
|
|
this._leaveRoom(peer, 'public-id', peer.publicRoomId, disconnect);
|
|
|
|
peer.publicRoomId = null;
|
|
}
|
|
|
|
_leaveRoom(peer, roomType, roomId, disconnect = false) {
|
|
if (!this._rooms[roomId] || !this._rooms[roomId][peer.id]) return;
|
|
|
|
// remove peer from room
|
|
delete this._rooms[roomId][peer.id];
|
|
|
|
// delete room if empty and abort
|
|
if (!Object.keys(this._rooms[roomId]).length) {
|
|
delete this._rooms[roomId];
|
|
return;
|
|
}
|
|
|
|
// notify all other peers that remain in room that peer left
|
|
for (const otherPeerId in this._rooms[roomId]) {
|
|
const otherPeer = this._rooms[roomId][otherPeerId];
|
|
|
|
let msg = {
|
|
type: 'peer-left',
|
|
peerId: peer.id,
|
|
roomType: roomType,
|
|
roomId: roomId,
|
|
disconnect: disconnect
|
|
};
|
|
|
|
this._send(otherPeer, msg);
|
|
}
|
|
}
|
|
|
|
_notifyPeers(peer, roomType, roomId) {
|
|
if (!this._rooms[roomId]) return;
|
|
|
|
// notify all other peers that peer joined
|
|
for (const otherPeerId in this._rooms[roomId]) {
|
|
if (otherPeerId === peer.id) continue;
|
|
const otherPeer = this._rooms[roomId][otherPeerId];
|
|
|
|
let msg = {
|
|
type: 'peer-joined',
|
|
peer: peer.getInfo(),
|
|
roomType: roomType,
|
|
roomId: roomId
|
|
};
|
|
|
|
this._send(otherPeer, msg);
|
|
}
|
|
|
|
// notify peer about peers already in the room
|
|
const otherPeers = [];
|
|
for (const otherPeerId in this._rooms[roomId]) {
|
|
if (otherPeerId === peer.id) continue;
|
|
otherPeers.push(this._rooms[roomId][otherPeerId].getInfo());
|
|
}
|
|
|
|
let msg = {
|
|
type: 'peers',
|
|
peers: otherPeers,
|
|
roomType: roomType,
|
|
roomId: roomId
|
|
};
|
|
|
|
this._send(peer, msg);
|
|
}
|
|
|
|
_joinSecretRooms(peer, roomSecrets) {
|
|
for (let i=0; i<roomSecrets.length; i++) {
|
|
this._joinSecretRoom(peer, roomSecrets[i])
|
|
}
|
|
}
|
|
|
|
_leaveAllSecretRooms(peer, disconnect = false) {
|
|
for (let i=0; i<peer.roomSecrets.length; i++) {
|
|
this._leaveSecretRoom(peer, peer.roomSecrets[i], disconnect);
|
|
}
|
|
}
|
|
|
|
_send(peer, message) {
|
|
if (!peer) return;
|
|
if (this._wss.readyState !== this._wss.OPEN) return;
|
|
message = JSON.stringify(message);
|
|
peer.socket.send(message);
|
|
}
|
|
|
|
_keepAlive(peer) {
|
|
this._cancelKeepAlive(peer);
|
|
let timeout = 1000;
|
|
|
|
if (!this._keepAliveTimers[peer.id]) {
|
|
this._keepAliveTimers[peer.id] = {
|
|
timer: 0,
|
|
lastBeat: Date.now()
|
|
};
|
|
}
|
|
|
|
if (Date.now() - this._keepAliveTimers[peer.id].lastBeat > 5 * timeout) {
|
|
// Disconnect peer if unresponsive for 10s
|
|
this._disconnect(peer);
|
|
return;
|
|
}
|
|
|
|
this._send(peer, { type: 'ping' });
|
|
|
|
this._keepAliveTimers[peer.id].timer = setTimeout(() => this._keepAlive(peer), timeout);
|
|
}
|
|
|
|
_cancelKeepAlive(peer) {
|
|
if (this._keepAliveTimers[peer.id]?.timer) {
|
|
clearTimeout(this._keepAliveTimers[peer.id].timer);
|
|
}
|
|
}
|
|
|
|
_setKeepAliveTimerToNow(peer) {
|
|
if (this._keepAliveTimers[peer.id]?.lastBeat) {
|
|
this._keepAliveTimers[peer.id].lastBeat = Date.now();
|
|
}
|
|
}
|
|
}
|
|
|