mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-15 23:24:52 +09:00
Approve standalone hosts like any other host
A standalone install no longer adds its host to the controller database by signing a token and writing hosts.json itself. The host asks to join when it first starts and is approved once with the link it prints.
This commit is contained in:
7 files changed
+16
-227
No files matched your search
@@ -19,7 +19,7 @@ The installer runs without prompts when all the questions for the mode are answe
|
||||
|
||||
## Modes
|
||||
|
||||
- `standalone`: controller and host on this computer.
|
||||
- `standalone`: controller and host on this computer. The host is approved like any other host the first time it starts.
|
||||
The host is added to the controller directly and does not need to be approved.
|
||||
- `controller`: controller only.
|
||||
- `host`: host only, joins the cluster of an existing controller, see [Setting up a host](#setting-up-a-host).
|
||||
|
||||
@@ -10,7 +10,6 @@ import util from "node:util";
|
||||
import yargs from "yargs";
|
||||
|
||||
import { levels, logger, setLogLevel } from "./logging.js";
|
||||
import { createLocalHost } from "./local_host.js";
|
||||
import { copyPluginTemplates } from "./template.js";
|
||||
let dev = false;
|
||||
const scriptExt = process.platform === "win32" ? ".cmd" : "";
|
||||
@@ -869,25 +868,15 @@ async function main() {
|
||||
}
|
||||
|
||||
if (answers.mode === "standalone") {
|
||||
// The controller is not running, so the host is added to its database directly. The id
|
||||
// from the host config is kept so running the installer again reuses the same host.
|
||||
let result = await execHost(["config", "show", "host.id"]);
|
||||
let hostId = Number.parseInt(result.stdout.split("\n").slice(-2)[0], 10);
|
||||
let host = await createLocalHost({ name: "local", id: Number.isInteger(hostId) ? hostId : undefined });
|
||||
await execHost(["config", "set", "host.id", host.hostId]);
|
||||
await execHost(["config", "set", "host.name", host.name]);
|
||||
// Default to localhost on correct port for host in standalone mode
|
||||
await execHost(["config", "set", "host.controller_url", `http://localhost:${answers.httpPort}/`]);
|
||||
await execHost(["config", "set", "host.controller_token", host.token]);
|
||||
}
|
||||
|
||||
if (answers.mode === "host") {
|
||||
// The host requests to join the cluster when it is started.
|
||||
await execHost(["config", "set", "host.name", answers.hostName]);
|
||||
await execHost(["config", "set", "host.controller_url", answers.controllerUrl]);
|
||||
answers.hostName = "local";
|
||||
answers.controllerUrl = `http://localhost:${answers.httpPort}/`;
|
||||
}
|
||||
|
||||
if (["standalone", "host"].includes(answers.mode)) {
|
||||
// The host requests to join the cluster when it is started.
|
||||
await execHost(["config", "set", "host.name", answers.hostName]);
|
||||
await execHost(["config", "set", "host.controller_url", answers.controllerUrl]);
|
||||
await execHost(["config", "set", "host.public_address", answers.publicAddress]);
|
||||
await execHost(["config", "set", "host.factorio_directory", answers.factorioDir]);
|
||||
await setRemoteUpdates(execHost, "host", answers.remoteNpm);
|
||||
@@ -906,7 +895,7 @@ async function main() {
|
||||
|
||||
/* eslint-disable no-console */
|
||||
console.log(`Successfully installed ${answers.mode}`);
|
||||
if (answers.mode === "host") {
|
||||
if (["standalone", "host"].includes(answers.mode)) {
|
||||
console.log(
|
||||
`Start the host with run-host${process.platform === "win32" ? ".cmd" : ".sh"}, then approve it on the ` +
|
||||
`Hosts page of ${answers.controllerUrl}`
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
// Adds the host of a standalone install to the controller database, used by the installer.
|
||||
// Only uses Node.js builtins as it runs before Clusterio is installed.
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
function signHostToken(payload, secret) {
|
||||
const encode = value => Buffer.from(JSON.stringify(value)).toString("base64url");
|
||||
const unsigned = `${encode({ alg: "HS256", typ: "JWT" })}.${encode(payload)}`;
|
||||
const signature = crypto.createHmac("sha256", secret).update(unsigned).digest("base64url");
|
||||
return `${unsigned}.${signature}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a host to the database of a controller installed in the same directory
|
||||
*
|
||||
* The controller must not be running, as it would overwrite the database with
|
||||
* what it has in memory.
|
||||
*
|
||||
* @param {object} options - Host options.
|
||||
* @param {string=} options.directory - Directory with config-controller.json in it.
|
||||
* @param {string} options.name - Name of the host.
|
||||
* @param {number=} options.id -
|
||||
* Id to give the host, picks a random unused id if not given. If a host with
|
||||
* this id exists, a new token is signed for it and its name is kept.
|
||||
* @param {string=} options.version - Version to record for the host.
|
||||
* @returns {Promise<{ hostId: number, token: string, name: string }>} id, token and name of the host.
|
||||
*/
|
||||
export async function createLocalHost({ directory = ".", name, id, version = "" }) {
|
||||
const configPath = path.join(directory, "config-controller.json");
|
||||
const config = JSON.parse(await fs.readFile(configPath, "utf8"));
|
||||
const secret = config["controller.auth_secret"];
|
||||
if (typeof secret !== "string" || !secret) {
|
||||
throw new Error(`${configPath} has no controller.auth_secret`);
|
||||
}
|
||||
|
||||
const databaseDirectory = path.resolve(directory, config["controller.database_directory"] ?? "database");
|
||||
const hostsPath = path.join(databaseDirectory, "hosts.json");
|
||||
let hosts = [];
|
||||
try {
|
||||
hosts = JSON.parse(await fs.readFile(hostsPath, "utf8"));
|
||||
} catch (err) {
|
||||
if (err.code !== "ENOENT") {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const existing = id === undefined ? undefined : hosts.find(host => host.id === id);
|
||||
let hostId = id;
|
||||
if (existing) {
|
||||
// Running the installer again signs a new token for the same host.
|
||||
name = existing.name;
|
||||
} else {
|
||||
const ids = new Set(hosts.map(host => host.id));
|
||||
while (hostId === undefined || ids.has(hostId)) {
|
||||
hostId = crypto.randomInt(1, 2 ** 31);
|
||||
}
|
||||
hosts.push({ id: hostId, name, version, plugins: {}, token_valid_after: now });
|
||||
await fs.mkdir(databaseDirectory, { recursive: true });
|
||||
const temporary = path.join(databaseDirectory, "hosts.tmp.json");
|
||||
await fs.writeFile(temporary, JSON.stringify(hosts, null, "\t"), { flush: true });
|
||||
await fs.rename(temporary, hostsPath);
|
||||
}
|
||||
|
||||
const token = signHostToken({ aud: "host", host: hostId, iat: now }, Buffer.from(secret, "base64"));
|
||||
return { hostId, token, name };
|
||||
}
|
||||
Reference in new issue
Block a user