@cimo/websocket
Version:
Websocket (server/client). Light, fast and secure.
385 lines • 15.9 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const Crypto = __importStar(require("crypto"));
const helperSrc = __importStar(require("../HelperSrc"));
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.clientRemove(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);
client.socket.end();
};
clientCheck = (clientId) => {
for (let a = 0; a < this.clientList.length; a++) {
if (this.clientList[a].id === clientId) {
return this.clientList[a];
}
}
return null;
};
clientRemove = (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 key = request.headers["sec-websocket-key"];
if (!key || Array.isArray(key)) {
helperSrc.writeLog("@cimo/webSocket - Server - Manager.ts - responseHeader()", "Invalid Sec-WebSocket-Key header!");
}
const hash = Crypto.createHash("sha1").update(`${key}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 (const handleReceiveData of this.handleReceiveDataList) {
if (handleReceiveData.tag === tag) {
handleReceiveData.callback(data, clientId);
break;
}
}
};
handleReceiveDataRemove = (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) => {
if (request.headers["upgrade"] && request.headers["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 messageConcat = Buffer.concat(clientFragmentList).toString();
if (!messageConcat.trim()) {
return;
}
if (!helperSrc.isJson(messageConcat)) {
return;
}
const messageObject = JSON.parse(messageConcat);
if (!messageObject || typeof messageObject.tag !== "string") {
return;
}
if (messageObject.tag === "cws_upload") {
messageTagUpload = messageObject.tag;
}
else if (messageObject.tag === "cws_broadcast") {
this.sendDataBroadcast(messageObject.data, clientId);
}
else if (messageObject.tag === "cws_disconnect") {
this.clientDisconnection(clientId);
}
this.handleReceiveData(messageObject.tag, messageObject.data, clientId);
}
else if ((clientOpCode === 0 || clientOpCode === 2) && messageTagUpload) {
this.handleReceiveData(messageTagUpload, clientFragmentList, clientId);
messageTagUpload = "";
}
});
});
socket.on("error", (error) => {
helperSrc.writeLog("@cimo/webSocket - Server - Manager.ts - create() - onerror()", `Client ${clientId} error: ${error.message}`);
});
socket.on("end", () => {
helperSrc.writeLog("@cimo/websocket - Server - Manager.ts - create() - onend()", `Disconnection request from Ip: ${socket.remoteAddress || ""} - Client ${clientId}.`);
this.sendDataBroadcast({ label: "disconnection", result: `Client ${clientId} disconnected.` }, clientId);
this.clientRemove(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 (const client of this.clientList) {
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 (const client of this.clientList) {
if (client.id !== excludeClientId) {
this.sendMessage("text", data, "broadcast", client.id);
}
}
};
sendDataDownload = (fileName, file, clientId) => {
this.sendMessage("text", fileName, "download", clientId);
this.sendMessage("binary", file, "", clientId, 100);
};
receiveData = (tag, callbackValue) => {
const cwsTag = `cws_${tag}`;
this.handleReceiveDataRemove(cwsTag);
this.handleReceiveDataList.push({
tag: cwsTag,
callback: (data, clientId) => {
let resultData;
if (typeof data === "string") {
const decoded = Buffer.from(data, "base64").toString();
resultData = !helperSrc.isJson(decoded) ? decoded : JSON.parse(decoded);
}
else {
resultData = data;
}
callbackValue(resultData, clientId);
}
});
};
receiveDataUpload = (callback) => {
let fileName = "";
this.receiveData("upload", (data, clientId) => {
if (typeof data === "string") {
fileName = data;
}
else {
callback(data, fileName, clientId);
fileName = "";
}
});
};
}
exports.default = Manager;
//# sourceMappingURL=Manager.js.map