mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-15 23:24:52 +09:00
Hosts join the cluster by running the installer, which shows a code and waits until the request is approved under Hosts > Add host in the web interface or with clusterioctl host approve. An admin can pass their user token to the installer to approve the host right away. The token is only used for that and is not saved. The controller only accepts hosts that have a record, and only approving a host creates one. Revoking a host deletes its record, so its token can no longer be used to reconnect. Hosts that have connected before keep working. Removes host generate-token, revoke-token and create-config from clusterioctl, bootstrap generate-host-token, the host token messages and the core.host.generate_token, revoke_token and create_config permissions, which are also stripped from stored roles. Adds clusterioctl host pending, approve, reject and revoke, and the core.host.approve and core.host.revoke permissions. Standalone installs add the host to the controller database directly, since the controller is not running during the install.
196 lines
6.8 KiB
JavaScript
196 lines
6.8 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import events from "node:events";
|
|
import jwt from "jsonwebtoken";
|
|
|
|
import {
|
|
TestControlConnector, TestHostConnector, TestControl, url, controlToken, getControl, hostId, pairTestHost,
|
|
} from "./index.js";
|
|
import * as lib from "@clusterio/lib";
|
|
import { ConnectionClosed, ProtocolError, PolicyViolation, AuthenticationFailed } from "@clusterio/lib";
|
|
import { slowTest } from "../common.js";
|
|
|
|
// Valid token for a host id that was never approved
|
|
let tokenUnregistered = jwt.sign({ aud: "host", host: 0 }, Buffer.from("TestSecretDoNotUse", "base64"));
|
|
|
|
describe("Integration of lib/link", function() {
|
|
let control;
|
|
let controlConnector;
|
|
let hostConnector;
|
|
// Hosts can only connect once approved, so the host connections use a paired host
|
|
let linkHost;
|
|
before(async function() {
|
|
linkHost = await pairTestHost("link-test");
|
|
});
|
|
after(async function() {
|
|
if (linkHost) {
|
|
await getControl().send(new lib.HostRevokeRequest(linkHost.hostId));
|
|
}
|
|
});
|
|
beforeEach(async function() {
|
|
controlConnector = new TestControlConnector(url, 0.2);
|
|
controlConnector.token = controlToken;
|
|
control = new TestControl(controlConnector, false);
|
|
hostConnector = new TestHostConnector(url, 0.2);
|
|
hostConnector.token = linkHost.token;
|
|
hostConnector.hostId = linkHost.hostId;
|
|
});
|
|
afterEach(async function() {
|
|
await controlConnector.disconnect();
|
|
await hostConnector.disconnect();
|
|
});
|
|
|
|
it("should emitt an error if authentication failed", async function() {
|
|
controlConnector.token = "gibberish";
|
|
await assert.rejects(
|
|
controlConnector.connect(),
|
|
new Error("Authentication failed: jwt malformed")
|
|
);
|
|
});
|
|
|
|
it("should connect with proper credentials", async function() {
|
|
await controlConnector.connect();
|
|
});
|
|
it("should emit an error if closed due to ProtocolError", async function() {
|
|
await controlConnector.connect();
|
|
controlConnector._socket.close(ConnectionClosed.ProtocolError, "Test");
|
|
await assert.rejects(
|
|
// event.once rejects if "error" is emitted before "close"
|
|
events.once(controlConnector, "close"),
|
|
new ProtocolError("Test")
|
|
);
|
|
});
|
|
it("should emit an error if closed due to PolicyError", async function() {
|
|
await controlConnector.connect();
|
|
controlConnector._socket.close(ConnectionClosed.PolicyViolation, "Test");
|
|
await assert.rejects(
|
|
// event.once rejects if "error" is emitted before "close"
|
|
events.once(controlConnector, "close"),
|
|
new PolicyViolation("Test")
|
|
);
|
|
});
|
|
it("should emit an error if closed due to Unauthorized", async function() {
|
|
await controlConnector.connect();
|
|
controlConnector._socket.close(ConnectionClosed.Unauthorized, "Test");
|
|
await assert.rejects(
|
|
// event.once rejects if "error" is emitted before "close"
|
|
events.once(controlConnector, "close"),
|
|
new AuthenticationFailed("Test")
|
|
);
|
|
});
|
|
|
|
it("should reconnect on connection lost", async function() {
|
|
await controlConnector.connect();
|
|
controlConnector._socket.close(ConnectionClosed.Normal, "Test");
|
|
await events.once(controlConnector, "resume");
|
|
});
|
|
|
|
it("should reconnect on connection terminated", async function() {
|
|
await controlConnector.connect();
|
|
controlConnector._socket.terminate();
|
|
await events.once(controlConnector, "resume");
|
|
});
|
|
|
|
it("should reconnect on connection heartbeat timeout", async function() {
|
|
await controlConnector.connect();
|
|
controlConnector.stopHeartbeat();
|
|
await events.once(controlConnector, "resume");
|
|
});
|
|
|
|
it("should invalidate on reconnection with bad session token", async function() {
|
|
await controlConnector.connect();
|
|
controlConnector._sessionToken = "blah";
|
|
controlConnector.stopHeartbeat();
|
|
await events.once(controlConnector, "invalidate");
|
|
await events.once(controlConnector, "connect");
|
|
});
|
|
|
|
it("should start a new session when the send buffer limit is exceeded", async function() {
|
|
await getControl().send(new lib.ControllerConfigSetFieldRequest("controller.max_send_buffer_size", "0.01"));
|
|
try {
|
|
await controlConnector.connect();
|
|
} finally {
|
|
await getControl().send(new lib.ControllerConfigSetFieldRequest("controller.max_send_buffer_size", "64"));
|
|
}
|
|
|
|
// Never acknowledge received messages
|
|
controlConnector._doHeartbeat = function() {
|
|
this._sendInternal(new lib.MessageHeartbeat());
|
|
};
|
|
let invalidated = false;
|
|
controlConnector.once("invalidate", () => { invalidated = true; });
|
|
const connected = events.once(controlConnector, "invalidate").then(
|
|
() => events.once(controlConnector, "connect")
|
|
);
|
|
for (let i = 0; i < 100; i++) {
|
|
if (invalidated) {
|
|
break;
|
|
}
|
|
await control.send(new lib.ControllerConfigGetRequest()).catch(() => {});
|
|
}
|
|
assert(invalidated, "session was not invalidated");
|
|
await connected;
|
|
});
|
|
|
|
it("should properly close connector if close is called during reconnect wait", async function() {
|
|
await controlConnector.connect();
|
|
controlConnector._socket.close(ConnectionClosed.Normal, "Test");
|
|
controlConnector.once("drop", () => {
|
|
controlConnector.close();
|
|
});
|
|
await events.once(controlConnector, "close");
|
|
});
|
|
|
|
it("should refuse connection with incorrect audience", async function() {
|
|
hostConnector.token = controlToken;
|
|
await assert.rejects(
|
|
hostConnector.connect(),
|
|
new Error("Authentication failed: jwt audience invalid. expected: host or slave")
|
|
);
|
|
});
|
|
|
|
it("should refuse connection with mismatched ids", async function() {
|
|
hostConnector.hostId = linkHost.hostId + 1;
|
|
await assert.rejects(
|
|
hostConnector.connect(),
|
|
new Error("Authentication failed: missmatched host id")
|
|
);
|
|
});
|
|
|
|
it("should refuse connection for a host that is not registered", async function() {
|
|
hostConnector.token = tokenUnregistered;
|
|
hostConnector.hostId = 0;
|
|
await assert.rejects(
|
|
hostConnector.connect(),
|
|
new Error(
|
|
"Authentication failed: host is not registered, run the installer again in the host directory"
|
|
)
|
|
);
|
|
});
|
|
|
|
it("should close old connections from the same address", async function() {
|
|
slowTest(this);
|
|
const hostConnectorFirst = new TestHostConnector(url, 0.2);
|
|
hostConnectorFirst.token = hostConnector.token;
|
|
hostConnectorFirst.hostId = hostConnector.hostId;
|
|
const onceClose = events.once(hostConnectorFirst, "close");
|
|
onceClose.catch(() => {}); // Prevent unhandled promise rejection.
|
|
await hostConnectorFirst.connect();
|
|
await hostConnector.connect();
|
|
await assert.rejects(
|
|
onceClose,
|
|
new PolicyViolation("Newer connection for host from same address")
|
|
);
|
|
});
|
|
|
|
it("should refuse duplicate host connections", async function() {
|
|
this.skip(); // Can't be checked because the remote address for both connections is the same
|
|
|
|
hostConnector.token = jwt.sign({ aud: "host", host: hostId }, Buffer.from("TestSecretDoNotUse", "base64"));
|
|
hostConnector.hostId = hostId; // Same as already running host
|
|
await assert.rejects(
|
|
hostConnector.connect(),
|
|
new Error("Authentication failed: missmatched host id")
|
|
);
|
|
});
|
|
});
|