import type WsServerConnector from "./WsServerConnector"; import events from "events"; import * as lib from "@clusterio/lib"; const { logFilter, logger } = lib; import BaseConnection from "./BaseConnection"; import ControllerUser from "./ControllerUser"; import * as routes from "./routes"; import Controller from "./Controller"; const strcmp = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare; const queryLogTime = new lib.Summary( "clusterio_controller_query_log_duration_seconds", "Time in seconds log queries took to execute." ); /** * Represents the connection to a control link * * @extends module:controller/src/BaseConnection * @alias module:controller/src/ControlConnection */ export default class ControlConnection extends BaseConnection { private _version: string; logTransport: lib.LinkTransport | null = null; logSubscriptions = { all: false, controller: false, hostIds: [] as number[], instanceIds: [] as number[], }; ws_dumper: ((...args: any[]) => void) | null = null; declare connector: WsServerConnector; constructor( registerData: { version: string }, connector: WsServerConnector, controller: Controller, public user: ControllerUser, // The user making this connection. public id: number ) { super(connector, controller); this._version = registerData.version; this.connector.on("connect", () => { this.connector._socket!.clusterio_ignore_dump = Boolean(this.ws_dumper); }); this.connector.on("close", () => { if (this.logTransport) { logger.remove(this.logTransport); this.logTransport = null; } if (this.ws_dumper) { this._controller.debugEvents.off("message", this.ws_dumper); } }); for (let event of ["connect", "drop", "resume", "close"] as const) { this.connector.on(event, () => { for (let controllerPlugin of this._controller.plugins.values()) { controllerPlugin.onControlConnectionEvent(this, event); } }); } this.handle(lib.ControllerStopRequest, this.handleControllerStopRequest.bind(this)); this.handle(lib.ControllerRestartRequest, this.handleControllerRestartRequest.bind(this)); this.handle(lib.ControllerConfigGetRequest, this.handleControllerConfigGetRequest.bind(this)); this.handle(lib.ControllerConfigSetFieldRequest, this.handleControllerConfigSetFieldRequest.bind(this)); this.handle(lib.ControllerConfigSetPropRequest, this.handleControllerConfigSetPropRequest.bind(this)); this.handle(lib.HostRevokeTokensRequest, this.handleHostRevokeTokensRequest.bind(this)); this.handle(lib.HostListRequest, this.handleHostListRequest.bind(this)); this.handle(lib.HostGenerateTokenRequest, this.handleHostGenerateTokenRequest.bind(this)); this.handle(lib.HostConfigCreateRequest, this.handleHostConfigCreateRequest.bind(this)); this.handle(lib.InstanceDetailsGetRequest, this.handleInstanceDetailsGetRequest.bind(this)); this.handle(lib.InstanceDetailsListRequest, this.handleInstanceDetailsListRequest.bind(this)); this.handle(lib.InstanceCreateRequest, this.handleInstanceCreateRequest.bind(this)); this.handle(lib.InstanceDeleteRequest, this.handleInstanceDeleteRequest.bind(this)); this.handle(lib.InstanceConfigGetRequest, this.handleInstanceConfigGetRequest.bind(this)); this.handle(lib.InstanceConfigSetFieldRequest, this.handleInstanceConfigSetFieldRequest.bind(this)); this.handle(lib.InstanceConfigSetPropRequest, this.handleInstanceConfigSetPropRequest.bind(this)); this.handle(lib.InstanceAssignRequest, this.handleInstanceAssignRequest.bind(this)); this.handle(lib.InstanceSaveDetailsListRequest, this.handleInstanceSaveDetailsListRequest.bind(this)); this.handle(lib.InstanceRenameSaveRequest, controller.sendRequestToHostByInstanceId.bind(controller)); this.handle(lib.InstanceCopySaveRequest, controller.sendRequestToHostByInstanceId.bind(controller)); this.handle(lib.InstanceDeleteSaveRequest, controller.sendRequestToHostByInstanceId.bind(controller)); this.handle(lib.InstanceDownloadSaveRequest, this.handleInstanceDownloadSaveRequest.bind(this)); this.handle(lib.InstanceTransferSaveRequest, this.handleInstanceTransferSaveRequest.bind(this)); this.handle(lib.ModPackListRequest, this.handleModPackListRequest.bind(this)); this.handle(lib.ModPackCreateRequest, this.handleModPackCreateRequest.bind(this)); this.handle(lib.ModPackUpdateRequest, this.handleModPackUpdateRequest.bind(this)); this.handle(lib.ModPackDeleteRequest, this.handleModPackDeleteRequest.bind(this)); this.handle(lib.ModGetRequest, this.handleModGetRequest.bind(this)); this.handle(lib.ModListRequest, this.handleModListRequest.bind(this)); this.handle(lib.ModSearchRequest, this.handleModSearchRequest.bind(this)); this.handle(lib.ModDeleteRequest, this.handleModDeleteRequest.bind(this)); this.handle(lib.LogSetSubscriptionsRequest, this.handleLogSetSubscriptionsRequest.bind(this)); this.handle(lib.LogQueryRequest, this.handleLogQueryRequest.bind(this)); this.handle(lib.PermissionListRequest, this.handlePermissionListRequest.bind(this)); this.handle(lib.RoleListRequest, this.handleRoleListRequest.bind(this)); this.handle(lib.RoleCreateRequest, this.handleRoleCreateRequest.bind(this)); this.handle(lib.RoleUpdateRequest, this.handleRoleUpdateRequest.bind(this)); this.handle(lib.RoleGrantDefaultPermissionsRequest, this.handleRoleGrantDefaultPermissionsRequest.bind(this)); this.handle(lib.RoleDeleteRequest, this.handleRoleDeleteRequest.bind(this)); this.handle(lib.UserGetRequest, this.handleUserGetRequest.bind(this)); this.handle(lib.UserListRequest, this.handleUserListRequest.bind(this)); this.handle(lib.UserCreateRequest, this.handleUserCreateRequest.bind(this)); this.handle(lib.UserRevokeTokenRequest, this.handleUserRevokeTokenRequest.bind(this)); this.handle(lib.UserUpdateRolesRequest, this.handleUserUpdateRolesRequest.bind(this)); this.handle(lib.UserSetAdminRequest, this.handleUserSetAdminRequest.bind(this)); this.handle(lib.UserSetBannedRequest, this.handleUserSetBannedRequest.bind(this)); this.handle(lib.UserSetWhitelistedRequest, this.handleUserSetWhitelistedRequest.bind(this)); this.handle(lib.UserDeleteRequest, this.handleUserDeleteRequest.bind(this)); this.handle(lib.DebugDumpWsRequest, this.handleDebugDumpWsRequest.bind(this)); } validateIngress(message: lib.MessageRequest | lib.MessageEvent) { let origin = this.connector.dst; if (origin.type !== message.src.type || origin.id !== message.src.id) { throw new lib.InvalidMessage(`Received message with invalid src ${message.src} from ${origin}`); } } validatePermission( message: lib.MessageRequest | lib.MessageEvent, entry: lib.RequestEntry | lib.EventEntry ) { try { this.checkPermission(message, entry); } catch (err: any) { this.connector.sendResponseError(new lib.ResponseError(err.message, err.code), message.src); logger.audit(`Permission denied for ${message.name} by ${this.user.id} from ${this.connector.dst}`); throw err; } } checkPermission( message: lib.MessageRequest | lib.MessageEvent, entry: lib.RequestEntry | lib.EventEntry ) { let permission; if (message.type === "request") { permission = (entry as lib.RequestEntry).Request.permission; } else if (message.type === "event") { permission = (entry as lib.EventEntry).Event.permission; } else { return; } if (permission === null) { return; } if (typeof permission === "string") { this.user.checkPermission(permission); return; } if (typeof permission === "function") { permission(this.user, message); return; } throw new Error("Should be unreachable"); } async handleControllerStopRequest() { this._controller.stop(); } async handleControllerRestartRequest() { if (!this._controller.canRestart) { throw new lib.RequestError("Cannot restart, controller does not have a process monitor to restart it."); } this._controller.shouldRestart = true; this._controller.stop(); } async handleControllerConfigGetRequest() { return this._controller.config.toRemote("control"); } async handleControllerConfigSetFieldRequest(request: lib.ControllerConfigSetFieldRequest) { this._controller.config.set(request.field as keyof lib.ControllerConfigFields, request.value, "control"); } async handleControllerConfigSetPropRequest(request: lib.ControllerConfigSetPropRequest) { let { field, prop, value } = request; this._controller.config.setProp(field as keyof lib.ControllerConfigFields, prop, value, "control"); } async handleHostRevokeTokensRequest(request: lib.HostRevokeTokensRequest) { const host = this._controller.hosts.getMutable(request.hostId); if (!host) { throw new Error(`Unknown host id (${request.hostId})`); } host.tokenValidAfter = Math.floor(Date.now() / 1000); const hostConnection = this._controller.wsServer.hostConnections.get(request.hostId); if (hostConnection) { hostConnection.connector.terminate(); } this._controller.hosts.set(host); } async handleHostListRequest(): Promise { return [...this._controller.hosts.values()].map(host => host.toHostDetails()); } async handleHostGenerateTokenRequest(message: lib.HostGenerateTokenRequest) { let hostId = message.hostId; if (hostId === undefined) { hostId = Math.random() * 2**31 | 0; } return this._controller.generateHostToken(hostId); } async handleHostConfigCreateRequest(request: lib.HostConfigCreateRequest) { const hostConfig = new lib.HostConfig("host"); hostConfig.set("host.controller_url", this._controller.getControllerUrl()); if (request.id !== undefined) { hostConfig.set("host.id", request.id); } if (request.name !== undefined) { hostConfig.set("host.name", request.name); } if (request.generateToken) { this.user.checkPermission("core.host.generate_token"); hostConfig.set("host.controller_token", this._controller.generateHostToken(hostConfig.get("host.id"))); } return hostConfig.toJSON(); } async handleInstanceDetailsGetRequest(request: lib.InstanceDetailsGetRequest) { return this._controller.getRequestInstance(request.instanceId).toInstanceDetails(); } async handleInstanceDetailsListRequest() { return [...this._controller.instances.values()].map(instance => instance.toInstanceDetails()); } async handleInstanceCreateRequest(request: lib.InstanceCreateRequest) { const instanceConfig = new lib.InstanceConfig("controller"); instanceConfig.update(request.config, false, "control"); await this._controller.instanceCreate(instanceConfig); } async handleInstanceDeleteRequest(request: lib.InstanceDeleteRequest) { await this._controller.instanceDelete(request.instanceId); } async handleInstanceConfigGetRequest(request: lib.InstanceConfigGetRequest) { let instance = this._controller.getRequestInstance(request.instanceId); return instance.config.toRemote("control"); } async handleInstanceConfigSetFieldRequest(request: lib.InstanceConfigSetFieldRequest) { let instance = this._controller.getRequestInstance(request.instanceId); if (request.field === "instance.assigned_host") { throw new lib.RequestError("instance.assigned_host must be set through the assign-host interface"); } if (request.field === "instance.id") { // XXX is this worth implementing? It's race condition galore. throw new lib.RequestError("Setting instance.id is not supported"); } instance.config.set(request.field as keyof lib.InstanceConfigFields, request.value, "control"); await this._controller.instanceConfigUpdated(instance); } async handleInstanceConfigSetPropRequest(request: lib.InstanceConfigSetPropRequest) { let instance = this._controller.getRequestInstance(request.instanceId); let { field, prop, value } = request; instance.config.setProp(field as keyof lib.InstanceConfigFields, prop, value, "control"); await this._controller.instanceConfigUpdated(instance); } async handleInstanceAssignRequest(request: lib.InstanceAssignRequest) { await this._controller.instanceAssign(request.instanceId, request.hostId); } async handleInstanceSaveDetailsListRequest() { return [...this._controller.saves.values()]; } async handleInstanceDownloadSaveRequest(request: lib.InstanceDownloadSaveRequest) { let { instanceId, name } = request; let stream = await routes.createProxyStream(this._controller.app); stream.filename = name; let ready = new Promise((resolve, reject) => { stream.events.on("source", resolve); stream.events.on("timeout", () => reject( new lib.RequestError("Timed out establishing stream from host") )); }); ready.catch(() => {}); await this._controller.sendToHostByInstanceId(new lib.InstancePushSaveRequest( instanceId, stream.id, name, )); await ready; return stream.id; } async handleInstanceTransferSaveRequest(request: lib.InstanceTransferSaveRequest) { if (request.sourceInstanceId === request.targetInstanceId) { throw new lib.RequestError("Source and target instance may not be the same"); } let sourceInstance = this._controller.getRequestInstance(request.sourceInstanceId); let targetInstance = this._controller.getRequestInstance(request.targetInstanceId); let sourceHostId = sourceInstance.config.get("instance.assigned_host"); let targetHostId = targetInstance.config.get("instance.assigned_host"); if (sourceHostId === null) { throw new lib.RequestError("Source instance is not assigned a host"); } if (targetHostId === null) { throw new lib.RequestError("Target instance is not assigned a host"); } // Let host handle request if source and target is on the same host. if (sourceHostId === targetHostId) { return await this._controller.sendTo({ hostId: sourceHostId }, request); } // Check connectivity let sourceHostConnection = this._controller.wsServer.hostConnections.get(sourceHostId); if (!sourceHostConnection || sourceHostConnection.connector.closing) { throw new lib.RequestError("Source host is not connected to the controller"); } let targetHostConnection = this._controller.wsServer.hostConnections.get(targetHostId); if (!targetHostConnection || targetHostConnection.connector.closing) { throw new lib.RequestError("Target host is not connected to the controller"); } // Create stream to proxy from target to source let stream = await routes.createProxyStream(this._controller.app); stream.events.on("timeout", () => { if (stream.source) { stream.source.destroy(); } stream.events.emit("error", new lib.RequestError("Timed out establishing transfer stream")); }); // Ignore errors if not listening for them to avoid crash. stream.events.on("error", () => { /* ignore */ }); // Establish push from source host to stream, this is done first to // ensure the file size is known prior to the target host pull. await Promise.all([ this._controller.sendTo( { hostId: sourceHostId }, new lib.InstancePushSaveRequest(request.sourceInstanceId, stream.id, request.sourceName), ), events.once(stream.events, "source"), ]); // Establish pull from target host to stream and wait for completion. let storedName = await this._controller.sendTo( { hostId: targetHostId }, new lib.InstancePullSaveRequest(request.targetInstanceId, stream.id, request.targetName), ); // Delete source save if this is not a copy if (!request.copy) { await this._controller.sendTo( { hostId: sourceHostId }, new lib.InstanceDeleteSaveRequest(request.sourceInstanceId, request.sourceName), ); } return storedName; } async handleModPackListRequest() { return [...this._controller.modPacks.values()]; } async handleModPackCreateRequest(request: lib.ModPackCreateRequest) { let modPack = request.modPack; if (modPack.id === undefined) { throw new lib.RequestError("Mod pack need an ID to be created"); } if (this._controller.modPacks.has(modPack.id)) { throw new lib.RequestError(`Mod pack with ID ${modPack.id} already exist`); } this._controller.modPacks.set(modPack); } async handleModPackUpdateRequest(request: lib.ModPackUpdateRequest) { let modPack = request.modPack; if (modPack.id === undefined || !this._controller.modPacks.has(modPack.id)) { throw new lib.RequestError(`Mod pack with ID ${modPack.id} does not exist`); } this._controller.modPacks.set(modPack); } async handleModPackDeleteRequest(request: lib.ModPackDeleteRequest) { let { id } = request; let modPack = this._controller.modPacks.getMutable(id); if (!modPack) { throw new lib.RequestError(`Mod pack with ID ${id} does not exist`); } this._controller.modPacks.delete(modPack); } async handleModGetRequest(request: lib.ModGetRequest) { return this.getMod(request); } async handleModListRequest() { return [...this._controller.modStore.mods()]; } static termsMatchesMod(terms: lib.ParsedTerm[], mod: lib.ModInfo) { for (let term of terms) { if (term.type === "word") { if (!lib.wordMatches(term, mod.name, mod.version, mod.title, mod.author, mod.contact, mod.homepage, mod.description, mod.filename )) { return false; } } else if (term.type === "attribute") { if (!lib.wordMatches(term.value, mod[term.name as keyof lib.ModInfo] as string)) { return false; } } } return true; } async handleModSearchRequest(request: lib.ModSearchRequest) { let query = lib.parseSearchString(request.query, { name: "word", // version title: "word", author: "word", contact: "word", homepage: "word", description: "word", // factorioVersion // dependencies filename: "word", // size sha1: "word", }); let factorioVersion = request.factorioVersion; type ModVersions = { name: string, versions: lib.ModInfo[] }; let results: Map = new Map(); for (let mod of this._controller.modStore.mods()) { if ( mod.factorioVersion !== factorioVersion || !ControlConnection.termsMatchesMod(query.terms, mod) ) { continue; } let result = results.get(mod.name); if (!result) { result = { name: mod.name, versions: [], }; results.set(mod.name, result); } result.versions.push(mod); } for (let result of results.values()) { result.versions.sort((a, b) => b.integerVersion - a.integerVersion); result.versions.map(e => e.toJSON()); } let resultList = [...results.values()]; const sort = request.sort; if (sort) { const sorters = { name: (a:ModVersions, b:ModVersions) => strcmp(a.versions[0].name, b.versions[0].name), title: (a:ModVersions, b:ModVersions) => strcmp(a.versions[0].title, b.versions[0].title), author: (a:ModVersions, b:ModVersions) => strcmp(a.versions[0].author, b.versions[0].author), }; if (!Object.prototype.hasOwnProperty.call(sorters, sort)) { throw new lib.RequestError(`Invalid value for sort: ${sort}`); } resultList.sort(sorters[sort as keyof typeof sorters]); let order = request.sortOrder; if (order === "desc") { resultList.reverse(); } } const page = request.page; const pageSize = request.pageSize || 10; resultList = resultList.slice((page - 1) * pageSize, page * pageSize); return { queryIssues: query.issues, pageCount: Math.ceil(results.size / pageSize), resultCount: results.size, results: resultList, }; } async handleModDeleteRequest(request: lib.ModDeleteRequest) { await this._controller.modStore.deleteMod(request.name, request.version); } async handleLogSetSubscriptionsRequest(request: lib.LogSetSubscriptionsRequest) { this.logSubscriptions = { all: request.all || false, controller: request.controller || false, hostIds: request.hostIds || [], instanceIds: request.instanceIds || [], }; this.updateLogSubscriptions(); } updateLogSubscriptions() { let { all, controller, hostIds, instanceIds } = this.logSubscriptions; if (all || controller || hostIds.length || instanceIds.length) { if (!this.logTransport) { this.logTransport = new lib.LinkTransport({ link: this }); this._controller.clusterLogger.add(this.logTransport); } this.logTransport.filter = logFilter(this.logSubscriptions); } else if (this.logTransport) { this._controller.clusterLogger.remove(this.logTransport); this.logTransport = null; } } async handleLogQueryRequest(request: lib.LogQueryRequest) { let _request = { limit: request.limit, order: request.order, maxLevel: request.maxLevel, all: request.all, controller: request.controller, hostIds: request.hostIds, instanceIds: request.instanceIds, }; let observeDuration = queryLogTime.startTimer(); let { all, controller, hostIds, instanceIds } = request; let log; if (!all && controller && !hostIds.length && !instanceIds.length) { log = await this._controller.queryControllerLog(_request); } else { log = await this._controller.queryClusterLog(_request); } observeDuration(); return { log }; } async handlePermissionListRequest() { return [...lib.permissions.values()]; } async handleRoleListRequest() { return [...this._controller.userManager.roles.values()]; } async handleRoleCreateRequest(request: lib.RoleCreateRequest) { let lastId = Math.max.apply(null, [...this._controller.userManager.roles.keys()]); // Start at 5 to leave space for future default roles let id = Math.max(5, lastId+1); this._controller.userManager.roles.set(id, lib.Role.fromJSON({ id, ...request })); this._controller.userManager.dirty = true; return id; } async handleRoleUpdateRequest(request: lib.RoleUpdateRequest) { let { id, name, description, permissions } = request; let role = this._controller.userManager.roles.get(id); if (!role) { throw new lib.RequestError(`Role with ID ${id} does not exist`); } role.name = name; role.description = description; role.permissions = new Set(permissions); this._controller.rolePermissionsUpdated(role); } async handleRoleGrantDefaultPermissionsRequest(request: lib.RoleGrantDefaultPermissionsRequest) { let role = this._controller.userManager.roles.get(request.id); if (!role) { throw new lib.RequestError(`Role with ID ${request.id} does not exist`); } role.grantDefaultPermissions(); this._controller.rolePermissionsUpdated(role); } async handleRoleDeleteRequest(request: lib.RoleDeleteRequest) { let id = request.id; if (!this._controller.userManager.roles.delete(id)) { throw new lib.RequestError(`Role with ID ${id} does not exist`); } this._controller.userManager.dirty = true; for (let user of this._controller.userManager.users.values()) { if (user.roleIds.delete(id)) { this._controller.userPermissionsUpdated(user); } } } async handleUserGetRequest(request: lib.UserGetRequest): Promise { let name = request.name; let user = this._controller.userManager.getByName(name); if (!user) { throw new lib.RequestError(`User ${name} does not exist`); } return user; } async handleUserListRequest(): Promise { return [...this._controller.userManager.users.values()]; } async handleUserCreateRequest(request: lib.UserCreateRequest) { let user = this._controller.userManager.createUser(request.name); this._controller.usersUpdated([user]); } async handleUserRevokeTokenRequest(request: lib.UserRevokeTokenRequest) { let user = this._controller.userManager.getByName(request.name); if (!user) { throw new lib.RequestError(`User '${request.name}' does not exist`); } if (user.id !== this.user.id) { this.user.checkPermission("core.user.revoke_other_token"); } user.invalidateToken(); for (let controlConnection of this._controller.wsServer.controlConnections.values()) { if (controlConnection.user.id === user.id) { controlConnection.connector.terminate(); } } this._controller.usersUpdated([user]); } async handleUserUpdateRolesRequest(request: lib.UserUpdateRolesRequest) { let user = this._controller.userManager.getByName(request.name); if (!user) { throw new lib.RequestError(`User '${request.name}' does not exist`); } for (let roleId of request.roles) { if (!this._controller.userManager.roles.has(roleId)) { throw new lib.RequestError(`Role with ID ${roleId} does not exist`); } } user.roleIds = new Set(request.roles); this._controller.userPermissionsUpdated(user); this._controller.usersUpdated([user]); } async handleUserSetAdminRequest(request: lib.UserSetAdminRequest) { let { name, create, admin } = request; let user = this._controller.userManager.getByName(name); if (!user) { if (create) { this.user.checkPermission("core.user.create"); user = this._controller.userManager.createUser(name); } else { throw new lib.RequestError(`User '${name}' does not exist`); } } user.isAdmin = admin; this._controller.usersUpdated([user]); this._controller.sendTo("allInstances", new lib.InstanceAdminlistUpdateEvent(name, admin)); } async handleUserSetBannedRequest(request: lib.UserSetBannedRequest) { let { name, create, banned, reason } = request; let user = this._controller.userManager.getByName(name); if (!user) { if (create) { this.user.checkPermission("core.user.create"); user = this._controller.userManager.createUser(name); } else { throw new lib.RequestError(`User '${name}' does not exist`); } } user.isBanned = banned; user.banReason = reason; this._controller.usersUpdated([user]); this._controller.sendTo("allInstances", new lib.InstanceBanlistUpdateEvent(name, banned, reason)); } async handleUserSetWhitelistedRequest(request: lib.UserSetWhitelistedRequest) { let { name, create, whitelisted } = request; let user = this._controller.userManager.getByName(name); if (!user) { if (create) { this.user.checkPermission("core.user.create"); user = this._controller.userManager.createUser(name); } else { throw new lib.RequestError(`User '${name}' does not exist`); } } user.isWhitelisted = whitelisted; this._controller.usersUpdated([user]); this._controller.sendTo("allInstances", new lib.InstanceWhitelistUpdateEvent(name, whitelisted)); } async handleUserDeleteRequest(request: lib.UserDeleteRequest) { let name = request.name; let user = this._controller.userManager.getByName(name); if (!user) { throw new lib.RequestError(`User '${name}' does not exist`); } user.isDeleted = true; this._controller.userManager.users.delete(name); this._controller.usersUpdated([user]); if (user.isAdmin) { this._controller.sendTo("allInstances", new lib.InstanceAdminlistUpdateEvent(name, false)); } if (user.isWhitelisted) { this._controller.sendTo("allInstances", new lib.InstanceWhitelistUpdateEvent(name, false)); } if (user.isBanned) { this._controller.sendTo("allInstances", new lib.InstanceBanlistUpdateEvent(name, false, "")); } } async handleDebugDumpWsRequest(request: lib.DebugDumpWsRequest) { this.ws_dumper = data => { if (this.connector.connected) { this.send(new lib.DebugWsMessageEvent(data.direction, data.content)); } }; this.connector._socket!.clusterio_ignore_dump = true; this._controller.debugEvents.on("message", this.ws_dumper); } } module.exports = ControlConnection;