mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 15:04:52 +09:00
Drop the special case that deferred terminating the connection issuing the delete, so deleting yourself behaves the same as revoke-token and every session of the deleted user is closed before the handler returns. Set tokenValidAfter to the current time in createUser instead of 0. Deleting a user and recreating it under the same name left tokens issued before the deletion valid, as the check is iat < tokenValidAfter. Doing it in createUser covers every path that makes a user. A token signed in the same second as creation is still accepted, so create-admin and the other production paths sign after the user exists and are unaffected. The integration harness signed the control token at import time, before bootstrap create-admin created the user. Sign it in the root before hook instead and reuse it in link.js. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
145 lines
5.0 KiB
JavaScript
145 lines
5.0 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import events from "node:events";
|
|
import jwt from "jsonwebtoken";
|
|
|
|
import {
|
|
TestControlConnector, TestHostConnector, TestControl, get, exec, url, controlToken, slowTest,
|
|
} from "./index.js";
|
|
import { ConnectionClosed, ProtocolError, PolicyViolation, AuthenticationFailed } from "@clusterio/lib";
|
|
|
|
let tokenHost = jwt.sign({ aud: "host", host: 0 }, Buffer.from("TestSecretDoNotUse", "base64"));
|
|
|
|
describe("Integration of lib/link", function() {
|
|
let control;
|
|
let controlConnector;
|
|
let hostConnector;
|
|
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 = tokenHost;
|
|
hostConnector.hostId = 0;
|
|
});
|
|
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 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 = 1;
|
|
await assert.rejects(
|
|
hostConnector.connect(),
|
|
new Error("Authentication failed: missmatched host id")
|
|
);
|
|
});
|
|
|
|
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: 4 }, Buffer.from("TestSecretDoNotUse", "base64"));
|
|
hostConnector.hostId = 4; // Same as already running host
|
|
await assert.rejects(
|
|
hostConnector.connect(),
|
|
new Error("Authentication failed: missmatched host id")
|
|
);
|
|
});
|
|
});
|