mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-14 15:24:52 +09:00
Rename Master to Controller
The role of the master server as the party controlling every part of the cluster is not well reflected in its name and has caused confusion among users on several occasions. Rename master to controller to better reflects its role.
This commit is contained in:
1 parent
6ae4c8ef46
commit
70ad295a89
119 files changed
+1394
-1376
No files matched your search
@@ -0,0 +1,959 @@
|
||||
"use strict";
|
||||
const events = require("events");
|
||||
const fs = require("fs-extra");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const path = require("path");
|
||||
|
||||
const libConfig = require("@clusterio/lib/config");
|
||||
const libData = require("@clusterio/lib/data");
|
||||
const libErrors = require("@clusterio/lib/errors");
|
||||
const libHelpers = require("@clusterio/lib/helpers");
|
||||
const libLink = require("@clusterio/lib/link");
|
||||
const { logFilter, logger } = require("@clusterio/lib/logging");
|
||||
const libLoggingUtils = require("@clusterio/lib/logging_utils");
|
||||
const libPlugin = require("@clusterio/lib/plugin");
|
||||
const libPrometheus = require("@clusterio/lib/prometheus");
|
||||
const libUsers = require("@clusterio/lib/users");
|
||||
|
||||
const BaseConnection = require("./BaseConnection");
|
||||
const routes = require("./routes");
|
||||
|
||||
const strcmp = new Intl.Collator(undefined, { numerice: "true", sensitivity: "base" }).compare;
|
||||
|
||||
const queryLogTime = new libPrometheus.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
|
||||
*/
|
||||
class ControlConnection extends BaseConnection {
|
||||
constructor(registerData, connector, controller, user) {
|
||||
super("control", connector, controller);
|
||||
|
||||
this._agent = registerData.agent;
|
||||
this._version = registerData.version;
|
||||
|
||||
/**
|
||||
* The user making this connection.
|
||||
* @type {module:lib/users.User}
|
||||
*/
|
||||
this.user = user;
|
||||
|
||||
this.slaveSubscriptions = {
|
||||
all: false,
|
||||
slave_ids: [],
|
||||
};
|
||||
|
||||
this.instanceSubscriptions = {
|
||||
all: false,
|
||||
instance_ids: [],
|
||||
};
|
||||
|
||||
this.saveListSubscriptions = {
|
||||
all: false,
|
||||
instance_ids: [],
|
||||
};
|
||||
|
||||
this.modPackSubscriptions = {
|
||||
all: false,
|
||||
mod_pack_ids: [],
|
||||
};
|
||||
|
||||
this.modSubscriptions = {
|
||||
all: false,
|
||||
mod_names: [],
|
||||
};
|
||||
|
||||
this.userSubscriptions = {
|
||||
all: false,
|
||||
names: [],
|
||||
};
|
||||
|
||||
this.logTransport = null;
|
||||
this.logSubscriptions = {
|
||||
all: false,
|
||||
controller: false,
|
||||
slave_ids: [],
|
||||
instance_ids: [],
|
||||
};
|
||||
|
||||
this.ws_dumper = null;
|
||||
this.connector.on("connect", () => {
|
||||
this.connector._socket.clusterio_ignore_dump = Boolean(this.ws_dumper);
|
||||
});
|
||||
this.connector.on("close", () => {
|
||||
if (this.logTransport) {
|
||||
this.logTransport = null;
|
||||
logger.remove(this.logTransport);
|
||||
}
|
||||
if (this.ws_dumper) {
|
||||
this._controller.debugEvents.off("message", this.ws_dumper);
|
||||
}
|
||||
});
|
||||
|
||||
for (let event of ["connect", "drop", "resume", "close"]) {
|
||||
this.connector.on(event, () => {
|
||||
for (let controllerPlugin of this._controller.plugins.values()) {
|
||||
controllerPlugin.onControlConnectionEvent(this, event);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getControllerConfigRequestHandler() {
|
||||
return { serialized_config: this._controller.config.serialize("control") };
|
||||
}
|
||||
|
||||
async setControllerConfigFieldRequestHandler(message) {
|
||||
this._controller.config.set(message.data.field, message.data.value, "control");
|
||||
}
|
||||
|
||||
async setControllerConfigPropRequestHandler(message) {
|
||||
let { field, prop, value } = message.data;
|
||||
this._controller.config.setProp(field, prop, value, "control");
|
||||
}
|
||||
|
||||
async listSlavesRequestHandler(message) {
|
||||
let list = [];
|
||||
for (let slave of this._controller.slaves.values()) {
|
||||
list.push({
|
||||
agent: slave.agent,
|
||||
version: slave.version,
|
||||
id: slave.id,
|
||||
name: slave.name,
|
||||
public_address: slave.public_address || null,
|
||||
connected: this._controller.wsServer.slaveConnections.has(slave.id),
|
||||
});
|
||||
}
|
||||
return { list };
|
||||
}
|
||||
|
||||
async setSlaveSubscriptionsRequestHandler(message) {
|
||||
this.slaveSubscriptions = message.data;
|
||||
}
|
||||
|
||||
slaveUpdated(slave, update) {
|
||||
if (
|
||||
this.slaveSubscriptions.all
|
||||
|| this.slaveSubscriptions.slave_ids.includes(slave.id)
|
||||
) {
|
||||
libLink.messages.slaveUpdate.send(this, update);
|
||||
}
|
||||
}
|
||||
|
||||
generateSlaveToken(slaveId) {
|
||||
return jwt.sign(
|
||||
{ aud: "slave", slave: slaveId },
|
||||
Buffer.from(this._controller.config.get("controller.auth_secret"), "base64")
|
||||
);
|
||||
}
|
||||
|
||||
async generateSlaveTokenRequestHandler(message) {
|
||||
let slaveId = message.data.slave_id;
|
||||
if (slaveId === null) {
|
||||
slaveId = Math.random() * 2**31 | 0;
|
||||
}
|
||||
return { token: this.generateSlaveToken(slaveId) };
|
||||
}
|
||||
|
||||
async createSlaveConfigRequestHandler(message) {
|
||||
let slaveConfig = new libConfig.SlaveConfig("control");
|
||||
await slaveConfig.init();
|
||||
|
||||
slaveConfig.set("slave.controller_url", this._controller.getControllerUrl());
|
||||
if (message.data.id !== null) {
|
||||
slaveConfig.set("slave.id", message.data.id);
|
||||
}
|
||||
if (message.data.name !== null) {
|
||||
slaveConfig.set("slave.name", message.data.name);
|
||||
}
|
||||
if (message.data.generate_token) {
|
||||
this.user.checkPermission("core.slave.generate_token");
|
||||
slaveConfig.set("slave.controller_token", this.generateSlaveToken(slaveConfig.get("slave.id")));
|
||||
}
|
||||
return { serialized_config: slaveConfig.serialize() };
|
||||
}
|
||||
|
||||
async getInstanceRequestHandler(message) {
|
||||
let id = message.data.id;
|
||||
let instance = this._controller.getRequestInstance(id);
|
||||
|
||||
return {
|
||||
id,
|
||||
name: instance.config.get("instance.name"),
|
||||
assigned_slave: instance.config.get("instance.assigned_slave"),
|
||||
game_port: instance.game_port || null,
|
||||
status: instance.status,
|
||||
};
|
||||
}
|
||||
|
||||
async listInstancesRequestHandler(message) {
|
||||
let list = [];
|
||||
for (let instance of this._controller.instances.values()) {
|
||||
list.push({
|
||||
id: instance.config.get("instance.id"),
|
||||
name: instance.config.get("instance.name"),
|
||||
assigned_slave: instance.config.get("instance.assigned_slave"),
|
||||
game_port: instance.game_port || null,
|
||||
status: instance.status,
|
||||
});
|
||||
}
|
||||
return { list };
|
||||
}
|
||||
|
||||
async setInstanceSubscriptionsRequestHandler(message) {
|
||||
this.instanceSubscriptions = message.data;
|
||||
}
|
||||
|
||||
instanceUpdated(instance) {
|
||||
if (
|
||||
this.instanceSubscriptions.all
|
||||
|| this.instanceSubscriptions.instance_ids.includes(instance.config.get("instance.id"))
|
||||
) {
|
||||
libLink.messages.instanceUpdate.send(this, {
|
||||
id: instance.config.get("instance.id"),
|
||||
name: instance.config.get("instance.name"),
|
||||
assigned_slave: instance.config.get("instance.assigned_slave"),
|
||||
game_port: instance.game_port || null,
|
||||
status: instance.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// XXX should probably add a hook for slave reuqests?
|
||||
async createInstanceRequestHandler(message) {
|
||||
let instanceConfig = new libConfig.InstanceConfig("controller");
|
||||
await instanceConfig.load(message.data.serialized_config);
|
||||
|
||||
let instanceId = instanceConfig.get("instance.id");
|
||||
if (this._controller.instances.has(instanceId)) {
|
||||
throw new libErrors.RequestError(`Instance with ID ${instanceId} already exists`);
|
||||
}
|
||||
|
||||
// Add common settings for the Factorio server
|
||||
let settings = {
|
||||
"name": `${this._controller.config.get("controller.name")} - ${instanceConfig.get("instance.name")}`,
|
||||
"description": `Clusterio instance for ${this._controller.config.get("controller.name")}`,
|
||||
"tags": ["clusterio"],
|
||||
"max_players": 0,
|
||||
"visibility": { "public": true, "lan": true },
|
||||
"username": "",
|
||||
"token": "",
|
||||
"game_password": "",
|
||||
"require_user_verification": true,
|
||||
"max_upload_in_kilobytes_per_second": 0,
|
||||
"max_upload_slots": 5,
|
||||
"ignore_player_limit_for_returning_players": false,
|
||||
"allow_commands": "admins-only",
|
||||
"autosave_interval": 10,
|
||||
"autosave_slots": 5,
|
||||
"afk_autokick_interval": 0,
|
||||
"auto_pause": false,
|
||||
"only_admins_can_pause_the_game": true,
|
||||
"autosave_only_on_server": true,
|
||||
|
||||
...instanceConfig.get("factorio.settings"),
|
||||
};
|
||||
instanceConfig.set("factorio.settings", settings);
|
||||
|
||||
let instance = { config: instanceConfig, status: "unassigned" };
|
||||
this._controller.instances.set(instanceId, instance);
|
||||
await libPlugin.invokeHook(this._controller.plugins, "onInstanceStatusChanged", instance, null);
|
||||
this._controller.addInstanceHooks(instance);
|
||||
}
|
||||
|
||||
async deleteInstanceRequestHandler(message, request) {
|
||||
let instance = this._controller.getRequestInstance(message.data.instance_id);
|
||||
if (instance.config.get("instance.assigned_slave") !== null) {
|
||||
await this.forwardRequestToInstance(message, request);
|
||||
}
|
||||
this._controller.instances.delete(message.data.instance_id);
|
||||
|
||||
let prev = instance.status;
|
||||
instance.status = "deleted";
|
||||
this._controller.instanceUpdated(instance);
|
||||
await libPlugin.invokeHook(this._controller.plugins, "onInstanceStatusChanged", instance, prev);
|
||||
}
|
||||
|
||||
async getInstanceConfigRequestHandler(message) {
|
||||
let instance = this._controller.getRequestInstance(message.data.instance_id);
|
||||
return {
|
||||
serialized_config: instance.config.serialize("control"),
|
||||
};
|
||||
}
|
||||
|
||||
async updateInstanceConfig(instance) {
|
||||
let slaveId = instance.config.get("instance.assigned_slave");
|
||||
if (slaveId !== null) {
|
||||
let connection = this._controller.wsServer.slaveConnections.get(slaveId);
|
||||
if (connection) {
|
||||
await libLink.messages.assignInstance.send(connection, {
|
||||
instance_id: instance.config.get("instance.id"),
|
||||
serialized_config: instance.config.serialize("slave"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async setInstanceConfigFieldRequestHandler(message) {
|
||||
let instance = this._controller.getRequestInstance(message.data.instance_id);
|
||||
if (message.data.field === "instance.assigned_slave") {
|
||||
throw new libErrors.RequestError("instance.assigned_slave must be set through the assign-slave interface");
|
||||
}
|
||||
|
||||
if (message.data.field === "instance.id") {
|
||||
// XXX is this worth implementing? It's race condition galore.
|
||||
throw new libErrors.RequestError("Setting instance.id is not supported");
|
||||
}
|
||||
|
||||
instance.config.set(message.data.field, message.data.value, "control");
|
||||
await this.updateInstanceConfig(instance);
|
||||
}
|
||||
|
||||
async setInstanceConfigPropRequestHandler(message) {
|
||||
let instance = this._controller.getRequestInstance(message.data.instance_id);
|
||||
let { field, prop, value } = message.data;
|
||||
instance.config.setProp(field, prop, value, "control");
|
||||
await this.updateInstanceConfig(instance);
|
||||
}
|
||||
|
||||
async assignInstanceCommandRequestHandler(message, request) {
|
||||
let { slave_id, instance_id } = message.data;
|
||||
let instance = this._controller.getRequestInstance(instance_id);
|
||||
|
||||
// Check if target slave is connected
|
||||
let newSlaveConnection;
|
||||
if (slave_id !== null) {
|
||||
newSlaveConnection = this._controller.wsServer.slaveConnections.get(slave_id);
|
||||
if (!newSlaveConnection) {
|
||||
// The case of the slave not getting the assign instance message
|
||||
// still have to be handled, so it's not a requirement that the
|
||||
// target slave be connected to the controller while doing the
|
||||
// assignment, but it is IMHO a better user experience if this
|
||||
// is the case.
|
||||
throw new libErrors.RequestError("Target slave is not connected to the controller");
|
||||
}
|
||||
}
|
||||
|
||||
// Unassign from currently assigned slave if it is connected.
|
||||
let currentAssignedSlave = instance.config.get("instance.assigned_slave");
|
||||
if (currentAssignedSlave !== null && slave_id !== currentAssignedSlave) {
|
||||
let oldSlaveConnection = this._controller.wsServer.slaveConnections.get(currentAssignedSlave);
|
||||
if (oldSlaveConnection && !oldSlaveConnection.connector.closing) {
|
||||
await libLink.messages.unassignInstance.send(oldSlaveConnection, { instance_id });
|
||||
}
|
||||
}
|
||||
|
||||
// Assign to target
|
||||
instance.config.set("instance.assigned_slave", slave_id);
|
||||
if (slave_id !== null) {
|
||||
await libLink.messages.assignInstance.send(newSlaveConnection, {
|
||||
instance_id,
|
||||
serialized_config: instance.config.serialize("slave"),
|
||||
});
|
||||
} else {
|
||||
instance.status = "unassigned";
|
||||
this._controller.instanceUpdated(instance);
|
||||
}
|
||||
}
|
||||
|
||||
async setSaveListSubscriptionsRequestHandler(message) {
|
||||
this.saveListSubscriptions = message.data;
|
||||
}
|
||||
|
||||
saveListUpdate(data) {
|
||||
if (
|
||||
this.saveListSubscriptions.all
|
||||
|| this.saveListSubscriptions.instance_ids.includes(data.instance_id)
|
||||
) {
|
||||
libLink.messages.saveListUpdate.send(this, data);
|
||||
}
|
||||
}
|
||||
|
||||
async downloadSaveRequestHandler(message) {
|
||||
let { instance_id, save } = message.data;
|
||||
let stream = await routes.createProxyStream(this._controller.app);
|
||||
stream.filename = save;
|
||||
|
||||
let ready = new Promise((resolve, reject) => {
|
||||
stream.events.on("source", resolve);
|
||||
stream.events.on("timeout", () => reject(
|
||||
new libErrors.RequestError("Timed out establishing stream from slave")
|
||||
));
|
||||
});
|
||||
ready.catch(() => {});
|
||||
|
||||
await this._controller.forwardRequestToInstance(libLink.messages.pushSave, {
|
||||
instance_id,
|
||||
stream_id: stream.id,
|
||||
save,
|
||||
});
|
||||
|
||||
await ready;
|
||||
return { stream_id: stream.id };
|
||||
}
|
||||
|
||||
async transferSaveRequestHandler(message, request) {
|
||||
let { source_save, target_save, copy, instance_id, target_instance_id } = message.data;
|
||||
if (copy) {
|
||||
this.user.checkPermission("core.instance.save.copy");
|
||||
} else if (source_save !== target_save) {
|
||||
this.user.checkPermission("core.instance.save.rename");
|
||||
}
|
||||
|
||||
if (instance_id === target_instance_id) {
|
||||
throw new libErrors.RequestError("Source and target instance may not be the same");
|
||||
}
|
||||
let sourceInstance = this._controller.getRequestInstance(instance_id);
|
||||
let targetInstance = this._controller.getRequestInstance(target_instance_id);
|
||||
let sourceSlaveId = sourceInstance.config.get("instance.assigned_slave");
|
||||
let targetSlaveId = targetInstance.config.get("instance.assigned_slave");
|
||||
if (sourceSlaveId === null) {
|
||||
throw new libErrors.RequestError("Source instance is not assigned a slave");
|
||||
}
|
||||
if (targetSlaveId === null) {
|
||||
throw new libErrors.RequestError("Target instance is not assigned a slave");
|
||||
}
|
||||
|
||||
// Let slave handle request if source and target is on the same slave.
|
||||
if (sourceSlaveId === targetSlaveId) {
|
||||
return await this.forwardRequestToInstance(message, request);
|
||||
}
|
||||
|
||||
// Check connectivity
|
||||
let sourceSlaveConnection = this._controller.wsServer.slaveConnections.get(sourceSlaveId);
|
||||
if (!sourceSlaveConnection || sourceSlaveConnection.closing) {
|
||||
throw new libErrors.RequestError("Source slave is not connected to the controller");
|
||||
}
|
||||
|
||||
let targetSlaveConnection = this._controller.wsServer.slaveConnections.get(targetSlaveId);
|
||||
if (!targetSlaveConnection || targetSlaveConnection.closing) {
|
||||
throw new libErrors.RequestError("Target slave 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 libErrors.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 slave to stream, this is done first to
|
||||
// ensure the file size is known prior to the target slave pull.
|
||||
await Promise.all([
|
||||
this._controller.forwardRequestToInstance(libLink.messages.pushSave, {
|
||||
instance_id,
|
||||
stream_id: stream.id,
|
||||
save: source_save,
|
||||
}),
|
||||
events.once(stream.events, "source"),
|
||||
]);
|
||||
|
||||
// Establish pull from target slave to stream and wait for completion.
|
||||
let result = await this._controller.forwardRequestToInstance(libLink.messages.pullSave, {
|
||||
instance_id: target_instance_id,
|
||||
stream_id: stream.id,
|
||||
filename: target_save,
|
||||
});
|
||||
|
||||
// Delete source save if this is not a copy
|
||||
if (!copy) {
|
||||
await this._controller.forwardRequestToInstance(libLink.messages.deleteSave, {
|
||||
instance_id,
|
||||
save: source_save,
|
||||
});
|
||||
}
|
||||
|
||||
return { save: result.save };
|
||||
}
|
||||
|
||||
async listModPacksRequestHandler(message) {
|
||||
return { list: [...this._controller.modPacks.values()].map(pack => pack.toJSON()) };
|
||||
}
|
||||
|
||||
async setModPackSubscriptionsRequestHandler(message) {
|
||||
this.modPackSubscriptions = message.data;
|
||||
}
|
||||
|
||||
async createModPackRequestHandler(message) {
|
||||
let modPack = new libData.ModPack(message.data.mod_pack);
|
||||
if (this._controller.modPacks.has(modPack.id)) {
|
||||
throw new libErrors.RequestError(`Mod pack with ID ${modPack.id} already exist`);
|
||||
}
|
||||
this._controller.modPacks.set(modPack.id, modPack);
|
||||
this._controller.modPackUpdated(modPack);
|
||||
}
|
||||
|
||||
async updateModPackRequestHandler(message) {
|
||||
let modPack = new libData.ModPack(message.data.mod_pack);
|
||||
if (!this._controller.modPacks.has(modPack.id)) {
|
||||
throw new libErrors.RequestError(`Mod pack with ID ${modPack.id} does not exist`);
|
||||
}
|
||||
this._controller.modPacks.set(modPack.id, modPack);
|
||||
this._controller.modPackUpdated(modPack);
|
||||
}
|
||||
|
||||
async deleteModPackRequestHandler(message) {
|
||||
let { id } = message.data;
|
||||
let modPack = this._controller.modPacks.get(id);
|
||||
if (!modPack) {
|
||||
throw new libErrors.RequestError(`Mod pack with ID ${id} does not exist`);
|
||||
}
|
||||
modPack.isDeleted = true;
|
||||
this._controller.modPacks.delete(id);
|
||||
this._controller.modPackUpdated(modPack);
|
||||
}
|
||||
|
||||
modPackUpdated(modPack) {
|
||||
if (
|
||||
this.modPackSubscriptions.all
|
||||
|| this.modPackSubscriptions.mod_pack_ids.includes(modPack.id)
|
||||
) {
|
||||
libLink.messages.modPackUpdate.send(this, { mod_pack: modPack.toJSON() });
|
||||
}
|
||||
}
|
||||
|
||||
async getModRequestHandler(message) {
|
||||
let { name, version } = message.data;
|
||||
let filename = `${name}_${version}.zip`;
|
||||
let mod = this._controller.mods.get(filename);
|
||||
if (!mod) {
|
||||
throw new libErrors.RequestError(`Mod ${filename} does not exist`);
|
||||
}
|
||||
return {
|
||||
mod: mod.toJSON(),
|
||||
};
|
||||
}
|
||||
|
||||
async listModsRequestHandler(message) {
|
||||
return { list: [...this._controller.mods.values()].map(mod => mod.toJSON()) };
|
||||
}
|
||||
|
||||
static termsMatchesMod(terms, mod) {
|
||||
for (let term of terms) {
|
||||
if (term.type === "word") {
|
||||
if (!libHelpers.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 (!libHelpers.wordMatches(term.value, mod[term.name])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async searchModsRequestHandler(message) {
|
||||
let query = libHelpers.parseSearchString(message.data.query, {
|
||||
name: "word",
|
||||
// version
|
||||
title: "word",
|
||||
author: "word",
|
||||
contact: "word",
|
||||
homepage: "word",
|
||||
description: "word",
|
||||
// factorioVersion
|
||||
// dependencies
|
||||
filename: "word",
|
||||
// size
|
||||
sha1: "word",
|
||||
});
|
||||
let factorioVersion = message.data.factorio_version;
|
||||
|
||||
let results = new Map();
|
||||
for (let mod of this._controller.mods.values()) {
|
||||
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 = message.data.sort;
|
||||
if (sort) {
|
||||
const sorters = {
|
||||
name: (a, b) => strcmp(a.versions[0].name, b.versions[0].name),
|
||||
title: (a, b) => strcmp(a.versions[0].title, b.versions[0].title),
|
||||
author: (a, b) => strcmp(a.versions[0].author, b.versions[0].author),
|
||||
};
|
||||
if (!Object.prototype.hasOwnProperty.call(sorters, sort)) {
|
||||
throw new libErrors.RequestError(`Invalid value for sort: ${sort}`);
|
||||
}
|
||||
resultList.sort(sorters[sort]);
|
||||
let order = message.data.sort_order;
|
||||
if (order === "desc") {
|
||||
resultList.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
const page = message.data.page;
|
||||
const pageSize = message.data.page_size || 10;
|
||||
resultList = resultList.slice((page - 1) * pageSize, page * pageSize);
|
||||
|
||||
return {
|
||||
query_issues: query.issues,
|
||||
page_count: Math.ceil(results.size / pageSize),
|
||||
result_count: results.size,
|
||||
results: resultList,
|
||||
};
|
||||
}
|
||||
|
||||
async setModSubscriptionsRequestHandler(message) {
|
||||
this.modSubscriptions = message.data;
|
||||
}
|
||||
|
||||
async downloadModRequestHandler(message) {
|
||||
let { name, version } = message.data;
|
||||
let filename = `${name}_${version}.zip`;
|
||||
let mod = this._controller.mods.get(filename);
|
||||
if (!mod) {
|
||||
throw new libErrors.RequestError(`Mod ${filename} does not exist`);
|
||||
}
|
||||
let modPath = path.join(this._controller.config.get("controller.mods_directory"), mod.filename);
|
||||
|
||||
let stream = await routes.createProxyStream(this._controller.app);
|
||||
stream.filename = mod.filename;
|
||||
stream.source = fs.createReadStream(modPath);
|
||||
stream.mime = "application/zip";
|
||||
stream.size = mod.size;
|
||||
|
||||
return { stream_id: stream.id };
|
||||
}
|
||||
|
||||
async deleteModRequestHandler(message) {
|
||||
await this._controller.deleteMod(message.data.name, message.data.version);
|
||||
}
|
||||
|
||||
async setLogSubscriptionsRequestHandler(message) {
|
||||
this.logSubscriptions = message.data;
|
||||
this.updateLogSubscriptions();
|
||||
}
|
||||
|
||||
modUpdated(mod) {
|
||||
if (
|
||||
this.modSubscriptions.all
|
||||
|| this.modSubscriptions.mod_names.includes(mod.name)
|
||||
) {
|
||||
libLink.messages.modUpdate.send(this, { mod: mod.toJSON() });
|
||||
}
|
||||
}
|
||||
|
||||
updateLogSubscriptions() {
|
||||
let { all, controller, slave_ids, instance_ids } = this.logSubscriptions;
|
||||
if (all || controller || slave_ids.length || instance_ids.length) {
|
||||
if (!this.logTransport) {
|
||||
this.logTransport = new libLoggingUtils.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 queryLogRequestHandler(message) {
|
||||
let observeDuration = queryLogTime.startTimer();
|
||||
let { all, controller, slave_ids, instance_ids } = message.data;
|
||||
|
||||
let log;
|
||||
if (!all && controller && !slave_ids.length && !instance_ids.length) {
|
||||
log = await this._controller.queryControllerLog(message.data);
|
||||
} else {
|
||||
log = await this._controller.queryClusterLog(message.data);
|
||||
}
|
||||
|
||||
observeDuration();
|
||||
return { log };
|
||||
}
|
||||
|
||||
async listPermissionsRequestHandler(message) {
|
||||
let list = [];
|
||||
for (let permission of libUsers.permissions.values()) {
|
||||
list.push({
|
||||
name: permission.name,
|
||||
title: permission.title,
|
||||
description: permission.description,
|
||||
});
|
||||
}
|
||||
return { list };
|
||||
}
|
||||
|
||||
async listRolesRequestHandler(message) {
|
||||
let list = [];
|
||||
for (let role of this._controller.userManager.roles.values()) {
|
||||
list.push({
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
description: role.description,
|
||||
permissions: [...role.permissions],
|
||||
});
|
||||
}
|
||||
return { list };
|
||||
}
|
||||
|
||||
async createRoleRequestHandler(message) {
|
||||
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, new libUsers.Role({ id, ...message.data }));
|
||||
return { id };
|
||||
}
|
||||
|
||||
async updateRoleRequestHandler(message) {
|
||||
let { id, name, description, permissions } = message.data;
|
||||
let role = this._controller.userManager.roles.get(id);
|
||||
if (!role) {
|
||||
throw new libErrors.RequestError(`Role with ID ${id} does not exist`);
|
||||
}
|
||||
|
||||
role.name = name;
|
||||
role.description = description;
|
||||
role.permissions = new Set(permissions);
|
||||
this._controller.rolePermissionsUpdated(role);
|
||||
}
|
||||
|
||||
async grantDefaultRolePermissionsRequestHandler(message) {
|
||||
let role = this._controller.userManager.roles.get(message.data.id);
|
||||
if (!role) {
|
||||
throw new libErrors.RequestError(`Role with ID ${message.data.id} does not exist`);
|
||||
}
|
||||
|
||||
role.grantDefaultPermissions();
|
||||
this._controller.rolePermissionsUpdated(role);
|
||||
}
|
||||
|
||||
async deleteRoleRequestHandler(message) {
|
||||
let id = message.data.id;
|
||||
let role = this._controller.userManager.roles.get(id);
|
||||
if (!role) {
|
||||
throw new libErrors.RequestError(`Role with ID ${id} does not exist`);
|
||||
}
|
||||
|
||||
this._controller.userManager.roles.delete(id);
|
||||
for (let user of this._controller.userManager.users.values()) {
|
||||
user.roles.delete(role);
|
||||
this._controller.userPermissionsUpdated(user);
|
||||
}
|
||||
}
|
||||
|
||||
async getUserRequestHandler(message) {
|
||||
let name = message.data.name;
|
||||
let user = this._controller.userManager.users.get(name);
|
||||
if (!user) {
|
||||
throw new libErrors.RequestError(`User ${name} does not exist`);
|
||||
}
|
||||
|
||||
return {
|
||||
name: user.name,
|
||||
roles: [...user.roles].map(role => role.id),
|
||||
is_admin: user.isAdmin,
|
||||
is_banned: user.isBanned,
|
||||
is_whitelisted: user.isWhitelisted,
|
||||
ban_reason: user.banReason,
|
||||
instances: [...user.instances],
|
||||
player_stats: user.playerStats,
|
||||
instance_stats: [...user.instanceStats],
|
||||
};
|
||||
}
|
||||
|
||||
async listUsersRequestHandler(message) {
|
||||
let list = [];
|
||||
for (let user of this._controller.userManager.users.values()) {
|
||||
list.push({
|
||||
name: user.name,
|
||||
roles: [...user.roles].map(role => role.id),
|
||||
is_admin: user.isAdmin,
|
||||
is_banned: user.isBanned,
|
||||
is_whitelisted: user.isWhitelisted,
|
||||
instances: [...user.instances],
|
||||
player_stats: user.playerStats,
|
||||
});
|
||||
}
|
||||
return { list };
|
||||
}
|
||||
|
||||
async setUserSubscriptionsRequestHandler(message) {
|
||||
this.userSubscriptions = message.data;
|
||||
}
|
||||
|
||||
userUpdated(user) {
|
||||
if (
|
||||
this.userSubscriptions.all
|
||||
|| this.userSubscriptions.names.includes(user.name)
|
||||
) {
|
||||
libLink.messages.userUpdate.send(this, {
|
||||
name: user.name,
|
||||
roles: [...user.roles].map(role => role.id),
|
||||
is_admin: user.isAdmin,
|
||||
is_banned: user.isBanned,
|
||||
is_whitelisted: user.isWhitelisted,
|
||||
ban_reason: user.banReason,
|
||||
instances: [...user.instances],
|
||||
player_stats: user.playerStats,
|
||||
instance_stats: [...user.instanceStats],
|
||||
is_deleted: user.isDeleted,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async createUserRequestHandler(message) {
|
||||
let user = this._controller.userManager.createUser(message.data.name);
|
||||
this._controller.userUpdated(user);
|
||||
}
|
||||
|
||||
async revokeUserTokenRequestHandler(message) {
|
||||
let user = this._controller.userManager.users.get(message.data.name);
|
||||
if (!user) {
|
||||
throw new libErrors.RequestError(`User '${message.data.name}' does not exist`);
|
||||
}
|
||||
if (user.name !== this.user.name) {
|
||||
this.user.checkPermission("core.user.revoke_other_token");
|
||||
}
|
||||
|
||||
user.invalidateToken();
|
||||
for (let controlConnection of this._controller.wsServer.controlConnections) {
|
||||
if (controlConnection.user.name === user.name) {
|
||||
controlConnection.connector.terminate();
|
||||
}
|
||||
}
|
||||
this._controller.userUpdated(user);
|
||||
}
|
||||
|
||||
async updateUserRolesRequestHandler(message) {
|
||||
let user = this._controller.userManager.users.get(message.data.name);
|
||||
if (!user) {
|
||||
throw new libErrors.RequestError(`User '${message.data.name}' does not exist`);
|
||||
}
|
||||
|
||||
let resolvedRoles = new Set();
|
||||
for (let roleId of message.data.roles) {
|
||||
let role = this._controller.userManager.roles.get(roleId);
|
||||
if (!role) {
|
||||
throw new libErrors.RequestError(`Role with ID ${roleId} does not exist`);
|
||||
}
|
||||
|
||||
resolvedRoles.add(role);
|
||||
}
|
||||
|
||||
user.roles = resolvedRoles;
|
||||
this._controller.userPermissionsUpdated(user);
|
||||
this._controller.userUpdated(user);
|
||||
}
|
||||
|
||||
async setUserAdminRequestHandler(message) {
|
||||
let { name, create, admin } = message.data;
|
||||
let user = this._controller.userManager.users.get(name);
|
||||
if (!user) {
|
||||
if (create) {
|
||||
this.user.checkPermission("core.user.create");
|
||||
user = this._controller.userManager.createUser(name);
|
||||
} else {
|
||||
throw new libErrors.RequestError(`User '${name}' does not exist`);
|
||||
}
|
||||
}
|
||||
|
||||
user.isAdmin = admin;
|
||||
this._controller.userUpdated(user);
|
||||
this.broadcastEventToSlaves({ data: { name, admin }}, libLink.messages.adminlistUpdate);
|
||||
}
|
||||
|
||||
async setUserBannedRequestHandler(message) {
|
||||
let { name, create, banned, reason } = message.data;
|
||||
let user = this._controller.userManager.users.get(name);
|
||||
if (!user) {
|
||||
if (create) {
|
||||
this.user.checkPermission("core.user.create");
|
||||
user = this._controller.userManager.createUser(name);
|
||||
} else {
|
||||
throw new libErrors.RequestError(`User '${name}' does not exist`);
|
||||
}
|
||||
}
|
||||
|
||||
user.isBanned = banned;
|
||||
user.banReason = reason;
|
||||
this._controller.userUpdated(user);
|
||||
this.broadcastEventToSlaves({ data: { name, banned, reason }}, libLink.messages.banlistUpdate);
|
||||
}
|
||||
|
||||
async setUserWhitelistedRequestHandler(message) {
|
||||
let { name, create, whitelisted } = message.data;
|
||||
let user = this._controller.userManager.users.get(name);
|
||||
if (!user) {
|
||||
if (create) {
|
||||
this.user.checkPermission("core.user.create");
|
||||
user = this._controller.userManager.createUser(name);
|
||||
} else {
|
||||
throw new libErrors.RequestError(`User '${name}' does not exist`);
|
||||
}
|
||||
}
|
||||
|
||||
user.isWhitelisted = whitelisted;
|
||||
this._controller.userUpdated(user);
|
||||
this.broadcastEventToSlaves({ data: { name, whitelisted }}, libLink.messages.whitelistUpdate);
|
||||
}
|
||||
|
||||
async deleteUserRequestHandler(message) {
|
||||
let user = this._controller.userManager.users.get(message.data.name);
|
||||
if (!user) {
|
||||
throw new libErrors.RequestError(`User '${message.data.name}' does not exist`);
|
||||
}
|
||||
|
||||
user.isDeleted = true;
|
||||
this._controller.userManager.users.delete(message.data.name);
|
||||
this._controller.userUpdated(user);
|
||||
|
||||
if (user.is_admin) {
|
||||
this.broadcastEventToSlaves({ data: { name, admin: false }}, libLink.messages.adminlistUpdate);
|
||||
}
|
||||
if (user.is_whitelisted) {
|
||||
this.broadcastEventToSlaves({ data: { name, whitelisted: false }}, libLink.messages.whitelistUpdate);
|
||||
}
|
||||
if (user.is_banned) {
|
||||
this.broadcastEventToSlaves({ data: { name, banned: false, reason: "" }}, libLink.messages.banlistUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
async debugDumpWsRequestHandler(message) {
|
||||
this.ws_dumper = data => {
|
||||
if (this.connector.connected) {
|
||||
libLink.messages.debugWsMessage.send(this, data);
|
||||
}
|
||||
};
|
||||
this.connector._socket.clusterio_ignore_dump = true;
|
||||
this._controller.debugEvents.on("message", this.ws_dumper);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ControlConnection;
|
||||
Reference in new issue
Block a user