diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ac866c1..08fc564b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,9 @@ Many thanks to the following for contributing to this release: - Added instance version display [#573](https://github.com/clusterio/clusterio/pull/573) - Added `factorio.shutdown_timeout` config to set the time the host will wait for a Factorio server to shut down befor killing it. - Changed shutdown logic to prefer sending a /quit command via RCON instead of using diverging logic on Windows and Linux. +- Added `controller.factorio_username` and `controller.factorio_token` config to set Factorio credentials used for the whole cluster. +- Added `controller.share_factorio_credential_with_hosts` config to optionally require hosts to provide their own credentials. +- Added `host.factorio_username` and `host.factorio_token` config to set Factorio credentials used on a given host. ### Breaking Changes diff --git a/packages/controller/src/Controller.ts b/packages/controller/src/Controller.ts index 4d9c2c80..22b54567 100644 --- a/packages/controller/src/Controller.ts +++ b/packages/controller/src/Controller.ts @@ -788,8 +788,6 @@ export default class Controller { "tags": ["clusterio"], "max_players": 0, "visibility": { "public": true, "lan": true }, - "username": "", - "token": "", "game_password": "", "require_user_verification": true, "max_upload_in_kilobytes_per_second": 0, diff --git a/packages/controller/src/HostConnection.ts b/packages/controller/src/HostConnection.ts index 388014d2..6da8e93c 100644 --- a/packages/controller/src/HostConnection.ts +++ b/packages/controller/src/HostConnection.ts @@ -74,6 +74,7 @@ export default class HostConnection extends BaseConnection { this._controller.hosts.set(this.info); }); + this.handle(lib.GetFactorioCredentialsRequest, this.handleGetFactorioCredentialsRequest.bind(this)); this.handle(lib.HostInfoUpdateEvent, this.handleHostInfoUpdateEvent.bind(this)); this.handle(lib.InstanceStatusChangedEvent, this.handleInstanceStatusChangedEvent.bind(this)); this.handle(lib.InstancesUpdateRequest, this.handleInstancesUpdateRequest.bind(this)); @@ -161,6 +162,16 @@ export default class HostConnection extends BaseConnection { return this.info.remoteAddress; } + async handleGetFactorioCredentialsRequest() { + if (this._controller.config.get("controller.share_factorio_credentials_with_hosts")) { + return { + username: this._controller.config.get("controller.factorio_username") ?? undefined, + token: this._controller.config.get("controller.factorio_token") ?? undefined, + }; + } + return {}; + } + async handleHostInfoUpdateEvent(event: lib.HostInfoUpdateEvent) { this.info.name = event.update.name; this.info.publicAddress = event.update.publicAddress; diff --git a/packages/host/src/Instance.ts b/packages/host/src/Instance.ts index 8cbc903b..9cbd3461 100644 --- a/packages/host/src/Instance.ts +++ b/packages/host/src/Instance.ts @@ -583,11 +583,23 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " "); * entries from the given settings object. * * @param overrides - Server settings to override. + * @param includeCredentials - Include Factorio username and token from host/controller config. * @returns * server example settings with the given settings applied over it. */ - async resolveServerSettings(overrides: Record) { + async resolveServerSettings(overrides: Record, includeCredentials: boolean) { let serverSettings = await this.server.exampleSettings(); + if (includeCredentials && !overrides.username && !overrides.token) { + let credentials = { + username: this._host.config.get("host.factorio_username") ?? undefined, + token: this._host.config.get("host.factorio_token") ?? undefined, + }; + if (!credentials.username && !credentials.token) { + Object.assign(credentials, await this.sendTo("controller", new lib.GetFactorioCredentialsRequest())); + } + if (credentials.username) { serverSettings.username = credentials.username; } + if (credentials.token) { serverSettings.token = credentials.token; } + } for (let [key, value] of Object.entries(overrides)) { if (!Object.hasOwnProperty.call(serverSettings, key)) { @@ -605,12 +617,14 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " "); * Generate the server-settings.json file from the example file in the * data directory and override any settings configured in the instance's * factorio_settings config entry. + * + * @param includeCredentials - Include Factorio username and token from host/controller config. */ - async writeServerSettings() { + async writeServerSettings(includeCredentials: boolean) { const warning = "Changes to this file will be overwitten by the factorio.settings config on the instance."; const serverSettings = { "_comment_warning": warning, - ...await this.resolveServerSettings(this.config.get("factorio.settings")), + ...await this.resolveServerSettings(this.config.get("factorio.settings"), includeCredentials), }; await lib.safeOutputFile( this.server.writePath("server-settings.json"), @@ -714,7 +728,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " "); */ async prepare() { this.logger.verbose("Writing server-settings.json"); - await this.writeServerSettings(); + await this.writeServerSettings(true); if (this.config.get("factorio.sync_adminlist")) { this.logger.verbose("Writing server-adminlist.json"); @@ -884,8 +898,8 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " "); } async updateFactorioSettings(current: Record, previous: Record) { - current = await this.resolveServerSettings(current); - previous = await this.resolveServerSettings(previous); + current = await this.resolveServerSettings(current, false); + previous = await this.resolveServerSettings(previous, false); for (let [key, action] of Object.entries(serverSettingsActions)) { if (current[key] !== undefined && !util.isDeepStrictEqual(current[key], previous[key])) { @@ -1008,21 +1022,26 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " "); async handleInstanceStartRequest(request: lib.InstanceStartRequest) { let saveName = request.save; try { - await this.prepare(); - saveName = await this.prepareSave(saveName); - } catch (err: any) { - this.logger.error(`Error preparing instance: ${err.message}`); - this.notifyExit(); - await this.sendSaveListUpdate(); - throw err; - } + try { + await this.prepare(); + saveName = await this.prepareSave(saveName); + } catch (err: any) { + this.logger.error(`Error preparing instance: ${err.message}`); + this.notifyExit(); + await this.sendSaveListUpdate(); + throw err; + } - try { - await this.start(saveName); - } catch (err: any) { - this.logger.error(`Error starting ${saveName}: ${err.message}`); - await this.stop(); - throw err; + try { + await this.start(saveName); + } catch (err: any) { + this.logger.error(`Error starting ${saveName}: ${err.message}`); + await this.stop(); + throw err; + } + } finally { + this.logger.verbose("Wiping credentials from server-settings.json"); + await this.writeServerSettings(false); } } @@ -1033,21 +1052,26 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " "); } try { - await this.prepare(); - } catch (err: any) { - this.logger.error(`Error preparing instance: ${err.message}`); - this.notifyExit(); - await this.sendSaveListUpdate(); - throw err; - } + try { + await this.prepare(); + } catch (err: any) { + this.logger.error(`Error preparing instance: ${err.message}`); + this.notifyExit(); + await this.sendSaveListUpdate(); + throw err; + } - let { scenario, seed, mapGenSettings, mapSettings } = request; - try { - await this.startScenario(scenario, seed, mapGenSettings, mapSettings); - } catch (err: any) { - this.logger.error(`Error starting scenario ${scenario}: ${err.message}`); - await this.stop(); - throw err; + let { scenario, seed, mapGenSettings, mapSettings } = request; + try { + await this.startScenario(scenario, seed, mapGenSettings, mapSettings); + } catch (err: any) { + this.logger.error(`Error starting scenario ${scenario}: ${err.message}`); + await this.stop(); + throw err; + } + } finally { + this.logger.verbose("Wiping credentials from server-settings.json"); + await this.writeServerSettings(false); } } @@ -1059,7 +1083,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " "); this.notifyStatus("creating_save"); try { this.logger.verbose("Writing server-settings.json"); - await this.writeServerSettings(); + await this.writeServerSettings(false); await this.syncMods(); } catch (err: any) { @@ -1087,7 +1111,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " "); this.notifyStatus("exporting_data"); try { this.logger.verbose("Writing server-settings.json"); - await this.writeServerSettings(); + await this.writeServerSettings(false); this.logger.info("Exporting data ....."); await this.syncMods(); diff --git a/packages/lib/src/config/definitions.ts b/packages/lib/src/config/definitions.ts index b50eeaa3..8f38de08 100644 --- a/packages/lib/src/config/definitions.ts +++ b/packages/lib/src/config/definitions.ts @@ -20,6 +20,9 @@ export interface ControllerConfigFields { "controller.metrics_timeout": number; "controller.system_metrics_interval": number; "controller.proxy_stream_timeout": number; + "controller.factorio_username": string | null; + "controller.factorio_token": string | null; + "controller.share_factorio_credentials_with_hosts": boolean; "controller.default_mod_pack_id": number | null; "controller.default_role_id": number | null; "controller.autosave_interval": number; @@ -147,6 +150,27 @@ export class ControllerConfig extends classes.Config { type: "number", initialValue: 15, }, + "controller.factorio_username": { + title: "Factorio Username", + description: "Username to authenticate with Factorio API with.", + type: "string", + optional: true, + }, + "controller.factorio_token": { + title: "Factorio Token", + description: "Token to authenticate with Factorio API with.", + type: "string", + credential: ["controller"], + optional: true, + }, + "controller.share_factorio_credentials_with_hosts": { + title: "Share Factorio credentials with Hosts", + description: + "If enabled, the Factorio Username and Token will be shared with hosts and " + + "used in the server settings of instances in this cluster.", + type: "boolean", + initialValue: true, + }, "controller.default_mod_pack_id": { title: "Default Mod Pack", description: "Mod pack used by default for instances.", @@ -175,6 +199,8 @@ export interface HostConfigFields { "host.tls_ca": string | null; "host.public_address": string; "host.factorio_port_range": string; + "host.factorio_username": string | null, + "host.factorio_token": string | null, "host.max_reconnect_delay": number; } @@ -245,6 +271,23 @@ export class HostConfig extends classes.Config { type: "string", initialValue: "34100-34199", }, + "host.factorio_username": { + title: "Factorio Username", + description: + "Username to authenticate with Factorio API with. If set this will be used in the server settings " + + "of all instances on this host.", + type: "string", + optional: true, + }, + "host.factorio_token": { + title: "Factorio Token", + description: + "Token to authenticate with Factorio API with. If set this will be used in the server settings " + + "of all instances on this host.", + type: "string", + credential: ["host"], + optional: true, + }, "host.max_reconnect_delay": { title: "Max Reconnect Delay", description: "Maximum delay to wait before attempting to reconnect WebSocket", diff --git a/packages/lib/src/data/messages_controller.ts b/packages/lib/src/data/messages_controller.ts index e24d0be4..41492305 100644 --- a/packages/lib/src/data/messages_controller.ts +++ b/packages/lib/src/data/messages_controller.ts @@ -122,6 +122,18 @@ export class HostConfigCreateRequest { static Response = plainJson(HostConfig.jsonSchema); } +export class GetFactorioCredentialsRequest { + declare ["constructor"]: typeof GetFactorioCredentialsRequest; + static type = "request" as const; + static src = ["host", "instance"] as const; + static dst = "controller" as const; + + static Response = plainJson(Type.Object({ + "username": Type.Optional(Type.String()), + "token": Type.Optional(Type.String()), + })); +} + export class LogSetSubscriptionsRequest { declare ["constructor"]: typeof LogSetSubscriptionsRequest; static type = "request" as const; diff --git a/packages/lib/src/link/messages.ts b/packages/lib/src/link/messages.ts index 7dd7ed99..4182db3a 100644 --- a/packages/lib/src/link/messages.ts +++ b/packages/lib/src/link/messages.ts @@ -24,6 +24,7 @@ export const dataClasses: (RequestClass | EventClass) controller.ControllerConfigSetPropRequest, controller.HostGenerateTokenRequest, controller.HostConfigCreateRequest, + controller.GetFactorioCredentialsRequest, controller.LogSetSubscriptionsRequest, controller.LogQueryRequest, controller.LogMessageEvent,