UNPKG

@cimo/websocket

Version:

Websocket (server/client). Light, fast and secure.

371 lines 15.6 kB
import * as Crypto from "crypto"; import * as helperSrc from "../HelperSrc.js"; export default class Manager { secretKey; intervalTimePing; clientList; handleReceiveDataList; generateClientId = () => { return Crypto.randomBytes(20).toString("hex"); }; generateSignature = (value) => { return Crypto.createHmac("sha256", this.secretKey).update(value).digest("hex"); }; verifySignature = (clientId, signature) => { if (signature !== this.generateSignature(clientId)) { return false; } return true; }; clientConnection = (socket) => { const clientId = this.generateClientId(); const signature = this.generateSignature(clientId); helperSrc.writeLog("@cimo/websocket - Server - Manager.ts - clientConnection()", `Connection request from Ip: ${socket.remoteAddress || ""} - Client ${clientId}.`); this.clientDelete(clientId); this.clientList.push({ id: clientId, signature, username: "", socket, buffer: Buffer.alloc(0), opCode: -1, fragmentList: [], intervalPing: undefined, lastPong: Date.now() }); this.sendMessage("text", clientId, "clientId_current", clientId); this.sendDataBroadcast({ label: "connection", result: `Client ${clientId} connected.` }, clientId); this.ping(clientId); return clientId; }; clientDisconnection = (clientId) => { const client = this.clientCheck(clientId); if (!client) { helperSrc.writeLog("@cimo/webSocket - Server - Manager.ts - clientDisconnection()", `Client ${clientId} doesn't exists!`); return; } clearInterval(client.intervalPing); const buffer = Buffer.from("Server disconnect", "utf8"); const payload = Buffer.alloc(2 + buffer.length); payload.writeUInt16BE(1000, 0); buffer.copy(payload, 2); const header = Buffer.from([0x88, payload.length]); const closeFrame = Buffer.concat([header, payload]); if (client.socket && client.socket.writable) { client.socket.write(closeFrame); } client.socket.destroy(); }; clientCheck = (clientId) => { for (let a = 0; a < this.clientList.length; a++) { if (this.clientList[a].id === clientId) { return this.clientList[a]; } } return undefined; }; clientDelete = (clientId) => { for (let a = this.clientList.length - 1; a >= 0; a--) { if (this.clientList[a].id === clientId) { this.clientList.splice(a, 1); } } }; ping = (clientId) => { const client = this.clientCheck(clientId); if (!client) { helperSrc.writeLog("@cimo/webSocket - Server - Manager.ts - ping()", `Client ${clientId} doesn't exists!`); return; } client.intervalPing = setInterval(() => { if (client.socket && client.socket.writable) { const now = Date.now(); if (now - client.lastPong > this.intervalTimePing * 2) { this.clientDisconnection(clientId); return; } const frame = Buffer.alloc(2); frame[0] = 0x89; frame[1] = 0x00; client.socket.write(frame); } }, this.intervalTimePing); }; responseHeader = (request) => { const websocketKey = request.headers["sec-websocket-key"]; if (!websocketKey || websocketKey.trim() === "") { helperSrc.writeLog("@cimo/webSocket - Server - Manager.ts - responseHeader()", "Invalid Sec-WebSocket-Key header!"); return []; } const hash = Crypto.createHash("sha1").update(`${websocketKey}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest("base64"); return ["HTTP/1.1 101 Switching Protocols", "Upgrade: websocket", "Connection: Upgrade", `Sec-WebSocket-Accept: ${hash}`, "\r\n"]; }; handleFrame = (clientId, data, callback) => { const client = this.clientCheck(clientId); if (!client) { helperSrc.writeLog("@cimo/webSocket - Server - Manager.ts - handleFrame()", `Client ${clientId} doesn't exists!`); return; } const isSignatureVerified = this.verifySignature(clientId, client.signature); if (!isSignatureVerified) { helperSrc.writeLog("@cimo/websocket - Server - Manager.ts - handleFrame()", "Wrong signature!"); return; } client.buffer = Buffer.concat([client.buffer, data]); while (true) { if (client.buffer.length < 2) { break; } let payloadLength = client.buffer[1] & 0x7f; let maskingKeyStart = 2; if (payloadLength === 126) { if (client.buffer.length < 4) { break; } payloadLength = client.buffer.readUInt16BE(2); maskingKeyStart = 4; } else if (payloadLength === 127) { if (client.buffer.length < 10) { break; } payloadLength = Number(client.buffer.readBigUInt64BE(2)); maskingKeyStart = 10; } const payloadStart = maskingKeyStart + 4; const frameLength = payloadStart + payloadLength; if (client.buffer.length < frameLength) { break; } const frame = client.buffer.subarray(0, frameLength); client.buffer = client.buffer.subarray(frameLength); const fin = (frame[0] & 0x80) === 0x80; client.opCode = frame[0] & 0x0f; const mask = frame[1] & 0x80; const payload = frame.subarray(payloadStart); if (mask) { const maskingKey = frame.subarray(maskingKeyStart, payloadStart); for (let a = 0; a < payload.length; a++) { payload[a] ^= maskingKey[a % 4]; } } if (client.opCode === 0x01 || client.opCode === 0x02 || client.opCode === 0x00) { client.fragmentList.push(payload); } else if (client.opCode === 0x0a) { client.lastPong = Date.now(); } if (fin) { const clientOpCode = client.opCode; const clientFragmentList = client.fragmentList; callback(clientOpCode, clientFragmentList); client.opCode = -1; client.fragmentList = []; } } }; handleReceiveData = (tag, data, clientId) => { for (let a = 0; a < this.handleReceiveDataList.length; a++) { const handleReceiveData = this.handleReceiveDataList[a]; if (handleReceiveData.tag === tag) { handleReceiveData.callback(data, clientId); break; } } }; handleReceiveDataDelete = (tag) => { for (let a = this.handleReceiveDataList.length - 1; a >= 0; a--) { if (this.handleReceiveDataList[a].tag === tag) { this.handleReceiveDataList.splice(a, 1); } } }; dataDirect = () => { this.receiveData("direct", (data) => { this.sendMessage("text", data, "direct", data.toClientId); }); }; create = (server) => { server.on("upgrade", (request, socket) => { const upgrade = request.headers["upgrade"]; if (!upgrade || upgrade.toLowerCase() !== "websocket") { socket.end("HTTP/1.1 400 Bad Request"); return; } socket.write(this.responseHeader(request).join("\r\n")); const clientId = this.clientConnection(socket); let messageTagUpload = ""; socket.on("data", (data) => { this.handleFrame(clientId, data, (clientOpCode, clientFragmentList) => { if (clientOpCode === 1) { const clientFragment = Buffer.concat(clientFragmentList).toString(); if (!clientFragment.trim()) { return; } if (!helperSrc.jsonCheck(clientFragment)) { return; } const clientFragmentObject = JSON.parse(clientFragment); if (!clientFragmentObject || typeof clientFragmentObject.tag !== "string") { return; } if (clientFragmentObject.tag === "cws_upload") { messageTagUpload = clientFragmentObject.tag; } else if (clientFragmentObject.tag === "cws_broadcast") { this.sendDataBroadcast(clientFragmentObject.data, clientId); } else if (clientFragmentObject.tag === "cws_disconnect") { this.clientDisconnection(clientId); } this.handleReceiveData(clientFragmentObject.tag, clientFragmentObject.data, clientId); } else if ((clientOpCode === 0 || clientOpCode === 2) && messageTagUpload) { this.handleReceiveData(messageTagUpload, clientFragmentList, clientId); messageTagUpload = ""; } else if (clientOpCode === 8) { socket.write(Buffer.from([0x88, 0x00])); this.clientDisconnection(clientId); return; } }); }); socket.on("error", (error) => { helperSrc.writeLog("@cimo/webSocket - Server - Manager.ts - create() - onerror()", `Client ${clientId} error: ${error.message}`); }); socket.on("close", (isError) => { helperSrc.writeLog("@cimo/websocket - Server - Manager.ts - create() - onclose()", `Disconnection request from Ip: ${socket.remoteAddress || ""} - Client ${clientId} - (Had error: ${isError}).`); this.sendDataBroadcast({ label: "disconnection", result: `Client ${clientId} disconnected.` }, clientId); this.clientDelete(clientId); }); }); }; constructor(server, secretKeyValue, intervalTimePingValue = 5000) { this.secretKey = secretKeyValue; this.intervalTimePing = intervalTimePingValue; this.clientList = []; this.handleReceiveDataList = []; this.create(server); this.dataDirect(); } clientIdList = () => { const resultList = []; for (let a = 0; a < this.clientList.length; a++) { const client = this.clientList[a]; resultList.push(client.id); } return resultList; }; sendMessage = (mode, data, tag = "", clientId, timeout = 0) => { const client = this.clientCheck(clientId); if (!client) { helperSrc.writeLog("@cimo/webSocket - Server - Manager.ts - sendMessage()", `Client ${clientId} doesn't exists!`); return; } if (client && client.socket && client.socket.writable) { let buffer = Buffer.alloc(0); let frame0 = 0; if (!Buffer.isBuffer(data) && !(typeof data === "string") && !(typeof data === "object" && data !== null)) { helperSrc.writeLog("@cimo/websocket - Server - Manager.ts - sendMessage()", "Invalid data type!"); } if (mode === "text" && !Buffer.isBuffer(data)) { let resultData = ""; if (typeof data === "string") { resultData = data; } else if (typeof data === "object") { resultData = JSON.stringify(data); } const messageObject = { tag: `cws_${tag}`, data: Buffer.from(resultData).toString("base64") }; buffer = Buffer.from(JSON.stringify(messageObject)); frame0 = 0x81; } else if (mode === "binary" && Buffer.isBuffer(data)) { buffer = Buffer.from(data); frame0 = 0x82; } else { helperSrc.writeLog("@cimo/websocket - Server - Manager.ts - sendMessage()", "Message type doesn't match mode!"); } const length = buffer.length; let frame; if (length <= 125) { frame = Buffer.alloc(length + 2); frame[0] = frame0; frame[1] = length; } else if (length <= 65535) { frame = Buffer.alloc(length + 4); frame[0] = frame0; frame[1] = 126; frame.writeUInt16BE(length, 2); } else { frame = Buffer.alloc(length + 10); frame[0] = frame0; frame[1] = 127; frame.writeBigUInt64BE(BigInt(length), 2); } buffer.copy(frame, frame.length - length); if (mode === "text" && timeout > 0) { const timeoutEvent = setTimeout(() => { clearTimeout(timeoutEvent); client.socket.write(frame); }, timeout); } else { client.socket.write(frame); } } }; sendDataBroadcast = (data, excludeClientId) => { for (let a = 0; a < this.clientList.length; a++) { const client = this.clientList[a]; if (client.id !== excludeClientId) { this.sendMessage("text", data, "broadcast", client.id); } } }; sendDataDownload = (mimeType, fileName, file, clientId) => { this.sendMessage("text", { mimeType, fileName }, "download", clientId); this.sendMessage("binary", file, "", clientId, 100); }; receiveData = (tag, callbackValue) => { const cwsTag = `cws_${tag}`; this.handleReceiveDataDelete(cwsTag); this.handleReceiveDataList.push({ tag: cwsTag, callback: (data, clientId) => { let resultData; if (typeof data === "string") { const decoded = Buffer.from(data, "base64").toString(); resultData = !helperSrc.jsonCheck(decoded) ? decoded : JSON.parse(decoded); } else { resultData = data; } callbackValue(resultData, clientId); } }); }; receiveDataUpload = (callback) => { let mimeType = ""; let fileName = ""; this.receiveData("upload", (data, clientId) => { if (Array.isArray(data)) { callback(data, mimeType, fileName, clientId); mimeType = ""; fileName = ""; } else { mimeType = data.mimeType; fileName = data.fileName; } }); }; } //# sourceMappingURL=Manager.js.map