mihawk
Version:
A tiny & simple mock server tool, support json,js,cjs,ts(typescript).
203 lines (202 loc) • 9.14 kB
JavaScript
;
import https from 'https';
import http from 'http';
import { promisify } from 'util';
import Colors from 'color-cc';
import * as WS from 'ws';
import { PKG_NAME } from '../consts';
import { parseStompMsg } from '../utils/parser';
import { Debugger, Printer } from '../utils/print';
import { getTimeNowStr } from '../utils/date';
import { getAddrInfoByServer } from '../utils/server';
import { getMyIp, supportLocalHost } from '../utils/net';
const LOGFLAG_WS = `${Colors.cyan('[WS]')}${Colors.gray(':')}`;
export default class WsCtrl {
_useStompMsg;
_host;
_port;
_secure;
_baseServer;
_wsSvr;
_clientIndex = 0;
_resolveFunc;
constructor(options) {
const { host, port, secure = false, server, stomp = false, resolve = _defaultResolveFunc, } = options;
this._useStompMsg = !!stomp;
this._host = host;
this._port = port;
this._secure = secure;
this._wsSvr = null;
this._baseServer = server;
const _resolve = typeof resolve === 'function' ? resolve : _defaultResolveFunc;
this._resolveFunc = function (socket, request, options) {
try {
const funcCtx = socket;
_resolve.call(funcCtx, socket, request, options);
}
catch (error) {
Printer.error(LOGFLAG_WS, 'Failed to exec socket logic resolveFunc:\n', error);
}
};
}
_autoCreateClientId() {
return `Client-${++this._clientIndex}__${getTimeNowStr()}`;
}
async start(server, isRestart) {
if (this._wsSvr) {
Printer.log(LOGFLAG_WS, 'WS server is already running.');
return;
}
const resolveFunc = this._resolveFunc;
const stomp = this._useStompMsg;
if (server && _isHttpOrHttpsServer(server)) {
this._baseServer = server;
}
const { isSecure = this._secure, host = this._host, port = this._port } = _getBaseServerInfo(this._baseServer);
const protocol = isSecure ? 'wss' : 'ws';
const hostFix = host === '0.0.0.0' ? '127.0.0.1' : host;
this._wsSvr = new WS.WebSocketServer({ server: this._baseServer });
this._wsSvr.on('listening', function listening() {
Printer.log(`🚀 ${Colors.green(`${isRestart ? 'Restart' : 'Start'} mock-socket-server success!`)}`);
!isRestart && Printer.log(`Mock Socket Server address:`);
Printer.log(`${Colors.gray('-')} ${Colors.cyan(`${protocol}://${hostFix}:${port}`)}`);
if (supportLocalHost(host)) {
const addr2 = `${protocol}://${getMyIp()}:${port}`;
Printer.log(`${Colors.gray('-')} ${Colors.cyan(addr2)}`);
}
console.log();
});
this._wsSvr.on('headers', function headers(headers, req) {
Debugger.log(LOGFLAG_WS, 'Headers:', headers);
headers.push(`X-Powered-By: ${PKG_NAME}`);
});
this._wsSvr.on('connection', (socket, request) => {
const { remoteAddress, remotePort } = request?.socket || {};
const clientId = remoteAddress ? `${remoteAddress}:${remotePort || ++this._clientIndex}` : this._autoCreateClientId();
Printer.log(LOGFLAG_WS, 'Socket client connected!', Colors.gray(`clientId=[${clientId}]`), Colors.gray(`time=${getTimeNowStr()}`));
resolveFunc(socket, request, { clientId, stomp });
setTimeout(() => {
const clientId4Log = Colors.gray(`clientId=[${clientId}]`);
const timeNow4Log = Colors.gray(`time=${getTimeNowStr()}`);
if (this._wsSvr) {
try {
socket?.emit('open', () => {
Printer.log(LOGFLAG_WS, "Socket client opened! (socket.emit('open') success!)", clientId4Log, timeNow4Log);
});
}
catch (error) {
Printer.error(LOGFLAG_WS, "Failed to exec socket.emit('open') ! ", clientId4Log, timeNow4Log, '\n', error);
}
}
else {
Printer.warn(LOGFLAG_WS, "Cancel to exec socket.emit('open'), because connection is closed.", clientId4Log, timeNow4Log);
}
}, 0);
});
this._wsSvr.on('error', function error(err) {
Printer.error(LOGFLAG_WS, 'WebSocket server error:', err);
});
this._wsSvr.on('close', function close() {
Debugger.log(LOGFLAG_WS, 'WebSocket server was closed!');
});
}
async _close(forceClose = false) {
if (!this._wsSvr) {
Printer.log(LOGFLAG_WS, 'WebSocket server is not running.');
return;
}
const clients = this._wsSvr?.clients;
try {
if (clients?.size) {
if (forceClose) {
clients?.forEach(client => client?.terminate());
}
else {
clients?.forEach(client => client?.close());
}
}
}
catch (error) {
Printer.error(LOGFLAG_WS, `Batch ${forceClose ? 'terminate' : 'close'} clients failed!\n`, error);
}
const closeAsync = promisify(this._wsSvr.close).bind(this._wsSvr);
try {
await closeAsync();
this._wsSvr = null;
Debugger.log(LOGFLAG_WS, `WebSocket server was already ${forceClose ? 'destoryed' : 'closed'}.`);
}
catch (error) {
Printer.error(LOGFLAG_WS, `${forceClose ? 'Destory' : 'Close'} WebSocket server failed!\n`, error);
}
}
async close() {
return await this._close();
}
async destory() {
return await this._close(true);
}
}
function _defaultResolveFunc(socket, request, options) {
const { clientId: cid, stomp } = options || {};
const clientId = cid || request.socket.remoteAddress;
const clientName = `[${clientId}]`;
const logTail = Colors.gray(`from ${clientName}`);
const logName = Colors.gray('socket:');
socket.send(JSON.stringify({ success: true, data: `WsServer: Connection established! Hello, client! ${clientName}` }));
socket.on('message', (message, isBinary) => {
const recived = typeof message === 'string' ? message : Buffer.isBuffer(message) ? message?.toString() : JSON.stringify(message);
let recivedData = recived;
if (stomp) {
recivedData = parseStompMsg(recived);
Printer.log(LOGFLAG_WS, logName, 'Received stomp message', logTail);
Printer.log(LOGFLAG_WS, logName, '<= stompData:', recivedData);
}
else {
Printer.log(LOGFLAG_WS, logName, `Received message <= "${Colors.green(recived)}"`, isBinary ? Colors.gray('binary') : '', logTail);
}
const recivedDataStr = typeof recivedData === 'string' ? recivedData : JSON.stringify(recivedData);
const msgData = {
success: true,
data: `WsServer: I've recived your message(${recivedDataStr})`,
};
Printer.log(LOGFLAG_WS, logName, `Send response to ${Colors.gray(clientName)} =>`, msgData);
socket.send(JSON.stringify(msgData));
console.log('\n');
});
socket.on('open', () => {
Printer.log(LOGFLAG_WS, logName, `Client ${Colors.gray(clientId)} open.`, logTail);
});
socket.on('upgrade', (request) => {
const clientAddr = request.socket.remoteAddress;
Printer.log(LOGFLAG_WS, logName, `Client ${clientAddr} upgraded.`, logTail);
});
socket.on('ping', (data) => {
Printer.log(LOGFLAG_WS, logName, `Received ping => ${data?.toString() || ''}`, logTail);
socket.pong(`Pong: ${data?.toString()}`);
});
socket.on('pong', (data) => {
Printer.log(LOGFLAG_WS, logName, `Received pong => ${data?.toString() || ''}`, logTail);
});
socket.on('error', (err) => {
const errMsg = err?.message || err?.toString() || 'unknow error';
Printer.error(LOGFLAG_WS, logName, `CLient error: ${Colors.red(errMsg)}`, logTail);
Printer.error(LOGFLAG_WS, err, '\n');
});
socket.on('unexpected-response', (request, response) => {
const exceptDetail = `${response.statusCode} ${response.statusMessage}`;
Printer.error(LOGFLAG_WS, logName, `CLient unexpected-response: ${Colors.red(exceptDetail)}`, logTail, '\n');
});
socket.on('close', (code, reason) => {
const closeDetail = `code=${code},reason=${reason.toString() || 'none'}`;
Printer.log(LOGFLAG_WS, logName, `Client close connection.(${Colors.yellow(closeDetail)})`, logTail, '\n');
});
}
function _getBaseServerInfo(server) {
const isSecure = server instanceof https.Server;
const protocol = isSecure ? 'https' : 'http';
const { port, address } = getAddrInfoByServer(server) || {};
return { isSecure, protocol, host: address, port };
}
function _isHttpOrHttpsServer(server) {
return server instanceof http.Server || server instanceof https.Server;
}