Files
xianyan/server/index.js
Developer 283950ea07 chore: 批量代码优化与功能迭代更新
本次提交包含大量代码优化、功能新增与服务端配置更新:
1. 修复分析报告统计数据,调整CMake策略设置
2. 优化APP权限配置、编辑器与聊天界面组件
3. 更新依赖库版本与pubspec配置
4. 新增文件传输服务端、信令服务器相关配置与脚本
5. 完善用户注销功能与数据库迁移脚本
6. 优化多处动画效果、代码风格与日志输出
7. 新增多种调试与部署脚本,修复已知BUG
2026-05-12 06:28:04 +08:00

611 lines
18 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._pairingRecords = {};
this._pendingPairRequests = {};
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;
}
const handledTypes = ['disconnect', 'pong', 'register', 'discoverMyDevices', 'transportNegotiate', 'wsRelay', 'pair-request', 'pairRequest', 'pair-accept', 'pairAccept', 'pair-reject', 'pairReject', 'heartbeat'];
if (message.to && !handledTypes.includes(message.type)) {
const recipientId = message.to;
let recipient = null;
if (this._rooms[sender.ip] && this._rooms[sender.ip][recipientId]) {
recipient = this._rooms[sender.ip][recipientId];
} else {
for (const roomId in this._rooms) {
if (this._rooms[roomId][recipientId]) {
recipient = this._rooms[roomId][recipientId];
break;
}
}
}
if (recipient) {
delete message.to;
message.sender = sender.id;
this._send(recipient, message);
}
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.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,
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 myDevices = [];
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) {
myDevices.push({
id: peer.id,
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: myDevices
});
}
_handleTransportNegotiate(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._send(sender, {
type: 'transportNegotiateResponse',
success: false,
error: 'Target device not found'
});
return;
}
this._send(targetPeer, {
type: 'transportNegotiate',
from: 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._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.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.id,
success: true,
requestId: requestId
});
}
this._send(sender, {
type: 'pair-response',
from: 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.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;
}
_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._send(sender, {
type: 'wsRelay',
from: 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.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]);
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.ipCity = '';
this.ipRange = '';
this.deviceModel = '';
this.platform = '';
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);