mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-13 15:14:52 +09:00
Implement shared Factorio credentials
Add config entries to controller and host for the Factorio username and token to use when talking to the Factorio APIs. If set an username and token is not set in factorio.settings then these settings are used, making it easy to configure one set of credentials for a cluster/host. For running more public provide your own hardware clusters, there's an option to disable controller sharing the credential with the hosts so that each host have to set their own credentials.
This commit is contained in:
1 parent
eedf5b8f67
commit
44e5847a1d
7 files changed
+130
-38
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<string, unknown>) {
|
||||
async resolveServerSettings(overrides: Record<string, unknown>, 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<string, unknown>, previous: Record<string, unknown>) {
|
||||
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();
|
||||
|
||||
@@ -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<ControllerConfigFields> {
|
||||
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<HostConfigFields> {
|
||||
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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -24,6 +24,7 @@ export const dataClasses: (RequestClass<unknown, unknown> | EventClass<unknown>)
|
||||
controller.ControllerConfigSetPropRequest,
|
||||
controller.HostGenerateTokenRequest,
|
||||
controller.HostConfigCreateRequest,
|
||||
controller.GetFactorioCredentialsRequest,
|
||||
controller.LogSetSubscriptionsRequest,
|
||||
controller.LogQueryRequest,
|
||||
controller.LogMessageEvent,
|
||||
|
||||
Reference in new issue
Block a user