mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 15:04:52 +09:00
Merge pull request #495 from Luciole0x/master
Transition controller package to TypeScript.
This commit is contained in:
28 files changed
+1164
-884
No files matched your search
Executable → Regular
+113
-66
@@ -14,27 +14,28 @@
|
||||
* npx clusteriocontroller run
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
const path = require("path");
|
||||
const fs = require("fs-extra");
|
||||
const crypto = require("crypto");
|
||||
const setBlocking = require("set-blocking");
|
||||
const yargs = require("yargs");
|
||||
const util = require("util");
|
||||
const winston = require("winston");
|
||||
require("winston-daily-rotate-file");
|
||||
const jwt = require("jsonwebtoken");
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
|
||||
import crypto from "crypto";
|
||||
import setBlocking from "set-blocking";
|
||||
import yargs from "yargs";
|
||||
import util from "util";
|
||||
import winston from "winston";
|
||||
import DailyRotateFile from "winston-daily-rotate-file";
|
||||
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
// homebrew modules
|
||||
const lib = require("@clusterio/lib");
|
||||
import * as lib from "@clusterio/lib";
|
||||
const { ConsoleTransport, levels, logger } = lib;
|
||||
|
||||
const Controller = require("./src/Controller");
|
||||
const UserManager = require("./src/UserManager");
|
||||
const version = require("./package").version;
|
||||
import Controller from "./src/Controller";
|
||||
import UserManager from "./src/UserManager";
|
||||
const version = require("./package.json").version;
|
||||
|
||||
// globals
|
||||
let controller;
|
||||
let controller: Controller;
|
||||
|
||||
|
||||
void new lib.Gauge(
|
||||
@@ -42,7 +43,7 @@ void new lib.Gauge(
|
||||
"Mapping of Host ID to name",
|
||||
{
|
||||
labels: ["host_id", "host_name"],
|
||||
callback: function(gauge) {
|
||||
callback: function(gauge:lib.Gauge) {
|
||||
gauge.clear();
|
||||
if (!controller || !controller.hosts) {
|
||||
return;
|
||||
@@ -62,7 +63,7 @@ void new lib.Gauge(
|
||||
"Mapping of Instance ID to name and host",
|
||||
{
|
||||
labels: ["instance_id", "instance_name", "host_id"],
|
||||
callback: function(gauge) {
|
||||
callback: function(gauge:lib.Gauge) {
|
||||
gauge.clear();
|
||||
if (!controller || !controller.instances) {
|
||||
return;
|
||||
@@ -81,19 +82,26 @@ void new lib.Gauge(
|
||||
void new lib.Gauge(
|
||||
"clusterio_controller_websocket_active_connections",
|
||||
"How many WebSocket connections are currently open to the controller",
|
||||
{ callback: function(gauge) { gauge.set(controller.wsServer.activeConnectors.size); }}
|
||||
{ callback: function(gauge:lib.Gauge) {
|
||||
gauge.set(controller.wsServer.activeConnectors.size);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
void new lib.Gauge(
|
||||
"clusterio_controller_active_hosts",
|
||||
"How many hosts are currently connected to the controller",
|
||||
{ callback: function(gauge) { gauge.set(controller.wsServer.hostConnections.size); }}
|
||||
{ callback: function(gauge:lib.Gauge) {
|
||||
gauge.set(controller.wsServer.hostConnections.size);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
void new lib.Gauge(
|
||||
"clusterio_controller_connected_clients_count", "How many clients are currently connected to this controller",
|
||||
"clusterio_controller_connected_clients_count",
|
||||
"How many clients are currently connected to this controller",
|
||||
{
|
||||
labels: ["type"], callback: async function(gauge) {
|
||||
labels: ["type"], callback: async function(gauge:lib.Gauge) {
|
||||
gauge.labels("host").set(controller.wsServer.hostConnections.size);
|
||||
gauge.labels("control").set(controller.wsServer.controlConnections.size);
|
||||
},
|
||||
@@ -101,7 +109,10 @@ void new lib.Gauge(
|
||||
);
|
||||
|
||||
|
||||
async function handleBootstrapCommand(args, controllerConfig) {
|
||||
async function handleBootstrapCommand(
|
||||
args: any,
|
||||
controllerConfig: lib.ControllerConfig
|
||||
): Promise<void> {
|
||||
let subCommand = args._[1];
|
||||
let userManager = new UserManager(controllerConfig);
|
||||
await userManager.load(path.join(controllerConfig.get("controller.database_directory"), "users.json"));
|
||||
@@ -166,18 +177,31 @@ async function handleBootstrapCommand(args, controllerConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
async function initialize() {
|
||||
let parameters = {
|
||||
args: null,
|
||||
shouldRun: false,
|
||||
clusterLogger: null,
|
||||
pluginInfos: null,
|
||||
controllerConfigPath: null,
|
||||
controllerConfig: null,
|
||||
};
|
||||
|
||||
export interface ControllerArgs {
|
||||
[x: string]: unknown;
|
||||
logLevel: string;
|
||||
logDirectory: string;
|
||||
pluginList: string;
|
||||
devPlugin?: (string | number)[] | undefined;
|
||||
dev?: boolean;
|
||||
config: string;
|
||||
_: (string | number)[];
|
||||
$0: string;
|
||||
}
|
||||
|
||||
interface InitializeParameters {
|
||||
args: ControllerArgs;
|
||||
shouldRun: boolean;
|
||||
clusterLogger: winston.Logger;
|
||||
pluginInfos: any[] | null;
|
||||
controllerConfigPath: string | null;
|
||||
controllerConfig: lib.ControllerConfig | null;
|
||||
}
|
||||
|
||||
async function initialize(): Promise<InitializeParameters> {
|
||||
// argument parsing
|
||||
parameters.args = yargs
|
||||
const args = await yargs
|
||||
.scriptName("controller")
|
||||
.usage("$0 <command> [options]")
|
||||
.option("log-level", {
|
||||
@@ -232,45 +256,46 @@ async function initialize() {
|
||||
;
|
||||
|
||||
// Combined log stream of the whole cluster.
|
||||
parameters.clusterLogger = winston.createLogger({
|
||||
const clusterLogger = winston.createLogger({
|
||||
format: winston.format.json(),
|
||||
level: "verbose",
|
||||
levels,
|
||||
});
|
||||
parameters.clusterLogger.add(new winston.transports.DailyRotateFile({
|
||||
clusterLogger.add(new DailyRotateFile({
|
||||
filename: "cluster-%DATE%.log",
|
||||
utc: true,
|
||||
dirname: path.join(parameters.args.logDirectory, "cluster"),
|
||||
dirname: path.join(args.logDirectory, "cluster"),
|
||||
}));
|
||||
|
||||
// Log stream for the controller.
|
||||
logger.add(new winston.transports.DailyRotateFile({
|
||||
logger.add(new DailyRotateFile({
|
||||
format: winston.format.json(),
|
||||
filename: "controller-%DATE%.log",
|
||||
dirname: path.join(parameters.args.logDirectory, "controller"),
|
||||
dirname: path.join(args.logDirectory, "controller"),
|
||||
}));
|
||||
logger.add(new winston.transports.Stream({
|
||||
stream: parameters.clusterLogger,
|
||||
stream: clusterLogger,
|
||||
}));
|
||||
if (parameters.args.logLevel !== "none") {
|
||||
if (args.logLevel !== "none") {
|
||||
logger.add(new ConsoleTransport({
|
||||
level: parameters.args.logLevel,
|
||||
level: args.logLevel,
|
||||
format: new lib.TerminalFormat(),
|
||||
}));
|
||||
}
|
||||
lib.handleUnhandledErrors(logger);
|
||||
lib.handleUnhandledErrors();
|
||||
|
||||
let command = parameters.args._[0];
|
||||
let command = args._[0];
|
||||
let shouldRun = false;
|
||||
if (command === "run") {
|
||||
logger.info(`Starting Clusterio controller ${version}`);
|
||||
parameters.shouldRun = true;
|
||||
shouldRun = true;
|
||||
}
|
||||
|
||||
logger.info(`Loading available plugins from ${parameters.args.pluginList}`);
|
||||
logger.info(`Loading available plugins from ${args.pluginList}`);
|
||||
let pluginList = new Map();
|
||||
try {
|
||||
pluginList = new Map(JSON.parse(await fs.readFile(parameters.args.pluginList)));
|
||||
} catch (err) {
|
||||
pluginList = new Map(JSON.parse(await fs.readFile(args.pluginList, { encoding: 'utf8' })));
|
||||
} catch (err: any) {
|
||||
if (err.code !== "ENOENT") {
|
||||
throw err;
|
||||
}
|
||||
@@ -278,52 +303,67 @@ async function initialize() {
|
||||
|
||||
// If the command is plugin management we don't try to load plugins
|
||||
if (command === "plugin") {
|
||||
await lib.handlePluginCommand(parameters.args, pluginList, parameters.args.pluginList);
|
||||
return parameters;
|
||||
await lib.handlePluginCommand(args, pluginList, args.pluginList);
|
||||
return {
|
||||
args,
|
||||
shouldRun,
|
||||
clusterLogger,
|
||||
pluginInfos: null,
|
||||
controllerConfigPath: null,
|
||||
controllerConfig: null,
|
||||
};
|
||||
}
|
||||
|
||||
logger.info("Loading Plugin info");
|
||||
parameters.pluginInfos = await lib.loadPluginInfos(pluginList);
|
||||
lib.registerPluginMessages(parameters.pluginInfos);
|
||||
lib.registerPluginConfigGroups(parameters.pluginInfos);
|
||||
const pluginInfos = await lib.loadPluginInfos(pluginList);
|
||||
lib.registerPluginMessages(pluginInfos);
|
||||
lib.registerPluginConfigGroups(pluginInfos);
|
||||
lib.finalizeConfigs();
|
||||
|
||||
parameters.controllerConfigPath = parameters.args.config;
|
||||
logger.info(`Loading config from ${parameters.controllerConfigPath}`);
|
||||
parameters.controllerConfig = new lib.ControllerConfig("controller");
|
||||
const controllerConfigPath = args.config;
|
||||
logger.info(`Loading config from ${controllerConfigPath}`);
|
||||
const controllerConfig = new lib.ControllerConfig("controller");
|
||||
try {
|
||||
await parameters.controllerConfig.load(JSON.parse(await fs.readFile(parameters.controllerConfigPath)));
|
||||
let fileData = await fs.readFile(controllerConfigPath, { encoding: 'utf8' })
|
||||
await controllerConfig.load(JSON.parse(fileData));
|
||||
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.code === "ENOENT") {
|
||||
logger.info("Config not found, initializing new config");
|
||||
await parameters.controllerConfig.init();
|
||||
await controllerConfig.init();
|
||||
|
||||
} else {
|
||||
throw new lib.StartupError(`Failed to load ${parameters.controllerConfigPath}: ${err.message}`);
|
||||
throw new lib.StartupError(`Failed to load ${controllerConfigPath}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!parameters.controllerConfig.get("controller.auth_secret")) {
|
||||
if (!controllerConfig.get("controller.auth_secret")) {
|
||||
logger.info("Generating new controller authentication secret");
|
||||
let asyncRandomBytes = util.promisify(crypto.randomBytes);
|
||||
let bytes = await asyncRandomBytes(256);
|
||||
parameters.controllerConfig.set("controller.auth_secret", bytes.toString("base64"));
|
||||
controllerConfig.set("controller.auth_secret", bytes.toString("base64"));
|
||||
await lib.safeOutputFile(
|
||||
parameters.controllerConfigPath, JSON.stringify(parameters.controllerConfig.serialize(), null, 4)
|
||||
controllerConfigPath, JSON.stringify(controllerConfig.serialize(), null, 4)
|
||||
);
|
||||
}
|
||||
|
||||
if (command === "config") {
|
||||
await lib.handleConfigCommand(
|
||||
parameters.args, parameters.controllerConfig, parameters.controllerConfigPath
|
||||
args, controllerConfig, controllerConfigPath
|
||||
);
|
||||
|
||||
} else if (command === "bootstrap") {
|
||||
await handleBootstrapCommand(parameters.args, parameters.controllerConfig);
|
||||
await handleBootstrapCommand(args, controllerConfig);
|
||||
}
|
||||
|
||||
return parameters;
|
||||
return {
|
||||
args,
|
||||
clusterLogger,
|
||||
pluginInfos,
|
||||
controllerConfigPath,
|
||||
controllerConfig,
|
||||
shouldRun,
|
||||
}
|
||||
}
|
||||
|
||||
async function startup() {
|
||||
@@ -331,8 +371,15 @@ async function startup() {
|
||||
// and as the process name in ps/top on linux.
|
||||
process.title = "clusteriocontroller";
|
||||
|
||||
let { args, shouldRun, clusterLogger, pluginInfos, controllerConfigPath, controllerConfig } = await initialize();
|
||||
if (!shouldRun) {
|
||||
let {
|
||||
args,
|
||||
shouldRun,
|
||||
clusterLogger,
|
||||
pluginInfos,
|
||||
controllerConfigPath,
|
||||
controllerConfig
|
||||
} = await initialize();
|
||||
if (!shouldRun || !pluginInfos || !controllerConfigPath || !controllerConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.13",
|
||||
"main": "controller.js",
|
||||
"main": "dist/controller/controller.js",
|
||||
"scripts": {
|
||||
"prepare": "webpack-cli --env production",
|
||||
"prepare": "webpack-cli --env production && pnpm build",
|
||||
"build": "tsc --build",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"bin": {
|
||||
"clusteriocontroller": "./controller.js"
|
||||
"clusteriocontroller": "./dist/controller/controller.js"
|
||||
},
|
||||
"keywords": [
|
||||
"factorio"
|
||||
@@ -35,9 +36,19 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clusterio/web_ui": "^2.0.0-alpha.13",
|
||||
"@types/busboy": "^1.5.0",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/jsonwebtoken": "^9.0.2",
|
||||
"@types/node": "^20.4.5",
|
||||
"@types/set-blocking": "^2.0.0",
|
||||
"@types/ws": "^8.5.5",
|
||||
"@types/yargs": "^17.0.24",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.4.14",
|
||||
"typescript": "^5.1.6",
|
||||
"webpack": "^5.88.2",
|
||||
"webpack-dev-middleware": "^6.1.1",
|
||||
"webpack-merge": "^5.9.0"
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
"use strict";
|
||||
const lib = require("@clusterio/lib");
|
||||
const { logger } = lib;
|
||||
const ControllerRouter = require("./ControllerRouter");
|
||||
|
||||
|
||||
/**
|
||||
* Base class for controller connections
|
||||
*
|
||||
* @extends module:lib.Link
|
||||
* @alias module:controller/src/BaseConnection
|
||||
*/
|
||||
class BaseConnection extends lib.Link {
|
||||
constructor(connector, controller) {
|
||||
/** @member {module:controller/src/WsServerConnector} module:controller/src/BaseConnection#connector */
|
||||
super(connector);
|
||||
this.router = new ControllerRouter(controller);
|
||||
this._controller = controller;
|
||||
for (let [Request, handler] of controller._registeredRequests) { this.handle(Request, handler); }
|
||||
for (let [Request, handler] of controller._fallbackedRequests) { this.fallbackRequest(Request, handler); }
|
||||
for (let [Event, handler] of controller._registeredEvents) { this.handle(Event, handler); }
|
||||
for (let [Event, handler] of controller._snoopedEvents) { this.snoopEvent(Event, handler); }
|
||||
|
||||
this.handle(lib.ModPackGetRequest, this.handleModPackGetRequest.bind(this));
|
||||
this.handle(lib.ModPackGetDefaultRequest, this.handleModPackGetDefaultRequest.bind(this));
|
||||
}
|
||||
|
||||
async disconnect(code, reason) {
|
||||
try {
|
||||
await this.connector.disconnect();
|
||||
} catch (err) {
|
||||
if (!(err instanceof lib.SessionLost)) {
|
||||
logger.error(`"Unexpected error preparing disconnect:\n${err.stack}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the link is connected, not in the dropped state and not in
|
||||
* the process of disconnecting.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get connected() {
|
||||
return this.connector.connected;
|
||||
}
|
||||
|
||||
async handleModPackGetRequest(request) {
|
||||
let { id } = request;
|
||||
let modPack = this._controller.modPacks.get(id);
|
||||
if (!modPack) {
|
||||
throw new lib.RequestError(`Mod pack with ID ${id} does not exist`);
|
||||
}
|
||||
return modPack;
|
||||
}
|
||||
|
||||
async handleModPackGetDefaultRequest() {
|
||||
let id = this._controller.config.get("controller.default_mod_pack_id");
|
||||
if (id === null) {
|
||||
throw new lib.RequestError("Default mod pack not set on controller");
|
||||
}
|
||||
let modPack = this._controller.modPacks.get(id);
|
||||
if (!modPack) {
|
||||
throw new lib.RequestError(`Default mod pack configured (${id}) does not exist`);
|
||||
}
|
||||
return modPack;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseConnection;
|
||||
@@ -0,0 +1,71 @@
|
||||
import type Controller from "./Controller";
|
||||
import type WsServerConnector from "./WsServerConnector";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
const { logger } = lib;
|
||||
import ControllerRouter from "./ControllerRouter";
|
||||
|
||||
|
||||
/**
|
||||
* Base class for controller connections
|
||||
*
|
||||
* @extends module:lib.Link
|
||||
* @alias module:controller/src/BaseConnection
|
||||
*/
|
||||
export default class BaseConnection extends lib.Link {
|
||||
declare connector: lib.WebSocketBaseConnector;
|
||||
|
||||
constructor(
|
||||
connector: lib.WebSocketBaseConnector,
|
||||
public _controller: Controller
|
||||
) {
|
||||
super(connector);
|
||||
this.router = new ControllerRouter(this._controller);
|
||||
for (let [Request, handler] of this._controller._registeredRequests) { this.handle(Request, handler); }
|
||||
for (let [Request, handler] of this._controller._fallbackedRequests) { this.fallbackRequest(Request, handler); }
|
||||
for (let [Event, handler] of this._controller._registeredEvents) { this.handle(Event, handler); }
|
||||
for (let [Event, handler] of this._controller._snoopedEvents) { this.snoopEvent(Event, handler); }
|
||||
|
||||
this.handle(lib.ModPackGetRequest, this.handleModPackGetRequest.bind(this));
|
||||
this.handle(lib.ModPackGetDefaultRequest, this.handleModPackGetDefaultRequest.bind(this));
|
||||
}
|
||||
|
||||
async disconnect(code: number, reason: string) {
|
||||
try {
|
||||
await this.connector.disconnect();
|
||||
} catch (err: any) {
|
||||
if (!(err instanceof lib.SessionLost)) {
|
||||
logger.error(`"Unexpected error preparing disconnect:\n${err.stack}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the link is connected, not in the dropped state and not in
|
||||
* the process of disconnecting.
|
||||
*/
|
||||
get connected(): boolean {
|
||||
return this.connector.connected;
|
||||
}
|
||||
|
||||
async handleModPackGetRequest(request: lib.ModPackGetRequest): Promise<lib.ModPack> {
|
||||
let { id } = request;
|
||||
let modPack = this._controller.modPacks!.get(id);
|
||||
if (!modPack) {
|
||||
throw new lib.RequestError(`Mod pack with ID ${id} does not exist`);
|
||||
}
|
||||
return modPack;
|
||||
}
|
||||
|
||||
async handleModPackGetDefaultRequest(): Promise<lib.ModPack> {
|
||||
let id = this._controller.config.get("controller.default_mod_pack_id");
|
||||
if (id === null) {
|
||||
throw new lib.RequestError("Default mod pack not set on controller");
|
||||
}
|
||||
let modPack = this._controller.modPacks!.get(id);
|
||||
if (!modPack) {
|
||||
throw new lib.RequestError(`Default mod pack configured (${id}) does not exist`);
|
||||
}
|
||||
return modPack;
|
||||
}
|
||||
}
|
||||
+210
-171
@@ -1,15 +1,20 @@
|
||||
"use strict";
|
||||
const events = require("events");
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
import type WsServerConnector from "./WsServerConnector";
|
||||
import type { HostInfo } from "./HostConnection";
|
||||
|
||||
const lib = require("@clusterio/lib");
|
||||
import events from "events";
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
const { logFilter, logger } = lib;
|
||||
|
||||
const BaseConnection = require("./BaseConnection");
|
||||
const routes = require("./routes");
|
||||
import BaseConnection from "./BaseConnection";
|
||||
import * as routes from "./routes";
|
||||
import Controller from "./Controller";
|
||||
import { promises } from "dns";
|
||||
import InstanceInfo from "./InstanceInfo";
|
||||
|
||||
const strcmp = new Intl.Collator(undefined, { numerice: "true", sensitivity: "base" }).compare;
|
||||
const strcmp = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare;
|
||||
|
||||
const queryLogTime = new lib.Summary(
|
||||
"clusterio_controller_query_log_duration_seconds",
|
||||
@@ -22,66 +27,49 @@ const queryLogTime = new lib.Summary(
|
||||
* @extends module:controller/src/BaseConnection
|
||||
* @alias module:controller/src/ControlConnection
|
||||
*/
|
||||
class ControlConnection extends BaseConnection {
|
||||
constructor(registerData, connector, controller, user, id) {
|
||||
export default class ControlConnection extends BaseConnection {
|
||||
private _agent: string;
|
||||
private _version: string;
|
||||
hostSubscriptions: { all:boolean, hostIds:number[] } = { all:false, hostIds:[] };
|
||||
instanceSubscriptions: { all:boolean, instanceIds:number[] } = { all:false, instanceIds:[] };
|
||||
saveListSubscriptions: { all:boolean, instanceIds:number[] } = { all:false, instanceIds:[] };
|
||||
modPackSubscriptions: { all:boolean, modPackIds:number[] } = { all:false, modPackIds:[] };
|
||||
modSubscriptions: { all:boolean, modNames:string[] } = { all:false, modNames:[] };
|
||||
userSubscriptions: { all:boolean, names:string[]} = { all:false, names:[] };
|
||||
logTransport: lib.LinkTransport | null = null;
|
||||
logSubscriptions: {
|
||||
all: boolean,
|
||||
controller: boolean,
|
||||
hostIds: number[],
|
||||
instanceIds: number[],
|
||||
} = {
|
||||
all: false,
|
||||
controller: false,
|
||||
hostIds: [],
|
||||
instanceIds: [],
|
||||
};
|
||||
ws_dumper: ((...args: any[]) => void) | null = null;
|
||||
declare connector: WsServerConnector;
|
||||
|
||||
constructor(
|
||||
registerData: { agent:string, version:string },
|
||||
connector: WsServerConnector,
|
||||
controller: Controller,
|
||||
public user: lib.User, // The user making this connection.
|
||||
public id: number
|
||||
) {
|
||||
super(connector, controller);
|
||||
|
||||
this._agent = registerData.agent;
|
||||
this._version = registerData.version;
|
||||
this.id = id;
|
||||
|
||||
/**
|
||||
* The user making this connection.
|
||||
* @type {module:lib.User}
|
||||
*/
|
||||
this.user = user;
|
||||
|
||||
this.hostSubscriptions = {
|
||||
all: false,
|
||||
hostIds: [],
|
||||
};
|
||||
|
||||
this.instanceSubscriptions = {
|
||||
all: false,
|
||||
instanceIds: [],
|
||||
};
|
||||
|
||||
this.saveListSubscriptions = {
|
||||
all: false,
|
||||
instanceIds: [],
|
||||
};
|
||||
|
||||
this.modPackSubscriptions = {
|
||||
all: false,
|
||||
modPackIds: [],
|
||||
};
|
||||
|
||||
this.modSubscriptions = {
|
||||
all: false,
|
||||
modNames: [],
|
||||
};
|
||||
|
||||
this.userSubscriptions = {
|
||||
all: false,
|
||||
names: [],
|
||||
};
|
||||
|
||||
this.logTransport = null;
|
||||
this.logSubscriptions = {
|
||||
all: false,
|
||||
controller: false,
|
||||
hostIds: [],
|
||||
instanceIds: [],
|
||||
};
|
||||
|
||||
this.ws_dumper = null;
|
||||
this.connector.on("connect", () => {
|
||||
this.connector._socket.clusterio_ignore_dump = Boolean(this.ws_dumper);
|
||||
this.connector._socket!.clusterio_ignore_dump = Boolean(this.ws_dumper);
|
||||
});
|
||||
this.connector.on("close", () => {
|
||||
if (this.logTransport) {
|
||||
this.logTransport = null;
|
||||
logger.remove(this.logTransport);
|
||||
this.logTransport = null;
|
||||
}
|
||||
if (this.ws_dumper) {
|
||||
this._controller.debugEvents.off("message", this.ws_dumper);
|
||||
@@ -91,7 +79,10 @@ class ControlConnection extends BaseConnection {
|
||||
for (let event of ["connect", "drop", "resume", "close"]) {
|
||||
this.connector.on(event, () => {
|
||||
for (let controllerPlugin of this._controller.plugins.values()) {
|
||||
controllerPlugin.onControlConnectionEvent(this, event);
|
||||
controllerPlugin.onControlConnectionEvent(
|
||||
this,
|
||||
event as "connect" | "drop" | "resume" | "close"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -105,26 +96,17 @@ class ControlConnection extends BaseConnection {
|
||||
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.InstanceDetailsSetSubscriptionsRequest, this.handleInstanceDetailsSetSubscriptionsRequest.bind(this)
|
||||
);
|
||||
this.handle(lib.InstanceDetailsSetSubscriptionsRequest, this.handleInstanceDetailsSetSubscriptionsRequest.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.InstanceSetSaveListSubscriptionsRequest,
|
||||
this.handleInstanceSetSaveListSubscriptionsRequest.bind(this)
|
||||
);
|
||||
this.handle(
|
||||
lib.InstanceRenameSaveRequest, this._controller.sendToHostByInstanceId.bind(this._controller)
|
||||
);
|
||||
this.handle(lib.InstanceSetSaveListSubscriptionsRequest, this.handleInstanceSetSaveListSubscriptionsRequest.bind(this));
|
||||
this.handle(lib.InstanceRenameSaveRequest, this._controller.sendToHostByInstanceId.bind(this._controller));
|
||||
this.handle(lib.InstanceCopySaveRequest, this._controller.sendToHostByInstanceId.bind(this._controller));
|
||||
this.handle(
|
||||
lib.InstanceDeleteSaveRequest, this._controller.sendToHostByInstanceId.bind(this._controller)
|
||||
);
|
||||
this.handle(lib.InstanceDeleteSaveRequest, this._controller.sendToHostByInstanceId.bind(this._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));
|
||||
@@ -144,9 +126,7 @@ class ControlConnection extends BaseConnection {
|
||||
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.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));
|
||||
@@ -161,29 +141,35 @@ class ControlConnection extends BaseConnection {
|
||||
this.handle(lib.DebugDumpWsRequest, this.handleDebugDumpWsRequest.bind(this));
|
||||
}
|
||||
|
||||
validateIngress(message) {
|
||||
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, entry) {
|
||||
validatePermission(
|
||||
message: lib.MessageRequest | lib.MessageEvent,
|
||||
entry: lib.RequestEntry | lib.EventEntry
|
||||
) {
|
||||
try {
|
||||
this.checkPermission(message, entry);
|
||||
} catch (err) {
|
||||
} 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.name} from ${this.connector.dst}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
checkPermission(message, entry) {
|
||||
checkPermission(
|
||||
message: lib.MessageRequest | lib.MessageEvent,
|
||||
entry: lib.RequestEntry | lib.EventEntry
|
||||
) {
|
||||
let permission;
|
||||
if (message.type === "request") {
|
||||
permission = entry.Request.permission;
|
||||
permission = (entry as lib.RequestEntry).Request.permission;
|
||||
} else if (message.type === "event") {
|
||||
permission = entry.Event.permission;
|
||||
permission = (entry as lib.EventEntry).Event.permission;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@@ -205,22 +191,22 @@ class ControlConnection extends BaseConnection {
|
||||
throw new Error("Should be unreachable");
|
||||
}
|
||||
|
||||
async handleControllerConfigGetRequest() {
|
||||
async handleControllerConfigGetRequest(): Promise<lib.RawConfig> {
|
||||
return new lib.RawConfig(this._controller.config.serialize("control"));
|
||||
}
|
||||
|
||||
async handleControllerConfigSetFieldRequest(request) {
|
||||
async handleControllerConfigSetFieldRequest(request: lib.ControllerConfigSetFieldRequest) {
|
||||
this._controller.config.set(request.field, request.value, "control");
|
||||
}
|
||||
|
||||
async handleControllerConfigSetPropRequest(request) {
|
||||
async handleControllerConfigSetPropRequest(request: lib.ControllerConfigSetPropRequest) {
|
||||
let { field, prop, value } = request;
|
||||
this._controller.config.setProp(field, prop, value, "control");
|
||||
}
|
||||
|
||||
async handleHostListRequest() {
|
||||
async handleHostListRequest(): Promise<lib.HostDetails[]> {
|
||||
let list = [];
|
||||
for (let host of this._controller.hosts.values()) {
|
||||
for (let host of this._controller.hosts!.values()) {
|
||||
list.push(new lib.HostDetails(
|
||||
host.agent,
|
||||
host.version,
|
||||
@@ -233,11 +219,14 @@ class ControlConnection extends BaseConnection {
|
||||
return list;
|
||||
}
|
||||
|
||||
async handleHostSetSubscriptionsRequest(request) {
|
||||
async handleHostSetSubscriptionsRequest(request: lib.HostSetSubscriptionsRequest) {
|
||||
this.hostSubscriptions = { ...request };
|
||||
}
|
||||
|
||||
hostUpdated(host, update) {
|
||||
hostUpdated(
|
||||
host: HostInfo,
|
||||
update: lib.HostDetails
|
||||
) {
|
||||
if (
|
||||
this.hostSubscriptions.all
|
||||
|| this.hostSubscriptions.hostIds.includes(host.id)
|
||||
@@ -246,15 +235,15 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
}
|
||||
|
||||
async handleHostGenerateTokenRequest(message) {
|
||||
let hostId = message.host_id;
|
||||
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) {
|
||||
async handleHostConfigCreateRequest(request: lib.HostConfigCreateRequest) {
|
||||
let hostConfig = new lib.HostConfig("control");
|
||||
await hostConfig.init();
|
||||
|
||||
@@ -272,47 +261,78 @@ class ControlConnection extends BaseConnection {
|
||||
return new lib.RawConfig(hostConfig.serialize());
|
||||
}
|
||||
|
||||
async handleInstanceDetailsGetRequest(request) {
|
||||
async handleInstanceDetailsGetRequest(request: lib.InstanceDetailsGetRequest) {
|
||||
let instance = this._controller.getRequestInstance(request.instanceId);
|
||||
|
||||
let assigned_host: number|null|undefined = instance.config.get("instance.assigned_host");
|
||||
if (assigned_host === null) {
|
||||
assigned_host = undefined;
|
||||
}
|
||||
|
||||
let game_port: number|null|undefined = instance.game_port
|
||||
if (game_port === null) {
|
||||
game_port = undefined;
|
||||
}
|
||||
|
||||
return new lib.InstanceDetails(
|
||||
instance.config.get("instance.name"),
|
||||
instance.id,
|
||||
instance.config.get("instance.assigned_host"),
|
||||
instance.game_port || undefined,
|
||||
assigned_host,
|
||||
game_port,
|
||||
instance.status,
|
||||
);
|
||||
}
|
||||
|
||||
async handleInstanceDetailsListRequest() {
|
||||
let list = [];
|
||||
for (let instance of this._controller.instances.values()) {
|
||||
for (let instance of this._controller.instances!.values()) {
|
||||
let assigned_host: number|null|undefined = instance.config.get("instance.assigned_host");
|
||||
if (assigned_host === null) {
|
||||
assigned_host = undefined;
|
||||
}
|
||||
|
||||
let game_port: number|null|undefined = instance.game_port
|
||||
if (game_port === null) {
|
||||
game_port = undefined;
|
||||
}
|
||||
|
||||
list.push(new lib.InstanceDetails(
|
||||
instance.config.get("instance.name"),
|
||||
instance.id,
|
||||
instance.config.get("instance.assigned_host") || undefined,
|
||||
instance.game_port || undefined,
|
||||
assigned_host,
|
||||
game_port,
|
||||
instance.status,
|
||||
));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
async handleInstanceDetailsSetSubscriptionsRequest(request) {
|
||||
async handleInstanceDetailsSetSubscriptionsRequest(request: lib.InstanceDetailsSetSubscriptionsRequest) {
|
||||
this.instanceSubscriptions = { ...request };
|
||||
}
|
||||
|
||||
instanceUpdated(instance) {
|
||||
instanceUpdated(instance: InstanceInfo) {
|
||||
if (
|
||||
this.instanceSubscriptions.all
|
||||
|| this.instanceSubscriptions.instanceIds.includes(instance.id)
|
||||
) {
|
||||
let assigned_host: number|null|undefined = instance.config.get("instance.assigned_host");
|
||||
if (assigned_host === null) {
|
||||
assigned_host = undefined;
|
||||
}
|
||||
|
||||
let game_port: number|null|undefined = instance.game_port
|
||||
if (game_port === null) {
|
||||
game_port = undefined;
|
||||
}
|
||||
|
||||
|
||||
this.send(new lib.InstanceDetailsUpdateEvent(
|
||||
new lib.InstanceDetails(
|
||||
instance.config.get("instance.name"),
|
||||
instance.id,
|
||||
instance.config.get("instance.assigned_host") || undefined,
|
||||
instance.game_port || undefined,
|
||||
assigned_host,
|
||||
game_port,
|
||||
instance.status,
|
||||
)
|
||||
));
|
||||
@@ -320,22 +340,22 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
// XXX should probably add a hook for host reuqests?
|
||||
async handleInstanceCreateRequest(request) {
|
||||
async handleInstanceCreateRequest(request: lib.InstanceCreateRequest) {
|
||||
let instanceConfig = new lib.InstanceConfig("controller");
|
||||
await instanceConfig.load(request.config);
|
||||
await instanceConfig.load(request.config as lib.SerializedConfig);
|
||||
await this._controller.instanceCreate(instanceConfig);
|
||||
}
|
||||
|
||||
async handleInstanceDeleteRequest(request) {
|
||||
async handleInstanceDeleteRequest(request: lib.InstanceDeleteRequest) {
|
||||
await this._controller.instanceDelete(request.instanceId);
|
||||
}
|
||||
|
||||
async handleInstanceConfigGetRequest(request) {
|
||||
async handleInstanceConfigGetRequest(request: lib.InstanceConfigGetRequest) {
|
||||
let instance = this._controller.getRequestInstance(request.instanceId);
|
||||
return new lib.InstanceConfigGetRequest.Response(instance.config.serialize("control"));
|
||||
}
|
||||
|
||||
async handleInstanceConfigSetFieldRequest(request) {
|
||||
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");
|
||||
@@ -350,22 +370,22 @@ class ControlConnection extends BaseConnection {
|
||||
await this._controller.instanceConfigUpdated(instance);
|
||||
}
|
||||
|
||||
async handleInstanceConfigSetPropRequest(request) {
|
||||
async handleInstanceConfigSetPropRequest(request: lib.InstanceConfigSetPropRequest) {
|
||||
let instance = this._controller.getRequestInstance(request.instanceId);
|
||||
let { field, prop, value } = request;
|
||||
instance.config.setProp(field, prop, value, "control");
|
||||
await this._controller.instanceConfigUpdated(instance);
|
||||
}
|
||||
|
||||
async handleInstanceAssignRequest(request) {
|
||||
async handleInstanceAssignRequest(request: lib.InstanceAssignRequest) {
|
||||
await this._controller.instanceAssign(request.instanceId, request.hostId);
|
||||
}
|
||||
|
||||
async handleInstanceSetSaveListSubscriptionsRequest(request) {
|
||||
async handleInstanceSetSaveListSubscriptionsRequest(request: lib.InstanceSetSaveListSubscriptionsRequest) {
|
||||
this.saveListSubscriptions = { ...request };
|
||||
}
|
||||
|
||||
saveListUpdate(instanceId, saves) {
|
||||
saveListUpdate(instanceId: number, saves: lib.SaveDetails[]) {
|
||||
if (
|
||||
this.saveListSubscriptions.all
|
||||
|| this.saveListSubscriptions.instanceIds.includes(instanceId)
|
||||
@@ -374,7 +394,7 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
}
|
||||
|
||||
async handleInstanceDownloadSaveRequest(request) {
|
||||
async handleInstanceDownloadSaveRequest(request: lib.InstanceDownloadSaveRequest) {
|
||||
let { instanceId, name } = request;
|
||||
let stream = await routes.createProxyStream(this._controller.app);
|
||||
stream.filename = name;
|
||||
@@ -397,7 +417,7 @@ class ControlConnection extends BaseConnection {
|
||||
return stream.id;
|
||||
}
|
||||
|
||||
async handleInstanceTransferSaveRequest(request) {
|
||||
async handleInstanceTransferSaveRequest(request: lib.InstanceTransferSaveRequest) {
|
||||
if (request.sourceInstanceId === request.targetInstanceId) {
|
||||
throw new lib.RequestError("Source and target instance may not be the same");
|
||||
}
|
||||
@@ -419,12 +439,12 @@ class ControlConnection extends BaseConnection {
|
||||
|
||||
// Check connectivity
|
||||
let sourceHostConnection = this._controller.wsServer.hostConnections.get(sourceHostId);
|
||||
if (!sourceHostConnection || sourceHostConnection.closing) {
|
||||
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.closing) {
|
||||
if (!targetHostConnection || targetHostConnection.connector.closing) {
|
||||
throw new lib.RequestError("Target host is not connected to the controller");
|
||||
}
|
||||
|
||||
@@ -468,55 +488,58 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
async handleModPackListRequest() {
|
||||
return [...this._controller.modPacks.values()];
|
||||
return [...this._controller.modPacks!.values()];
|
||||
}
|
||||
|
||||
async handleModPackSetSubscriptionsRequest(request) {
|
||||
async handleModPackSetSubscriptionsRequest(request: lib.ModPackSetSubscriptionsRequest) {
|
||||
this.modPackSubscriptions = { ...request };
|
||||
}
|
||||
|
||||
async handleModPackCreateRequest(request) {
|
||||
async handleModPackCreateRequest(request: lib.ModPackCreateRequest) {
|
||||
let modPack = request.modPack;
|
||||
if (this._controller.modPacks.has(modPack.id)) {
|
||||
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.id, modPack);
|
||||
this._controller.modPacks!.set(modPack.id, modPack);
|
||||
this._controller.modPackUpdated(modPack);
|
||||
}
|
||||
|
||||
async handleModPackUpdateRequest(request) {
|
||||
async handleModPackUpdateRequest(request: lib.ModPackUpdateRequest) {
|
||||
let modPack = request.modPack;
|
||||
if (!this._controller.modPacks.has(modPack.id)) {
|
||||
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.id, modPack);
|
||||
this._controller.modPacks!.set(modPack.id, modPack);
|
||||
this._controller.modPackUpdated(modPack);
|
||||
}
|
||||
|
||||
async handleModPackDeleteRequest(request) {
|
||||
async handleModPackDeleteRequest(request: lib.ModPackDeleteRequest) {
|
||||
let { id } = request;
|
||||
let modPack = this._controller.modPacks.get(id);
|
||||
let modPack = this._controller.modPacks!.get(id);
|
||||
if (!modPack) {
|
||||
throw new lib.RequestError(`Mod pack with ID ${id} does not exist`);
|
||||
}
|
||||
modPack.isDeleted = true;
|
||||
this._controller.modPacks.delete(id);
|
||||
this._controller.modPacks!.delete(id);
|
||||
this._controller.modPackUpdated(modPack);
|
||||
}
|
||||
|
||||
modPackUpdated(modPack) {
|
||||
modPackUpdated(modPack: lib.ModPack) {
|
||||
if (
|
||||
this.modPackSubscriptions.all
|
||||
|| this.modPackSubscriptions.modPackIds.includes(modPack.id)
|
||||
|| this.modPackSubscriptions.modPackIds.includes(modPack.id!)
|
||||
) {
|
||||
this.send(new lib.ModPackUpdateEvent(modPack));
|
||||
}
|
||||
}
|
||||
|
||||
async handleModGetRequest(request) {
|
||||
async handleModGetRequest(request: lib.ModGetRequest) {
|
||||
let { name, version } = request;
|
||||
let filename = `${name}_${version}.zip`;
|
||||
let mod = this._controller.mods.get(filename);
|
||||
let mod = this._controller.mods!.get(filename);
|
||||
if (!mod) {
|
||||
throw new lib.RequestError(`Mod ${filename} does not exist`);
|
||||
}
|
||||
@@ -524,10 +547,10 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
async handleModListRequest() {
|
||||
return [...this._controller.mods.values()];
|
||||
return [...this._controller.mods!.values()];
|
||||
}
|
||||
|
||||
static termsMatchesMod(terms, mod) {
|
||||
static termsMatchesMod(terms: lib.ParsedTerm[], mod: lib.ModInfo) {
|
||||
for (let term of terms) {
|
||||
if (term.type === "word") {
|
||||
if (!lib.wordMatches(term,
|
||||
@@ -537,7 +560,7 @@ class ControlConnection extends BaseConnection {
|
||||
return false;
|
||||
}
|
||||
} else if (term.type === "attribute") {
|
||||
if (!lib.wordMatches(term.value, mod[term.name])) {
|
||||
if (!lib.wordMatches(term.value, mod[term.name as keyof lib.ModInfo] as string)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -545,7 +568,7 @@ class ControlConnection extends BaseConnection {
|
||||
return true;
|
||||
}
|
||||
|
||||
async handleModSearchRequest(request) {
|
||||
async handleModSearchRequest(request: lib.ModSearchRequest) {
|
||||
let query = lib.parseSearchString(request.query, {
|
||||
name: "word",
|
||||
// version
|
||||
@@ -562,8 +585,9 @@ class ControlConnection extends BaseConnection {
|
||||
});
|
||||
let factorioVersion = request.factorioVersion;
|
||||
|
||||
let results = new Map();
|
||||
for (let mod of this._controller.mods.values()) {
|
||||
type ModVersions = { name: string, versions: lib.ModInfo[] };
|
||||
let results: Map<string, ModVersions> = new Map();
|
||||
for (let mod of this._controller.mods!.values()) {
|
||||
if (
|
||||
mod.factorioVersion !== factorioVersion
|
||||
|| !ControlConnection.termsMatchesMod(query.terms, mod)
|
||||
@@ -589,14 +613,14 @@ class ControlConnection extends BaseConnection {
|
||||
const sort = request.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),
|
||||
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]);
|
||||
resultList.sort(sorters[sort as keyof typeof sorters]);
|
||||
let order = request.sortOrder;
|
||||
if (order === "desc") {
|
||||
resultList.reverse();
|
||||
@@ -615,14 +639,14 @@ class ControlConnection extends BaseConnection {
|
||||
};
|
||||
}
|
||||
|
||||
async handleModSetSubscriptionsRequest(request) {
|
||||
async handleModSetSubscriptionsRequest(request: lib.ModSetSubscriptionsRequest) {
|
||||
this.modSubscriptions = { ...request };
|
||||
}
|
||||
|
||||
async handleModDownloadRequest(request) {
|
||||
async handleModDownloadRequest(request: lib.ModDownloadRequest) {
|
||||
let { name, version } = request;
|
||||
let filename = `${name}_${version}.zip`;
|
||||
let mod = this._controller.mods.get(filename);
|
||||
let mod = this._controller.mods!.get(filename);
|
||||
if (!mod) {
|
||||
throw new lib.RequestError(`Mod ${filename} does not exist`);
|
||||
}
|
||||
@@ -637,16 +661,21 @@ class ControlConnection extends BaseConnection {
|
||||
return stream.id;
|
||||
}
|
||||
|
||||
async handleModDeleteRequest(request) {
|
||||
async handleModDeleteRequest(request: lib.ModDeleteRequest) {
|
||||
await this._controller.deleteMod(request.name, request.version);
|
||||
}
|
||||
|
||||
async handleLogSetSubscriptionsRequest(request) {
|
||||
this.logSubscriptions = { ...request };
|
||||
async handleLogSetSubscriptionsRequest(request: lib.LogSetSubscriptionsRequest) {
|
||||
this.logSubscriptions = {
|
||||
all: request.all || false,
|
||||
controller: request.controller || false,
|
||||
hostIds: request.hostIds || [],
|
||||
instanceIds: request.instanceIds || [],
|
||||
};
|
||||
this.updateLogSubscriptions();
|
||||
}
|
||||
|
||||
modUpdated(mod) {
|
||||
modUpdated(mod: lib.ModInfo) {
|
||||
if (
|
||||
this.modSubscriptions.all
|
||||
|| this.modSubscriptions.modNames.includes(mod.name)
|
||||
@@ -670,15 +699,25 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
}
|
||||
|
||||
async handleLogQueryRequest(request) {
|
||||
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);
|
||||
log = await this._controller.queryControllerLog(_request);
|
||||
} else {
|
||||
log = await this._controller.queryClusterLog(request);
|
||||
log = await this._controller.queryClusterLog(_request);
|
||||
}
|
||||
|
||||
observeDuration();
|
||||
@@ -710,7 +749,7 @@ class ControlConnection extends BaseConnection {
|
||||
return list;
|
||||
}
|
||||
|
||||
async handleRoleCreateRequest(request) {
|
||||
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
|
||||
@@ -719,7 +758,7 @@ class ControlConnection extends BaseConnection {
|
||||
return id;
|
||||
}
|
||||
|
||||
async handleRoleUpdateRequest(request) {
|
||||
async handleRoleUpdateRequest(request: lib.RoleUpdateRequest) {
|
||||
let { id, name, description, permissions } = request;
|
||||
let role = this._controller.userManager.roles.get(id);
|
||||
if (!role) {
|
||||
@@ -732,7 +771,7 @@ class ControlConnection extends BaseConnection {
|
||||
this._controller.rolePermissionsUpdated(role);
|
||||
}
|
||||
|
||||
async handleRoleGrantDefaultPermissionsRequest(request) {
|
||||
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`);
|
||||
@@ -742,7 +781,7 @@ class ControlConnection extends BaseConnection {
|
||||
this._controller.rolePermissionsUpdated(role);
|
||||
}
|
||||
|
||||
async handleRoleDeleteRequest(request) {
|
||||
async handleRoleDeleteRequest(request: lib.RoleDeleteRequest) {
|
||||
let id = request.id;
|
||||
let role = this._controller.userManager.roles.get(id);
|
||||
if (!role) {
|
||||
@@ -756,7 +795,7 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
}
|
||||
|
||||
async handleUserGetRequest(request) {
|
||||
async handleUserGetRequest(request: lib.UserGetRequest) {
|
||||
let name = request.name;
|
||||
let user = this._controller.userManager.users.get(name);
|
||||
if (!user) {
|
||||
@@ -773,7 +812,7 @@ class ControlConnection extends BaseConnection {
|
||||
user.banReason,
|
||||
undefined,
|
||||
user.playerStats,
|
||||
[...user.instanceStats],
|
||||
user.instanceStats,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -796,11 +835,11 @@ class ControlConnection extends BaseConnection {
|
||||
return list;
|
||||
}
|
||||
|
||||
async handleUserSetSubscriptionsRequest(request) {
|
||||
async handleUserSetSubscriptionsRequest(request: lib.UserSetSubscriptionsRequest) {
|
||||
this.userSubscriptions = { ...request };
|
||||
}
|
||||
|
||||
userUpdated(user) {
|
||||
userUpdated(user: any) {
|
||||
if (
|
||||
this.userSubscriptions.all
|
||||
|| this.userSubscriptions.names.includes(user.name)
|
||||
@@ -816,18 +855,18 @@ class ControlConnection extends BaseConnection {
|
||||
user.banReason,
|
||||
user.isDeleted,
|
||||
user.playerStats,
|
||||
[...user.instanceStats],
|
||||
user.instanceStats,
|
||||
)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
async handleUserCreateRequest(request) {
|
||||
async handleUserCreateRequest(request: lib.UserCreateRequest) {
|
||||
let user = this._controller.userManager.createUser(request.name);
|
||||
this._controller.userUpdated(user);
|
||||
}
|
||||
|
||||
async handleUserRevokeTokenRequest(request) {
|
||||
async handleUserRevokeTokenRequest(request: lib.UserRevokeTokenRequest) {
|
||||
let user = this._controller.userManager.users.get(request.name);
|
||||
if (!user) {
|
||||
throw new lib.RequestError(`User '${request.name}' does not exist`);
|
||||
@@ -845,13 +884,13 @@ class ControlConnection extends BaseConnection {
|
||||
this._controller.userUpdated(user);
|
||||
}
|
||||
|
||||
async handleUserUpdateRolesRequest(request) {
|
||||
async handleUserUpdateRolesRequest(request: lib.UserUpdateRolesRequest) {
|
||||
let user = this._controller.userManager.users.get(request.name);
|
||||
if (!user) {
|
||||
throw new lib.RequestError(`User '${request.name}' does not exist`);
|
||||
}
|
||||
|
||||
let resolvedRoles = new Set();
|
||||
let resolvedRoles: Set<lib.Role> = new Set();
|
||||
for (let roleId of request.roles) {
|
||||
let role = this._controller.userManager.roles.get(roleId);
|
||||
if (!role) {
|
||||
@@ -866,7 +905,7 @@ class ControlConnection extends BaseConnection {
|
||||
this._controller.userUpdated(user);
|
||||
}
|
||||
|
||||
async handleUserSetAdminRequest(request) {
|
||||
async handleUserSetAdminRequest(request: lib.UserSetAdminRequest) {
|
||||
let { name, create, admin } = request;
|
||||
let user = this._controller.userManager.users.get(name);
|
||||
if (!user) {
|
||||
@@ -883,7 +922,7 @@ class ControlConnection extends BaseConnection {
|
||||
this._controller.sendTo("allInstances", new lib.InstanceAdminlistUpdateEvent(name, admin));
|
||||
}
|
||||
|
||||
async handleUserSetBannedRequest(request) {
|
||||
async handleUserSetBannedRequest(request: lib.UserSetBannedRequest) {
|
||||
let { name, create, banned, reason } = request;
|
||||
let user = this._controller.userManager.users.get(name);
|
||||
if (!user) {
|
||||
@@ -901,7 +940,7 @@ class ControlConnection extends BaseConnection {
|
||||
this._controller.sendTo("allInstances", new lib.InstanceBanlistUpdateEvent(name, banned, reason));
|
||||
}
|
||||
|
||||
async handleUserSetWhitelistedRequest(request) {
|
||||
async handleUserSetWhitelistedRequest(request: lib.UserSetWhitelistedRequest) {
|
||||
let { name, create, whitelisted } = request;
|
||||
let user = this._controller.userManager.users.get(name);
|
||||
if (!user) {
|
||||
@@ -918,7 +957,7 @@ class ControlConnection extends BaseConnection {
|
||||
this._controller.sendTo("allInstances", new lib.InstanceWhitelistUpdateEvent(name, whitelisted));
|
||||
}
|
||||
|
||||
async handleUserDeleteRequest(request) {
|
||||
async handleUserDeleteRequest(request: lib.UserDeleteRequest) {
|
||||
let name = request.name;
|
||||
let user = this._controller.userManager.users.get(name);
|
||||
if (!user) {
|
||||
@@ -929,24 +968,24 @@ class ControlConnection extends BaseConnection {
|
||||
this._controller.userManager.users.delete(name);
|
||||
this._controller.userUpdated(user);
|
||||
|
||||
if (user.is_admin) {
|
||||
if (user.isAdmin) {
|
||||
this._controller.sendTo("allInstances", new lib.InstanceAdminlistUpdateEvent(name, false));
|
||||
}
|
||||
if (user.is_whitelisted) {
|
||||
if (user.isWhitelisted) {
|
||||
this._controller.sendTo("allInstances", new lib.InstanceWhitelistUpdateEvent(name, false));
|
||||
}
|
||||
if (user.is_banned) {
|
||||
if (user.isBanned) {
|
||||
this._controller.sendTo("allInstances", new lib.InstanceBanlistUpdateEvent(name, false, ""));
|
||||
}
|
||||
}
|
||||
|
||||
async handleDebugDumpWsRequest(request) {
|
||||
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.connector._socket!.clusterio_ignore_dump = true;
|
||||
this._controller.debugEvents.on("message", this.ws_dumper);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,39 @@
|
||||
"use strict";
|
||||
const compression = require("compression");
|
||||
const events = require("events");
|
||||
const express = require("express");
|
||||
const fs = require("fs-extra");
|
||||
const http = require("http");
|
||||
const https = require("https");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const path = require("path");
|
||||
const stream = require("stream");
|
||||
|
||||
const lib = require("@clusterio/lib");
|
||||
const { logger } = lib;
|
||||
|
||||
const HttpCloser = require("./HttpCloser");
|
||||
const InstanceInfo = require("./InstanceInfo");
|
||||
const metrics = require("./metrics");
|
||||
const routes = require("./routes");
|
||||
const UserManager = require("./UserManager");
|
||||
const WsServer = require("./WsServer");
|
||||
import type winston from "winston";
|
||||
import type { AddressInfo } from "net";
|
||||
import type { ControllerArgs } from "../controller";
|
||||
import type { HostInfo } from "./HostConnection";
|
||||
import type { Request, Response, Application } from "express";
|
||||
|
||||
|
||||
const endpointDurationSummary = new lib.Summary(
|
||||
import compression from "compression";
|
||||
import events, { EventEmitter } from "events";
|
||||
import express from "express";
|
||||
import fs from "fs-extra";
|
||||
import http from "http";
|
||||
import https from "https";
|
||||
import jwt from "jsonwebtoken";
|
||||
import path from "path";
|
||||
import stream from "stream";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
const { logger, Summary, Gauge } = lib;
|
||||
|
||||
import HttpCloser from "./HttpCloser";
|
||||
import InstanceInfo, { InstanceStatus } from "./InstanceInfo";
|
||||
import metrics from "./metrics";
|
||||
import * as routes from "./routes";
|
||||
import UserManager from "./UserManager";
|
||||
import WsServer from "./WsServer";
|
||||
import yargs from "yargs";
|
||||
import HostConnection from "./HostConnection";
|
||||
|
||||
const endpointDurationSummary = new Summary(
|
||||
"clusterio_controller_http_endpoint_duration_seconds",
|
||||
"Time it took to respond to a an HTTP request",
|
||||
{ labels: ["route"] }
|
||||
);
|
||||
|
||||
const logSizeGauge = new lib.Gauge(
|
||||
const logSizeGauge = new Gauge(
|
||||
"clusterio_controller_log_bytes",
|
||||
"Size of all log files currently stored on the controller."
|
||||
);
|
||||
@@ -35,86 +42,73 @@ const logSizeGauge = new lib.Gauge(
|
||||
* Manages all controller related operations
|
||||
* @alias module:controller/src/Controller
|
||||
*/
|
||||
class Controller {
|
||||
constructor(clusterLogger, pluginInfos, configPath, config) {
|
||||
export default class Controller {
|
||||
clusterLogger: winston.Logger;
|
||||
/** Array of plugin info objects for known plugins */
|
||||
pluginInfos: lib.PluginInfo[];
|
||||
configPath: string;
|
||||
/** Controller config. */
|
||||
config: lib.ControllerConfig;
|
||||
app: Application;
|
||||
|
||||
/** Mapping of host id to host info */
|
||||
hosts: Map<number, HostInfo> | null = null;
|
||||
/** Mapping of instance id to instance info */
|
||||
instances: Map<number, InstanceInfo> | null = null;
|
||||
/** Mapping of mod pack id to mod pack */
|
||||
modPacks: Map<number, lib.ModPack> | null = null;
|
||||
/** Mapping of mod names to mod infos */
|
||||
mods: Map<string, lib.ModInfo> | null = null;
|
||||
/** User and roles manager for the cluster */
|
||||
userManager: UserManager;
|
||||
|
||||
httpServer: http.Server | null = null;
|
||||
httpServerCloser: HttpCloser | null = null;
|
||||
httpsServer: https.Server | null = null;
|
||||
httpsServerCloser: HttpCloser | null = null;
|
||||
|
||||
/** Mapping of plugin name to loaded plugin */
|
||||
plugins: Map<string, lib.BaseControllerPlugin> = new Map();
|
||||
|
||||
/** WebSocket server */
|
||||
wsServer: WsServer;
|
||||
debugEvents: events.EventEmitter = new events.EventEmitter();
|
||||
private _events: events.EventEmitter = new events.EventEmitter();
|
||||
|
||||
// Possible states are new, starting, running, stopping, stopped
|
||||
private _state: string = "new";
|
||||
private _shouldStop: boolean = false;
|
||||
_fallbackedRequests: Map<lib.RequestClass<unknown, unknown>, lib.RequestHandler<unknown, unknown>> = new Map();
|
||||
_registeredRequests: Map<lib.RequestClass<unknown, unknown>, lib.RequestHandler<unknown, unknown>> = new Map();
|
||||
_registeredEvents = new Map();
|
||||
_snoopedEvents = new Map();
|
||||
|
||||
devMiddleware: any | null = null;
|
||||
|
||||
logDirectory: string = "";
|
||||
clusterLogIndex: lib.LogIndex | null = null;
|
||||
clusterLogBuildInterval: NodeJS.Timer | null = null;
|
||||
|
||||
constructor(
|
||||
clusterLogger: winston.Logger,
|
||||
pluginInfos: any[],
|
||||
configPath: string,
|
||||
config: lib.ControllerConfig
|
||||
) {
|
||||
this.clusterLogger = clusterLogger;
|
||||
/**
|
||||
* Array of plugin info objects for known plugins
|
||||
* @type {Array<object>}
|
||||
*/
|
||||
this.pluginInfos = pluginInfos;
|
||||
this.configPath = configPath;
|
||||
/**
|
||||
* Controller config.
|
||||
* @type {module:lib.ControllerConfig}
|
||||
*/
|
||||
this.config = config;
|
||||
|
||||
this.app = express();
|
||||
this.app.locals.controller = this;
|
||||
this.app.locals.streams = new Map();
|
||||
|
||||
/**
|
||||
* Mapping of host id to host info
|
||||
* @type {Map<number, Object>}
|
||||
*/
|
||||
this.hosts = null;
|
||||
|
||||
/**
|
||||
* Mapping of instance id to instance info
|
||||
* @type {Map<number, module:controller/src/InstanceInfo>}
|
||||
*/
|
||||
this.instances = null;
|
||||
|
||||
/**
|
||||
* Mapping of mod pack id to mod pack
|
||||
* @type {Map<number, module:lib.ModPack>}
|
||||
*/
|
||||
this.modPacks = null;
|
||||
|
||||
/**
|
||||
* Mapping of mod names to mod infos
|
||||
* @type {Map<string, module:lib.ModInfo>}
|
||||
*/
|
||||
this.mods = null;
|
||||
|
||||
/**
|
||||
* User and roles manager for the cluster
|
||||
* @type {module:controller/src/UserManager}
|
||||
*/
|
||||
this.userManager = null;
|
||||
this.httpServer = null;
|
||||
this.httpServerCloser = null;
|
||||
this.httpsServer = null;
|
||||
this.httpsServerCloser = null;
|
||||
|
||||
/**
|
||||
* Mapping of plugin name to loaded plugin
|
||||
* @type {Map<string, module:lib.BaseControllerPlugin>}
|
||||
*/
|
||||
this.plugins = new Map();
|
||||
|
||||
/**
|
||||
* WebSocket server
|
||||
* @type {module:controller/src/WsServer}
|
||||
*/
|
||||
this.wsServer = new WsServer(this);
|
||||
|
||||
this.debugEvents = new events.EventEmitter();
|
||||
this._events = new events.EventEmitter();
|
||||
// Possible states are new, starting, running, stopping, stopped
|
||||
this._state = "new";
|
||||
this._shouldStop = false;
|
||||
|
||||
this._fallbackedRequests = new Map();
|
||||
this._registeredRequests = new Map();
|
||||
this._registeredEvents = new Map();
|
||||
this._snoopedEvents = new Map();
|
||||
|
||||
this.devMiddleware = null;
|
||||
this.userManager = new UserManager(this.config);
|
||||
}
|
||||
|
||||
async start(args) {
|
||||
async start(args: ControllerArgs) {
|
||||
if (this._state !== "new") {
|
||||
throw new Error(`Cannot start in state ${this._state}`);
|
||||
}
|
||||
@@ -148,13 +142,16 @@ class Controller {
|
||||
])).reduce((a, v) => a + v, 0);
|
||||
}
|
||||
|
||||
async _startInternal(args) {
|
||||
async _startInternal(args: ControllerArgs) {
|
||||
this.logDirectory = args.logDirectory;
|
||||
this.clusterLogIndex = await lib.LogIndex.load(path.join(this.logDirectory, "cluster"));
|
||||
|
||||
this.clusterLogBuildInterval = setInterval(() => {
|
||||
this.clusterLogIndex.buildIndex().catch(
|
||||
err => logger.error(`Error building cluster log index:\n${err.stack}`)
|
||||
);
|
||||
if (this.clusterLogIndex) {
|
||||
this.clusterLogIndex.buildIndex().catch(
|
||||
err => logger.error(`Error building cluster log index:\n${err.stack}`)
|
||||
);
|
||||
}
|
||||
}, 600e3);
|
||||
logSizeGauge.callback = async () => { logSizeGauge.set(await this.logSize()); };
|
||||
|
||||
@@ -165,6 +162,7 @@ class Controller {
|
||||
const webpack = require("webpack");
|
||||
const webpackDevMiddleware = require("webpack-dev-middleware");
|
||||
const webpackConfigs = [];
|
||||
|
||||
if (args.dev) {
|
||||
webpackConfigs.push(require("../webpack.config")({}));
|
||||
}
|
||||
@@ -194,7 +192,6 @@ class Controller {
|
||||
this.hosts = await Controller.loadHosts(path.join(databaseDirectory, "hosts.json"));
|
||||
this.instances = await Controller.loadInstances(path.join(databaseDirectory, "instances.json"));
|
||||
this.modPacks = await Controller.loadModPacks(path.join(databaseDirectory, "mod-packs.json"));
|
||||
this.userManager = new UserManager(this.config);
|
||||
await this.userManager.load(path.join(databaseDirectory, "users.json"));
|
||||
|
||||
let modsDirectory = this.config.get("controller.mods_directory");
|
||||
@@ -204,7 +201,7 @@ class Controller {
|
||||
this.config.on("fieldChanged", (group, field, prev) => {
|
||||
lib.invokeHook(this.plugins, "onControllerConfigFieldChanged", group, field, prev);
|
||||
});
|
||||
for (let instance of this.instances.values()) {
|
||||
for (let instance of this.instances!.values()) {
|
||||
this.addInstanceHooks(instance);
|
||||
}
|
||||
|
||||
@@ -230,7 +227,9 @@ class Controller {
|
||||
Controller.addAppRoutes(this.app, this.pluginInfos);
|
||||
|
||||
if (!args.dev) {
|
||||
let manifest = await Controller.loadJsonObject(path.join(__dirname, "..", "dist", "web", "manifest.json"));
|
||||
let manifestPath = path.join(__dirname, "..", "..", "web", "manifest.json")
|
||||
|
||||
let manifest = await Controller.loadJsonObject(manifestPath);
|
||||
if (!manifest["main.js"]) {
|
||||
logger.error("Missing main.js entry in dist/web/manifest.json");
|
||||
}
|
||||
@@ -247,16 +246,16 @@ class Controller {
|
||||
this.httpServer = http.createServer(this.app);
|
||||
this.httpServerCloser = new HttpCloser(this.httpServer);
|
||||
await this.listen(this.httpServer, httpPort, bindAddress);
|
||||
logger.info(`Listening for HTTP on port ${this.httpServer.address().port}`);
|
||||
logger.info(`Listening for HTTP on port ${(this.httpServer.address() as AddressInfo).port}`);
|
||||
}
|
||||
|
||||
if (httpsPort) {
|
||||
if (httpsPort && tls_cert && tls_key) {
|
||||
let certificate, privateKey;
|
||||
try {
|
||||
certificate = await fs.readFile(tls_cert);
|
||||
privateKey = await fs.readFile(tls_key);
|
||||
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
throw new lib.StartupError(
|
||||
`Error loading ssl certificate: ${err.message}`
|
||||
);
|
||||
@@ -268,7 +267,7 @@ class Controller {
|
||||
}, this.app);
|
||||
this.httpsServerCloser = new HttpCloser(this.httpsServer);
|
||||
await this.listen(this.httpsServer, httpsPort, bindAddress);
|
||||
logger.info(`Listening for HTTPS on port ${this.httpsServer.address().port}`);
|
||||
logger.info(`Listening for HTTPS on port ${(this.httpsServer.address() as AddressInfo).port}`);
|
||||
}
|
||||
|
||||
logger.info("Started controller");
|
||||
@@ -299,7 +298,11 @@ class Controller {
|
||||
// This function should never throw.
|
||||
this._state = "stopping";
|
||||
logger.info("Stopping controller");
|
||||
clearInterval(this.clusterLogBuildInterval);
|
||||
|
||||
if (this.clusterLogBuildInterval) {
|
||||
clearInterval(this.clusterLogBuildInterval);
|
||||
}
|
||||
|
||||
if (this.clusterLogIndex) {
|
||||
await this.clusterLogIndex.save();
|
||||
}
|
||||
@@ -336,23 +339,26 @@ class Controller {
|
||||
|
||||
let stopTasks = [];
|
||||
logger.info("Stopping HTTP(S) server");
|
||||
if (this.httpServer && this.httpServer.listening) { stopTasks.push(this.httpServerCloser.close()); }
|
||||
if (this.httpsServer && this.httpsServer.listening) { stopTasks.push(this.httpsServerCloser.close()); }
|
||||
if (this.httpServer && this.httpServer.listening && this.httpServerCloser) {
|
||||
stopTasks.push(this.httpServerCloser.close());
|
||||
}
|
||||
if (this.httpsServer && this.httpsServer.listening && this.httpsServerCloser) {
|
||||
stopTasks.push(this.httpsServerCloser.close());
|
||||
}
|
||||
await Promise.all(stopTasks);
|
||||
|
||||
logger.info("Goodbye");
|
||||
}
|
||||
|
||||
static async loadHosts(filePath) {
|
||||
let serialized;
|
||||
static async loadHosts(filePath: string): Promise<Map<number, HostInfo>> {
|
||||
let serialized: [number, HostInfo][];
|
||||
try {
|
||||
serialized = JSON.parse(await fs.readFile(filePath));
|
||||
serialized = JSON.parse(await fs.readFile(filePath, { encoding: 'utf8' }));
|
||||
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.code !== "ENOENT") {
|
||||
throw err;
|
||||
}
|
||||
|
||||
return new Map();
|
||||
}
|
||||
|
||||
@@ -364,60 +370,64 @@ class Controller {
|
||||
return new Map(serialized);
|
||||
}
|
||||
|
||||
static async saveHosts(filePath, hosts) {
|
||||
static async saveHosts(filePath: string, hosts: Map<number, HostInfo>) {
|
||||
await lib.safeOutputFile(filePath, JSON.stringify([...hosts.entries()], null, 4));
|
||||
}
|
||||
|
||||
static async loadInstances(filePath) {
|
||||
static async loadInstances(filePath: string): Promise<Map<number, InstanceInfo>> {
|
||||
logger.info(`Loading ${filePath}`);
|
||||
|
||||
let instances = new Map();
|
||||
try {
|
||||
let serialized = JSON.parse(await fs.readFile(filePath));
|
||||
let serialized = JSON.parse(await fs.readFile(filePath, { encoding: "utf8" }));
|
||||
for (let serializedConfig of serialized) {
|
||||
let instanceConfig = new lib.InstanceConfig("controller");
|
||||
await instanceConfig.load(serializedConfig);
|
||||
let status = instanceConfig.get("instance.assigned_host") === null ? "unassigned" : "unknown";
|
||||
let instance = new InstanceInfo({ config: instanceConfig, status });
|
||||
let instance = new InstanceInfo({
|
||||
config: instanceConfig,
|
||||
status: status as InstanceStatus,
|
||||
});
|
||||
instances.set(instanceConfig.get("instance.id"), instance);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.code !== "ENOENT") {
|
||||
throw err;
|
||||
}
|
||||
return new Map();
|
||||
}
|
||||
|
||||
return instances;
|
||||
}
|
||||
|
||||
static async saveInstances(filePath, instances) {
|
||||
static async saveInstances(filePath: string, instances: Map<number, InstanceInfo>) {
|
||||
let serialized = [];
|
||||
for (let instance of instances.values()) {
|
||||
for (const instance of instances.values()) {
|
||||
serialized.push(instance.config.serialize());
|
||||
}
|
||||
|
||||
await lib.safeOutputFile(filePath, JSON.stringify(serialized, null, 4));
|
||||
}
|
||||
|
||||
static async loadModPacks(filePath) {
|
||||
static async loadModPacks(filePath: string): Promise<Map<number, lib.ModPack>> {
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(await fs.readFile(filePath));
|
||||
} catch (err) {
|
||||
json = JSON.parse(await fs.readFile(filePath, { encoding : "utf8" }));
|
||||
} catch (err: any) {
|
||||
if (err.code !== "ENOENT") {
|
||||
throw err;
|
||||
}
|
||||
return new Map();
|
||||
}
|
||||
return new Map(json.map(e => [e.id, lib.ModPack.fromJSON(e)]));
|
||||
return new Map(json.map((e: any) => [e.id, lib.ModPack.fromJSON(e)]));
|
||||
}
|
||||
|
||||
static async saveModPacks(filePath, modPacks) {
|
||||
static async saveModPacks(filePath: string, modPacks: Map<number, lib.ModPack>) {
|
||||
await lib.safeOutputFile(filePath, JSON.stringify([...modPacks.values()], null, 4));
|
||||
}
|
||||
|
||||
static async loadModInfos(modsDirectory) {
|
||||
static async loadModInfos(modsDirectory: string): Promise<Map<string,lib.ModInfo>> {
|
||||
let mods = new Map();
|
||||
for (let entry of await fs.readdir(modsDirectory, { withFileTypes: true })) {
|
||||
if (entry.isDirectory()) {
|
||||
@@ -433,7 +443,7 @@ class Controller {
|
||||
let modInfo;
|
||||
try {
|
||||
modInfo = await lib.ModInfo.fromModFile(path.join(modsDirectory, entry.name));
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
logger.error(`Error loading mod ${entry.name}: ${err.message}`);
|
||||
continue;
|
||||
}
|
||||
@@ -451,11 +461,11 @@ class Controller {
|
||||
return mods;
|
||||
}
|
||||
|
||||
static async loadJsonObject(filePath, throwOnMissing = false) {
|
||||
static async loadJsonObject(filePath: string, throwOnMissing: boolean = false): Promise<any> {
|
||||
let manifest = {};
|
||||
try {
|
||||
manifest = JSON.parse(await fs.readFile(filePath));
|
||||
} catch (err) {
|
||||
manifest = JSON.parse(await fs.readFile(filePath, { encoding: 'utf8' }));
|
||||
} catch (err: any) {
|
||||
if (!throwOnMissing && err.code !== "ENOENT") {
|
||||
throw err;
|
||||
}
|
||||
@@ -463,16 +473,16 @@ class Controller {
|
||||
return manifest;
|
||||
}
|
||||
|
||||
async deleteMod(name, version) {
|
||||
async deleteMod(name: string, version: string): Promise<void> {
|
||||
let filename = `${name}_${version}.zip`;
|
||||
let mod = this.mods.get(filename);
|
||||
let mod = this.mods!.get(filename);
|
||||
if (!mod) {
|
||||
throw new Error(`Mod ${filename} does not exist`);
|
||||
}
|
||||
|
||||
let modsDirectory = this.config.get("controller.mods_directory");
|
||||
await fs.unlink(path.join(modsDirectory, filename));
|
||||
this.mods.delete(filename);
|
||||
this.mods!.delete(filename);
|
||||
mod.isDeleted = true;
|
||||
this.modUpdated(mod);
|
||||
}
|
||||
@@ -480,32 +490,32 @@ class Controller {
|
||||
/**
|
||||
* Query controller log
|
||||
*
|
||||
* @param {module:lib~QueryLogFilter} filter -
|
||||
* @param filter -
|
||||
* Filter to limit entries with. Note that only the controller log can
|
||||
* be queried from this function.
|
||||
* @returns {Promise<Array<Object>>} log entries matching the filter
|
||||
* @returns log entries matching the filter
|
||||
*/
|
||||
async queryControllerLog(filter) {
|
||||
return await lib.queryLog(
|
||||
path.join(this.logDirectory, "controller"), filter,
|
||||
async queryControllerLog(filter: lib.QueryLogFilter): Promise<object[]> {
|
||||
return lib.queryLog(
|
||||
path.join(this.logDirectory, "controller"), filter, null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query cluster log
|
||||
*
|
||||
* @param {module:lib~QueryLogFilter} filter -
|
||||
* @param filter -
|
||||
* Filter to limit entries with.
|
||||
* @returns {Promise<Array<Object>>} log entries matching the filter
|
||||
* @returns log entries matching the filter
|
||||
*/
|
||||
async queryClusterLog(filter) {
|
||||
async queryClusterLog(filter: lib.QueryLogFilter): Promise<object[]> {
|
||||
return await lib.queryLog(
|
||||
path.join(this.logDirectory, "cluster"), filter, this.clusterLogIndex,
|
||||
);
|
||||
}
|
||||
|
||||
static addAppRoutes(app, pluginInfos) {
|
||||
app.use((req, res, next) => {
|
||||
static addAppRoutes(app: Application, pluginInfos: any[]) {
|
||||
app.use((req: Request, res: Response, next) => {
|
||||
let start = process.hrtime.bigint();
|
||||
stream.finished(res, () => {
|
||||
let routePath = "static";
|
||||
@@ -521,7 +531,7 @@ class Controller {
|
||||
|
||||
// Set folder to serve static content from (the website)
|
||||
const staticOptions = { immutable: true, maxAge: 1000 * 86400 * 365 };
|
||||
app.use("/static", express.static(path.join(__dirname, "..", "dist", "web", "static"), staticOptions));
|
||||
app.use("/static", express.static(path.join(__dirname, "..", "..", "..", "dist", "web", "static"), staticOptions));
|
||||
app.use("/static", express.static("static", staticOptions)); // Used for data export files
|
||||
|
||||
// Add API routes
|
||||
@@ -545,17 +555,17 @@ class Controller {
|
||||
/**
|
||||
* Generate access token for host
|
||||
*
|
||||
* @param {number} hostId - ID of host to generate a token for.
|
||||
* @returns {string} access token for host
|
||||
* @returns access token for host
|
||||
* @param hostId - ID of host to generate a token for.
|
||||
*/
|
||||
generateHostToken(hostId) {
|
||||
generateHostToken(hostId: number): string {
|
||||
return jwt.sign(
|
||||
{ aud: "host", host: hostId },
|
||||
Buffer.from(this.config.get("controller.auth_secret"), "base64")
|
||||
);
|
||||
}
|
||||
|
||||
hostUpdated(host) {
|
||||
hostUpdated(host: HostInfo) {
|
||||
let update = new lib.HostDetails(
|
||||
host.agent,
|
||||
host.version,
|
||||
@@ -577,12 +587,11 @@ class Controller {
|
||||
/**
|
||||
* Get instance by ID for a request
|
||||
*
|
||||
* @param {number} instanceId - ID of instance to get.
|
||||
* @returns {object} instance
|
||||
* @param instanceId - ID of instance to get.
|
||||
* @throws {module:lib.RequestError} if the instance does not exist.
|
||||
*/
|
||||
getRequestInstance(instanceId) {
|
||||
let instance = this.instances.get(instanceId);
|
||||
getRequestInstance(instanceId:number):InstanceInfo {
|
||||
let instance = this.instances!.get(instanceId);
|
||||
if (!instance) {
|
||||
throw new lib.RequestError(`Instance with ID ${instanceId} does not exist`);
|
||||
}
|
||||
@@ -602,13 +611,13 @@ class Controller {
|
||||
* let instance = await controller.instanceAssign(instanceConfig);
|
||||
* await controller.instanceAssign(instance.id, hostId);
|
||||
*
|
||||
* @param {module:lib.InstanceConfig} instanceConfig -
|
||||
* @param instanceConfig -
|
||||
* Config to base newly created instance on.
|
||||
* @returns {module:controller/src/InstanceInfo} The created instance
|
||||
* @returns The created instance
|
||||
*/
|
||||
async instanceCreate(instanceConfig) {
|
||||
async instanceCreate(instanceConfig: lib.InstanceConfig): Promise<InstanceInfo> {
|
||||
let instanceId = instanceConfig.get("instance.id");
|
||||
if (this.instances.has(instanceId)) {
|
||||
if (this.instances!.has(instanceId)) {
|
||||
throw new lib.RequestError(`Instance with ID ${instanceId} already exists`);
|
||||
}
|
||||
|
||||
@@ -639,9 +648,10 @@ class Controller {
|
||||
instanceConfig.set("factorio.settings", settings);
|
||||
|
||||
let instance = new InstanceInfo({ config: instanceConfig, status: "unassigned" });
|
||||
this.instances.set(instanceId, instance);
|
||||
this.instances!.set(instanceId, instance);
|
||||
await lib.invokeHook(this.plugins, "onInstanceStatusChanged", instance, null);
|
||||
this.addInstanceHooks(instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -654,15 +664,15 @@ class Controller {
|
||||
* Note: this will not transfer any files or saves stored on the host
|
||||
* the instance was previously assgined to the new one.
|
||||
*
|
||||
* @param {number} instanceId - ID of Instance to assign.
|
||||
* @param {number} hostId - ID of host to assign instance to.
|
||||
* @param instanceId - ID of Instance to assign.
|
||||
* @param hostId - ID of host to assign instance to.
|
||||
*/
|
||||
async instanceAssign(instanceId, hostId) {
|
||||
async instanceAssign(instanceId: number, hostId?: number) {
|
||||
let instance = this.getRequestInstance(instanceId);
|
||||
|
||||
// Check if target host is connected
|
||||
let newHostConnection;
|
||||
if (hostId !== null) {
|
||||
let newHostConnection: HostConnection | undefined;
|
||||
if (hostId !== undefined) {
|
||||
newHostConnection = this.wsServer.hostConnections.get(hostId);
|
||||
if (!newHostConnection) {
|
||||
// The case of the host not getting the assign instance message
|
||||
@@ -685,7 +695,7 @@ class Controller {
|
||||
|
||||
// Assign to target
|
||||
instance.config.set("instance.assigned_host", hostId);
|
||||
if (hostId !== null) {
|
||||
if (hostId !== null && newHostConnection) {
|
||||
await newHostConnection.send(
|
||||
new lib.InstanceAssignInternalRequest(instanceId, instance.config.serialize("host"))
|
||||
);
|
||||
@@ -701,15 +711,15 @@ class Controller {
|
||||
* Permanently deletes the instance from its assigned host (if assigned)
|
||||
* and the controller. This action cannot be undone.
|
||||
*
|
||||
* @param {number} instanceId - ID of instance to delete.
|
||||
* @param instanceId - ID of instance to delete.
|
||||
*/
|
||||
async instanceDelete(instanceId) {
|
||||
async instanceDelete(instanceId: number) {
|
||||
let instance = this.getRequestInstance(instanceId);
|
||||
let hostId = instance.config.get("instance.assigned_host");
|
||||
if (hostId !== null) {
|
||||
await this.sendTo({ hostId }, new lib.InstanceDeleteInternalRequest(instanceId));
|
||||
}
|
||||
this.instances.delete(instanceId);
|
||||
this.instances!.delete(instanceId);
|
||||
|
||||
let prev = instance.status;
|
||||
instance.status = "deleted";
|
||||
@@ -723,10 +733,10 @@ class Controller {
|
||||
* Used to push config changes to the assigned host when the config of
|
||||
* an instance has changed.
|
||||
*
|
||||
* @param {module:controller/src/InstanceInfo} instance -
|
||||
* @param instance -
|
||||
* Instance to nodify the config updated.
|
||||
*/
|
||||
async instanceConfigUpdated(instance) {
|
||||
async instanceConfigUpdated(instance: InstanceInfo) {
|
||||
let hostId = instance.config.get("instance.assigned_host");
|
||||
if (hostId !== null) {
|
||||
let connection = this.wsServer.hostConnections.get(hostId);
|
||||
@@ -738,8 +748,8 @@ class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
addInstanceHooks(instance) {
|
||||
instance.config.on("fieldChanged", (group, field, prev) => {
|
||||
addInstanceHooks(instance: InstanceInfo) {
|
||||
instance.config.on("fieldChanged", (group: lib.ConfigGroup, field: string, prev: any) => {
|
||||
if (group.name === "instance" && field === "name") {
|
||||
this.instanceUpdated(instance);
|
||||
}
|
||||
@@ -750,7 +760,7 @@ class Controller {
|
||||
this.instanceUpdated(instance);
|
||||
}
|
||||
|
||||
instanceUpdated(instance) {
|
||||
instanceUpdated(instance: InstanceInfo) {
|
||||
for (let controlConnection of this.wsServer.controlConnections.values()) {
|
||||
if (controlConnection.connector.closing) {
|
||||
continue;
|
||||
@@ -760,7 +770,7 @@ class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
saveListUpdate(instanceId, saves) {
|
||||
saveListUpdate(instanceId: number, saves: lib.SaveDetails[]) {
|
||||
for (let controlConnection of this.wsServer.controlConnections.values()) {
|
||||
if (controlConnection.connector.closing) {
|
||||
continue;
|
||||
@@ -770,7 +780,7 @@ class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
modPackUpdated(modPack) {
|
||||
modPackUpdated(modPack: lib.ModPack) {
|
||||
for (let controlConnection of this.wsServer.controlConnections.values()) {
|
||||
if (controlConnection.connector.closing) {
|
||||
continue;
|
||||
@@ -781,7 +791,7 @@ class Controller {
|
||||
lib.invokeHook(this.plugins, "onModPackUpdated", modPack);
|
||||
}
|
||||
|
||||
modUpdated(mod) {
|
||||
modUpdated(mod: lib.ModInfo) {
|
||||
for (let controlConnection of this.wsServer.controlConnections.values()) {
|
||||
if (controlConnection.connector.closing) {
|
||||
continue;
|
||||
@@ -793,7 +803,7 @@ class Controller {
|
||||
lib.invokeHook(this.plugins, "onModUpdated", mod);
|
||||
}
|
||||
|
||||
userUpdated(user) {
|
||||
userUpdated(user: lib.User) {
|
||||
for (let controlConnection of this.wsServer.controlConnections.values()) {
|
||||
if (controlConnection.connector.closing) {
|
||||
continue;
|
||||
@@ -806,9 +816,9 @@ class Controller {
|
||||
/**
|
||||
* Notify connected control clients under the given user that the
|
||||
* permissions for this user may have changed.
|
||||
* @param {module:lib.User} user - User permisions updated for.
|
||||
* @param user - User permisions updated for.
|
||||
*/
|
||||
userPermissionsUpdated(user) {
|
||||
userPermissionsUpdated(user: lib.User) {
|
||||
for (let controlConnection of this.wsServer.controlConnections.values()) {
|
||||
if (controlConnection.user === user) {
|
||||
controlConnection.send(
|
||||
@@ -825,9 +835,9 @@ class Controller {
|
||||
/**
|
||||
* Notify connected control clients with the given role that the
|
||||
* permissions may have changed.
|
||||
* @param {module:lib.Role} role - Role permisions updated for.
|
||||
* @param role - Role permisions updated for.
|
||||
*/
|
||||
rolePermissionsUpdated(role) {
|
||||
rolePermissionsUpdated(role: lib.Role) {
|
||||
for (let controlConnection of this.wsServer.controlConnections.values()) {
|
||||
if (controlConnection.user.roles.has(role)) {
|
||||
controlConnection.send(
|
||||
@@ -862,11 +872,12 @@ class Controller {
|
||||
ControllerPluginClass = await lib.loadControllerPluginClass(pluginInfo);
|
||||
}
|
||||
|
||||
//@ts-ignore //TODO::: metrics is lib.Counter but ControllerPluginClass expect any[], can't figure out how the metrics is used.
|
||||
let controllerPlugin = new ControllerPluginClass(pluginInfo, this, metrics, logger);
|
||||
await controllerPlugin.init();
|
||||
this.plugins.set(pluginInfo.name, controllerPlugin);
|
||||
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
throw new lib.PluginError(pluginInfo.name, err);
|
||||
}
|
||||
|
||||
@@ -883,14 +894,14 @@ class Controller {
|
||||
* @param {*} args - Arguments to the .listen() call on the server.
|
||||
* @returns {Promise} promise that resolves the server is listening.
|
||||
*/
|
||||
listen(server, ...args) {
|
||||
listen(server: http.Server|https.Server, ...args: any[]): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
server.on("upgrade", (req, socket, head) => {
|
||||
logger.verbose("handling WebSocket upgrade");
|
||||
this.wsServer.handleUpgrade(req, socket, head);
|
||||
});
|
||||
|
||||
function wrapError(err) {
|
||||
function wrapError(err: any) {
|
||||
reject(new lib.StartupError(
|
||||
`Server listening failed: ${err.message}`
|
||||
));
|
||||
@@ -908,13 +919,13 @@ class Controller {
|
||||
/**
|
||||
* Returns the URL needed to connect to the controller.
|
||||
*
|
||||
* @returns {string} controller URL.
|
||||
* @returns controller URL.
|
||||
*/
|
||||
getControllerUrl() {
|
||||
getControllerUrl(): string {
|
||||
return Controller.calculateControllerUrl(this.config);
|
||||
}
|
||||
|
||||
static calculateControllerUrl(config) {
|
||||
static calculateControllerUrl(config: lib.ControllerConfig) {
|
||||
let url = config.get("controller.external_address");
|
||||
if (!url) {
|
||||
if (config.get("controller.https_port")) {
|
||||
@@ -932,24 +943,25 @@ class Controller {
|
||||
* @param {string} route - route the interface is served under.
|
||||
* @returns {function} Experess.js route handler.
|
||||
*/
|
||||
static serveWeb(route) {
|
||||
static serveWeb(route: string) {
|
||||
// The depth is is the number of slashes in the route minus one, but due
|
||||
// to lenient matching on trailing slashes in the route we need to
|
||||
// compensate if the request path contains a slash but not the route,
|
||||
// and vice versa.
|
||||
let routeDepth = (route.match(/\//g) || []).length - 1 - (route.slice(-1) === "/");
|
||||
return function(req, res, next) {
|
||||
let depth = routeDepth + (req.path.slice(-1) === "/");
|
||||
let routeDepth = (route.match(/\//g) || []).length - 1 - Number(route.slice(-1) === "/");
|
||||
return function(req: Request, res: Response, next: Function) {
|
||||
let depth = routeDepth + Number(req.path.slice(-1) === "/");
|
||||
let webRoot = "../".repeat(depth) || "./";
|
||||
let staticRoot = webRoot;
|
||||
let mainBundle;
|
||||
let mainBundle: string = "";
|
||||
if (res.app.locals.mainBundle) {
|
||||
mainBundle = res.app.locals.mainBundle;
|
||||
} else {
|
||||
let stats = res.locals.webpack.devMiddleware.stats.stats[0];
|
||||
mainBundle = stats.toJson().assetsByChunkName["main"];
|
||||
}
|
||||
fs.readFile(path.join(__dirname, "..", "web", "index.html"), "utf8").then((content) => {
|
||||
|
||||
fs.readFile(path.join(__dirname, "..", "..", "..", "web", "index.html"), "utf8").then((content) => {
|
||||
res.type("text/html");
|
||||
res.send(content
|
||||
.replace(/__CLUSTER_NAME__/g, res.app.locals.controller.config.get("controller.name"))
|
||||
@@ -963,7 +975,7 @@ class Controller {
|
||||
};
|
||||
}
|
||||
|
||||
handle(Class, handler) {
|
||||
handle(Class: any, handler: any) {
|
||||
if (Class.type === "request") {
|
||||
this.handleRequest(Class, handler);
|
||||
} else if (Class.type === "event") {
|
||||
@@ -973,7 +985,7 @@ class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
handleRequest(Request, handler) {
|
||||
handleRequest(Request: any, handler: any) {
|
||||
if (!lib.Link._requestsByClass.has(Request)) {
|
||||
throw new Error(`Unregistered Request class ${Request.name}`);
|
||||
}
|
||||
@@ -983,7 +995,7 @@ class Controller {
|
||||
this._registeredRequests.set(Request, handler);
|
||||
}
|
||||
|
||||
fallbackRequest(Request, handler) {
|
||||
fallbackRequest(Request: any, handler: any) {
|
||||
if (!lib.Link._requestsByClass.has(Request)) {
|
||||
throw new Error(`Unregistered Request class ${Request.name}`);
|
||||
}
|
||||
@@ -993,7 +1005,7 @@ class Controller {
|
||||
this._fallbackedRequests.set(Request, handler);
|
||||
}
|
||||
|
||||
handleEvent(Event, handler) {
|
||||
handleEvent(Event: any, handler: any) {
|
||||
if (!lib.Link._eventsByClass.has(Event)) {
|
||||
throw new Error(`Unregistered Event class ${Event.name}`);
|
||||
}
|
||||
@@ -1003,7 +1015,7 @@ class Controller {
|
||||
this._registeredEvents.set(Event, handler);
|
||||
}
|
||||
|
||||
snoopEvent(Event, handler) {
|
||||
snoopEvent(Event: any, handler: any) {
|
||||
if (!lib.Link._eventsByClass.has(Event)) {
|
||||
throw new Error(`Unregistered Event class ${Event.name}`);
|
||||
}
|
||||
@@ -1020,13 +1032,13 @@ class Controller {
|
||||
* destination argument supports address shorthands, see {@link
|
||||
* module:lib.Address.fromShorthand}
|
||||
*
|
||||
* @param {*|module:lib.Address} address - Where to send it.
|
||||
* @param {*} requestOrEvent - The request or event to send.
|
||||
* @returns {Promise<*>|undefined}
|
||||
* @param address - Where to send it.
|
||||
* @param requestOrEvent - The request or event to send.
|
||||
* @returns
|
||||
* Promise that resolves to the response if a request was sent or
|
||||
* undefined if it was an event.
|
||||
*/
|
||||
sendTo(address, requestOrEvent) {
|
||||
sendTo(address: lib.AddressShorthand, requestOrEvent: any): Promise<any> | void {
|
||||
let dst = lib.Address.fromShorthand(address);
|
||||
if (requestOrEvent.constructor.type === "request") {
|
||||
return this.sendRequest(requestOrEvent, dst);
|
||||
@@ -1037,7 +1049,7 @@ class Controller {
|
||||
throw Error(`Unknown type ${requestOrEvent.constructor.type}.`);
|
||||
}
|
||||
|
||||
async sendRequest(request, dst) {
|
||||
async sendRequest(request: any, dst: lib.Address) {
|
||||
let connection;
|
||||
if (dst.type === lib.Address.controller) {
|
||||
throw new Error("controller as dst is not supported.");
|
||||
@@ -1072,7 +1084,7 @@ class Controller {
|
||||
return connection.sendRequest(request, dst);
|
||||
}
|
||||
|
||||
sendEvent(event, dst) {
|
||||
sendEvent<T>(event: lib.Event<T>, dst: lib.Address) {
|
||||
let connection;
|
||||
if (dst.type === lib.Address.controller) {
|
||||
throw new Error("controller as dst is not supported.");
|
||||
@@ -1081,7 +1093,7 @@ class Controller {
|
||||
connection = this.wsServer.controlConnections.get(dst.id);
|
||||
|
||||
} else if (dst.type === lib.Address.instance) {
|
||||
let instance = this.instances.get(dst.id);
|
||||
let instance = this.instances!.get(dst.id);
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
@@ -1119,7 +1131,7 @@ class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
sendToHostByInstanceId(requestOrEvent) {
|
||||
async sendToHostByInstanceId(requestOrEvent: any) {
|
||||
if (requestOrEvent.constructor.type === "request") {
|
||||
return this.sendRequestToHostByInstanceId(requestOrEvent);
|
||||
}
|
||||
@@ -1129,7 +1141,7 @@ class Controller {
|
||||
throw Error(`Unknown type ${requestOrEvent.constructor.type}.`);
|
||||
}
|
||||
|
||||
async sendRequestToHostByInstanceId(request) {
|
||||
async sendRequestToHostByInstanceId(request: any) {
|
||||
let instance = this.getRequestInstance(request.instanceId);
|
||||
let hostId = instance.config.get("instance.assigned_host");
|
||||
if (hostId === null) {
|
||||
@@ -1142,7 +1154,7 @@ class Controller {
|
||||
return await connection.send(request);
|
||||
}
|
||||
|
||||
sendEventToHostByInstanceId(event) {
|
||||
sendEventToHostByInstanceId(event: any) {
|
||||
let instance = this.getRequestInstance(event.instanceId);
|
||||
let hostId = instance.config.get("instance.assigned_host");
|
||||
if (hostId === null) {
|
||||
@@ -1155,5 +1167,3 @@ class Controller {
|
||||
connection.send(event);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Controller;
|
||||
+34
-31
@@ -1,39 +1,36 @@
|
||||
"use strict";
|
||||
const lib = require("@clusterio/lib");
|
||||
import type Controller from "./Controller";
|
||||
import type BaseConnection from "./BaseConnection";
|
||||
|
||||
class ControllerRouter {
|
||||
/** @type {module:controller/src/Controller} */
|
||||
controller;
|
||||
import * as lib from "@clusterio/lib";
|
||||
import HostConnection from "./HostConnection";
|
||||
import ControlConnection from "./ControlConnection";
|
||||
const { logger } = lib;
|
||||
|
||||
pendingRequests = new Map();
|
||||
export default class ControllerRouter {
|
||||
pendingRequests: Map<any, any> = new Map();
|
||||
|
||||
constructor(controller) {
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
addHostConnection(connection) {
|
||||
this.hostConnections.set(connection.id, connection);
|
||||
}
|
||||
|
||||
addControlConnection(connection) {
|
||||
this.controlConnections.set(connection.id, connection);
|
||||
}
|
||||
constructor(
|
||||
public controller: Controller
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param {module:lib.Link} origin - Source link of the message.
|
||||
* @param {module:lib.Message} message
|
||||
* Link the message originated from.
|
||||
* @param {boolean} hasFallback - true if fallback handling is available.
|
||||
* @returns {boolean} true if the message was handled
|
||||
* @param origin - Source link of the message.
|
||||
* @param message - Link the message originated from.
|
||||
* @param hasFallback - true if fallback handling is available.
|
||||
* @returns true if the message was handled
|
||||
*/
|
||||
forwardMessage(origin, message, hasFallback) {
|
||||
forwardMessage(
|
||||
origin: ControlConnection | HostConnection,
|
||||
message: lib.MessageSrcDst,
|
||||
hasFallback: boolean
|
||||
): boolean {
|
||||
if (!["request", "response", "responseError", "event"].includes(message.type)) {
|
||||
throw new Error(`Message type ${message.type} can't be forwarded`);
|
||||
}
|
||||
|
||||
let dst = message.dst;
|
||||
let nextHop;
|
||||
let msg;
|
||||
let dst: lib.Address = message.dst;
|
||||
let nextHop: ControlConnection | HostConnection | undefined;
|
||||
let msg: string = "";
|
||||
if (dst.type === lib.Address.broadcast) {
|
||||
this.broadcastMessage(origin, message);
|
||||
return true;
|
||||
@@ -43,7 +40,7 @@ class ControllerRouter {
|
||||
msg = `Host ${dst.id} is offline`;
|
||||
}
|
||||
} else if (dst.type === lib.Address.instance) {
|
||||
let instance = this.controller.instances.get(dst.id);
|
||||
let instance = this.controller.instances!.get(dst.id);
|
||||
if (!instance) {
|
||||
msg = `Instance ${dst.id} does not exist`;
|
||||
} else {
|
||||
@@ -67,7 +64,7 @@ class ControllerRouter {
|
||||
}
|
||||
|
||||
if (nextHop === origin) {
|
||||
msg = `Message would return back to sender ${origin.dst}.`;
|
||||
msg = `Message would return back to sender ${origin.connector.dst}.`;
|
||||
nextHop = undefined;
|
||||
}
|
||||
|
||||
@@ -85,7 +82,7 @@ class ControllerRouter {
|
||||
|
||||
if (nextHop) {
|
||||
if (message.type === "request") {
|
||||
nextHop.forwardRequest(message, origin);
|
||||
nextHop.forwardRequest(message as lib.MessageRequest, origin);
|
||||
} else {
|
||||
nextHop.connector.send(message);
|
||||
}
|
||||
@@ -96,7 +93,10 @@ class ControllerRouter {
|
||||
return true;
|
||||
}
|
||||
|
||||
broadcastMessage(origin, message) {
|
||||
broadcastMessage(
|
||||
origin: lib.Link,
|
||||
message: lib.MessageSrcDst,
|
||||
) {
|
||||
let dst = message.dst;
|
||||
if (message.type !== "event") {
|
||||
this.warnUnrouted(message, `Unexpected broadcast of ${message.type}`);
|
||||
@@ -117,7 +117,10 @@ class ControllerRouter {
|
||||
}
|
||||
}
|
||||
|
||||
warnUnrouted(message, msg) {
|
||||
warnUnrouted(
|
||||
message: lib.MessageSrcDst,
|
||||
msg: string
|
||||
) {
|
||||
let dst = message.dst;
|
||||
let baseMsg = `No destination for ${message.constructor.name} routed from ${message.src} to ${dst}`;
|
||||
logger.warn(msg ? `${baseMsg}: ${msg}.` : `${baseMsg}.`);
|
||||
+60
-35
@@ -1,19 +1,41 @@
|
||||
"use strict";
|
||||
const lib = require("@clusterio/lib");
|
||||
import type Controller from "./Controller";
|
||||
import type WsServerConnector from "./WsServerConnector";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
const { logger, PlayerStats } = lib;
|
||||
|
||||
const BaseConnection = require("./BaseConnection");
|
||||
const InstanceInfo = require("./InstanceInfo");
|
||||
import BaseConnection from "./BaseConnection";
|
||||
import InstanceInfo, { InstanceStatus } from "./InstanceInfo";
|
||||
|
||||
|
||||
export interface HostInfo {
|
||||
agent: string;
|
||||
id: number;
|
||||
name: string;
|
||||
version: string;
|
||||
plugins: [number, any][];
|
||||
public_address: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the connection to a host
|
||||
*
|
||||
* @extends module:controller/src/BaseConnection
|
||||
* @alias module:controller/src/HostConnection
|
||||
*/
|
||||
class HostConnection extends BaseConnection {
|
||||
constructor(registerData, connector, controller) {
|
||||
export default class HostConnection extends BaseConnection {
|
||||
declare connector: WsServerConnector;
|
||||
private _agent: any;
|
||||
private _id: any;
|
||||
private _name: any;
|
||||
private _version: any;
|
||||
plugins: Map<string, Object>;
|
||||
|
||||
constructor(
|
||||
registerData: any,
|
||||
connector: WsServerConnector,
|
||||
controller: Controller,
|
||||
) {
|
||||
super(connector, controller);
|
||||
|
||||
this._agent = registerData.agent;
|
||||
@@ -23,7 +45,7 @@ class HostConnection extends BaseConnection {
|
||||
this.plugins = new Map(Object.entries(registerData.plugins));
|
||||
this._checkPluginVersions();
|
||||
|
||||
this._controller.hosts.set(this._id, {
|
||||
this._controller.hosts!.set(this._id, {
|
||||
agent: this._agent,
|
||||
id: this._id,
|
||||
name: this._name,
|
||||
@@ -36,14 +58,14 @@ class HostConnection extends BaseConnection {
|
||||
// eslint-disable-next-line no-loop-func
|
||||
this.connector.on(event, () => {
|
||||
for (let plugin of this._controller.plugins.values()) {
|
||||
plugin.onHostConnectionEvent(this, event);
|
||||
plugin.onHostConnectionEvent(this, event as "connect"|"drop"|"resume"|"close");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.connector.on("close", () => {
|
||||
// Update status to unknown for instances on this host.
|
||||
for (let instance of this._controller.instances.values()) {
|
||||
for (let instance of this._controller.instances!.values()) {
|
||||
if (instance.config.get("instance.assigned_host") !== this._id) {
|
||||
continue;
|
||||
}
|
||||
@@ -62,7 +84,7 @@ class HostConnection extends BaseConnection {
|
||||
this.handle(lib.InstancePlayerUpdateEvent, this.handleInstancePlayerUpdateEvent.bind(this));
|
||||
}
|
||||
|
||||
validateIngress(message) {
|
||||
validateIngress(message: lib.MessageSrcDst) {
|
||||
let origin = this.connector.dst;
|
||||
switch (message.src.type) {
|
||||
case lib.Address.control:
|
||||
@@ -78,7 +100,7 @@ class HostConnection extends BaseConnection {
|
||||
break;
|
||||
|
||||
case lib.Address.instance:
|
||||
let instance = this._controller.instances.get(message.src.id);
|
||||
let instance = this._controller.instances!.get(message.src.id);
|
||||
if (!instance || instance.config.get("instance.assigned_host") !== this._id) {
|
||||
throw new lib.InvalidMessage(
|
||||
`Received message with invalid src ${message.src} from ${origin}`
|
||||
@@ -129,8 +151,8 @@ class HostConnection extends BaseConnection {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
async handleInstanceStatusChangedEvent(request) {
|
||||
let instance = this._controller.instances.get(request.instanceId);
|
||||
async handleInstanceStatusChangedEvent(request: lib.InstanceStatusChangedEvent) {
|
||||
let instance = this._controller.instances!.get(request.instanceId);
|
||||
|
||||
// It's possible to get updates from an instance that does not exist
|
||||
// or is not assigned to the host it originated from if it was
|
||||
@@ -151,16 +173,16 @@ class HostConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
let prev = instance.status;
|
||||
instance.status = request.status;
|
||||
instance.game_port = request.gamePort;
|
||||
instance.status = request.status as lib.InstanceStatus;
|
||||
instance.game_port = request.gamePort || null;
|
||||
logger.verbose(`Instance ${instance.config.get("instance.name")} State: ${instance.status}`);
|
||||
this._controller.instanceUpdated(instance);
|
||||
await lib.invokeHook(this._controller.plugins, "onInstanceStatusChanged", instance, prev);
|
||||
}
|
||||
|
||||
async handleInstancesUpdateRequest(request) {
|
||||
async handleInstancesUpdateRequest(request: lib.InstancesUpdateRequest) {
|
||||
// Push updated instance configs
|
||||
for (let instance of this._controller.instances.values()) {
|
||||
for (let instance of this._controller.instances!.values()) {
|
||||
if (instance.config.get("instance.assigned_host") === this._id) {
|
||||
await this.send(
|
||||
new lib.InstanceAssignInternalRequest(instance.id, instance.config.serialize("host"))
|
||||
@@ -171,9 +193,9 @@ class HostConnection extends BaseConnection {
|
||||
// Assign instances the host has but controller does not
|
||||
for (let instanceData of request.instances) {
|
||||
let instanceConfig = new lib.InstanceConfig("controller");
|
||||
await instanceConfig.load(instanceData.config, "host");
|
||||
await instanceConfig.load(instanceData.config as lib.SerializedConfig, "host");
|
||||
|
||||
let controllerInstance = this._controller.instances.get(instanceConfig.get("instance.id"));
|
||||
let controllerInstance = this._controller.instances!.get(instanceConfig.get("instance.id"));
|
||||
if (controllerInstance) {
|
||||
// Check if this instance is assigned somewhere else.
|
||||
if (controllerInstance.config.get("instance.assigned_host") !== this._id) {
|
||||
@@ -186,7 +208,7 @@ class HostConnection extends BaseConnection {
|
||||
// Already have this instance, update state instead
|
||||
if (controllerInstance.status !== instanceData.status) {
|
||||
let prev = controllerInstance.status;
|
||||
controllerInstance.status = instanceData.status;
|
||||
controllerInstance.status = instanceData.status as InstanceStatus;
|
||||
logger.verbose(`Instance ${instanceConfig.get("instance.name")} State: ${instanceData.status}`);
|
||||
this._controller.instanceUpdated(controllerInstance);
|
||||
await lib.invokeHook(
|
||||
@@ -197,8 +219,8 @@ class HostConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
instanceConfig.set("instance.assigned_host", this._id);
|
||||
let newInstance = new InstanceInfo({ config: instanceConfig, status: instanceData.status });
|
||||
this._controller.instances.set(instanceConfig.get("instance.id"), newInstance);
|
||||
let newInstance = new InstanceInfo({ config: instanceConfig, status: instanceData.status as InstanceStatus });
|
||||
this._controller.instances!.set(instanceConfig.get("instance.id"), newInstance);
|
||||
this._controller.addInstanceHooks(newInstance);
|
||||
await this.send(
|
||||
new lib.InstanceAssignInternalRequest(
|
||||
@@ -209,30 +231,30 @@ class HostConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
// Push lists to make sure they are in sync.
|
||||
let adminlist = [];
|
||||
let banlist = [];
|
||||
let whitelist = [];
|
||||
let adminlist: Set<string> = new Set();
|
||||
let banlist: Map<string, string> = new Map();
|
||||
let whitelist: Set<string> = new Set();
|
||||
|
||||
for (let user of this._controller.userManager.users.values()) {
|
||||
if (user.isAdmin) {
|
||||
adminlist.push(user.name);
|
||||
adminlist.add(user.name);
|
||||
}
|
||||
if (user.isBanned) {
|
||||
banlist.push([user.name, user.banReason]);
|
||||
banlist.set(user.name, user.banReason);
|
||||
}
|
||||
if (user.isWhitelisted) {
|
||||
whitelist.push(user.name);
|
||||
whitelist.add(user.name);
|
||||
}
|
||||
}
|
||||
|
||||
await this.send(new lib.SyncUserListsEvent(adminlist, banlist, whitelist));
|
||||
}
|
||||
|
||||
async handleInstanceSaveListUpdateEvent(event) {
|
||||
async handleInstanceSaveListUpdateEvent(event: lib.InstanceSaveListUpdateEvent) {
|
||||
this._controller.saveListUpdate(event.instanceId, event.saves);
|
||||
}
|
||||
|
||||
async handleLogMessageEvent(event) {
|
||||
async handleLogMessageEvent(event: lib.LogMessageEvent) {
|
||||
this._controller.clusterLogger.log({
|
||||
...event.info,
|
||||
host_id: this._id,
|
||||
@@ -240,7 +262,8 @@ class HostConnection extends BaseConnection {
|
||||
});
|
||||
}
|
||||
|
||||
async handleInstancePlayerUpdateEvent(event, src) {
|
||||
async handleInstancePlayerUpdateEvent(event: lib.InstancePlayerUpdateEvent, src: lib.Address) {
|
||||
|
||||
let instanceId = src.id;
|
||||
let user = this._controller.userManager.users.get(event.name);
|
||||
if (!user) {
|
||||
@@ -252,11 +275,15 @@ class HostConnection extends BaseConnection {
|
||||
} else if (event.type === "leave") {
|
||||
user.notifyLeave(instanceId);
|
||||
}
|
||||
user.instanceStats.set(instanceId, new PlayerStats(event.stats));
|
||||
|
||||
if (event.stats) {
|
||||
user.instanceStats.set(instanceId, event.stats);
|
||||
}
|
||||
|
||||
user.recalculatePlayerStats();
|
||||
this._controller.userUpdated(user);
|
||||
|
||||
let instance = this._controller.instances.get(instanceId);
|
||||
let instance = this._controller.instances!.get(instanceId);
|
||||
await lib.invokeHook(this._controller.plugins, "onPlayerEvent", instance, {
|
||||
type: event.type,
|
||||
name: event.name,
|
||||
@@ -264,5 +291,3 @@ class HostConnection extends BaseConnection {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HostConnection;
|
||||
@@ -1,6 +1,7 @@
|
||||
"use strict";
|
||||
const http = require("http");
|
||||
const https = require("https");
|
||||
import type { Socket } from "net";
|
||||
|
||||
import http from "http";
|
||||
import https from "https";
|
||||
|
||||
/**
|
||||
* Gracefully close a Node.js HTTP(S) server
|
||||
@@ -19,34 +20,37 @@ const https = require("https");
|
||||
*
|
||||
* @alias module:controller/src/HttpCloser
|
||||
*/
|
||||
class HttpCloser {
|
||||
export default class HttpCloser {
|
||||
private _server: http.Server | https.Server;
|
||||
private _sockets: Set<Socket>;
|
||||
private _responses: Set<http.ServerResponse>;
|
||||
|
||||
/**
|
||||
* Server to attach this closer to.
|
||||
* @param {https.Server | http.Server} server - HTTP(S) to attach to
|
||||
*/
|
||||
constructor(server) {
|
||||
constructor(server: http.Server | https.Server) {
|
||||
this._server = server;
|
||||
this._sockets = new Set();
|
||||
this._responses = new Set();
|
||||
|
||||
if (this._server instanceof http.Server) {
|
||||
this._server.on("connection", this._addSocket.bind(this));
|
||||
} else if (this._server instanceof https.Server) {
|
||||
if (this._server instanceof https.Server) {
|
||||
this._server.on("secureConnection", this._addSocket.bind(this));
|
||||
} else if (this._server instanceof http.Server) {
|
||||
this._server.on("connection", this._addSocket.bind(this));
|
||||
} else {
|
||||
throw Error("server must be an instance of either http.Server or https.Server.");
|
||||
}
|
||||
this._server.on("request", this._addRequest.bind(this));
|
||||
}
|
||||
|
||||
_addSocket(socket) {
|
||||
_addSocket(socket:Socket) {
|
||||
this._sockets.add(socket);
|
||||
socket.once("close", () => {
|
||||
this._sockets.delete(socket);
|
||||
});
|
||||
}
|
||||
|
||||
_addRequest(request, response) {
|
||||
_addRequest(request: http.IncomingMessage, response: http.ServerResponse) {
|
||||
this._responses.add(response);
|
||||
response.once("close", () => this._responses.delete(response));
|
||||
response.once("finish", () => this._responses.delete(response));
|
||||
@@ -62,16 +66,16 @@ class HttpCloser {
|
||||
* If there's requests in progress that still haven't been completed
|
||||
* after the timeout value passed they will be aborted.
|
||||
*
|
||||
* @param {number} timeout -
|
||||
* @param timeout -
|
||||
* Time in ms to wait for connections to close.
|
||||
*/
|
||||
async close(timeout = 5000) {
|
||||
async close(timeout:number = 5000): Promise<void> {
|
||||
let activeSockets = new Set();
|
||||
for (let response of this._responses) {
|
||||
if (!response.headersSent) {
|
||||
response.setHeader("Connection", "close");
|
||||
} else {
|
||||
let socket = response.socket;
|
||||
let socket = response.socket!;
|
||||
response.once("finish", () => {
|
||||
socket.end();
|
||||
});
|
||||
@@ -104,5 +108,3 @@ class HttpCloser {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = HttpCloser;
|
||||
@@ -1,47 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Runtime status of an instance on the controller
|
||||
* @alias module:controller/src/InstanceInfo
|
||||
*/
|
||||
class InstanceInfo {
|
||||
/** @type {module:lib.InstanceConfig} */
|
||||
config;
|
||||
|
||||
/**
|
||||
* Current status of the instance. One of:
|
||||
* - `unknown`: Instance is assigned to a host but this host is currently
|
||||
* not connected to the contreller.
|
||||
* - `unassigned`: Instance is not assigned to a a host and exists only on
|
||||
* the controller.
|
||||
* - `stopped`: Instance is stopped.
|
||||
* - `starting`: Instance is in the process of starting up.
|
||||
* - `running`: Instance is running normally.
|
||||
* - `stopping`: Instance is in the process of stopping.
|
||||
* - `creating_save`: Instance is in the process of creating a save.
|
||||
* - `exporting_data`: Instance is in the process of exporting game data.
|
||||
* - `deleted`: Instance has been deleted.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
status;
|
||||
|
||||
constructor({ config, status }) {
|
||||
this.config = config;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
config: this.config.serialize(),
|
||||
status: this.status,
|
||||
};
|
||||
}
|
||||
|
||||
/** Shorthand for `instance.config.get("instance.id")` @type {number} */
|
||||
get id() {
|
||||
return this.config.get("instance.id");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InstanceInfo;
|
||||
@@ -0,0 +1,50 @@
|
||||
import type * as lib from "@clusterio/lib";
|
||||
|
||||
/**
|
||||
* Current status of the instance. One of:
|
||||
* - `unknown`: Instance is assigned to a host but this host is currently
|
||||
* not connected to the contreller.
|
||||
* - `unassigned`: Instance is not assigned to a a host and exists only on
|
||||
* the controller.
|
||||
* - `stopped`: Instance is stopped.
|
||||
* - `starting`: Instance is in the process of starting up.
|
||||
* - `running`: Instance is running normally.
|
||||
* - `stopping`: Instance is in the process of stopping.
|
||||
* - `creating_save`: Instance is in the process of creating a save.
|
||||
* - `exporting_data`: Instance is in the process of exporting game data.
|
||||
* - `deleted`: Instance has been deleted.
|
||||
*/
|
||||
export type InstanceStatus =
|
||||
"unknown" | "unassigned" | "stopped" | "starting" | "running" |
|
||||
"stopping" | "creating_save" | "exporting_data" | "deleted";
|
||||
|
||||
|
||||
/**
|
||||
* Runtime status of an instance on the controller
|
||||
* @alias module:controller/src/InstanceInfo
|
||||
*/
|
||||
export default class InstanceInfo {
|
||||
config: lib.InstanceConfig;
|
||||
status: InstanceStatus;
|
||||
game_port: number | null = null;
|
||||
|
||||
constructor(
|
||||
{ config, status }:
|
||||
{ config: lib.InstanceConfig, status: InstanceStatus }
|
||||
) {
|
||||
this.config = config;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
toJSON(): Object {
|
||||
return {
|
||||
config: this.config.serialize(),
|
||||
status: this.status,
|
||||
};
|
||||
}
|
||||
|
||||
/** Shorthand for `instance.config.get("instance.id")` */
|
||||
get id():number {
|
||||
return this.config.get("instance.id");
|
||||
}
|
||||
}
|
||||
@@ -1,51 +1,47 @@
|
||||
"use strict";
|
||||
const fs = require("fs-extra");
|
||||
const jwt = require("jsonwebtoken");
|
||||
import fs from "fs-extra";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
const lib = require("@clusterio/lib");
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
/**
|
||||
* Manages users and roles
|
||||
* @alias module:controller/src/UserManager
|
||||
*/
|
||||
class UserManager {
|
||||
constructor(config) {
|
||||
this._config = config;
|
||||
this.roles = null;
|
||||
this.users = null;
|
||||
export default class UserManager {
|
||||
roles: Map<number, lib.Role> = new Map();
|
||||
users: Map<string, lib.User> = new Map();
|
||||
|
||||
constructor(
|
||||
private _config: lib.ControllerConfig
|
||||
) {
|
||||
}
|
||||
|
||||
async load(filePath) {
|
||||
let loadedRoles = new Map();
|
||||
let loadedUsers = new Map();
|
||||
async load(filePath: string): Promise<void> {
|
||||
try {
|
||||
let content = JSON.parse(await fs.readFile(filePath));
|
||||
let content = JSON.parse(await fs.readFile(filePath, { encoding: 'utf8' }));
|
||||
for (let serializedRole of content.roles) {
|
||||
let role = new lib.Role(serializedRole);
|
||||
loadedRoles.set(role.id, role);
|
||||
this.roles.set(role.id, role);
|
||||
}
|
||||
|
||||
for (let serializedUser of content.users) {
|
||||
let user = new lib.User(serializedUser, loadedRoles);
|
||||
loadedUsers.set(user.name, user);
|
||||
let user = new lib.User(serializedUser, this.roles);
|
||||
this.users.set(user.name, user);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.code !== "ENOENT") {
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Create default roles if loading failed
|
||||
lib.ensureDefaultAdminRole(loadedRoles);
|
||||
lib.ensureDefaultPlayerRole(loadedRoles);
|
||||
lib.ensureDefaultAdminRole(this.roles);
|
||||
lib.ensureDefaultPlayerRole(this.roles);
|
||||
}
|
||||
|
||||
this.roles = loadedRoles;
|
||||
this.users = loadedUsers;
|
||||
}
|
||||
|
||||
async save(filePath) {
|
||||
if (!this.roles || !this.users) {
|
||||
async save(filePath:string): Promise<void> {
|
||||
if (this.roles.size === 0 || this.users.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -67,20 +63,21 @@ class UserManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new user
|
||||
*
|
||||
* Creates a new user and add it to the user database.
|
||||
*
|
||||
* @param {string} name - Name of the user to create.
|
||||
* @returns {module:lib.User} newly created user.
|
||||
* @param name - Name of the user to create.
|
||||
*/
|
||||
createUser(name) {
|
||||
createUser(name:string): lib.User {
|
||||
if (this.users.has(name)) {
|
||||
throw new Error(`User '${name}' already exists`);
|
||||
}
|
||||
|
||||
let roles = [];
|
||||
let defaultRoleId = this._config.get("controller.default_role_id");
|
||||
let user = new lib.User({ name, roles: [defaultRoleId] }, this.roles);
|
||||
if (defaultRoleId !== null) {
|
||||
roles.push(defaultRoleId)
|
||||
}
|
||||
|
||||
let user = new lib.User({ name, roles }, this.roles);
|
||||
this.users.set(name, user);
|
||||
return user;
|
||||
}
|
||||
@@ -88,16 +85,13 @@ class UserManager {
|
||||
/**
|
||||
* Sign access token for the given user name
|
||||
*
|
||||
* @param {string} name - user name to sign token for
|
||||
* @returns {string} JWT access token for the user.
|
||||
* @param name - user name to sign token for
|
||||
* @returns JWT access token for the user.
|
||||
*/
|
||||
signUserToken(name) {
|
||||
signUserToken(name: string): string {
|
||||
return jwt.sign(
|
||||
{ aud: "user", user: name },
|
||||
Buffer.from(this._config.get("controller.auth_secret"), "base64")
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = UserManager;
|
||||
@@ -1,15 +1,19 @@
|
||||
"use strict";
|
||||
const jwt = require("jsonwebtoken");
|
||||
const WebSocket = require("ws");
|
||||
import type { IncomingMessage } from "http";
|
||||
import type { Duplex } from "stream";
|
||||
import type Controller from "./Controller";
|
||||
import type { WebSocketClusterio } from "./WsServerConnector";
|
||||
|
||||
const lib = require("@clusterio/lib");
|
||||
import jwt from "jsonwebtoken";
|
||||
import WebSocket, { WebSocketServer } from "ws";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
const { logger } = lib;
|
||||
|
||||
const ControlConnection = require("./ControlConnection");
|
||||
const packageVersion = require("../package").version;
|
||||
const HostConnection = require("./HostConnection");
|
||||
const WsServerConnector = require("./WsServerConnector");
|
||||
import ControlConnection from "./ControlConnection";
|
||||
import HostConnection, { HostInfo } from "./HostConnection";
|
||||
import WsServerConnector from "./WsServerConnector";
|
||||
|
||||
const packageVersion = require("../package.json").version;
|
||||
|
||||
const wsMessageCounter = new lib.Counter(
|
||||
"clusterio_controller_websocket_message_total",
|
||||
@@ -31,35 +35,30 @@ const wsRejectedConnectionsCounter = new lib.Counter(
|
||||
* WebSocket server
|
||||
* @alias module:controller/src/WsServer
|
||||
*/
|
||||
class WsServer {
|
||||
constructor(controller) {
|
||||
export default class WsServer {
|
||||
controller: Controller;
|
||||
stopAcceptingNewSessions: boolean = false;
|
||||
controlConnections: Map<number, ControlConnection> = new Map();
|
||||
hostConnections: Map<number, HostConnection> = new Map();
|
||||
activeConnectors: Map<number, WsServerConnector> = new Map();
|
||||
pendingSockets: Set<WebSocket> = new Set();
|
||||
// Unique string for the session token audience
|
||||
sessionAud: string = `session-${Date.now()}`;
|
||||
nextSessionId: number = 1;
|
||||
nextControlId: number = 1;
|
||||
wss: WebSocket.Server;
|
||||
|
||||
constructor(controller: Controller) {
|
||||
this.controller = controller;
|
||||
|
||||
this.stopAcceptingNewSessions = false;
|
||||
|
||||
/** @type {Array<module:controller/src/ControlConnection>} */
|
||||
this.controlConnections = new Map();
|
||||
/** @type {Map<number, module:controller/src/HostConnection>} */
|
||||
this.hostConnections = new Map();
|
||||
/** @type {Map<number, module:controller/src/WsServerConnector>} */
|
||||
this.activeConnectors = new Map();
|
||||
/** @type {Set<module:ws>} */
|
||||
this.pendingSockets = new Set();
|
||||
|
||||
// Unique string for the session token audience
|
||||
this.sessionAud = `session-${Date.now()}`;
|
||||
this.nextSessionId = 1;
|
||||
this.nextControlId = 1;
|
||||
|
||||
/** @type {module:ws.Server} */
|
||||
this.wss = new WebSocket.Server({
|
||||
noServer: true,
|
||||
path: "/api/socket",
|
||||
});
|
||||
this.wss.on("connection", (socket, req) => this.handleConnection(socket, req));
|
||||
this.wss.on("connection", (socket: WebSocketClusterio, req: IncomingMessage) => this.handleConnection(socket, req));
|
||||
}
|
||||
|
||||
async stop() {
|
||||
async stop(): Promise<void> {
|
||||
this.stopAcceptingNewSessions = true;
|
||||
|
||||
let disconnectTasks = [];
|
||||
@@ -75,7 +74,7 @@ class WsServer {
|
||||
for (let task of disconnectTasks) {
|
||||
try {
|
||||
await task;
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (!(err instanceof lib.SessionLost)) {
|
||||
logger.warn(`Unexpected error disconnecting connector:\n${err.stack}`);
|
||||
}
|
||||
@@ -87,7 +86,11 @@ class WsServer {
|
||||
}
|
||||
}
|
||||
|
||||
handleUpgrade(req, socket, head) {
|
||||
handleUpgrade(
|
||||
req: IncomingMessage,
|
||||
socket: Duplex,
|
||||
head: Buffer
|
||||
) {
|
||||
// For reasons that defy common sense, the connection event has
|
||||
// to be emitted explictly when using noServer.
|
||||
this.wss.handleUpgrade(req, socket, head, (ws) => {
|
||||
@@ -95,7 +98,7 @@ class WsServer {
|
||||
});
|
||||
}
|
||||
|
||||
handleConnection(socket, req) {
|
||||
handleConnection(socket: WebSocketClusterio, req: IncomingMessage) {
|
||||
logger.verbose(`WsServer | new connection from ${req.socket.remoteAddress}`);
|
||||
|
||||
// Track statistics
|
||||
@@ -112,11 +115,12 @@ class WsServer {
|
||||
if (typeof args[0] === "string" && !socket.clusterio_ignore_dump) {
|
||||
this.controller.debugEvents.emit("message", { direction: "out", content: args[0] });
|
||||
}
|
||||
// @ts-ignore TODO::: Issue with originalSend having multiple signatures ?
|
||||
return originalSend.call(socket, ...args);
|
||||
};
|
||||
|
||||
// Start connection handshake.
|
||||
let loadedPlugins = {};
|
||||
let loadedPlugins: any = {};
|
||||
for (let [name, plugin] of this.controller.plugins) {
|
||||
loadedPlugins[name] = plugin.info.version;
|
||||
}
|
||||
@@ -125,7 +129,10 @@ class WsServer {
|
||||
this.attachHandler(socket, req);
|
||||
}
|
||||
|
||||
attachHandler(socket, req) {
|
||||
attachHandler(
|
||||
socket: WebSocket,
|
||||
req: IncomingMessage
|
||||
) {
|
||||
this.pendingSockets.add(socket);
|
||||
|
||||
let timeoutId = setTimeout(() => {
|
||||
@@ -135,7 +142,7 @@ class WsServer {
|
||||
this.pendingSockets.delete(socket);
|
||||
}, 30*1000);
|
||||
|
||||
socket.once("message", (message) => {
|
||||
socket.once("message", (message: string) => {
|
||||
clearTimeout(timeoutId);
|
||||
this.pendingSockets.delete(socket);
|
||||
this.handleHandshake(
|
||||
@@ -154,13 +161,13 @@ ${err.stack}`
|
||||
});
|
||||
}
|
||||
|
||||
async handleHandshake(rawMessage, socket, req) {
|
||||
async handleHandshake(rawMessage: string, socket: WebSocket, req: IncomingMessage) {
|
||||
let message;
|
||||
try {
|
||||
const json = JSON.parse(rawMessage);
|
||||
lib.Message.validate(json);
|
||||
message = lib.Message.fromJSON(json);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
logger.verbose(`WsServer | closing ${req.socket.remoteAddress} after receiving invalid message`);
|
||||
wsRejectedConnectionsCounter.inc();
|
||||
socket.close(1002, `Invalid message: ${err.message}`);
|
||||
@@ -168,7 +175,7 @@ ${err.stack}`
|
||||
}
|
||||
|
||||
if (message.type === "resume") {
|
||||
this.resumeSession(message.data, socket, req);
|
||||
this.resumeSession((message as lib.MessageResume).data, socket, req);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -187,19 +194,19 @@ ${err.stack}`
|
||||
}
|
||||
|
||||
if (message.type === "registerHost") {
|
||||
await this.registerHost(message.data, socket, req);
|
||||
await this.registerHost((message as lib.MessageRegisterHost).data, socket, req);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === "registerControl") {
|
||||
await this.registerControl(message.data, socket, req);
|
||||
await this.registerControl((message as lib.MessageRegisterControl).data, socket, req);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error("This should be unreachable");
|
||||
}
|
||||
|
||||
createSession(dst) {
|
||||
createSession(dst: lib.Address): [ WsServerConnector, string ] {
|
||||
let sessionId = this.nextSessionId;
|
||||
this.nextSessionId += 1;
|
||||
let sessionToken = jwt.sign(
|
||||
@@ -217,7 +224,7 @@ ${err.stack}`
|
||||
return [connector, sessionToken];
|
||||
}
|
||||
|
||||
async registerHost(data, socket, req) {
|
||||
async registerHost(data: lib.RegisterHostData, socket: WebSocket, req: IncomingMessage) {
|
||||
try {
|
||||
let tokenPayload = jwt.verify(
|
||||
data.token,
|
||||
@@ -226,11 +233,15 @@ ${err.stack}`
|
||||
{ audience: ["host", "slave"] }
|
||||
);
|
||||
|
||||
if (typeof tokenPayload === "string") {
|
||||
throw new Error("unexpected JsonWebToken type");
|
||||
}
|
||||
|
||||
// migrate: allow pre-rename tokens issued to hosts before alpha-14
|
||||
if ((tokenPayload.host !== undefined ? tokenPayload.host : tokenPayload.slave) !== data.id) {
|
||||
throw new Error("missmatched host id");
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
logger.verbose(`WsServer | authentication failed for ${req.socket.remoteAddress}`);
|
||||
wsRejectedConnectionsCounter.inc();
|
||||
socket.close(4003, `Authentication failed: ${err.message}`);
|
||||
@@ -258,18 +269,18 @@ ${err.stack}`
|
||||
connector.on("close", () => {
|
||||
if (this.hostConnections.get(data.id) === connection) {
|
||||
this.hostConnections.delete(data.id);
|
||||
this.controller.hostUpdated(this.controller.hosts.get(data.id));
|
||||
this.controller.hostUpdated(this.controller.hosts!.get(data.id) as HostInfo);
|
||||
} else {
|
||||
logger.warn("Unlisted HostConnection closed");
|
||||
}
|
||||
});
|
||||
this.hostConnections.set(data.id, connection);
|
||||
this.controller.hostUpdated(this.controller.hosts.get(data.id));
|
||||
this.controller.hostUpdated(this.controller.hosts!.get(data.id) as HostInfo);
|
||||
let src = new lib.Address(lib.Address.host, data.id);
|
||||
connector.ready(socket, src, sessionToken);
|
||||
connector.ready(socket, src, sessionToken, undefined);
|
||||
}
|
||||
|
||||
async registerControl(data, socket, req) {
|
||||
async registerControl(data: lib.RegisterControlData, socket: WebSocket, req: IncomingMessage) {
|
||||
let user;
|
||||
try {
|
||||
let tokenPayload = jwt.verify(
|
||||
@@ -278,16 +289,20 @@ ${err.stack}`
|
||||
{ audience: "user" }
|
||||
);
|
||||
|
||||
if (typeof tokenPayload === "string") {
|
||||
throw new Error("unexpected JsonWebToken type");
|
||||
}
|
||||
|
||||
user = this.controller.userManager.users.get(tokenPayload.user);
|
||||
if (!user) {
|
||||
throw new Error("invalid user");
|
||||
}
|
||||
if (tokenPayload.iat < user.tokenValidAfter) {
|
||||
if ((tokenPayload.iat || 0) < user.tokenValidAfter) {
|
||||
throw new Error("invalid token");
|
||||
}
|
||||
user.checkPermission("core.control.connect");
|
||||
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
logger.verbose(`WsServer | authentication failed for ${req.socket.remoteAddress}`);
|
||||
wsRejectedConnectionsCounter.inc();
|
||||
socket.close(4003, `Authentication failed: ${err.message}`);
|
||||
@@ -317,7 +332,7 @@ ${err.stack}`
|
||||
connector.ready(socket, src, sessionToken, account);
|
||||
}
|
||||
|
||||
resumeSession(data, socket, req) {
|
||||
resumeSession(data: lib.ResumeData, socket: WebSocket, req: IncomingMessage) {
|
||||
let connector;
|
||||
try {
|
||||
let payload = jwt.verify(
|
||||
@@ -326,6 +341,10 @@ ${err.stack}`
|
||||
{ audience: this.sessionAud }
|
||||
);
|
||||
|
||||
if (typeof payload === "string") {
|
||||
throw new Error("unexpected JsonWebToken type");
|
||||
}
|
||||
|
||||
connector = this.activeConnectors.get(payload.sid);
|
||||
if (!connector) {
|
||||
throw new Error();
|
||||
@@ -337,10 +356,6 @@ ${err.stack}`
|
||||
return;
|
||||
}
|
||||
|
||||
connector.continue(socket, data.lastSeq);
|
||||
|
||||
connector.continue(socket, data.lastSeq as number);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = WsServer;
|
||||
+57
-36
@@ -1,9 +1,14 @@
|
||||
"use strict";
|
||||
const events = require("events");
|
||||
import type WebSocket from "ws";
|
||||
|
||||
const lib = require("@clusterio/lib");
|
||||
import events from "events";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
const { logger } = lib;
|
||||
|
||||
export interface WebSocketClusterio extends WebSocket {
|
||||
clusterio_ignore_dump?: boolean;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Connector for controller connections
|
||||
@@ -11,15 +16,17 @@ const { logger } = lib;
|
||||
* @extends module:lib.WebSocketBaseConnector
|
||||
* @alias module:controller/src/WsServerConnector
|
||||
*/
|
||||
class WsServerConnector extends lib.WebSocketBaseConnector {
|
||||
constructor(dst, sessionId, sessionTimeout, heartbeatInterval) {
|
||||
super(new lib.Address(lib.Address.controller, 0), dst);
|
||||
export default class WsServerConnector extends lib.WebSocketBaseConnector {
|
||||
_timeoutId: NodeJS.Timeout | null = null;
|
||||
declare _socket: WebSocketClusterio | null;
|
||||
|
||||
this._sessionTimeout = sessionTimeout;
|
||||
this._socket = null;
|
||||
this._sessionId = sessionId;
|
||||
this._heartbeatInterval = heartbeatInterval;
|
||||
this._timeoutId = null;
|
||||
constructor(
|
||||
dst: lib.Address,
|
||||
private _sessionId: number,
|
||||
private _sessionTimeout: number,
|
||||
public _heartbeatInterval: number,
|
||||
) {
|
||||
super(new lib.Address(lib.Address.controller, 0), dst);
|
||||
|
||||
// The following states are used in the server connector
|
||||
// closed: Connection is closed
|
||||
@@ -28,8 +35,10 @@ class WsServerConnector extends lib.WebSocketBaseConnector {
|
||||
}
|
||||
|
||||
_reset() {
|
||||
clearTimeout(this._timeoutId);
|
||||
this._timeoutId = null;
|
||||
if (this._timeoutId) {
|
||||
clearTimeout(this._timeoutId);
|
||||
this._timeoutId = null;
|
||||
}
|
||||
super._reset();
|
||||
}
|
||||
|
||||
@@ -38,14 +47,17 @@ class WsServerConnector extends lib.WebSocketBaseConnector {
|
||||
*
|
||||
* Sends the ready message over the socket to initiate the session.
|
||||
*
|
||||
* @param {Object} socket - WebSocket connection to client.
|
||||
* @param {module:lib.Address} src - Source address for this link.
|
||||
* @param {string} sessionToken -
|
||||
* the session token to send to the client.
|
||||
* @param {module:lib.AccountDetails=} account -
|
||||
* account data to provide to control connection
|
||||
* @param socket - WebSocket connection to client.
|
||||
* @param src - Source address for this link.
|
||||
* @param sessionToken - the session token to send to the client.
|
||||
* @param account - account data to provide to control connection
|
||||
*/
|
||||
ready(socket, src, sessionToken, account) {
|
||||
ready(
|
||||
socket: WebSocket.WebSocket,
|
||||
src: lib.Address,
|
||||
sessionToken: string,
|
||||
account: lib.AccountDetails | undefined
|
||||
) {
|
||||
this._socket = socket;
|
||||
this._sendInternal(new lib.MessageReady(
|
||||
new lib.ReadyData(
|
||||
@@ -68,14 +80,17 @@ class WsServerConnector extends lib.WebSocketBaseConnector {
|
||||
* Terminates the current socket and contiunes the session over the
|
||||
* socket given from the message sequence given.
|
||||
*
|
||||
* @param {module:net.Socket} socket - New socket to continue on.
|
||||
* @param {number=} lastSeq - The last message the client received.
|
||||
* @param socket - New socket to continue on.
|
||||
* @param lastSeq - The last message the client received.
|
||||
*/
|
||||
continue(socket, lastSeq) {
|
||||
continue(
|
||||
socket: WebSocket.WebSocket,
|
||||
lastSeq: number
|
||||
) {
|
||||
|
||||
// It's possible the previous connection hasn't closed yet due to a
|
||||
// stale connection. Terminate it if so.
|
||||
if (this._state === "connected") {
|
||||
if (this._state === "connected" && this._socket !== null) {
|
||||
this._socket.terminate();
|
||||
this._socket.once("close", () => this.continue(socket, lastSeq));
|
||||
return;
|
||||
@@ -108,9 +123,13 @@ class WsServerConnector extends lib.WebSocketBaseConnector {
|
||||
}
|
||||
|
||||
_attachSocketHandlers() {
|
||||
if (this._socket === null) {
|
||||
throw new Error("this._socket is null");
|
||||
}
|
||||
|
||||
this.startHeartbeat();
|
||||
|
||||
this._socket.on("close", (code, reason) => {
|
||||
this._socket.on("close", (code: number, reason: string) => {
|
||||
logger.verbose(`Connector | Close (code: ${code}, reason: ${reason})`);
|
||||
this.stopHeartbeat();
|
||||
this._socket = null;
|
||||
@@ -124,10 +143,9 @@ class WsServerConnector extends lib.WebSocketBaseConnector {
|
||||
this.emit("drop");
|
||||
this._timeoutId = setTimeout(() => { this._timedOut(); }, this._sessionTimeout * 1000);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
this._socket.on("error", err => {
|
||||
this._socket.on("error", (err: any) => {
|
||||
// It's assumed that close is always called by ws
|
||||
logger.verbose("Connector | Error:", err);
|
||||
});
|
||||
@@ -135,15 +153,15 @@ class WsServerConnector extends lib.WebSocketBaseConnector {
|
||||
this._socket.on("open", () => {
|
||||
logger.verbose("Connector | Open");
|
||||
});
|
||||
this._socket.on("ping", data => {
|
||||
this._socket.on("ping", (data: string) => {
|
||||
logger.verbose(`Connector | Ping (data: ${data}`);
|
||||
});
|
||||
this._socket.on("pong", data => {
|
||||
this._socket.on("pong", (data: string) => {
|
||||
logger.verbose(`Connector | Pong (data: ${data}`);
|
||||
});
|
||||
|
||||
// Handle messages
|
||||
this._socket.on("message", data => {
|
||||
this._socket.on("message", (data: string) => {
|
||||
let message = this._parseMessage(data);
|
||||
if (!message) {
|
||||
return;
|
||||
@@ -162,10 +180,13 @@ class WsServerConnector extends lib.WebSocketBaseConnector {
|
||||
*
|
||||
* Sends a close frame and disconnects the connector.
|
||||
*
|
||||
* @param {number} code - WebSocket close code.
|
||||
* @param {string} reason - WebSocket close reason.
|
||||
* @param code - WebSocket close code.
|
||||
* @param reason - WebSocket close reason.
|
||||
*/
|
||||
async close(code, reason) {
|
||||
async close(
|
||||
code: number,
|
||||
reason: string
|
||||
): Promise<void> {
|
||||
if (this._state === "closed") {
|
||||
return;
|
||||
}
|
||||
@@ -177,7 +198,9 @@ class WsServerConnector extends lib.WebSocketBaseConnector {
|
||||
}
|
||||
|
||||
this._closing = true;
|
||||
this._socket.close(code, reason);
|
||||
if (this._socket !== null) {
|
||||
this._socket.close(code, reason);
|
||||
}
|
||||
await events.once(this, "close");
|
||||
}
|
||||
|
||||
@@ -213,5 +236,3 @@ class WsServerConnector extends lib.WebSocketBaseConnector {
|
||||
this._closing = true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WsServerConnector;
|
||||
@@ -1,14 +0,0 @@
|
||||
// Shared metrics
|
||||
"use strict";
|
||||
const lib = require("@clusterio/lib");
|
||||
const util = require("util");
|
||||
|
||||
exports.endpointHitCounter = new lib.Counter(
|
||||
"clusterio_controller_http_endpoint_hits_total",
|
||||
"How many requests a particular HTTP endpoint has gotten",
|
||||
{ labels: ["route"], register: false }
|
||||
);
|
||||
|
||||
exports.endpointHitCounter.labels = util.deprecate(
|
||||
exports.endpointHitCounter.labels, "incrementing endpointHitCounter is no longer needed and has no effect"
|
||||
);
|
||||
@@ -0,0 +1,15 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
import util from "util";
|
||||
|
||||
const endpointHitCounter = new lib.Counter(
|
||||
"clusterio_controller_http_endpoint_hits_total",
|
||||
"How many requests a particular HTTP endpoint has gotten",
|
||||
{ labels: ["route"], register: false }
|
||||
);
|
||||
|
||||
endpointHitCounter.labels = util.deprecate(
|
||||
endpointHitCounter.labels,
|
||||
"incrementing endpointHitCounter is no longer needed and has no effect"
|
||||
);
|
||||
|
||||
export default endpointHitCounter
|
||||
@@ -1,26 +1,28 @@
|
||||
"use strict";
|
||||
const busboy = require("busboy");
|
||||
const crypto = require("crypto");
|
||||
const events = require("events");
|
||||
const fs = require("fs-extra");
|
||||
const JSZip = require("jszip");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const path = require("path");
|
||||
const nodeStream = require("stream");
|
||||
const util = require("util");
|
||||
import type { Application, Request, Response } from "express";
|
||||
import type Controller from "./Controller";
|
||||
|
||||
const lib = require("@clusterio/lib");
|
||||
import busboy from "busboy";
|
||||
import crypto from "crypto";
|
||||
import events from "events";
|
||||
import fs from "fs-extra";
|
||||
import JSZip from "jszip";
|
||||
import jwt from "jsonwebtoken";
|
||||
import path from "path";
|
||||
import nodeStream from "stream";
|
||||
import util from "util";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
const { logger } = lib;
|
||||
|
||||
const finished = util.promisify(nodeStream.finished);
|
||||
|
||||
|
||||
// Merges samples from sourceResult to destinationResult
|
||||
function mergeSamples(destinationResult, sourceResult) {
|
||||
function mergeSamples(destinationResult: any, sourceResult: any) {
|
||||
let receivedSamples = new Map(sourceResult.samples);
|
||||
for (let [suffix, suffixSamples] of destinationResult.samples) {
|
||||
if (receivedSamples.has(suffix)) {
|
||||
suffixSamples.push(...receivedSamples.get(suffix));
|
||||
suffixSamples.push(...receivedSamples.get(suffix) as any[]);
|
||||
receivedSamples.delete(suffix);
|
||||
}
|
||||
}
|
||||
@@ -31,23 +33,25 @@ function mergeSamples(destinationResult, sourceResult) {
|
||||
}
|
||||
|
||||
// Prometheus polling endpoint
|
||||
async function getMetrics(req, res, next) {
|
||||
let results = [];
|
||||
let pluginResults = await lib.invokeHook(req.app.locals.controller.plugins, "onMetrics");
|
||||
async function getMetrics(req: Request, res: Response, next: any) {
|
||||
const controller: Controller = req.app.locals.controller
|
||||
|
||||
let results: any[] = [];
|
||||
let pluginResults: any[] = await lib.invokeHook(controller.plugins, "onMetrics");
|
||||
for (let metricIterator of pluginResults) {
|
||||
for await (let metric of metricIterator) {
|
||||
results.push(metric);
|
||||
}
|
||||
}
|
||||
|
||||
let requests = [];
|
||||
let timeout = req.app.locals.controller.config.get("controller.metrics_timeout") * 1000;
|
||||
for (let [hostId, hostConnection] of req.app.locals.controller.wsServer.hostConnections) {
|
||||
let requests: Promise<any>[] = [];
|
||||
let timeout = controller.config.get("controller.metrics_timeout") * 1000;
|
||||
for (let [hostId, hostConnection] of controller.wsServer.hostConnections) {
|
||||
if (!hostConnection.connected) {
|
||||
continue;
|
||||
}
|
||||
requests.push(lib.timeout(
|
||||
hostConnection.send(new lib.HostMetricsRequest()).catch(err => {
|
||||
hostConnection.send(new lib.HostMetricsRequest()).catch((err: any) => {
|
||||
if (!(err instanceof lib.SessionLost)) {
|
||||
logger.error(`Unexpected error gathering metrics from host:\n${err.stack}`);
|
||||
}
|
||||
@@ -61,7 +65,7 @@ async function getMetrics(req, res, next) {
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
let resultMap = new Map();
|
||||
let resultMap: Map<string, any> = new Map();
|
||||
for (let response of await Promise.all(requests)) {
|
||||
if (!response) {
|
||||
// TODO: Log timeout occured?
|
||||
@@ -74,28 +78,34 @@ async function getMetrics(req, res, next) {
|
||||
|
||||
} else {
|
||||
// Merge metrics received by multiple hosts
|
||||
mergeSamples(resultMap.get(result.metric.name), result);
|
||||
mergeSamples(resultMap.get(result.metric.name)!, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// TODO: results seem to mix deserialized and serialized lib.CollectorResult ?
|
||||
// mergeSamples imply samples be [string, [string, number][]]
|
||||
// but lib.defaultRegistry.collect() imply Map<string, Map<string, number>>
|
||||
for (let result of resultMap.values()) {
|
||||
results.push(lib.deserializeResult(result));
|
||||
}
|
||||
|
||||
|
||||
// lib.exposition expect AsyncIterable<CollectorResult> as parameter.
|
||||
// Should wrap results into a Generator functions ?
|
||||
// @ts-ignore
|
||||
let text = await lib.exposition(results);
|
||||
res.set("Content-Type", lib.exposition.contentType);
|
||||
res.send(text);
|
||||
}
|
||||
|
||||
function getPlugins(req, res) {
|
||||
function getPlugins(req: Request, res: Response) {
|
||||
let plugins = [];
|
||||
for (let pluginInfo of req.app.locals.controller.pluginInfos) {
|
||||
let name = pluginInfo.name;
|
||||
let loaded = req.app.locals.controller.plugins.has(name);
|
||||
let enabled = loaded && req.app.locals.controller.config.group(pluginInfo.name).get("load_plugin");
|
||||
let web = {};
|
||||
let web: { main:any, error:string|undefined } = { main: undefined, error: undefined};
|
||||
let devPlugins = req.app.locals.devPlugins;
|
||||
if (devPlugins && devPlugins.has(name)) {
|
||||
let stats = res.locals.webpack.devMiddleware.stats.stats[devPlugins.get(name)];
|
||||
@@ -116,7 +126,7 @@ function getPlugins(req, res) {
|
||||
res.send(plugins);
|
||||
}
|
||||
|
||||
function validateHostToken(req, res, next) {
|
||||
function validateHostToken(req: Request, res: Response, next: any) {
|
||||
let token = req.header("x-access-token");
|
||||
if (!token) {
|
||||
res.sendStatus(401);
|
||||
@@ -138,7 +148,7 @@ function validateHostToken(req, res, next) {
|
||||
next();
|
||||
}
|
||||
|
||||
function validateUserToken(req, res, next) {
|
||||
function validateUserToken(req: Request, res: Response, next: any) {
|
||||
let token = req.header("x-access-token");
|
||||
if (!token) {
|
||||
res.sendStatus(401);
|
||||
@@ -151,11 +161,15 @@ function validateUserToken(req, res, next) {
|
||||
Buffer.from(req.app.locals.controller.config.get("controller.auth_secret"), "base64"),
|
||||
{ audience: "user" }
|
||||
);
|
||||
|
||||
if (typeof tokenPayload === "string") {
|
||||
throw new Error("unexpected JsonWebToken type");
|
||||
}
|
||||
let user = req.app.locals.controller.userManager.users.get(tokenPayload.user);
|
||||
if (!user) {
|
||||
throw new Error("invalid user");
|
||||
}
|
||||
if (tokenPayload.iat < user.tokenValidAfter) {
|
||||
if (!tokenPayload.iat || tokenPayload.iat < user.tokenValidAfter) {
|
||||
throw new Error("invalid token");
|
||||
}
|
||||
res.locals.user = user;
|
||||
@@ -169,7 +183,7 @@ function validateUserToken(req, res, next) {
|
||||
}
|
||||
|
||||
// Handle an uploaded export package.
|
||||
async function uploadExport(req, res) {
|
||||
async function uploadExport(req: Request, res: Response) {
|
||||
if (req.get("Content-Type") !== "application/zip") {
|
||||
res.sendStatus(415);
|
||||
return;
|
||||
@@ -194,9 +208,9 @@ async function uploadExport(req, res) {
|
||||
for await (let chunk of req) {
|
||||
data.push(chunk);
|
||||
}
|
||||
data = Buffer.concat(data);
|
||||
let zip = await JSZip.loadAsync(data);
|
||||
data = null;
|
||||
let buffer: Buffer | null = Buffer.concat(data);
|
||||
let zip = await JSZip.loadAsync(buffer);
|
||||
buffer = null;
|
||||
|
||||
// This is hardcoded to prevent path expansion attacks
|
||||
let exportFiles = [
|
||||
@@ -207,7 +221,7 @@ async function uploadExport(req, res) {
|
||||
"export/locale.json",
|
||||
];
|
||||
|
||||
let assets = {};
|
||||
let assets: any = {};
|
||||
let settingPrototypes = {};
|
||||
for (let filePath of exportFiles) {
|
||||
let file = zip.file(filePath);
|
||||
@@ -232,11 +246,24 @@ async function uploadExport(req, res) {
|
||||
res.sendStatus(200);
|
||||
}
|
||||
|
||||
async function createProxyStream(app) {
|
||||
|
||||
interface ProxyStream {
|
||||
id: string;
|
||||
source: nodeStream.Readable | null;
|
||||
flowing: boolean;
|
||||
size: number | null;
|
||||
mime: string | null;
|
||||
filename: string | null;
|
||||
events: events.EventEmitter;
|
||||
timeout: NodeJS.Timeout;
|
||||
}
|
||||
|
||||
export async function createProxyStream(app: Application): Promise<ProxyStream> {
|
||||
let asyncRandomBytes = util.promisify(crypto.randomBytes);
|
||||
let id = (await asyncRandomBytes(8)).toString("hex");
|
||||
let stream = {
|
||||
let stream: ProxyStream = {
|
||||
id,
|
||||
source: null,
|
||||
flowing: false,
|
||||
size: null,
|
||||
mime: null,
|
||||
@@ -257,7 +284,7 @@ async function createProxyStream(app) {
|
||||
return stream;
|
||||
}
|
||||
|
||||
async function putStream(req, res) {
|
||||
async function putStream(req: Request, res: Response) {
|
||||
let stream = req.app.locals.streams.get(req.params.id);
|
||||
if (!stream || stream.source) {
|
||||
res.sendStatus(404);
|
||||
@@ -278,7 +305,7 @@ async function putStream(req, res) {
|
||||
});
|
||||
}
|
||||
|
||||
async function getStream(req, res) {
|
||||
async function getStream(req: Request, res: Response) {
|
||||
let stream = req.app.locals.streams.get(req.params.id);
|
||||
if (!stream || stream.flowing) {
|
||||
res.sendStatus(404);
|
||||
@@ -321,15 +348,15 @@ const zipMimes = [
|
||||
|
||||
const contentTypeRegExp = /^([!#$%&'*+\-.^_`|~0-9A-Za-z]+\/[!#$%&'*+\-.^_`|~0-9A-Za-z]+)/;
|
||||
|
||||
async function uploadSave(req, res) {
|
||||
async function uploadSave(req: Request, res: Response) {
|
||||
try {
|
||||
res.locals.user.checkPermission("core.instance.save.upload");
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
res.status(403).json({ request_errors: [err.message] });
|
||||
return;
|
||||
}
|
||||
|
||||
let contentType = req.get("Content-Type");
|
||||
let contentType = req.get("Content-Type") || "";
|
||||
let match = contentTypeRegExp.exec(contentType);
|
||||
if (!match) {
|
||||
res.status(415).json({ request_errors: ["invalid Content-Type"] });
|
||||
@@ -338,11 +365,11 @@ async function uploadSave(req, res) {
|
||||
let contentMime = match[1].toLowerCase();
|
||||
|
||||
let tasks = [];
|
||||
let errors = [];
|
||||
let errors: string[] = [];
|
||||
let requestErrors = [];
|
||||
let saves = [];
|
||||
let saves: string[] = [];
|
||||
|
||||
async function handleFile(instanceId, stream, filename, streamMime) {
|
||||
async function handleFile(instanceId:number, stream: nodeStream.Readable, filename: string, streamMime: string) {
|
||||
let proxyStream = await createProxyStream(req.app);
|
||||
proxyStream.source = stream;
|
||||
proxyStream.mime = streamMime;
|
||||
@@ -354,7 +381,7 @@ async function uploadSave(req, res) {
|
||||
});
|
||||
|
||||
try {
|
||||
let storedName = await Promise.race([
|
||||
let storedName: string = await Promise.race([
|
||||
req.app.locals.controller.sendToHostByInstanceId(
|
||||
new lib.InstancePullSaveRequest(
|
||||
instanceId,
|
||||
@@ -366,7 +393,7 @@ async function uploadSave(req, res) {
|
||||
]);
|
||||
saves.push(storedName);
|
||||
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
proxyStream.events.emit("close");
|
||||
logger.error(`Error uploading save: ${err.message}`);
|
||||
errors.push(err.message);
|
||||
@@ -376,10 +403,10 @@ async function uploadSave(req, res) {
|
||||
|
||||
if (contentMime === "multipart/form-data") {
|
||||
await new Promise(resolve => {
|
||||
let fields = {};
|
||||
let instanceId:number | undefined;
|
||||
let parser = busboy({ headers: req.headers });
|
||||
parser.on("file", (name, stream, { filename, mimeType }) => {
|
||||
if (fields.instanceId === undefined) {
|
||||
parser.on("file", (name: string, stream: nodeStream.Readable, { filename, mimeType }: { filename: string, mimeType: string } ) => {
|
||||
if (instanceId === undefined) {
|
||||
requestErrors.push("instance_id must come before files uploaded");
|
||||
}
|
||||
|
||||
@@ -396,18 +423,18 @@ async function uploadSave(req, res) {
|
||||
return;
|
||||
}
|
||||
|
||||
tasks.push(handleFile(fields.instanceId, stream, filename, mimeType));
|
||||
tasks.push(handleFile(instanceId as number, stream, filename, mimeType));
|
||||
});
|
||||
parser.on("field", (name, value, info) => {
|
||||
if (name === "instance_id") {
|
||||
fields.instanceId = Number.parseInt(value, 10);
|
||||
if (Number.isNaN(fields.instanceId)) {
|
||||
instanceId = Number.parseInt(value, 10);
|
||||
if (Number.isNaN(instanceId)) {
|
||||
requestErrors.push("invalid instance_id");
|
||||
}
|
||||
}
|
||||
});
|
||||
parser.on("close", resolve);
|
||||
parser.on("error", (err) => {
|
||||
parser.on("error", (err: any) => {
|
||||
logger.error(`Error parsing multipart request in upload-save:\n${err.stack}`);
|
||||
errors.push(err.message);
|
||||
});
|
||||
@@ -421,7 +448,7 @@ async function uploadSave(req, res) {
|
||||
} else if (!filename.endsWith(".zip")) {
|
||||
requestErrors.push("filename must end with .zip");
|
||||
}
|
||||
let instanceId = Number.parseInt(req.query.instance_id, 10);
|
||||
let instanceId = Number.parseInt(String(req.query.instance_id), 10);
|
||||
if (Number.isNaN(instanceId)) {
|
||||
requestErrors.push("Missing or invalid instance_id parameter");
|
||||
}
|
||||
@@ -429,7 +456,7 @@ async function uploadSave(req, res) {
|
||||
if (errors.length || requestErrors.length) {
|
||||
req.resume();
|
||||
} else {
|
||||
tasks.push(handleFile(instanceId, req, filename, contentMime));
|
||||
tasks.push(handleFile(instanceId, req, filename as string, contentMime));
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -453,23 +480,23 @@ async function uploadSave(req, res) {
|
||||
res.json({ saves });
|
||||
}
|
||||
|
||||
function checkModName(name) {
|
||||
function checkModName(name: string) {
|
||||
try {
|
||||
lib.checkFilename(name);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
throw new lib.RequestError(`Mod name ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadMod(req, res) {
|
||||
async function uploadMod(req: Request, res: Response) {
|
||||
try {
|
||||
res.locals.user.checkPermission("core.mod.upload");
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
res.status(403).json({ request_errors: [err.message] });
|
||||
return;
|
||||
}
|
||||
|
||||
let contentType = req.get("Content-Type");
|
||||
let contentType = req.get("Content-Type")!;
|
||||
let match = contentTypeRegExp.exec(contentType);
|
||||
if (!match) {
|
||||
res.status(415).json({ request_errors: ["invalid Content-Type"] });
|
||||
@@ -478,15 +505,15 @@ async function uploadMod(req, res) {
|
||||
let contentMime = match[1].toLowerCase();
|
||||
|
||||
let tasks = [];
|
||||
let errors = [];
|
||||
let errors: string[] = [];
|
||||
let requestErrors = [];
|
||||
let mods = [];
|
||||
let mods: any[] = [];
|
||||
|
||||
async function handleFile(stream, filename) {
|
||||
async function handleFile(stream: nodeStream.Readable, filename: string) {
|
||||
try {
|
||||
checkModName(filename);
|
||||
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
logger.error(`Error uploading mod: ${err.message}`);
|
||||
errors.push(err.message);
|
||||
stream.resume();
|
||||
@@ -495,15 +522,16 @@ async function uploadMod(req, res) {
|
||||
|
||||
const modsDirectory = req.app.locals.controller.config.get("controller.mods_directory");
|
||||
let tempFilename = filename.replace(/(\.zip)?$/, ".tmp.zip");
|
||||
|
||||
let writeStream;
|
||||
try {
|
||||
|
||||
let writeStream;
|
||||
while (true) {
|
||||
try {
|
||||
writeStream = fs.createWriteStream(path.join(modsDirectory, tempFilename), { flags: "wx" });
|
||||
await events.once(writeStream, "open");
|
||||
break;
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.code === "EEXIST") {
|
||||
tempFilename = await lib.findUnusedName(modsDirectory, tempFilename, ".tmp.zip");
|
||||
} else {
|
||||
@@ -520,18 +548,21 @@ async function uploadMod(req, res) {
|
||||
req.app.locals.controller.modUpdated(modInfo);
|
||||
mods.push(modInfo.toJSON());
|
||||
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
logger.error(`Error uploading mod: ${err.message}`);
|
||||
errors.push(err.message);
|
||||
stream.resume();
|
||||
|
||||
// Attempt to clean up.
|
||||
writeStream.destroy();
|
||||
if (writeStream) {
|
||||
writeStream.destroy();
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.unlink(path.join(modsDirectory, tempFilename));
|
||||
} catch (unlinkErr) {
|
||||
} catch (unlinkErr: any) {
|
||||
if (unlinkErr.code !== "ENOENT") {
|
||||
logger.error(`Error removing ${tempFilename}: ${err.message}`);
|
||||
logger.error(`Error removing ${tempFilename}: ${unlinkErr.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -540,7 +571,7 @@ async function uploadMod(req, res) {
|
||||
if (contentMime === "multipart/form-data") {
|
||||
await new Promise(resolve => {
|
||||
let parser = busboy({ headers: req.headers });
|
||||
parser.on("file", (name, stream, { filename, mimeType }) => {
|
||||
parser.on("file", (name:string, stream: nodeStream.Readable, { filename, mimeType }: { filename: string, mimeType: string }) => {
|
||||
if (!zipMimes.includes(mimeType)) {
|
||||
requestErrors.push("invalid file Content-Type");
|
||||
}
|
||||
@@ -557,7 +588,7 @@ async function uploadMod(req, res) {
|
||||
tasks.push(handleFile(stream, filename));
|
||||
});
|
||||
parser.on("close", resolve);
|
||||
parser.on("error", (err) => {
|
||||
parser.on("error", (err: any) => {
|
||||
logger.error(`Error parsing multipart request in upload-mod:\n${err.stack}`);
|
||||
errors.push(err.message);
|
||||
});
|
||||
@@ -575,7 +606,7 @@ async function uploadMod(req, res) {
|
||||
if (errors.length || requestErrors.length) {
|
||||
req.resume();
|
||||
} else {
|
||||
tasks.push(handleFile(req, filename));
|
||||
tasks.push(handleFile(req, filename as string));
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -600,27 +631,27 @@ async function uploadMod(req, res) {
|
||||
}
|
||||
|
||||
|
||||
function addRouteHandlers(app) {
|
||||
app.get("/metrics", (req, res, next) => getMetrics(req, res, next).catch(next));
|
||||
export function addRouteHandlers(app: Application) {
|
||||
app.get("/metrics", (req:Request, res:Response, next:any) => getMetrics(req, res, next).catch(next));
|
||||
app.get("/api/plugins", getPlugins);
|
||||
app.put("/api/upload-export",
|
||||
validateHostToken,
|
||||
(req, res, next) => uploadExport(req, res).catch(next)
|
||||
(req:Request, res:Response, next:any) => uploadExport(req, res).catch(next)
|
||||
);
|
||||
app.put("/api/stream/:id", (req, res, next) => putStream(req, res).catch(next));
|
||||
app.get("/api/stream/:id", (req, res, next) => getStream(req, res).catch(next));
|
||||
app.put("/api/stream/:id", (req:Request, res:Response, next:any) => putStream(req, res).catch(next));
|
||||
app.get("/api/stream/:id", (req:Request, res:Response, next:any) => getStream(req, res).catch(next));
|
||||
app.post("/api/upload-save",
|
||||
validateUserToken,
|
||||
(req, res, next) => uploadSave(req, res).catch(next)
|
||||
(req:Request, res:Response, next:any) => uploadSave(req, res).catch(next)
|
||||
);
|
||||
app.post("/api/upload-mod",
|
||||
validateUserToken,
|
||||
(req, res, next) => uploadMod(req, res).catch(next)
|
||||
(req:Request, res:Response, next:any) => uploadMod(req, res).catch(next)
|
||||
);
|
||||
}
|
||||
|
||||
// Routes used in the web interface and served by the controller
|
||||
const webRoutes = [
|
||||
export const webRoutes = [
|
||||
"/",
|
||||
"/controller",
|
||||
"/hosts",
|
||||
@@ -636,9 +667,3 @@ const webRoutes = [
|
||||
"/plugins",
|
||||
"/plugins/:name/view",
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
addRouteHandlers,
|
||||
webRoutes,
|
||||
createProxyStream,
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"include": ["controller.ts", "package.json", "src/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"lib": ["es2023"],
|
||||
"module": "commonjs",
|
||||
"target": "es2022",
|
||||
"rootDir": ".",
|
||||
"outDir": "dist/controller",
|
||||
"composite": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"alwaysStrict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"entryPoints": ["controller.ts"],
|
||||
"includeVersion": true
|
||||
}
|
||||
@@ -58,7 +58,7 @@ function splitOn(separator: string, string: string) {
|
||||
return [string.slice(0, index), string.slice(index + separator.length)];
|
||||
}
|
||||
|
||||
type SerializedConfig = ReturnType<Config["serialize"]>;
|
||||
export type SerializedConfig = ReturnType<Config["serialize"]>;
|
||||
type SerializedGroup = ReturnType<ConfigGroup["serialize"]>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -140,7 +140,27 @@ ControllerGroup.finalize();
|
||||
* Controller Config class
|
||||
* @extends classes.Config
|
||||
*/
|
||||
export class ControllerConfig extends classes.Config { }
|
||||
export class ControllerConfig extends classes.Config {
|
||||
get(name: "controller.name"): string;
|
||||
get(name: "controller.mods_directory"): string;
|
||||
get(name: "controller.database_directory"): string;
|
||||
get(name: "controller.http_port"): number | null;
|
||||
get(name: "controller.https_port"): number | null;
|
||||
get(name: "controller.bind_address"): string | null;
|
||||
get(name: "controller.external_address"): string | null;
|
||||
get(name: "controller.tls_certificate"): string | null;
|
||||
get(name: "controller.tls_private_key"): string | null;
|
||||
get(name: "controller.auth_secret"): string;
|
||||
get(name: "controller.heartbeat_interval"): number;
|
||||
get(name: "controller.session_timeout"): number;
|
||||
get(name: "controller.metrics_timeout"): number;
|
||||
get(name: "controller.proxy_stream_timeout"): number;
|
||||
get(name: "controller.default_mod_pack_id"): number | null;
|
||||
get(name: "controller.default_role_id"): number | null;
|
||||
get(name: string) {
|
||||
return super.get(name);
|
||||
}
|
||||
}
|
||||
ControllerConfig.registerGroup(ControllerGroup);
|
||||
|
||||
|
||||
@@ -230,7 +250,22 @@ HostGroup.finalize();
|
||||
* Host Config class
|
||||
* @extends classes.Config
|
||||
*/
|
||||
export class HostConfig extends classes.Config { }
|
||||
export class HostConfig extends classes.Config {
|
||||
get(name: "host.name"): string;
|
||||
get(name: "host.id"): number;
|
||||
get(name: "host.factorio_directory"): string;
|
||||
get(name: "host.mods_directory"): string;
|
||||
get(name: "host.mods_directory_is_shared"): boolean;
|
||||
get(name: "host.instances_directory"): string;
|
||||
get(name: "host.controller_url"): string;
|
||||
get(name: "host.controller_token"): string;
|
||||
get(name: "host.tls_ca"): string | null;
|
||||
get(name: "host.public_address"): string;
|
||||
get(name: "host.max_reconnect_delay"): number;
|
||||
get(name: string) {
|
||||
return super.get(name);
|
||||
}
|
||||
}
|
||||
HostConfig.registerGroup(HostGroup);
|
||||
|
||||
|
||||
@@ -393,7 +428,31 @@ FactorioGroup.finalize();
|
||||
* Instance config class
|
||||
* @extends classes.Config
|
||||
*/
|
||||
export class InstanceConfig extends classes.Config { }
|
||||
export class InstanceConfig extends classes.Config {
|
||||
get(name: "instance.name"): string;
|
||||
get(name: "instance.id"): number;
|
||||
get(name: "instance.assigned_host"): number | null;
|
||||
get(name: "instance.auto_start"): boolean;
|
||||
|
||||
get(name: "factorio.version"): string;
|
||||
get(name: "factorio.game_port"): number | null;
|
||||
get(name: "factorio.rcon_port"): number | null;
|
||||
get(name: "factorio.rcon_password"): string | null;
|
||||
get(name: "factorio.player_online_autosave_slots"): number;
|
||||
get(name: "factorio.mod_pack"): string | null;
|
||||
get(name: "factorio.enable_save_patching"): boolean;
|
||||
get(name: "factorio.enable_whitelist"): boolean;
|
||||
get(name: "factorio.enable_authserver_bans"): boolean;
|
||||
get(name: "factorio.settings"): Object;
|
||||
get(name: "factorio.verbose_logging"): boolean;
|
||||
get(name: "factorio.strip_paths"): boolean;
|
||||
get(name: "factorio.sync_adminlist"): boolean;
|
||||
get(name: "factorio.sync_whitelist"): boolean;
|
||||
get(name: "factorio.max_concurrent_commands"): number;
|
||||
get(name: string) {
|
||||
return super.get(name);
|
||||
}
|
||||
}
|
||||
InstanceConfig.registerGroup(InstanceGroup);
|
||||
InstanceConfig.registerGroup(FactorioGroup);
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ export class LogMessageEvent {
|
||||
static dst = ["controller", "control"] as const;
|
||||
|
||||
constructor(
|
||||
public info: object,
|
||||
public info: { level: string, message: string },
|
||||
) { }
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
|
||||
@@ -172,6 +172,8 @@ export class Message {
|
||||
}
|
||||
}
|
||||
|
||||
export type MessageSrcDst = MessageRequest | MessageResponse | MessageResponseError | MessageEvent;
|
||||
|
||||
export class HelloData {
|
||||
constructor(
|
||||
public version: string,
|
||||
|
||||
@@ -37,7 +37,7 @@ export interface Event<T> {
|
||||
}
|
||||
export type EventClass<T> = Event<T>["constructor"];
|
||||
|
||||
interface RequestEntry {
|
||||
export interface RequestEntry {
|
||||
Request: RequestClass<unknown, unknown>;
|
||||
name: string,
|
||||
allowedSrcTypes: Set<number>;
|
||||
@@ -60,15 +60,15 @@ interface ForwardedRequest {
|
||||
dst: libData.Address;
|
||||
}
|
||||
|
||||
interface EventEntry {
|
||||
export interface EventEntry {
|
||||
Event: EventClass<unknown>;
|
||||
name: string,
|
||||
allowedSrcTypes: Set<number>;
|
||||
allowedDstTypes: Set<number>;
|
||||
eventFromJSON: (json: any) => Event<unknown>;
|
||||
}
|
||||
type RequestHandler<Req, Res> = (request: Req, src: libData.Address, dst: libData.Address) => Promise<Res>;
|
||||
type EventHandler<T> = (event: T, src: libData.Address, dst: libData.Address) => Promise<void>;
|
||||
export type RequestHandler<Req, Res> = (request: Req, src: libData.Address, dst: libData.Address) => Promise<Res>;
|
||||
export type EventHandler<T> = (event: T, src: libData.Address, dst: libData.Address) => Promise<void>;
|
||||
|
||||
interface Router {
|
||||
forwardMessage(origin: Link, message: Message, fallback: boolean): boolean,
|
||||
|
||||
@@ -421,7 +421,7 @@ export interface QueryLogFilter {
|
||||
/**
|
||||
* Maximum log level to include. Higher levels are more verbose.
|
||||
*/
|
||||
maxLevel: keyof typeof levels;
|
||||
maxLevel?: keyof typeof levels;
|
||||
/**
|
||||
* Include log entries from controller, all hosts and all instances.
|
||||
*/
|
||||
@@ -451,7 +451,7 @@ export interface QueryLogFilter {
|
||||
* Index to speed up query with.
|
||||
* @returns log entries matching the filter
|
||||
*/
|
||||
export async function queryLog(logDirectory: string, filter: QueryLogFilter, index: LogIndex) {
|
||||
export async function queryLog(logDirectory: string, filter: QueryLogFilter, index: LogIndex | null) {
|
||||
let files = (await fs.readdir(logDirectory)).filter(entry => logFileGlob.test(entry));
|
||||
|
||||
filter = {
|
||||
|
||||
@@ -7,7 +7,7 @@ const util = require("util");
|
||||
|
||||
const phin = require("phin");
|
||||
|
||||
const HttpCloser = require("@clusterio/controller/src/HttpCloser");
|
||||
const HttpCloser = require("@clusterio/controller/dist/controller/src/HttpCloser").default;
|
||||
|
||||
// Time to wait during async operations to ensure they happened in order.
|
||||
const tick = 20;
|
||||
|
||||
@@ -7,7 +7,7 @@ const http = require("http");
|
||||
const phin = require("phin");
|
||||
|
||||
const { wait } = require("@clusterio/lib");
|
||||
const routes = require("@clusterio/controller/src/routes");
|
||||
const routes = require("@clusterio/controller/dist/controller/src/routes");
|
||||
const mock = require("../mock");
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ const express = require("express");
|
||||
|
||||
const lib = require("@clusterio/lib");
|
||||
|
||||
const UserManager = require("@clusterio/controller/src/UserManager");
|
||||
const UserManager = require("@clusterio/controller/dist/controller/src/UserManager").default;
|
||||
|
||||
const addr = lib.Address.fromShorthand;
|
||||
|
||||
|
||||
Reference in new issue
Block a user