mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-14 07:24:52 +09:00
Guard the export, save, mod and stream routes with one middleware that closes the connection of an upload answered before its body was read, and of a transfer that has made no progress for the new controller.transfer_idle_timeout. The save and mod uploads share one handler that responds on the first error instead of reading the rest of the upload, and uploads cut short now fail the transfer instead of leaving it hanging. Node's 5 minute limit on receiving a request is turned off so large uploads over slow connections are not cut off.
286 lines
6.8 KiB
JavaScript
286 lines
6.8 KiB
JavaScript
import events from "node:events";
|
|
import http from "node:http";
|
|
import express from "express";
|
|
|
|
import * as lib from "@clusterio/lib";
|
|
import { User, UserManager, UserRecord, HostManager, InstanceManager, ControllerHooks } from "@clusterio/controller";
|
|
import { HostHooks, InstanceHooks } from "@clusterio/host";
|
|
|
|
const addr = lib.Address.fromShorthand;
|
|
|
|
export class MockLogger {
|
|
child() { return this; }
|
|
|
|
fatal() { }
|
|
|
|
error() { }
|
|
|
|
warn() { }
|
|
|
|
audit() { }
|
|
|
|
info() { }
|
|
|
|
server() { }
|
|
|
|
verbose() { }
|
|
}
|
|
|
|
export class MockSocket {
|
|
constructor() {
|
|
this.sentMessages = [];
|
|
this.events = new Map();
|
|
}
|
|
|
|
send(message) {
|
|
this.sentMessages.push(message);
|
|
}
|
|
|
|
on(event, fn) {
|
|
this.events.set(event, fn);
|
|
}
|
|
|
|
/* eslint-disable accessor-pairs */
|
|
set onclose(fn) {
|
|
this.on("close", (code, reason) => fn({ code, reason }));
|
|
}
|
|
|
|
set onerror(fn) {
|
|
this.on("error", fn);
|
|
}
|
|
|
|
set onopen(fn) {
|
|
this.on("open", fn);
|
|
}
|
|
|
|
set onmessage(fn) {
|
|
this.on("message", (data) => fn({ data }));
|
|
}
|
|
|
|
terminate() {
|
|
this.terminateCalled = true;
|
|
}
|
|
|
|
close() {
|
|
this.closeCalled = true;
|
|
}
|
|
}
|
|
|
|
export class MockConnector extends lib.BaseConnector {
|
|
constructor(src, dst) {
|
|
super(src, dst);
|
|
|
|
this.valid = true;
|
|
this.connected = true;
|
|
this.hasSession = true;
|
|
this.sentMessages = [];
|
|
}
|
|
|
|
send(message) {
|
|
this.sentMessages.push(message);
|
|
setImmediate(() => this.emit("send", message));
|
|
}
|
|
|
|
sendDisconnectReady() {
|
|
this.send(new lib.MessageDisconnect("ready"));
|
|
}
|
|
}
|
|
|
|
export class MockServer extends events.EventEmitter {
|
|
constructor() {
|
|
super();
|
|
this.reset();
|
|
}
|
|
|
|
reset() {
|
|
this.rconCommands = [];
|
|
this.rconCommandResults = new Map();
|
|
this.udpMessages = [];
|
|
}
|
|
|
|
async sendUdp(message) {
|
|
this.udpMessages.push(message);
|
|
}
|
|
|
|
async sendRcon(command) {
|
|
this.rconCommands.push(command);
|
|
let result = this.rconCommandResults.get(command);
|
|
if (!result) {
|
|
result = { response: "" };
|
|
}
|
|
if (result instanceof Error || typeof result === "string") {
|
|
result = { response: result };
|
|
}
|
|
if (result.time) {
|
|
await lib.wait(result.time);
|
|
}
|
|
if (result.response instanceof Error) {
|
|
throw result.response;
|
|
}
|
|
return result.response || "";
|
|
}
|
|
}
|
|
|
|
export class MockInstance extends lib.Link {
|
|
constructor() {
|
|
super(new MockConnector(addr({ instanceId: 7357 }), addr({ hostId: 1 })));
|
|
this.logger = new MockLogger();
|
|
this.server = new MockServer();
|
|
this.name = "test";
|
|
this.id = 7357;
|
|
this.status = "running";
|
|
this.autoPaused = false;
|
|
this.hooks = new InstanceHooks(this.logger);
|
|
this.loadedPlugins = new Set();
|
|
this.mockConfigEntries = new Map([
|
|
["instance.id", 7357],
|
|
["factorio.enable_save_patching", true],
|
|
["factorio.enable_script_commands", true],
|
|
]);
|
|
this.config = {
|
|
get: (name) => {
|
|
if (this.mockConfigEntries.has(name)) {
|
|
return this.mockConfigEntries.get(name);
|
|
}
|
|
throw Error(`mock for field ${name} is not implemented`);
|
|
},
|
|
};
|
|
}
|
|
|
|
async sendRcon(command, expectEmpty, plugin) {
|
|
return await this.server.sendRcon(command);
|
|
}
|
|
|
|
async sendUdp(message) {
|
|
await this.server.sendUdp(message);
|
|
}
|
|
}
|
|
|
|
export class MockHost extends lib.Link {
|
|
constructor() {
|
|
super(new MockConnector(addr({ hostId: 1 }), addr("controller")));
|
|
this.hooks = new HostHooks(new MockLogger());
|
|
this.loadedPlugins = new Set();
|
|
}
|
|
}
|
|
|
|
export class MockControl extends lib.Link { }
|
|
|
|
export class MockController {
|
|
constructor() {
|
|
this.app = express();
|
|
this.app.locals.controller = this;
|
|
this.app.locals.streams = new Map();
|
|
this.mockConfigEntries = new Map([
|
|
["controller.public_url", "test"],
|
|
["controller.auth_secret", "TestSecretDoNotUse"],
|
|
["controller.proxy_stream_timeout", 1],
|
|
["controller.transfer_idle_timeout", 10],
|
|
["controller.metrics_timeout", 1],
|
|
["controller.host_pairing_timeout", 3600],
|
|
["controller.host_pairing_max_pending", 50],
|
|
]);
|
|
this.config = {
|
|
get: (name) => {
|
|
if (this.mockConfigEntries.has(name)) {
|
|
return this.mockConfigEntries.get(name);
|
|
}
|
|
throw Error(`mock for field ${name} is not implemented`);
|
|
},
|
|
};
|
|
|
|
const roleProvider = new lib.MemoryDatastoreProvider(new Map([
|
|
[0, lib.Role.fromJSON({ id: 0, name: "Admin", description: "admin", permissions: ["core.admin"] })],
|
|
[1, lib.Role.fromJSON({ id: 1, name: "Player", description: "player", permissions: [] })],
|
|
]));
|
|
|
|
this.roles = new lib.SubscribableDatastore(roleProvider, roleProvider.value);
|
|
this.roles.get(1).grantDefaultPermissions(); // Todo replace "core.admin" above with grantAdminPermissions
|
|
|
|
const userProvider = new lib.MemoryDatastoreProvider(new Map([
|
|
["test", UserRecord.fromJSON({ name: "test", roles: [0, 1] })],
|
|
["player", UserRecord.fromJSON({ name: "player", roles: [1] })],
|
|
]));
|
|
|
|
this.users = new UserManager(
|
|
new lib.SubscribableDatastore(userProvider, userProvider.value),
|
|
this.roles, this.config
|
|
);
|
|
|
|
this.instances = new InstanceManager(new lib.KeyValueDatastore(), this);
|
|
|
|
this.hosts = new HostManager(new lib.SubscribableDatastore(), this);
|
|
this.handles = new Map();
|
|
this.hooks = new ControllerHooks(new MockLogger());
|
|
this.loadedPlugins = new Set();
|
|
this.wsServer = { hostConnections: new Map() };
|
|
}
|
|
|
|
get authSecret() {
|
|
return Buffer.from(this.config.get("controller.auth_secret"), "base64");
|
|
}
|
|
|
|
handle(eventClass, handler) {
|
|
this.handles.set(eventClass, handler);
|
|
}
|
|
|
|
deleteNote() { }
|
|
|
|
getControllerUrl() {
|
|
return "http://controller.example/";
|
|
}
|
|
|
|
async startServer() {
|
|
this.server = http.createServer(this.app);
|
|
await new Promise(resolve => {
|
|
this.server.listen(0, "127.0.0.1", resolve);
|
|
});
|
|
let address = this.server.address();
|
|
return `http://127.0.0.1:${address.port}`;
|
|
}
|
|
|
|
async stopServer() {
|
|
if (this.server) {
|
|
this.server.close();
|
|
this.server.unref();
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function createControllerPlugin(ControllerPluginClass, info) {
|
|
let controller = new MockController();
|
|
let metrics = {};
|
|
let logger = new MockLogger();
|
|
let plugin = new ControllerPluginClass(info, controller, metrics, logger);
|
|
await plugin.init();
|
|
return plugin;
|
|
}
|
|
|
|
export async function createInstancePlugin(InstancePluginClass, info) {
|
|
let instance = new MockInstance();
|
|
let host = new MockHost();
|
|
let plugin = new InstancePluginClass(info, instance, host, new MockLogger());
|
|
await plugin.init();
|
|
return plugin;
|
|
}
|
|
|
|
export async function loadControllerPlugin(entrypoint, info) {
|
|
const controller = new MockController();
|
|
await entrypoint({ controller, metrics: {}, logger: new MockLogger(), plugin: info });
|
|
return controller;
|
|
}
|
|
|
|
export async function loadInstancePlugin(entrypoint, info) {
|
|
const instance = new MockInstance();
|
|
const host = new MockHost();
|
|
await entrypoint({ instance, host, logger: new MockLogger(), plugin: info });
|
|
return { instance, host };
|
|
}
|
|
|
|
export function getHandler(link, Class) {
|
|
if (link instanceof MockController) {
|
|
return link.handles.get(Class);
|
|
}
|
|
return link._requestHandlers.get(Class) ?? link._eventHandlers.get(Class);
|
|
}
|