mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-14 15:24:52 +09:00
Replace controller.transfer_idle_timeout with controller.transfer_min_progress, the amount a request has to move every 30 seconds, so a request trickled in is closed too. The check now runs for every route. Both ends of a proxy stream are judged on what the receiver has taken, so a sender held back by a slow receiver is not closed. Keep the limit on receiving headers, which turning off the request timeout also turned 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_min_progress", 30],
|
|
["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);
|
|
}
|