本次更新涵盖多个功能模块的优化与新增: 1. 新增Wi-Fi直连配对方式与协作画布模块 2. 完成设备管理重命名API与前端适配 3. 优化日签卡片空数据保护与UI细节 4. 新增剪贴板工具与每日运势会话 5. 修复应用锁恢复、语音消息录制等已知问题 6. 完善文件传输统计与配对逻辑 7. 更新安卓权限配置与iOS隐私描述 8. 新增自动化测试脚本与文档 9. 清理旧版审计报告与测试文件
907 lines
30 KiB
JavaScript
907 lines
30 KiB
JavaScript
var process = require('process')
|
|
var fs = require('fs')
|
|
var path = require('path')
|
|
// 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)
|
|
})
|
|
|
|
const parser = require('ua-parser-js');
|
|
const { uniqueNamesGenerator, animals, colors } = 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 = {};
|
|
this._userIdIndex = {};
|
|
this._fingerprintIndex = {};
|
|
this._pairingRecords = {};
|
|
this._pendingPairRequests = {};
|
|
this._canvasRooms = {};
|
|
this._loadPairingRecords();
|
|
|
|
console.log('Snapdrop is running on port', port);
|
|
}
|
|
|
|
_loadPairingRecords() {
|
|
const dataFile = path.join(__dirname, 'data', 'pairing_records.json');
|
|
try {
|
|
if (fs.existsSync(dataFile)) {
|
|
this._pairingRecords = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load pairing records:', e.message);
|
|
}
|
|
}
|
|
|
|
_savePairingRecords() {
|
|
const dataDir = path.join(__dirname, 'data');
|
|
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
|
|
const dataFile = path.join(dataDir, 'pairing_records.json');
|
|
try {
|
|
fs.writeFileSync(dataFile, JSON.stringify(this._pairingRecords, null, 2));
|
|
} catch (e) {
|
|
console.error('Failed to save pairing records:', e.message);
|
|
}
|
|
}
|
|
|
|
_onConnection(peer) {
|
|
this._joinRoom(peer);
|
|
peer.socket.on('message', message => this._onMessage(peer, message));
|
|
peer.socket.on('error', console.error);
|
|
this._keepAlive(peer);
|
|
|
|
this._send(peer, {
|
|
type: 'display-name',
|
|
message: {
|
|
displayName: peer.name.displayName,
|
|
deviceName: peer.name.deviceName
|
|
}
|
|
});
|
|
}
|
|
|
|
_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 + "; SameSite=Strict; Secure");
|
|
}
|
|
|
|
_onMessage(sender, message) {
|
|
try {
|
|
message = JSON.parse(message);
|
|
} catch (e) {
|
|
return;
|
|
}
|
|
|
|
switch (message.type) {
|
|
case 'disconnect':
|
|
this._leaveRoom(sender);
|
|
break;
|
|
case 'pong':
|
|
sender.lastBeat = Date.now();
|
|
break;
|
|
case 'register':
|
|
this._handleRegister(sender, message);
|
|
break;
|
|
case 'discoverMyDevices':
|
|
this._handleDiscoverMyDevices(sender, message);
|
|
break;
|
|
case 'transportNegotiate':
|
|
this._handleTransportNegotiate(sender, message);
|
|
break;
|
|
case 'wsRelay':
|
|
this._handleWsRelay(sender, message);
|
|
break;
|
|
case 'pair-request':
|
|
case 'pairRequest':
|
|
this._handlePairRequest(sender, message);
|
|
break;
|
|
case 'pair-accept':
|
|
case 'pairAccept':
|
|
this._handlePairAccept(sender, message);
|
|
break;
|
|
case 'pair-reject':
|
|
case 'pairReject':
|
|
this._handlePairReject(sender, message);
|
|
break;
|
|
case 'heartbeat':
|
|
sender.lastBeat = Date.now();
|
|
break;
|
|
case 'textMessage':
|
|
this._handleTextMessage(sender, message);
|
|
break;
|
|
case 'fileMeta':
|
|
this._handleFileMeta(sender, message);
|
|
break;
|
|
case 'canvas-stroke':
|
|
this._handleCanvasStroke(sender, message);
|
|
break;
|
|
case 'canvas-cursor':
|
|
this._handleCanvasCursor(sender, message);
|
|
break;
|
|
case 'canvas-join':
|
|
this._handleCanvasJoin(sender, message);
|
|
break;
|
|
case 'canvas-leave':
|
|
this._handleCanvasLeave(sender, message);
|
|
break;
|
|
case 'canvas-snapshot':
|
|
this._handleCanvasSnapshot(sender, message);
|
|
break;
|
|
}
|
|
|
|
const handledTypes = ['disconnect', 'pong', 'register', 'discoverMyDevices', 'transportNegotiate', 'wsRelay', 'pair-request', 'pairRequest', 'pair-accept', 'pairAccept', 'pair-reject', 'pairReject', 'heartbeat', 'textMessage', 'fileMeta', 'canvas-stroke', 'canvas-cursor', 'canvas-join', 'canvas-leave', 'canvas-snapshot'];
|
|
if (message.to && !handledTypes.includes(message.type)) {
|
|
let recipientId = message.to;
|
|
let recipient = this._resolveTarget(sender, recipientId);
|
|
|
|
if (recipient) {
|
|
delete message.to;
|
|
message.sender = sender.id;
|
|
message.from = sender.fingerprint || sender.id;
|
|
message.fromPeerId = sender.id;
|
|
this._send(recipient, message);
|
|
} else {
|
|
console.log('Message routing failed: target not found for', recipientId, 'from', sender.id);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
_handleRegister(sender, message) {
|
|
const data = message.payload || message.data || {};
|
|
if (data.userId) {
|
|
const oldUserId = sender.userId;
|
|
sender.userId = data.userId;
|
|
if (oldUserId && this._userIdIndex[oldUserId]) {
|
|
this._userIdIndex[oldUserId].delete(sender.id);
|
|
if (this._userIdIndex[oldUserId].size === 0) {
|
|
delete this._userIdIndex[oldUserId];
|
|
}
|
|
}
|
|
if (!this._userIdIndex[sender.userId]) {
|
|
this._userIdIndex[sender.userId] = new Set();
|
|
}
|
|
this._userIdIndex[sender.userId].add(sender.id);
|
|
}
|
|
if (data.fingerprint) {
|
|
const oldFingerprint = sender.fingerprint;
|
|
sender.fingerprint = data.fingerprint;
|
|
if (oldFingerprint && this._fingerprintIndex[oldFingerprint] === sender.id) {
|
|
delete this._fingerprintIndex[oldFingerprint];
|
|
}
|
|
const existingPeerId = this._fingerprintIndex[sender.fingerprint];
|
|
if (existingPeerId && existingPeerId !== sender.id) {
|
|
const existingPeer = this._findPeer(existingPeerId);
|
|
if (existingPeer) {
|
|
console.log('Replacing old connection for fingerprint:', sender.fingerprint, 'old peer:', existingPeerId, 'new peer:', sender.id);
|
|
this._leaveRoom(existingPeer);
|
|
}
|
|
}
|
|
this._fingerprintIndex[sender.fingerprint] = sender.id;
|
|
console.log('Register fingerprint mapping:', sender.fingerprint, '->', sender.id);
|
|
}
|
|
if (data.ipCity) sender.ipCity = data.ipCity;
|
|
if (data.ipRange) sender.ipRange = data.ipRange;
|
|
if (data.deviceModel) sender.deviceModel = data.deviceModel;
|
|
if (data.platform) sender.platform = data.platform;
|
|
if (data.alias) sender.name.displayName = data.alias;
|
|
if (data.deviceType) sender.name.type = data.deviceType;
|
|
|
|
this._send(sender, {
|
|
type: 'registered',
|
|
id: sender.id,
|
|
fingerprint: sender.fingerprint || '',
|
|
userId: sender.userId
|
|
});
|
|
}
|
|
|
|
_handleDiscoverMyDevices(sender, message) {
|
|
const payload = message.payload || message.data || {};
|
|
const userId = payload.userId || message.userId || sender.userId;
|
|
if (!userId) {
|
|
this._send(sender, {
|
|
type: 'myDevicesResponse',
|
|
devices: [],
|
|
error: 'userId required'
|
|
});
|
|
return;
|
|
}
|
|
|
|
const seenFingerprints = new Map();
|
|
for (const roomId in this._rooms) {
|
|
for (const peerId in this._rooms[roomId]) {
|
|
const peer = this._rooms[roomId][peerId];
|
|
if (peer.id === sender.id) continue;
|
|
if (peer.userId === userId) {
|
|
const fp = peer.fingerprint || peer.id;
|
|
if (!seenFingerprints.has(fp)) {
|
|
seenFingerprints.set(fp, {
|
|
id: peer.id,
|
|
fingerprint: peer.fingerprint || '',
|
|
alias: peer.name.displayName,
|
|
deviceName: peer.name.deviceName,
|
|
deviceModel: peer.deviceModel || '',
|
|
platform: peer.platform || '',
|
|
ip: peer.ip,
|
|
ipCity: peer.ipCity || '',
|
|
isOnline: true,
|
|
rtcSupported: peer.rtcSupported
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
this._send(sender, {
|
|
type: 'myDevicesResponse',
|
|
devices: Array.from(seenFingerprints.values())
|
|
});
|
|
}
|
|
|
|
_handleTransportNegotiate(sender, message) {
|
|
const targetId = message.to;
|
|
if (!targetId) return;
|
|
|
|
let targetPeer = this._findPeer(targetId);
|
|
|
|
if (!targetPeer && this._fingerprintIndex[targetId]) {
|
|
targetPeer = this._findPeer(this._fingerprintIndex[targetId]);
|
|
}
|
|
|
|
if (!targetPeer) {
|
|
this._send(sender, {
|
|
type: 'transportNegotiateResponse',
|
|
success: false,
|
|
error: 'Target device not found'
|
|
});
|
|
return;
|
|
}
|
|
|
|
this._send(targetPeer, {
|
|
type: 'transportNegotiate',
|
|
from: sender.fingerprint || sender.id,
|
|
transport: message.transport,
|
|
payload: message.payload || {}
|
|
});
|
|
}
|
|
|
|
_handlePairRequest(sender, message) {
|
|
const targetId = message.to;
|
|
if (!targetId) return;
|
|
|
|
let targetPeer = this._findPeer(targetId);
|
|
|
|
if (!targetPeer && this._fingerprintIndex[targetId]) {
|
|
targetPeer = this._findPeer(this._fingerprintIndex[targetId]);
|
|
}
|
|
|
|
if (!targetPeer) {
|
|
this._send(sender, {
|
|
type: 'pair-response',
|
|
from: sender.id,
|
|
success: false,
|
|
error: 'Target device not found'
|
|
});
|
|
return;
|
|
}
|
|
|
|
const requestId = Date.now().toString(36) + Math.random().toString(36).substr(2);
|
|
this._pendingPairRequests[requestId] = {
|
|
id: requestId,
|
|
fromId: sender.id,
|
|
toId: targetId,
|
|
fingerprint: message.fingerprint || message.payload?.fingerprint || '',
|
|
createdAt: Date.now(),
|
|
expiresAt: Date.now() + 300000
|
|
};
|
|
|
|
this._send(targetPeer, {
|
|
type: 'pair-request',
|
|
from: sender.fingerprint || sender.id,
|
|
requestId: requestId,
|
|
fingerprint: this._pendingPairRequests[requestId].fingerprint,
|
|
payload: message.payload || {}
|
|
});
|
|
}
|
|
|
|
_handlePairAccept(sender, message) {
|
|
const requestId = message.requestId || message.payload?.requestId;
|
|
if (!requestId || !this._pendingPairRequests[requestId]) {
|
|
this._send(sender, {
|
|
type: 'pair-response',
|
|
success: false,
|
|
error: 'Request not found or expired'
|
|
});
|
|
return;
|
|
}
|
|
|
|
const req = this._pendingPairRequests[requestId];
|
|
delete this._pendingPairRequests[requestId];
|
|
|
|
const pairKey1 = req.fromId + ':' + sender.id;
|
|
const pairKey2 = sender.id + ':' + req.fromId;
|
|
this._pairingRecords[pairKey1] = {
|
|
deviceId: req.fromId,
|
|
peerDeviceId: sender.id,
|
|
peerFingerprint: req.fingerprint,
|
|
pairedAt: Date.now(),
|
|
isTrusted: true
|
|
};
|
|
this._pairingRecords[pairKey2] = {
|
|
deviceId: sender.id,
|
|
peerDeviceId: req.fromId,
|
|
peerFingerprint: req.fingerprint,
|
|
pairedAt: Date.now(),
|
|
isTrusted: true
|
|
};
|
|
this._savePairingRecords();
|
|
|
|
const fromPeer = this._findPeer(req.fromId);
|
|
if (fromPeer) {
|
|
this._send(fromPeer, {
|
|
type: 'pair-response',
|
|
from: sender.fingerprint || sender.id,
|
|
success: true,
|
|
requestId: requestId
|
|
});
|
|
}
|
|
|
|
this._send(sender, {
|
|
type: 'pair-response',
|
|
from: fromPeer ? (fromPeer.fingerprint || fromPeer.id) : req.fromId,
|
|
success: true,
|
|
requestId: requestId
|
|
});
|
|
}
|
|
|
|
_handlePairReject(sender, message) {
|
|
const requestId = message.requestId || message.payload?.requestId;
|
|
if (!requestId || !this._pendingPairRequests[requestId]) return;
|
|
|
|
const req = this._pendingPairRequests[requestId];
|
|
delete this._pendingPairRequests[requestId];
|
|
|
|
const fromPeer = this._findPeer(req.fromId);
|
|
if (fromPeer) {
|
|
this._send(fromPeer, {
|
|
type: 'pair-response',
|
|
from: sender.fingerprint || sender.id,
|
|
success: false,
|
|
requestId: requestId,
|
|
error: 'Rejected'
|
|
});
|
|
}
|
|
}
|
|
|
|
_findPeer(peerId) {
|
|
for (const roomId in this._rooms) {
|
|
if (this._rooms[roomId][peerId]) {
|
|
return this._rooms[roomId][peerId];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
_resolveTarget(sender, targetId) {
|
|
if (this._rooms[sender.ip] && this._rooms[sender.ip][targetId]) {
|
|
return this._rooms[sender.ip][targetId];
|
|
}
|
|
|
|
let recipient = this._findPeer(targetId);
|
|
if (recipient) return recipient;
|
|
|
|
if (this._fingerprintIndex[targetId]) {
|
|
const mappedPeerId = this._fingerprintIndex[targetId];
|
|
console.log('Fingerprint mapping:', targetId, '->', mappedPeerId);
|
|
recipient = this._findPeer(mappedPeerId);
|
|
if (recipient) return recipient;
|
|
}
|
|
|
|
if (sender.userId) {
|
|
const sameUserPeerIds = this._userIdIndex[sender.userId];
|
|
if (sameUserPeerIds) {
|
|
for (const peerId of sameUserPeerIds) {
|
|
if (peerId !== sender.id) {
|
|
recipient = this._findPeer(peerId);
|
|
if (recipient) return recipient;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this._userIdIndex[targetId]) {
|
|
for (const peerId of this._userIdIndex[targetId]) {
|
|
recipient = this._findPeer(peerId);
|
|
if (recipient) return recipient;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
_handleTextMessage(sender, message) {
|
|
const targetId = message.to;
|
|
if (!targetId) return;
|
|
|
|
const recipient = this._resolveTarget(sender, targetId);
|
|
if (recipient) {
|
|
this._send(recipient, {
|
|
type: 'textMessage',
|
|
from: sender.fingerprint || sender.id,
|
|
fromPeerId: sender.id,
|
|
to: recipient.id,
|
|
message: message.message || message.payload || {},
|
|
timestamp: message.timestamp || Date.now()
|
|
});
|
|
} else {
|
|
console.log('textMessage routing failed: target not found for', targetId, 'from', sender.id);
|
|
}
|
|
}
|
|
|
|
_handleFileMeta(sender, message) {
|
|
const targetId = message.to;
|
|
if (!targetId) return;
|
|
|
|
const recipient = this._resolveTarget(sender, targetId);
|
|
if (recipient) {
|
|
this._send(recipient, {
|
|
type: 'fileMeta',
|
|
from: sender.fingerprint || sender.id,
|
|
fromPeerId: sender.id,
|
|
to: recipient.id,
|
|
file: message.file || message.payload || {},
|
|
timestamp: message.timestamp || Date.now()
|
|
});
|
|
} else {
|
|
console.log('fileMeta routing failed: target not found for', targetId, 'from', sender.id);
|
|
}
|
|
}
|
|
|
|
_handleWsRelay(sender, message) {
|
|
const targetId = message.to;
|
|
if (!targetId) return;
|
|
|
|
let targetPeer = null;
|
|
for (const roomId in this._rooms) {
|
|
if (this._rooms[roomId][targetId]) {
|
|
targetPeer = this._rooms[roomId][targetId];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!targetPeer && this._fingerprintIndex[targetId]) {
|
|
const mappedPeerId = this._fingerprintIndex[targetId];
|
|
for (const roomId in this._rooms) {
|
|
if (this._rooms[roomId][mappedPeerId]) {
|
|
targetPeer = this._rooms[roomId][mappedPeerId];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!targetPeer && sender.userId) {
|
|
const sameUserPeerIds = this._userIdIndex[sender.userId];
|
|
if (sameUserPeerIds) {
|
|
for (const peerId of sameUserPeerIds) {
|
|
if (peerId !== sender.id) {
|
|
for (const roomId in this._rooms) {
|
|
if (this._rooms[roomId][peerId]) {
|
|
targetPeer = this._rooms[roomId][peerId];
|
|
break;
|
|
}
|
|
}
|
|
if (targetPeer) break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!targetPeer) {
|
|
this._send(sender, {
|
|
type: 'wsRelay',
|
|
from: sender.fingerprint || sender.id,
|
|
payload: {
|
|
relayType: 'error',
|
|
data: { error: 'Target peer not found', targetId: targetId }
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
const relayType = message.relayType || (message.payload && message.payload.relayType) || 'data';
|
|
const payload = message.payload || {};
|
|
if (!payload.relayType) {
|
|
payload.relayType = relayType;
|
|
}
|
|
|
|
this._send(targetPeer, {
|
|
type: 'wsRelay',
|
|
from: sender.fingerprint || sender.id,
|
|
payload: payload
|
|
});
|
|
}
|
|
|
|
_handleCanvasJoin(sender, message) {
|
|
const payload = message.payload || {};
|
|
const canvasId = payload.canvasId;
|
|
if (!canvasId) return;
|
|
|
|
if (!this._canvasRooms[canvasId]) {
|
|
this._canvasRooms[canvasId] = new Set();
|
|
}
|
|
this._canvasRooms[canvasId].add(sender.id);
|
|
sender.canvasRoomId = canvasId;
|
|
|
|
const members = Array.from(this._canvasRooms[canvasId]);
|
|
for (const peerId of this._canvasRooms[canvasId]) {
|
|
if (peerId === sender.id) continue;
|
|
const peer = this._findPeer(peerId);
|
|
if (peer) {
|
|
this._send(peer, {
|
|
type: 'canvas-join',
|
|
from: sender.fingerprint || sender.id,
|
|
payload: { canvasId: canvasId, members: members }
|
|
});
|
|
}
|
|
}
|
|
|
|
this._send(sender, {
|
|
type: 'canvas-join',
|
|
from: 'server',
|
|
payload: { canvasId: canvasId, members: members }
|
|
});
|
|
console.log('Canvas join:', sender.id, 'joined canvas', canvasId, 'members:', members.length);
|
|
}
|
|
|
|
_handleCanvasLeave(sender, message) {
|
|
const payload = message.payload || {};
|
|
const canvasId = payload.canvasId || sender.canvasRoomId;
|
|
if (!canvasId || !this._canvasRooms[canvasId]) return;
|
|
|
|
this._canvasRooms[canvasId].delete(sender.id);
|
|
sender.canvasRoomId = null;
|
|
|
|
for (const peerId of this._canvasRooms[canvasId]) {
|
|
const peer = this._findPeer(peerId);
|
|
if (peer) {
|
|
this._send(peer, {
|
|
type: 'canvas-leave',
|
|
from: sender.fingerprint || sender.id,
|
|
payload: { canvasId: canvasId }
|
|
});
|
|
}
|
|
}
|
|
|
|
if (this._canvasRooms[canvasId].size === 0) {
|
|
delete this._canvasRooms[canvasId];
|
|
}
|
|
console.log('Canvas leave:', sender.id, 'left canvas', canvasId);
|
|
}
|
|
|
|
_handleCanvasStroke(sender, message) {
|
|
const payload = message.payload || {};
|
|
const canvasId = payload.canvasId || sender.canvasRoomId;
|
|
if (!canvasId || !this._canvasRooms[canvasId]) return;
|
|
|
|
for (const peerId of this._canvasRooms[canvasId]) {
|
|
if (peerId === sender.id) continue;
|
|
const peer = this._findPeer(peerId);
|
|
if (peer) {
|
|
this._send(peer, {
|
|
type: 'canvas-stroke',
|
|
from: sender.fingerprint || sender.id,
|
|
payload: payload
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
_handleCanvasCursor(sender, message) {
|
|
const payload = message.payload || {};
|
|
const canvasId = payload.canvasId || sender.canvasRoomId;
|
|
if (!canvasId || !this._canvasRooms[canvasId]) return;
|
|
|
|
for (const peerId of this._canvasRooms[canvasId]) {
|
|
if (peerId === sender.id) continue;
|
|
const peer = this._findPeer(peerId);
|
|
if (peer) {
|
|
this._send(peer, {
|
|
type: 'canvas-cursor',
|
|
from: sender.fingerprint || sender.id,
|
|
payload: payload
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
_handleCanvasSnapshot(sender, message) {
|
|
const payload = message.payload || {};
|
|
const canvasId = payload.canvasId || sender.canvasRoomId;
|
|
const action = payload.action || 'request';
|
|
if (!canvasId || !this._canvasRooms[canvasId]) return;
|
|
|
|
if (action === 'request') {
|
|
const members = Array.from(this._canvasRooms[canvasId]);
|
|
const otherMember = members.find(id => id !== sender.id);
|
|
if (otherMember) {
|
|
const peer = this._findPeer(otherMember);
|
|
if (peer) {
|
|
this._send(peer, {
|
|
type: 'canvas-snapshot',
|
|
from: sender.fingerprint || sender.id,
|
|
payload: { canvasId: canvasId, action: 'request' }
|
|
});
|
|
}
|
|
}
|
|
} else if (action === 'response') {
|
|
const targetId = message.to;
|
|
if (targetId) {
|
|
const targetPeer = this._findPeer(targetId);
|
|
if (targetPeer) {
|
|
this._send(targetPeer, {
|
|
type: 'canvas-snapshot',
|
|
from: sender.fingerprint || sender.id,
|
|
payload: payload
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_joinRoom(peer) {
|
|
if (!this._rooms[peer.ip]) {
|
|
this._rooms[peer.ip] = {};
|
|
}
|
|
|
|
for (const otherPeerId in this._rooms[peer.ip]) {
|
|
const otherPeer = this._rooms[peer.ip][otherPeerId];
|
|
this._send(otherPeer, {
|
|
type: 'peer-joined',
|
|
peer: peer.getInfo()
|
|
});
|
|
}
|
|
|
|
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
|
|
});
|
|
|
|
this._rooms[peer.ip][peer.id] = peer;
|
|
|
|
if (peer.userId) {
|
|
if (!this._userIdIndex[peer.userId]) {
|
|
this._userIdIndex[peer.userId] = new Set();
|
|
}
|
|
this._userIdIndex[peer.userId].add(peer.id);
|
|
}
|
|
}
|
|
|
|
_leaveRoom(peer) {
|
|
if (!this._rooms[peer.ip] || !this._rooms[peer.ip][peer.id]) return;
|
|
this._cancelKeepAlive(this._rooms[peer.ip][peer.id]);
|
|
|
|
if (peer.canvasRoomId && this._canvasRooms[peer.canvasRoomId]) {
|
|
this._canvasRooms[peer.canvasRoomId].delete(peer.id);
|
|
for (const peerId of this._canvasRooms[peer.canvasRoomId]) {
|
|
const otherPeer = this._findPeer(peerId);
|
|
if (otherPeer) {
|
|
this._send(otherPeer, {
|
|
type: 'canvas-leave',
|
|
from: peer.fingerprint || peer.id,
|
|
payload: { canvasId: peer.canvasRoomId }
|
|
});
|
|
}
|
|
}
|
|
if (this._canvasRooms[peer.canvasRoomId].size === 0) {
|
|
delete this._canvasRooms[peer.canvasRoomId];
|
|
}
|
|
peer.canvasRoomId = null;
|
|
}
|
|
|
|
if (peer.fingerprint && this._fingerprintIndex[peer.fingerprint] === peer.id) {
|
|
delete this._fingerprintIndex[peer.fingerprint];
|
|
}
|
|
|
|
delete this._rooms[peer.ip][peer.id];
|
|
|
|
if (peer.userId && this._userIdIndex[peer.userId]) {
|
|
this._userIdIndex[peer.userId].delete(peer.id);
|
|
if (this._userIdIndex[peer.userId].size === 0) {
|
|
delete this._userIdIndex[peer.userId];
|
|
}
|
|
}
|
|
|
|
peer.socket.terminate();
|
|
if (!Object.keys(this._rooms[peer.ip]).length) {
|
|
delete this._rooms[peer.ip];
|
|
} else {
|
|
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 = 30000;
|
|
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) {
|
|
this.socket = socket;
|
|
|
|
this._setIP(request);
|
|
this._setPeerId(request);
|
|
this.rtcSupported = request.url.indexOf('webrtc') > -1;
|
|
this._setName(request);
|
|
|
|
this.userId = '';
|
|
this.fingerprint = '';
|
|
this.ipCity = '';
|
|
this.ipRange = '';
|
|
this.deviceModel = '';
|
|
this.platform = '';
|
|
this.canvasRoomId = null;
|
|
|
|
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;
|
|
}
|
|
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} userId=${this.userId} 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,
|
|
userId: this.userId,
|
|
ipCity: this.ipCity,
|
|
deviceModel: this.deviceModel,
|
|
platform: this.platform
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
return hash;
|
|
}
|
|
});
|
|
|
|
const server = new SnapdropServer(process.env.PORT || 3000);
|