const process = require('process') // 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) }) function isObjNull(obj) { for (const key in obj) { return false; }; return true; } const express = require('express'); const http = require('http'); const path = require("path"); const fs = require("fs"); const parser = require('ua-parser-js'); const app = express(); const port = 3001; const publicRun = process.argv[2]; const nameGenerator = require('./name-generator.js'); app.get('*', (req, res) => { const file = path.join(__dirname, 'public', req.url); fs.stat(file, function (err, stat) { if (!err && stat.isFile()) { //是文件 res.sendFile(file); return; } if (/^\/[^\/]*\//.test(req.url)) { //如果字符串中包含了两个或以上的斜杆 req.url = req.url.match(/^\/[^\/]*/)[0]; //截取第一个二个斜杠之间的字符串 res.redirect(req.url); } else { res.sendFile(path.join(__dirname, 'public', '/index.html')); } }); }); const server = http.createServer(app); (!publicRun == "public") ? server.listen(port) : server.listen(port, '0.0.0.0'); console.log(new Date().toISOString(), ' Snapchat is running on port', port); class SnapchatServer { constructor() { const WebSocket = require('ws'); this._wss = new WebSocket.Server({ server }); 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._msgs = {}; this._timer = {}; this._timeDelMsgs = 259200000; } _onConnection(peer) { this._joinRoom(peer); peer.socket.on('message', message => this._onMessage(peer, message)); this._keepAlive(peer); this._sendDispName(peer); } _onHeaders(headers, response) { let match; if (response.headers.cookie) match = response.headers.cookie.match(/peerid=([0-9a-zA-Z]{8}-[0-9a-zA-Z]{4}-4[0-9a-zA-Z]{3}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{12})/); if (match) { response.peerId = match[1]; } else { response.peerId = Peer.uuid(); } headers.push('Set-Cookie: peerid=' + response.peerId + "; SameSite=Strict; Max-Age=259200"); //; Secure } _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; case 'getmsgs': this._sendHistoryMsgs(sender, message.time); break; case 'pub': this._pubMsg(sender, message); break; } // relay message to recipient if (message.to && this._rooms[sender.room]) { const recipientId = message.to; // TODO: sanitize const recipient = this._rooms[sender.room][recipientId]; delete message.to; // add sender id message.sender = sender.id; this._send(recipient, message); return; } } _sendDispName(peer) { this._send(peer, { type: 'display-name', dispName: peer.name.displayName, peerId: peer.id }); } _pubMsg(sender, message) { const serverTime = Date.now(); this._send(sender, { type: 'msg-received', id: message.time, time: serverTime }); message.time = serverTime; //将消息的时间戳替换成服务器时间 message.sender = sender.id; for (const peerId in this._rooms[sender.room]) { //群发消息给其他群成员 if (sender.id != peerId) this._send(this._rooms[sender.room][peerId], message); }; for (const timeId in this._msgs[sender.room]) { //删除72小时之前的记录 if (Date.now() - timeId > this._timeDelMsgs) { delete this._msgs[sender.room][timeId]; } else { break; } }; let delCmd = message.text.match(/^\/del:(.+)/); if (delCmd) { //如果为删除指令 if (delCmd[1] == 'all') { for (const timeId in this._msgs[sender.room]) { //删除所有记录 if (this._msgs[sender.room][timeId].sender == sender.id) { this._msgs[sender.room][timeId].text = '[此消息已被删除]'; } } } else { for (const timeId in this._msgs[sender.room]) { //删除单条记录 if (timeId == delCmd[1] && sender.id == this._msgs[sender.room][timeId].sender) { this._msgs[sender.room][timeId].text = '[此消息已被删除]'; break; } } } } else { //如果不是指令,则添加到对象:_msgs[roomID],然后添加到数据库 if (!this._msgs[sender.room]) this._msgs[sender.room] = {}; this._msgs[sender.room][message.time] = { sender: sender.id, name: message.name, text: message.text } } } _sendHistoryMsgs(peer, time) { for (const timeId in this._msgs[peer.room]) { if (Date.now() - timeId > this._timeDelMsgs) { delete this._msgs[peer.room][timeId]; } else { break; } } if (time > 0) { let msgs = {}; for (const timeId in this._msgs[peer.room]) { if (timeId > time) { msgs[timeId] = this._msgs[peer.room][timeId]; } } this._send(peer, { type: 'history-msgs', time: Date.now(), msgs: msgs }); } else { this._send(peer, { type: 'history-msgs', time: Date.now(), msgs: this._msgs[peer.room] }); } } _joinRoom(peer) { // if room doesn't exist, create it if (!this._rooms[peer.room]) { this._rooms[peer.room] = {}; //this._msgs[peer.room] = {}; this._send(peer, { type: 'peers', peers: [] }); } else { // notify all other peers const otherPeers = []; const count = Object.keys(this._rooms[peer.room]).length + 1; for (const peerId in this._rooms[peer.room]) { if (peerId != peer.id) { otherPeers.push(this._rooms[peer.room][peerId].getInfo()); this._send(this._rooms[peer.room][peerId], { type: 'peer-joined', peer: peer.getInfo(), count: count }); } } // notify peer about the other peers this._send(peer, { type: 'peers', peers: otherPeers }); } // add peer to room this._rooms[peer.room][peer.id] = peer; if (this._timer[peer.room]) clearTimeout(this._timer[peer.room]); //有成员进群则取消清空消息定时器 } _leaveRoom(peer) { if (!this._rooms[peer.room] || !this._rooms[peer.room][peer.id]) return; this._cancelKeepAlive(this._rooms[peer.room][peer.id]); // delete the peer delete this._rooms[peer.room][peer.id]; peer.socket.terminate(); //if room is empty, delete the room if (isObjNull(this._rooms[peer.room])) { //最后一个群成员退出 if (isObjNull(this._msgs[peer.room])) { //如果消息记录为空,最后一个成员退出群聊之后直接删除房间和消息对象 delete this._rooms[peer.room]; delete this._msgs[peer.room]; } else { //如果消息记录不为空,最后一个成员退出房间之后设置定时器延时72小时删除房间和消息对象 this._timer[peer.room] = setTimeout(() => this._delMsgs(peer.room), this._timeDelMsgs); } } else { const count = Object.keys(this._rooms[peer.room]).length; // notify all other peers for (const otherPeerId in this._rooms[peer.room]) { const otherPeer = this._rooms[peer.room][otherPeerId]; this._send(otherPeer, { type: 'peer-left', peerId: peer.id, peerName: peer.name.displayName, count: count }); } } } _delMsgs(room) { delete this._rooms[room]; delete this._msgs[room]; delete this._timer[room]; } _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); const 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._setRoom(request); this.id = request.peerId; this.rtcSupported = request.url.lastIndexOf('/false') < 1; this._setName(request); this.timerId = 0; this.lastBeat = Date.now(); //console.log(new Date().toISOString(), this.room, this.name.deviceName, 'RTC:', this.rtcSupported); } _setRoom(request) { let match = request.url.match(/@([^\/]+)\//); if (match) { this.room = decodeURIComponent(match[1]); } else { this.room = '未命名'; } } toString() { return ``; } _setName(request) { let ua = parser(request.headers['user-agent']); let deviceName = ''; if (ua.os && ua.os.name) deviceName = ua.os.name.replace('Mac OS', 'Mac') + ' '; if (ua.browser.name) deviceName += ua.browser.name; if (!deviceName) deviceName = 'Unknown Device'; let displayName = ''; let match = request.url.match(/\/([^@]*)@/); if (match) displayName = decodeURIComponent(match[1]); if (!displayName) { displayName = nameGenerator.getName(this.id); } 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 = ''; for (let i = 0; i < 32; i += 1) { switch (i) { 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; } } const snapchatServer = new SnapchatServer();