mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 07:04:52 +09:00
Double timeout for the refuse connnection test as it's taking 2 seconds on Windows for some reason.
238 lines
7.7 KiB
JavaScript
238 lines
7.7 KiB
JavaScript
"use strict";
|
|
const assert = require("assert").strict;
|
|
const http = require("http");
|
|
const https = require("https");
|
|
const util = require("util");
|
|
const tls = require("tls");
|
|
|
|
const forge = require("node-forge");
|
|
const phin = require("phin");
|
|
|
|
const { wait } = require("@clusterio/lib/helpers");
|
|
const HttpCloser = require("@clusterio/master/src/HttpCloser");
|
|
|
|
// Time to wait during async operations to ensure they happened in order.
|
|
const tick = 20;
|
|
|
|
const origCreateSecureContext = tls.createSecureContext;
|
|
tls.createSecureContext = function(options) {
|
|
if (!options.cert || !options.key) {
|
|
return origCreateSecureContext(options);
|
|
}
|
|
|
|
let lessOptions = { ...options };
|
|
delete lessOptions.key;
|
|
delete lessOptions.cert;
|
|
let ctx = origCreateSecureContext(lessOptions);
|
|
ctx.context.setCert(options.cert);
|
|
ctx.context.setKey(options.key, undefined);
|
|
return ctx;
|
|
};
|
|
|
|
function serverSuite(proto) {
|
|
let api = proto === "http" ? http : https;
|
|
let server;
|
|
let serverOptions = {};
|
|
let clientOptions = {};
|
|
let closer;
|
|
let isClosed;
|
|
let addr;
|
|
|
|
function setClosed() {
|
|
isClosed = true;
|
|
}
|
|
|
|
async function get(url, options = {}) {
|
|
let core = { ...clientOptions, ...(options.core || {}) };
|
|
return await phin({ ...options, url, core });
|
|
}
|
|
|
|
before(async function() {
|
|
if (proto === "https") {
|
|
// Generate TLS certificate
|
|
let keypair = forge.pki.rsa.generateKeyPair(512);
|
|
let cert = forge.pki.createCertificate();
|
|
cert.publicKey = keypair.publicKey;
|
|
cert.serialNumber = "01";
|
|
cert.validity.notBefore = new Date();
|
|
cert.validity.notAfter = new Date(cert.validity.notBefore);
|
|
cert.validity.notAfter.setFullYear(cert.validity.notAfter.getFullYear() + 1);
|
|
let attrs = [{ name: "commonName", value: "localhost" }];
|
|
cert.setSubject(attrs);
|
|
cert.setIssuer(attrs);
|
|
cert.sign(keypair.privateKey);
|
|
serverOptions.key = forge.pki.privateKeyToPem(keypair.privateKey);
|
|
let pem = forge.pki.certificateToPem(cert);
|
|
serverOptions.cert = pem;
|
|
serverOptions.ciphers = "DEFAULT@SECLEVEL=0";
|
|
clientOptions.ca = pem;
|
|
clientOptions.ciphers = "DEFAULT@SECLEVEL=0";
|
|
}
|
|
});
|
|
beforeEach(async function() {
|
|
isClosed = false;
|
|
server = api.createServer(serverOptions);
|
|
server.unref();
|
|
server.on("close", setClosed);
|
|
closer = new HttpCloser(server);
|
|
await util.promisify(server.listen.bind(server))();
|
|
addr = `${proto}://localhost:${server.address().port}/`;
|
|
});
|
|
|
|
afterEach(function() {
|
|
server.off("close", setClosed);
|
|
});
|
|
|
|
it("should close the server", async function() {
|
|
await closer.close();
|
|
assert(isClosed, "Server was not closed");
|
|
});
|
|
|
|
it("should reject with error if the close fails", async function() {
|
|
await closer.close();
|
|
await assert.rejects(closer.close(), { code: "ERR_SERVER_NOT_RUNNING" });
|
|
});
|
|
|
|
it("should gracefully let pending requests finish", async function() {
|
|
server.on("request", (request, response) => {
|
|
setTimeout(() => { response.end("ok-1"); }, 3*tick);
|
|
});
|
|
|
|
let request = get(addr);
|
|
request.catch(() => {});
|
|
await wait(tick);
|
|
await closer.close();
|
|
|
|
let response = await request;
|
|
assert.equal(response.statusCode, 200, "non-200 response status code");
|
|
assert.equal(response.body.toString(), "ok-1", "response content does not match expected");
|
|
});
|
|
|
|
it("should set Connection header to close for pending requests", async function() {
|
|
server.on("request", (request, response) => {
|
|
setTimeout(() => { response.end("ok-2"); }, 3*tick);
|
|
assert.equal(request.headers.connection, "keep-alive", "Connection not kept alive");
|
|
});
|
|
|
|
let agent = new api.Agent({ keepAlive: true });
|
|
let request = get(addr, { core: { agent }});
|
|
request.catch(() => {});
|
|
await wait(tick);
|
|
await closer.close();
|
|
|
|
let response = await request;
|
|
assert.equal(response.statusCode, 200, "non-200 response status code");
|
|
assert.equal(response.body.toString(), "ok-2", "response content does not match expected");
|
|
assert.equal(response.headers.connection, "close", "response Connection header not set to close");
|
|
});
|
|
|
|
it("should gracefully close open connections", async function() {
|
|
server.on("request", (request, response) => {
|
|
response.end("ok-3");
|
|
assert.equal(request.headers.connection, "keep-alive", "Connection not kept alive");
|
|
});
|
|
|
|
let agent = new api.Agent({ keepAlive: true });
|
|
let request = get(addr, { core: { agent }});
|
|
request.catch(() => {});
|
|
await wait(tick);
|
|
await closer.close();
|
|
|
|
let response = await request;
|
|
assert.equal(response.statusCode, 200, "Request replied with non-200 status code");
|
|
assert.equal(response.body.toString(), "ok-3", "Request content does not match expected");
|
|
});
|
|
|
|
it("should refuse connection if request sent after starting closure", async function() {
|
|
this.timeout(4000); // On Windows this test takes 2 seconds for some reason.
|
|
server.on("request", (request, response) => {
|
|
setTimeout(() => { response.end("ok-4"); }, 3*tick);
|
|
});
|
|
|
|
let request = get(addr);
|
|
request.catch(() => {});
|
|
await wait(tick);
|
|
let close = closer.close();
|
|
|
|
let agent = new api.Agent({ keepAlive: true });
|
|
await assert.rejects(get(addr, { core: { agent }}), { code: "ECONNREFUSED" });
|
|
|
|
let response = await request;
|
|
assert.equal(response.statusCode, 200, "non-200 response status code");
|
|
assert.equal(response.body.toString(), "ok-4", "response content does not match expected");
|
|
|
|
await close;
|
|
});
|
|
|
|
it("should close connection after in-flight request is over", async function() {
|
|
server.on("request", (request, response) => {
|
|
response.write("ok");
|
|
setTimeout(() => { response.end("-5"); }, 3*tick);
|
|
assert.equal(request.headers.connection, "keep-alive", "Connection not kept alive");
|
|
});
|
|
|
|
let agent = new api.Agent({ keepAlive: true });
|
|
let request = get(addr, { core: { agent }});
|
|
request.catch(() => {});
|
|
await wait(tick);
|
|
await closer.close();
|
|
|
|
let response = await request;
|
|
assert.equal(response.statusCode, 200, "non-200 response status code");
|
|
assert.equal(response.body.toString(), "ok-5", "response content does not match expected");
|
|
});
|
|
|
|
it("should cause connection reset for qeueud request after in-flight request", async function() {
|
|
server.on("request", (request, response) => {
|
|
response.write("ok");
|
|
setTimeout(() => { response.end("-6"); }, 3*tick);
|
|
assert.equal(request.headers.connection, "keep-alive", "Connection not kept alive");
|
|
});
|
|
|
|
let agent = new api.Agent({ keepAlive: true, maxSockets: 1 });
|
|
let request = get(addr, { core: { agent }});
|
|
request.catch(() => {});
|
|
let droppedRequest = get(addr, { core: { agent }});
|
|
droppedRequest.catch(() => {});
|
|
await wait(tick);
|
|
await closer.close();
|
|
|
|
let response = await request;
|
|
assert.equal(response.statusCode, 200, "non-200 response status code");
|
|
assert.equal(response.body.toString(), "ok-6", "response content does not match expected");
|
|
await assert.rejects(droppedRequest, { code: "ECONNRESET" });
|
|
});
|
|
|
|
it("should abort requests that take too long to respond", async function() {
|
|
server.on("request", (request, response) => {
|
|
response.write("ok");
|
|
setTimeout(() => { response.end("-7"); }, 5*tick);
|
|
});
|
|
|
|
let request = get(addr);
|
|
request.catch(() => {});
|
|
await wait(tick);
|
|
await closer.close(tick*2);
|
|
|
|
// Due to a bug in phin [https://github.com/ethanent/phin/issues/59]
|
|
// the request Promise never settles.
|
|
// TODO: uncomment when bug is fixed in phin.
|
|
// await assert.rejects(request);
|
|
});
|
|
}
|
|
|
|
describe("master/src/HttpCloser.js", function() {
|
|
describe("class HttpCloser", function() {
|
|
describe("constructor()", function() {
|
|
it("should throw if not passed a Server instance", function() {
|
|
assert.throws(() => new HttpCloser({}));
|
|
});
|
|
});
|
|
for (let proto of ["http", "https"]) {
|
|
describe(`${proto} server`, function() {
|
|
serverSuite(proto);
|
|
});
|
|
}
|
|
});
|
|
});
|