text: #!/usr/bin/env node
  
  /**
   * Clusterio master server
   *
   * Facilitates communication between slaves and control of the cluster
   * through WebSocet connections, and hosts a webserver for browser
   * interfaces and Prometheus statistics export.  It is remotely controlled
   * by {@link module:ctl/ctl}.
   *
   * @module master/master
   * @author Danielv123, Hornwitser
   * @example
   * npx clusteriomaster run
   */
  
  // Attempt updating
  // const updater = require("./updater");
  // updater.update().then(console.log);
  
  "use strict";
  const path = require("path");
  const fs = require("fs-extra");
  const jwt = require("jsonwebtoken");
  const crypto = require("crypto");
  const setBlocking = require("set-blocking");
  const events = require("events");
  const yargs = require("yargs");
  const WebSocket = require("ws");
  const JSZip = require("jszip");
  const version = require("./package").version;
  const util = require("util");
  const winston = require("winston");
  
  // ugly globals
  let masterConfig;
  let masterConfigPath;
  let masterPlugins = new Map();
  let pluginInfos = new Map();
  let stopAcceptingNewSessions = false;
  let debugEvents = new events.EventEmitter();
  let loadedPlugins = {};
  let devMiddleware;
  let clusterLogger;
  let db = {
  	instances: new Map(),
  	slaves: new Map(),
  };
  
  // homebrew modules
  const generateSslCert = require("./src/generate_ssl_cert");
  const routes = require("./src/routes");
  const libDatabase = require("@clusterio/lib/database");
  const libSchema = require("@clusterio/lib/schema");
  const libLink = require("@clusterio/lib/link");
  const libErrors = require("@clusterio/lib/errors");
  const libPlugin = require("@clusterio/lib/plugin");
  const libPluginLoader = require("@clusterio/lib/plugin_loader");
  const libPrometheus = require("@clusterio/lib/prometheus");
  const libConfig = require("@clusterio/lib/config");
  const libUsers = require("@clusterio/lib/users");
  const libSharedCommands = require("@clusterio/lib/shared_commands");
  const libHelpers = require("@clusterio/lib/helpers");
  const { ConsoleTransport, levels, logger } = require("@clusterio/lib/logging");
  const libLoggingUtils = require("@clusterio/lib/logging_utils");
  
  const express = require("express");
  const compression = require("compression");
  const cookieParser = require("cookie-parser");
  // Required for express post requests
  const bodyParser = require("body-parser");
  const fileUpload = require("express-fileupload");
  const app = express();
  let httpServer;
  let httpsServer;
  
  app.use(cookieParser());
  app.use(bodyParser.json({
  	limit: "10mb",
  }));
  app.use(bodyParser.urlencoded({
  	parameterLimit: 100000,
  	limit: "10mb",
  	extended: true,
  }));
  app.use(fileUpload());
  app.use(compression());
  
  
  // Servers the web interface with the root path set apropriately.
  function serveWeb(route) {
  	// The depth is is the number of slashes in the route minus one, but due
  	// to lenient matching on trailing slashes in the route we need to
  	// compensate if the request path contains a slash but not the route,
  	// and vice versa.
  	let routeDepth = (route.match(/\//g) || []).length - 1 - (route.slice(-1) === "/");
  	return function(req, res, next) {
  		let depth = routeDepth + (req.path.slice(-1) === "/");
  		let webRoot = "../".repeat(depth) || "./";
  		fs.readFile(path.join(__dirname, "web", "index.html"), "utf8").then((content) => {
  			res.type("text/html");
  			res.send(content.replace(/__WEB_ROOT__/g, webRoot));
  		}).catch(err => {
  			next(err);
  		});
  	};
  }
  
  // Set folder to serve static content from (the website)
  app.use(express.static(path.join(__dirname, "static")));
  app.use(express.static("static")); // Used for data export files
  
  const slaveMappingGauge = new libPrometheus.Gauge(
  	"clusterio_master_slave_mapping",
  	"Mapping of Slave ID to name",
  	{
  		labels: ["slave_id", "slave_name"],
  		callback: function() {
  			slaveMappingGauge.clear();
  			for (let [id, slave] of db.slaves) {
  				slaveMappingGauge.labels({
  					slave_id: String(id),
  					slave_name: slave.name,
  				}).set(1);
  			};
  		},
  	}
  );
  
  const instanceMappingGauge = new libPrometheus.Gauge(
  	"clusterio_master_instance_mapping",
  	"Mapping of Instance ID to name and slave",
  	{
  		labels: ["instance_id", "instance_name", "slave_id"],
  		callback: function() {
  			instanceMappingGauge.clear();
  			for (let [id, instance] of db.instances) {
  				instanceMappingGauge.labels({
  					instance_id: String(id),
  					instance_name: String(instance.config.get("instance.name")),
  					slave_id: String(instance.config.get("instance.assigned_slave")),
  				}).set(1);
  			}
  		},
  	}
  );
  
  const endpointHitCounter = new libPrometheus.Counter(
  	"clusterio_master_http_endpoint_hits_total",
  	"How many requests a particular HTTP endpoint has gotten",
  	{ labels: ["route"] }
  );
  
  const wsMessageCounter = new libPrometheus.Counter(
  	"clusterio_master_websocket_message_total",
  	"How many messages have been received over WebSocket on the master server",
  	{ labels: ["direction"] }
  );
  
  const wsConnectionsCounter = new libPrometheus.Counter(
  	"clusterio_master_websocket_connections_total",
  	"How many WebSocket connections have been initiated on the master server"
  );
  
  const wsRejectedConnectionsCounter = new libPrometheus.Counter(
  	"clusterio_master_websocket_rejected_connections_total",
  	"How many WebSocket connections have been rejected during the handshake on the master server"
  );
  
  const wsActiveConnectionsGauge = new libPrometheus.Gauge(
  	"clusterio_master_websocket_active_connections",
  	"How many WebSocket connections are currently open to the master server"
  );
  
  const wsActiveSlavesGauge = new libPrometheus.Gauge(
  	"clusterio_master_active_slaves",
  	"How many slaves are currently connected to the master"
  );
  
  // Prometheus polling endpoint
  async function getMetrics(req, res, next) {
  	endpointHitCounter.labels(req.route.path).inc();
  
  	let results = [];
  	let pluginResults = await libPlugin.invokeHook(masterPlugins, "onMetrics");
  	for (let metricIterator of pluginResults) {
  		for await (let metric of metricIterator) {
  			results.push(metric);
  		}
  	}
  
  	let requests = [];
  	for (let slaveConnection of slaveConnections.values()) {
  		requests.push(libHelpers.timeout(libLink.messages.getMetrics.send(slaveConnection), 8e3, null));
  	}
  
  	for await (let result of await libPrometheus.defaultRegistry.collect()) {
  		results.push(result);
  	}
  
  	let resultMap = new Map();
  	for (let response of await Promise.all(requests)) {
  		if (!response) {
  			// TODO: Log timeout occured?
  			continue;
  		}
  
  		for (let result of response.results) {
  			if (!resultMap.has(result.metric.name)) {
  				resultMap.set(result.metric.name, result);
  
  			} else {
  				// Merge metrics received by multiple slaves
  				let receivedSamples = new Map(result.samples);
  				for (let [suffix, suffixSamples] of resultMap.get(result.metric.name).samples) {
  					if (receivedSamples.has(suffix)) {
  						suffixSamples.push(...receivedSamples.get(suffix));
  						receivedSamples.delete(suffix);
  					}
  				}
  
  				for (let entry of receivedSamples) {
  					result.samples.push(entry);
  				}
  			}
  		}
  	}
  
  	for (let result of resultMap.values()) {
  		results.push(libPrometheus.deserializeResult(result));
  	}
  
  	wsActiveConnectionsGauge.set(activeConnectors.size);
  	wsActiveSlavesGauge.set(slaveConnections.size);
  
  
  	let text = await libPrometheus.exposition(results);
  	res.set("Content-Type", libPrometheus.exposition.contentType);
  	res.send(text);
  }
  app.get("/metrics", (req, res, next) => getMetrics(req, res, next).catch(next));
  
  
  function validateSlaveToken(req, res, next) {
  	let token = req.header("x-access-token");
  	if (!token) {
  		res.sendStatus(401);
  		return;
  	}
  
  	try {
  		jwt.verify(token, masterConfig.get("master.auth_secret"), { audience: "slave" });
  
  	} catch (err) {
  		res.sendStatus(401);
  		return;
  	}
  
  	next();
  }
  
  app.get("/api/plugins", (req, res) => {
  	let plugins = [];
  	for (let pluginInfo of pluginInfos) {
  		let name = pluginInfo.name;
  		let enabled = masterPlugins.has(name) && masterConfig.group(name).get("enabled");
  		plugins.push({ name, enabled });
  	}
  	res.send(plugins);
  });
  
  // Handle an uploaded export package.
  async function uploadExport(req, res, next) {
  	endpointHitCounter.labels(req.route.path).inc();
  	if (req.get("Content-Type") !== "application/zip") {
  		res.sendStatus(415);
  		return;
  	}
  
  	let data = [];
  	for await (let chunk of req) {
  		data.push(chunk);
  	}
  	data = Buffer.concat(data);
  	let zip = await JSZip.loadAsync(data);
  	data = null;
  
  	// This is hardcoded to prevent path expansion attacks
  	let exportFiles = [
  		"export/item-spritesheet.png",
  		"export/item-metadata.json",
  		"export/locale.json",
  	];
  
  	for (let filePath of exportFiles) {
  		let file = zip.file(filePath);
  		if (!file) {
  			continue;
  		}
  
  		let name = path.posix.basename(filePath);
  		await fs.outputFile(path.join("static", "export", name), await file.async("nodebuffer"));
  	}
  
  	res.sendStatus(200);
  }
  app.put("/api/upload-export",
  	validateSlaveToken,
  	(req, res, next) => uploadExport(req, res, next).catch(next)
  );
  
  const masterConnectedClientsCount = new libPrometheus.Gauge(
  	"clusterio_master_connected_clients_count", "How many clients are currently connected to this master server",
  	{
  		labels: ["type"], callback: async function(gauge) {
  			gauge.labels("slave").set(slaveConnections.size);
  			gauge.labels("control").set(controlConnections.length);
  		},
  	},
  );
  
  /**
   * Load Map from JSON file in the database directory.
   *
   * @param {string} databaseDirectory - Path to master database directory.
   * @param {string} file - Name of file to load.
   * @returns {Map} file loaded as a map.
   */
  async function loadMap(databaseDirectory, file) {
  	let databasePath = path.resolve(databaseDirectory, file);
  	logger.info(`Loading ${databasePath}`);
  	return await libDatabase.loadJsonArrayAsMap(databasePath);
  }
  
  async function loadInstances(databaseDirectory, file) {
  	let filePath = path.join(databaseDirectory, file);
  	logger.info(`Loading ${filePath}`);
  
  	let instances = new Map();
  	try {
  		let serialized = JSON.parse(await fs.readFile(filePath));
  		for (let serializedConfig of serialized) {
  			let instanceConfig = new libConfig.InstanceConfig();
  			await instanceConfig.load(serializedConfig);
  			let status = instanceConfig.get("instance.assigned_slave") === null ? "unassigned" : "unknown";
  			instances.set(instanceConfig.get("instance.id"), { config: instanceConfig, status });
  		}
  
  	} catch (err) {
  		if (err.code !== "ENOENT") {
  			throw err;
  		}
  	}
  
  	return instances;
  }
  
  async function loadUsers(databaseDirectory, file) {
  	let loadedRoles = new Map();
  	let loadedUsers = new Map();
  	try {
  		let content = JSON.parse(await fs.readFile(path.join(databaseDirectory, file)));
  		for (let serializedRole of content.roles) {
  			let role = new libUsers.Role(serializedRole);
  			loadedRoles.set(role.id, role);
  		}
  
  		for (let serializedUser of content.users) {
  			let user = new libUsers.User(serializedUser, loadedRoles);
  			loadedUsers.set(user.name, user);
  		}
  
  	} catch (err) {
  		if (err.code !== "ENOENT") {
  			throw err;
  		}
  
  		// Create default roles if loading failed
  		libUsers.ensureDefaultAdminRole(loadedRoles);
  		libUsers.ensureDefaultPlayerRole(loadedRoles);
  	}
  
  	db.roles = loadedRoles;
  	db.users = loadedUsers;
  }
  
  /**
   * Create a new user
   *
   * Creates a new user instances and add it to the user database.
   *
   * @param {string} name - Name of the user to create.
   * @returns {module:lib/users.User} newly created user.
   */
  function createUser(name) {
  	if (db.users.has(name)) {
  		throw new Error(`User '${name}' already exists`);
  	}
  
  	let defaultRoleId = masterConfig.get("master.default_role_id");
  	let user = new libUsers.User({ name, roles: [defaultRoleId] }, db.roles);
  	db.users.set(name, user);
  	return user;
  }
  
  /**
   * Save Map to JSON file in the database directory.
   *
   * @param {string} databaseDirectory - Path to master database directory.
   * @param {string} file - Name of file to save.
   * @param {Map} map - Mapping to save into file.
   */
  async function saveMap(databaseDirectory, file, map) {
  	let databasePath = path.resolve(databaseDirectory, file);
  	logger.info(`Saving ${databasePath}`);
  	await libDatabase.saveMapAsJsonArray(databasePath, map);
  }
  
  async function saveInstances(databaseDirectory, file, instances) {
  	let filePath = path.join(databaseDirectory, file);
  	let serialized = [];
  	for (let instance of instances.values()) {
  		serialized.push(instance.config.serialize());
  	}
  
  	await fs.outputFile(filePath, JSON.stringify(serialized, null, 4));
  }
  
  async function saveUsers(databaseDirectory, file) {
  	let filePath = path.join(databaseDirectory, file);
  	let serializedRoles = [];
  	for (let role of db.roles.values()) {
  		serializedRoles.push(role.serialize());
  	}
  
  	let serializedUsers = [];
  	for (let user of db.users.values()) {
  		serializedUsers.push(user.serialize());
  	}
  
  	let serialized = {
  		users: serializedUsers,
  		roles: serializedRoles,
  	};
  	await fs.outputFile(filePath, JSON.stringify(serialized, null, 4));
  }
  
  /**
   * Innitiate shutdown of master server
   */
  async function shutdown() {
  	logger.info("Shutting down");
  	let exitStartTime = Date.now();
  	try {
  		logger.info("Saving configs");
  		await fs.outputFile(masterConfigPath, JSON.stringify(masterConfig.serialize(), null, 4));
  
  		await saveMap(masterConfig.get("master.database_directory"), "slaves.json", db.slaves);
  		await saveInstances(masterConfig.get("master.database_directory"), "instances.json", db.instances);
  		await saveUsers(masterConfig.get("master.database_directory"), "users.json");
  
  		await libPlugin.invokeHook(masterPlugins, "onShutdown");
  
  		stopAcceptingNewSessions = true;
  
  		if (devMiddleware) {
  			await new Promise((resolve, reject) => { devMiddleware.close(resolve); });
  		}
  
  		let disconnectTasks = [];
  		for (let controlConnection of controlConnections) {
  			controlConnection.connector.setTimeout(masterConfig.get("master.connector_shutdown_timeout"));
  			disconnectTasks.push(controlConnection.disconnect(1001, "Server Quit"));
  		}
  
  		for (let slaveConnection of slaveConnections.values()) {
  			slaveConnection.connector.setTimeout(masterConfig.get("master.connector_shutdown_timeout"));
  			disconnectTasks.push(slaveConnection.disconnect(1001, "Server Quit"));
  		}
  
  		logger.info(`Waiting for ${disconnectTasks.length} connectors to close`);
  		for (let task of disconnectTasks) {
  			try {
  				await task;
  			} catch (err) {
  				if (!(err instanceof libErrors.SessionLost)) {
  					logger.warn(`Unexpected error disconnecting connector:\n${err.stack}`);
  				}
  			}
  		}
  
  		for (let socket of pendingSockets) {
  			socket.close(1001, "Server Quit");
  		}
  
  		let stopTasks = [];
  		logger.info("Stopping HTTP(S) server");
  		if (httpServer) { stopTasks.push(new Promise(resolve => httpServer.close(resolve))); }
  		if (httpsServer) { stopTasks.push(new Promise(resolve => httpsServer.close(resolve))); }
  		await Promise.all(stopTasks);
  
  		logger.info(`Clusterio cleanly exited in ${Date.now() - exitStartTime}ms`);
  
  	} catch(err) {
  		setBlocking(true);
  		logger.fatal(`
  +--------------------------------------------------------------------+
  | Unexpected error occured while shutting down master, please report |
  | it to https://github.com/clusterio/factorioClusterio/issues        |
  +--------------------------------------------------------------------+
  ${err.stack}`
  		);
  		process.exit(1);
  	}
  }
  
  /**
   * Base class for master server connections
   *
   * @extends module:lib/link.Link
   */
  class BaseConnection extends libLink.Link {
  	constructor(target, connector) {
  		super("master", target, connector);
  		libLink.attachAllMessages(this);
  		for (let masterPlugin of masterPlugins.values()) {
  			libPlugin.attachPluginMessages(this, masterPlugin.info, masterPlugin);
  		}
  	}
  
  	async forwardRequestToInstance(message, request) {
  		let instance = db.instances.get(message.data.instance_id);
  		if (!instance) {
  			throw new libErrors.RequestError(`Instance with ID ${message.data.instance_id} does not exist`);
  		}
  
  		let slaveId = instance.config.get("instance.assigned_slave");
  		if (slaveId === null) {
  			throw new libErrors.RequestError("Instance is not assigned to a slave");
  		}
  
  		let connection = slaveConnections.get(slaveId);
  		if (!connection) {
  			throw new libErrors.RequestError("Slave containing instance is not connected");
  		}
  		if (request.plugin && !connection.plugins.has(request.plugin)) {
  			throw new libErrors.RequestError(`Slave containing instance does not have ${request.plugin} plugin`);
  		}
  
  		return await request.send(connection, message.data);
  	}
  
  	async forwardEventToInstance(message, event) {
  		let instance = db.instances.get(message.data.instance_id);
  		if (!instance) { return; }
  
  		let slaveId = instance.config.get("instance.assigned_slave");
  		if (slaveId === null) { return; }
  
  		let connection = slaveConnections.get(slaveId);
  		if (!connection || connection.closing) { return; }
  		if (event.plugin && !connection.plugins.has(event.plugin)) { return; }
  
  		event.send(connection, message.data);
  	}
  
  	async broadcastEventToSlaves(message, event) {
  		for (let slaveConnection of slaveConnections.values()) {
  			// Do not broadcast back to the source
  			if (slaveConnection === this) { continue; }
  			if (slaveConnection.connector.closing) { continue; }
  			if (event.plugin && !slaveConnection.plugins.has(event.plugin)) { continue; }
  
  			event.send(slaveConnection, message.data);
  		}
  	}
  
  	async broadcastEventToInstance(message, event) {
  		await this.broadcastEventToSlaves(message, event);
  	}
  
  	async prepareDisconnectRequestHandler(message, request) {
  		await libPlugin.invokeHook(masterPlugins, "onPrepareSlaveDisconnect", this);
  		this.connector.setClosing();
  		return await super.prepareDisconnectRequestHandler(message, request);
  	}
  
  	async disconnect(code, reason) {
  		try {
  			await libLink.messages.prepareDisconnect.send(this);
  		} catch (err) {
  			if (!(err instanceof libErrors.SessionLost)) {
  				logger.error(`"Unexpected error preparing disconnect:\n${err.stack}`);
  			}
  		}
  
  		await this.connector.close(code, reason);
  	}
  }
  
  const lastQueryLogTime = new libPrometheus.Gauge(
  	"clusterio_master_last_query_log_duration_seconds",
  	"Time in seconds the last log query took to execute."
  );
  
  let controlConnections = [];
  class ControlConnection extends BaseConnection {
  	constructor(registerData, connector, user) {
  		super("control", connector);
  
  		this._agent = registerData.agent;
  		this._version = registerData.version;
  
  		/**
  		 * The user making this connection.
  		 * @type {module:lib/user.User}
  		 */
  		this.user = user;
  
  		this.connector.on("close", () => {
  			let index = controlConnections.indexOf(this);
  			if (index !== -1) {
  				controlConnections.splice(index, 1);
  			}
  		});
  
  		this.logTransport = null;
  		this.logSubscriptions = {
  			all: false,
  			master: false,
  			slave_ids: [],
  			instance_ids: [],
  		};
  
  		this.ws_dumper = null;
  		this.connector.on("connect", () => {
  			this.connector._socket.clusterio_ignore_dump = Boolean(this.ws_dumper);
  		});
  		this.connector.on("close", () => {
  			if (this.logTransport) {
  				this.logTransport = null;
  				logger.remove(this.logTransport);
  			}
  			if (this.ws_dumper) {
  				debugEvents.off("message", this.ws_dumper);
  			}
  		});
  	}
  
  	async listSlavesRequestHandler(message) {
  		let list = [];
  		for (let slave of db.slaves.values()) {
  			list.push({
  				agent: slave.agent,
  				version: slave.version,
  				id: slave.id,
  				name: slave.name,
  				connected: slaveConnections.has(slave.id),
  			});
  		}
  		return { list };
  	}
  
  	generateSlaveToken(slaveId) {
  		return jwt.sign({ aud: "slave", slave: slaveId }, masterConfig.get("master.auth_secret"));
  	}
  
  	async generateSlaveTokenRequestHandler(message) {
  		return { token: this.generateSlaveToken(message.data.slave_id) };
  	}
  
  	async createSlaveConfigRequestHandler(message) {
  		let slaveConfig = new libConfig.SlaveConfig();
  		await slaveConfig.init();
  
  		slaveConfig.set("slave.master_url", getMasterUrl());
  		if (message.data.id !== null) {
  			slaveConfig.set("slave.id", message.data.id);
  		}
  		if (message.data.name !== null) {
  			slaveConfig.set("slave.name", message.data.name);
  		}
  		if (message.data.generate_token) {
  			this.user.checkPermission("core.slave.generate_token");
  			slaveConfig.set("slave.master_token", this.generateSlaveToken(slaveConfig.get("slave.id")));
  		}
  		return { serialized_config: slaveConfig.serialize() };
  	}
  
  	async listInstancesRequestHandler(message) {
  		let list = [];
  		for (let instance of db.instances.values()) {
  			list.push({
  				id: instance.config.get("instance.id"),
  				name: instance.config.get("instance.name"),
  				assigned_slave: instance.config.get("instance.assigned_slave"),
  				status: instance.status,
  			});
  		}
  		return { list };
  	}
  
  	// XXX should probably add a hook for slave reuqests?
  	async createInstanceRequestHandler(message) {
  		let instanceConfig = new libConfig.InstanceConfig();
  		await instanceConfig.load(message.data.serialized_config);
  
  		let instanceId = instanceConfig.get("instance.id");
  		if (db.instances.has(instanceId)) {
  			throw new libErrors.RequestError(`Instance with ID ${instanceId} already exists`);
  		}
  		db.instances.set(instanceId, { config: instanceConfig, status: "unassigned" });
  	}
  
  	async deleteInstanceRequestHandler(message, request) {
  		let instance = db.instances.get(message.data.instance_id);
  		if (!instance) {
  			throw new libErrors.RequestError(`Instance with ID ${message.data.instance_id} does not exist`);
  		}
  
  		if (instance.config.get("instance.assigned_slave") !== null) {
  			await this.forwardRequestToInstance(message, request);
  		}
  		db.instances.delete(message.data.instance_id);
  	}
  
  	async getInstanceConfigRequestHandler(message) {
  		let instance = db.instances.get(message.data.instance_id);
  		if (!instance) {
  			throw new libErrors.RequestError(`Instance with ID ${message.data.instance_id} does not exist`);
  		}
  
  		return {
  			serialized_config: instance.config.serialize(),
  		};
  	}
  
  	async updateInstanceConfig(instance) {
  		let slaveId = instance.config.get("instance.assigned_slave");
  		if (slaveId) {
  			let connection = slaveConnections.get(slaveId);
  			if (connection) {
  				await libLink.messages.assignInstance.send(connection, {
  					instance_id: instance.config.get("instance.id"),
  					serialized_config: instance.config.serialize(),
  				});
  			}
  		}
  	}
  
  	async setInstanceConfigFieldRequestHandler(message) {
  		let instance = db.instances.get(message.data.instance_id);
  		if (!instance) {
  			throw new libErrors.RequestError(`Instance with ID ${message.data.instance_id} does not exist`);
  		}
  
  		if (message.data.field === "instance.assigned_slave") {
  			throw new libErrors.RequestError("instance.assigned_slave must be set through the assign-slave interface");
  		}
  
  		if (message.data.field === "instance.id") {
  			// XXX is this worth implementing?  It's race condition galore.
  			throw new libErrors.RequestError("Setting instance.id is not supported");
  		}
  
  		instance.config.set(message.data.field, message.data.value);
  		await this.updateInstanceConfig(instance);
  	}
  
  	async setInstanceConfigPropRequestHandler(message) {
  		let instance = db.instances.get(message.data.instance_id);
  		if (!instance) {
  			throw new libErrors.RequestError(`Instance with ID ${message.data.instance_id} does not exist`);
  		}
  
  		let { field, prop, value } = message.data;
  		instance.config.setProp(field, prop, value);
  		await this.updateInstanceConfig(instance);
  	}
  
  	async assignInstanceCommandRequestHandler(message, request) {
  		let { slave_id, instance_id } = message.data;
  		let instance = db.instances.get(instance_id);
  		if (!instance) {
  			throw new libErrors.RequestError(`Instance with ID ${instance_id} does not exist`);
  		}
  
  		// Check if target slave is connected
  		let newSlaveConnection;
  		if (slave_id !== null) {
  			newSlaveConnection = slaveConnections.get(slave_id);
  			if (!newSlaveConnection) {
  				// The case of the slave not getting the assign instance message
  				// still have to be handled, so it's not a requirement that the
  				// target slave be connected to the master while doing the
  				// assignment, but it is IMHO a better user experience if this
  				// is the case.
  				throw new libErrors.RequestError("Target slave is not connected to the master server");
  			}
  		}
  
  		// Unassign from currently assigned slave if it is connected.
  		let currentAssignedSlave = instance.config.get("instance.assigned_slave");
  		if (currentAssignedSlave !== null && slave_id !== currentAssignedSlave) {
  			let oldSlaveConnection = slaveConnections.get(currentAssignedSlave);
  			if (oldSlaveConnection && !oldSlaveConnection.connector.closing) {
  				await libLink.messages.unassignInstance.send(oldSlaveConnection, { instance_id });
  			}
  		}
  
  		// Assign to target
  		instance.config.set("instance.assigned_slave", slave_id);
  		if (slave_id !== null) {
  			await libLink.messages.assignInstance.send(newSlaveConnection, {
  				instance_id,
  				serialized_config: instance.config.serialize(),
  			});
  		}
  	}
  
  	async setLogSubscriptionsRequestHandler(message) {
  		this.logSubscriptions = message.data;
  		this.updateLogSubscriptions();
  	}
  
  	static logFilter({ all, master, slave_ids, instance_ids, max_level }) {
  		return info => {
  			// Note: reversed to filter out undefined levels
  			if (max_level && !(levels[info.level] <= levels[max_level])) {
  				return false;
  			}
  
  			if (all) {
  				return true;
  			}
  			if (master && info.slave_id === undefined) {
  				return true;
  			}
  			if (info.slave_id && slave_ids.includes(info.slave_id)) {
  				return true;
  			}
  			if (info.instance_id && instance_ids.includes(info.instance_id)) {
  				return true;
  			}
  			return false;
  		};
  	}
  
  	updateLogSubscriptions() {
  		let { all, master, slave_ids, instance_ids } = this.logSubscriptions;
  		if (all || master || slave_ids.length || instance_ids.length) {
  			if (!this.logTransport) {
  				this.logTransport = new libLoggingUtils.LinkTransport({ link: this });
  				clusterLogger.add(this.logTransport);
  			}
  			this.logTransport.filter = this.constructor.logFilter(this.logSubscriptions);
  
  		} else if (this.logTransport) {
  			clusterLogger.remove(this.logTransport);
  			this.logTransport = null;
  		}
  	}
  
  	async queryLogRequestHandler(message) {
  		let transport = new winston.transports.File({ filename: "cluster.log" });
  		let query = util.promisify((options, callback) => transport.query(options, callback));
  
  		// Available options and defaults inferred from reading the source
  		// rows: number = limit || 10
  		// limit: number // alias of rows.
  		// start: number = 0
  		// until: Date = now
  		// from: Date = until - 24h
  		// order: "asc"|"desc" = "desc"
  		// fields: Array<string>
  		// level: string
  		// This interface is junk:
  		// - The level option filters by exact match
  		// - No way to add your own filter
  		// - No way to stream results
  		// - The log file is read starting from the beginning
  		let setDuration = lastQueryLogTime.startTimer();
  		let log = await query({
  			order: "asc",
  			limit: Infinity,
  			from: new Date(0),
  		});
  
  		log = log.filter(this.constructor.logFilter(message.data));
  		setDuration();
  		return { log };
  	}
  
  	async listPermissionsRequestHandler(message) {
  		let list = [];
  		for (let permission of libUsers.permissions.values()) {
  			list.push({
  				name: permission.name,
  				title: permission.title,
  				description: permission.description,
  			});
  		}
  		return { list };
  	}
  
  	async listRolesRequestHandler(message) {
  		let list = [];
  		for (let role of db.roles.values()) {
  			list.push({
  				id: role.id,
  				name: role.name,
  				description: role.description,
  				permissions: [...role.permissions],
  			});
  		}
  		return { list };
  	}
  
  	async createRoleRequestHandler(message) {
  		let lastId = Math.max.apply(null, [...db.roles.keys()]);
  
  		// Start at 5 to leave space for future default roles
  		let id = Math.max(5, lastId+1);
  		db.roles.set(id, new libUsers.Role({ id, ...message.data }));
  		return { id };
  	}
  
  	async updateRoleRequestHandler(message) {
  		let { id, name, description, permissions } = message.data;
  		let role = db.roles.get(id);
  		if (!role) {
  			throw new libErrors.RequestError(`Role with ID ${id} does not exist`);
  		}
  
  		role.name = name;
  		role.description = description;
  		role.permissions = new Set(permissions);
  	}
  
  	async grantDefaultRolePermissionsRequestHandler(message) {
  		let role = db.roles.get(message.data.id);
  		if (!role) {
  			throw new libErrors.RequestError(`Role with ID ${message.data.id} does not exist`);
  		}
  
  		role.grantDefaultPermissions();
  	}
  
  	async deleteRoleRequestHandler(message) {
  		let id = message.data.id;
  		let role = db.roles.get(id);
  		if (!role) {
  			throw new libErrors.RequestError(`Role with ID ${id} does not exist`);
  		}
  
  		db.roles.delete(id);
  		for (let user of db.users.values()) {
  			user.roles.delete(role);
  		}
  	}
  
  	async listUsersRequestHandler(message) {
  		let list = [];
  		for (let user of db.users.values()) {
  			list.push({
  				name: user.name,
  				roles: [...user.roles].map(role => role.id),
  				is_admin: user.isAdmin,
  				is_banned: user.isBanned,
  				is_whitelisted: user.isWhitelisted,
  				instances: [...user.instances],
  			});
  		}
  		return { list };
  	}
  
  	async createUserRequestHandler(message) {
  		createUser(message.data.name);
  	}
  
  	async updateUserRolesRequestHandler(message) {
  		let user = db.users.get(message.data.name);
  		if (!user) {
  			throw new libErrors.RequestError(`User '${message.data.name}' does not exist`);
  		}
  
  		let resolvedRoles = new Set();
  		for (let roleId of message.data.roles) {
  			let role = db.roles.get(roleId);
  			if (!role) {
  				throw new libErrors.RequestError(`Role with ID ${roleId} does not exist`);
  			}
  
  			resolvedRoles.add(role);
  		}
  
  		user.roles = resolvedRoles;
  	}
  
  	async setUserAdminRequestHandler(message) {
  		let { name, create, admin } = message.data;
  		let user = db.users.get(name);
  		if (!user) {
  			if (create) {
  				this.user.checkPermission("core.user.create");
  				user = createUser(name);
  			} else {
  				throw new libErrors.RequestError(`User '${name}' does not exist`);
  			}
  		}
  
  		user.isAdmin = admin;
  		this.broadcastEventToSlaves({ data: { name, admin }}, libLink.messages.adminlistUpdate);
  	}
  
  	async setUserBannedRequestHandler(message) {
  		let { name, create, banned, reason } = message.data;
  		let user = db.users.get(name);
  		if (!user) {
  			if (create) {
  				this.user.checkPermission("core.user.create");
  				user = createUser(name);
  			} else {
  				throw new libErrors.RequestError(`User '${name}' does not exist`);
  			}
  		}
  
  		user.isBanned = banned;
  		user.banReason = reason;
  		this.broadcastEventToSlaves({ data: { name, banned, reason }}, libLink.messages.banlistUpdate);
  	}
  
  	async setUserWhitelistedRequestHandler(message) {
  		let { name, create, whitelisted } = message.data;
  		let user = db.users.get(name);
  		if (!user) {
  			if (create) {
  				this.user.checkPermission("core.user.create");
  				user = createUser(name);
  			} else {
  				throw new libErrors.RequestError(`User '${name}' does not exist`);
  			}
  		}
  
  		user.isWhitelisted = whitelisted;
  		this.broadcastEventToSlaves({ data: { name, whitelisted }}, libLink.messages.whitelistUpdate);
  	}
  
  	async deleteUserRequestHandler(message) {
  		let user = db.users.get(message.data.name);
  		if (!user) {
  			throw new libErrors.RequestError(`User '${message.data.name}' does not exist`);
  		}
  
  		if (user.is_admin) {
  			this.broadcastEventToSlaves({ data: { name, admin: false }}, libLink.messages.adminlistUpdate);
  		}
  		if (user.is_whitelisted) {
  			this.broadcastEventToSlaves({ data: { name, whitelisted: false }}, libLink.messages.whitelistUpdate);
  		}
  		if (user.is_banned) {
  			this.broadcastEventToSlaves({ data: { name, banned: false, reason: "" }}, libLink.messages.banlistUpdate);
  		}
  		db.users.delete(message.data.name);
  	}
  
  	async debugDumpWsRequestHandler(message) {
  		this.ws_dumper = data => {
  			if (this.connector.connected) {
  				libLink.messages.debugWsMessage.send(this, data);
  			}
  		};
  		this.connector._socket.clusterio_ignore_dump = true;
  		debugEvents.on("message", this.ws_dumper);
  	}
  }
  
  let slaveConnections = new Map();
  /**
   * Represents the connection to a slave
   *
   * @extends module:master~BaseConnection
   */
  class SlaveConnection extends BaseConnection {
  	constructor(registerData, connector) {
  		super("slave", connector);
  
  		this._agent = registerData.agent;
  		this._id = registerData.id;
  		this._name = registerData.name;
  		this._version = registerData.version;
  		this.plugins = new Map(Object.entries(registerData.plugins));
  
  		db.slaves.set(this._id, {
  			agent: this._agent,
  			id: this._id,
  			name: this._name,
  			version: this._version,
  			plugins: registerData.plugins,
  		});
  
  		this.connector.on("close", () => {
  			if (slaveConnections.get(this._id) === this) {
  				slaveConnections.delete(this._id);
  			}
  		});
  
  		for (let event of ["connect", "drop", "close"]) {
  			// eslint-disable-next-line no-loop-func
  			this.connector.on(event, () => {
  				for (let masterPlugin of masterPlugins.values()) {
  					masterPlugin.onSlaveConnectionEvent(this, event);
  				}
  			});
  		}
  	}
  
  	async instanceStatusChangedEventHandler(message, event) {
  		let instance = db.instances.get(message.data.instance_id);
  
  		// It's possible to get updates from an instance that does not exist
  		// or is not assigned to the slave it originated from if it was
  		// reassigned or deleted while the connection to the slave it was
  		// originally on was down at the time.
  		if (!instance || instance.config.get("instance.assigned_slave") !== this._id) {
  			logger.warn(`Got bogus update for instance id ${message.data.instance_id}`);
  			return;
  		}
  
  		let prev = instance.status;
  		instance.status = message.data.status;
  		logger.verbose(`Instance ${instance.config.get("instance.name")} State: ${instance.status}`);
  		await libPlugin.invokeHook(masterPlugins, "onInstanceStatusChanged", instance, prev);
  	}
  
  	async updateInstancesRequestHandler(message) {
  		// Push updated instance configs
  		for (let instance of db.instances.values()) {
  			if (instance.config.get("instance.assigned_slave") === this._id) {
  				await libLink.messages.assignInstance.send(this, {
  					instance_id: instance.config.get("instance.id"),
  					serialized_config: instance.config.serialize(),
  				});
  			}
  		}
  
  		// Assign instances the slave has but master does not
  		for (let instance of message.data.instances) {
  			let instanceConfig = new libConfig.InstanceConfig();
  			await instanceConfig.load(instance.serialized_config);
  
  			let masterInstance = db.instances.get(instanceConfig.get("instance.id"));
  			if (masterInstance) {
  				// Check if this instance is assigned somewhere else.
  				if (masterInstance.config.get("instance.assigned_slave") !== this._id) {
  					await libLink.messages.unassignInstance.send(this, {
  						instance_id: masterInstance.config.get("instance.id"),
  					});
  					continue;
  				}
  
  				// Already have this instance, update state instead
  				if (masterInstance.status !== instance.status) {
  					let prev = masterInstance.status;
  					masterInstance.status = instance.status;
  					logger.verbose(`Instance ${instanceConfig.get("instance.name")} State: ${instance.status}`);
  					await libPlugin.invokeHook(masterPlugins, "onInstanceStatusChanged", instance, prev);
  				}
  				continue;
  			}
  
  			instanceConfig.set("instance.assigned_slave", this._id);
  			db.instances.set(instanceConfig.get("instance.id"), {
  				config: instanceConfig,
  				status: instance.status,
  			});
  			await libLink.messages.assignInstance.send(this, {
  				instance_id: instanceConfig.get("instance.id"),
  				serialized_config: instanceConfig.serialize(),
  			});
  		}
  
  		// Push lists to make sure they are in sync.
  		let adminlist = [];
  		let banlist = [];
  		let whitelist = [];
  
  		for (let user of db.users.values()) {
  			if (user.isAdmin) {
  				adminlist.push(user.name);
  			}
  			if (user.isBanned) {
  				banlist.push([user.name, user.banReason]);
  			}
  			if (user.isWhitelisted) {
  				whitelist.push(user.name);
  			}
  		}
  
  		libLink.messages.syncUserLists.send(this, { adminlist, banlist, whitelist });
  	}
  
  	async logMessageEventHandler(message) {
  		clusterLogger.log({
  			...message.data.info,
  			slave_id: this._id,
  			slave_name: this._name,
  		});
  	}
  
  	async playerEventEventHandler(message) {
  		let { instance_id, name, type } = message.data;
  		let user = db.users.get(name);
  		if (!user) {
  			user = createUser(name);
  		}
  
  		if (type === "join") {
  			user.notifyJoin(instance_id);
  		} else if (type === "leave") {
  			user.notifyLeave(instance_id);
  		}
  
  		let instance = db.instances.get(instance_id);
  		await libPlugin.invokeHook(masterPlugins, "onPlayerEvent", instance, message.data);
  	}
  }
  
  const wss = new WebSocket.Server({
  	noServer: true,
  	path: "/api/socket",
  });
  
  /**
   * Returns true if value is a signed 32-bit integer
   *
   * @param {number} value - value to test.
   * @returns {Boolean}
   *     true if value is an integer between -2<sup>31</sup> and
   *     2<sup>31</sup>-1.
   */
  function isInteger(value) {
  	return (value | 0) === value;
  }
  
  
  /**
   * Connector for master server connections
   *
   * @extends module:lib/link.WebSocketBaseConnector
   */
  class WebSocketServerConnector extends libLink.WebSocketBaseConnector {
  	constructor(socket, sessionId) {
  		super();
  
  		this._socket = socket;
  		this._sessionId = sessionId;
  
  		// The following states are used in the server connector
  		// handshake: Waiting for client to (re)connect.
  		// connected: Connection is online
  		// closing: Connection is in the process of being closed.
  		// closed: Connection has been closed
  		this._state = "handshake";
  		this._connected = false;
  		this._timeout = 15 * 60;
  	}
  
  	_check(expectedStates) {
  		if (!expectedStates.includes(this._state)) {
  			throw new Error(
  				`Expected state ${expectedStates} but state is ${this._state}`
  			);
  		}
  	}
  
  	/**
  	 * Send ready over the socket
  	 *
  	 * Sends the ready message over the socket to initiate the session.
  	 *
  	 * @param {string} sessionToken -
  	 *     the session token to send to the client.
  	 */
  	ready(sessionToken) {
  		this._heartbeatInterval = masterConfig.get("master.heartbeat_interval");
  		this._socket.send(JSON.stringify({
  			seq: null,
  			type: "ready",
  			data: {
  				session_token: sessionToken,
  				heartbeat_interval: this._heartbeatInterval,
  			},
  		}));
  
  		this._state = "connected";
  		this._connected = true;
  		this._attachSocketHandlers();
  		this.emit("connect");
  	}
  
  	/**
  	 * Continue connection with the given socket
  	 *
  	 * Terminates the current socket and contiunes the session over the
  	 * socket given from the message sequence given.
  	 *
  	 * @param {module:net.Socket} socket - New socket to continue on.
  	 * @param {number} lastSeq - The last message the client received.
  	 */
  	continue(socket, lastSeq) {
  		this._socket.terminate();
  		this._socket = socket;
  
  		if (this._timeoutId) {
  			clearTimeout(this._timeoutId);
  			this._timeoutId = null;
  		}
  
  		this._heartbeatInterval = masterConfig.get("master.heartbeat_interval");
  		this._socket.send(JSON.stringify({
  			seq: null,
  			type: "continue",
  			data: {
  				last_seq: this._lastReceivedSeq,
  				heartbeat_interval: this._heartbeatInterval,
  			},
  		}));
  
  		this._state = "connected";
  		this._connected = true;
  		this._attachSocketHandlers();
  		this._dropSendBufferSeq(lastSeq);
  		for (let message of this._sendBuffer) {
  			this._socket.send(JSON.stringify(message));
  		}
  		this.emit("connect");
  	}
  
  	setTimeout(timeout) {
  		if (this._timeoutId) {
  			clearTimeout(this._timeoutId);
  		}
  
  		this._timeoutId = setTimeout(() => { this._timedOut(); }, timeout * 1000);
  		this._timeout = timeout;
  	}
  
  	_timedOut() {
  		logger.verbose("SOCKET | Connection timed out");
  		this._timeoutId = null;
  		this._lastReceivedSeq = null;
  		this._sendBuffer.length = 0;
  		this._state = "closed";
  		activeConnectors.delete(this._sessionId);
  		this.emit("close");
  		this.emit("invalidate");
  	}
  
  	_attachSocketHandlers() {
  		this.startHeartbeat();
  
  		this._socket.on("close", (code, reason) => {
  			logger.verbose(`SOCKET | Close (code: ${code}, reason: ${reason})`);
  			if (this._state === "closing") {
  				this._lastReceivedSeq = null;
  				this._sendBuffer.length = 0;
  				this._state = "closed";
  				activeConnectors.delete(this._sessionId);
  
  				if (this._connected) {
  					this._connected = false;
  					this.emit("close");
  				}
  
  				if (this._timeoutId) {
  					clearTimeout(this._timeoutId);
  					this._timeoutId = null;
  				}
  
  			} else {
  				this._state = "handshake";
  				this.emit("drop");
  				this._timeoutId = setTimeout(() => { this._timedOut(); }, this._timeout * 1000);
  			}
  
  			this.stopHeartbeat();
  		});
  
  		this._socket.on("error", err => {
  			// It's assumed that close is always called by ws
  			logger.verbose("SOCKET | Error:", err);
  		});
  
  		this._socket.on("open", () => {
  			logger.verbose("SOCKET | Open");
  		});
  		this._socket.on("ping", data => {
  			logger.verbose(`SOCKET | Ping (data: ${data}`);
  		});
  		this._socket.on("pong", data => {
  			logger.verbose(`SOCKET | Pong (data: ${data}`);
  		});
  
  		// Handle messages
  		this._socket.on("message", data => {
  			let message = JSON.parse(data);
  			if (["connected", "closing"].includes(this._state)) {
  				if (message.seq !== null) {
  					this._lastReceivedSeq = message.seq;
  				}
  
  				if (message.type === "heartbeat") {
  					this._processHeartbeat(message);
  
  				} else {
  					this.emit("message", message);
  				}
  
  			} else {
  				throw new Error(`Received message in unexpected state ${this._state}`);
  			}
  		});
  	}
  
  	/**
  	 * Close the connection with the given reason.
  	 *
  	 * Sends a close frame and disconnects the connector.
  	 *
  	 * @param {number} code - WebSocket close code.
  	 * @param {string} reason - WebSocket close reason.
  	 */
  	async close(code, reason) {
  		if (this._state === "closed") {
  			return;
  		}
  
  		this.stopHeartbeat();
  		this._state = "closing";
  		this._socket.close(code, reason);
  		await events.once(this, "close");
  	}
  }
  
  let pendingSockets = new Set();
  let activeConnectors = new Map();
  
  wss.on("connection", function (socket, req) {
  	logger.verbose(`SOCKET | new connection from ${req.socket.remoteAddress}`);
  
  	// Track statistics
  	wsConnectionsCounter.inc();
  	socket.on("message", (message) => {
  		wsMessageCounter.labels("in").inc();
  		if (!socket.clusterio_ignore_dump) {
  			debugEvents.emit("message", { direction: "in", content: message });
  		}
  	});
  	let originalSend = socket.send;
  	socket.send = (...args) => {
  		wsMessageCounter.labels("out").inc();
  		if (typeof args[0] === "string" && !socket.clusterio_ignore_dump) {
  			debugEvents.emit("message", { direction: "out", content: args[0] });
  		}
  		return originalSend.call(socket, ...args);
  	};
  
  	// Start connection handshake.
  	socket.send(JSON.stringify({ seq: null, type: "hello", data: {
  		version,
  		plugins: loadedPlugins,
  	}}));
  
  	function attachHandler() {
  		pendingSockets.add(socket);
  
  		let timeoutId = setTimeout(() => {
  			logger.verbose(`SOCKET | closing ${req.socket.remoteAddress} after timing out on handshake`);
  			wsRejectedConnectionsCounter.inc();
  			socket.close(1008, "Handshake timeout");
  			pendingSockets.delete(socket);
  		}, 30*1000);
  
  		socket.once("message", (message) => {
  			clearTimeout(timeoutId);
  			pendingSockets.delete(socket);
  			handleHandshake(
  				message, socket, req, attachHandler
  			).catch(err => {
  				logger.error(`
  +----------------------------------------------------------------+
  | Unexpected error occured in WebSocket handshake, please report |
  | it to https://github.com/clusterio/factorioClusterio/issues    |
  +----------------------------------------------------------------+
  ${err.stack}`
  				);
  				wsRejectedConnectionsCounter.inc();
  				socket.close(1011, "Unexpected error");
  			});
  		});
  	}
  
  	attachHandler();
  });
  
  let nextSessionId = 1;
  
  // Unique string for the session token audience
  let masterSession = `session-${Date.now()}`;
  
  // Handle socket handshake
  async function handleHandshake(message, socket, req, attachHandler) {
  	try {
  		message = JSON.parse(message);
  	} catch (err) {
  		logger.verbose(`SOCKET | closing ${req.socket.remoteAddress} after receiving invalid JSON`);
  		wsRejectedConnectionsCounter.inc();
  		socket.close(1002, "Invalid JSON");
  		return;
  	}
  
  	if (!libSchema.clientHandshake(message)) {
  		logger.verbose(`SOCKET | closing ${req.socket.remoteAddress} after receiving invalid handshake`);
  		wsRejectedConnectionsCounter.inc();
  		socket.close(1002, "Bad handshake");
  		return;
  	}
  
  	let { seq, type, data } = message;
  
  	if (type === "resume") {
  		let connector;
  		try {
  			let payload = jwt.verify(
  				data.session_token,
  				masterConfig.get("master.auth_secret"),
  				{ audience: masterSession }
  			);
  
  			connector = activeConnectors.get(payload.sid);
  			if (!connector) {
  				throw new Error();
  			}
  
  		} catch(err) {
  			socket.send(JSON.stringify({ seq: null, type: "invalidate", data: {}}));
  			attachHandler();
  			return;
  		}
  
  		connector.continue(socket, data.last_seq);
  		return;
  	}
  
  	if (stopAcceptingNewSessions) {
  		logger.verbose(`SOCKET | closing ${req.socket.remoteAddress}, server is shutting down`);
  		wsRejectedConnectionsCounter.inc();
  		socket.close(1001, "Shutting down");
  		return;
  	}
  
  	// Validate token
  	let user;
  	try {
  		if (type === "register_slave") {
  			let tokenPayload = jwt.verify(
  				data.token,
  				masterConfig.get("master.auth_secret"),
  				{ audience: "slave" }
  			);
  
  			if (tokenPayload.slave !== data.id) {
  				throw new Error("missmatched slave id");
  			}
  
  		} else if (type === "register_control") {
  			let tokenPayload = jwt.verify(
  				data.token,
  				masterConfig.get("master.auth_secret"),
  				{ audience: "user" }
  			);
  
  			user = db.users.get(tokenPayload.user);
  			if (!user) {
  				throw new Error("invalid user");
  			}
  			if (tokenPayload.iat < user.tokenValidAfter) {
  				throw new Error("invalid token");
  			}
  			user.checkPermission("core.control.connect");
  		}
  
  	} catch (err) {
  		logger.verbose(`SOCKET | authentication failed for ${req.socket.remoteAddress}`);
  		wsRejectedConnectionsCounter.inc();
  		socket.close(4003, `Authentication failed: ${err.message}`);
  		return;
  	}
  
  	let sessionId = nextSessionId;
  	nextSessionId += 1;
  	let sessionToken = jwt.sign({ aud: masterSession, sid: sessionId }, masterConfig.get("master.auth_secret"));
  	let connector = new WebSocketServerConnector(socket, sessionId);
  	activeConnectors.set(sessionId, connector);
  
  	if (type === "register_slave") {
  		let connection = slaveConnections.get(data.id);
  		if (connection) {
  			logger.verbose(`SOCKET | disconnecting existing connection for slave ${data.id}`);
  			connection.connector.setTimeout(15); // Slave connection is likely stalled
  			await connection.disconnect(1008, "Registered from another connection");
  		}
  
  		logger.verbose(`SOCKET | registered slave ${data.id} version ${data.version}`);
  		slaveConnections.set(data.id, new SlaveConnection(data, connector));
  
  	} else if (type === "register_control") {
  		logger.verbose(`SOCKET | registered control from ${req.socket.remoteAddress}`);
  		controlConnections.push(new ControlConnection(data, connector, user));
  	}
  
  	connector.ready(sessionToken);
  }
  
  // handle plugins on the master
  async function pluginManagement() {
  	let startPluginLoad = Date.now();
  	masterPlugins = await loadPlugins();
  	logger.info(`All plugins loaded in ${Date.now() - startPluginLoad}ms`);
  }
  
  async function loadPlugins() {
  	let plugins = new Map();
  	for (let pluginInfo of pluginInfos) {
  		if (!masterConfig.group(pluginInfo.name).get("enabled")) {
  			continue;
  		}
  
  		loadedPlugins[pluginInfo.name] = pluginInfo.version;
  
  		let pluginLoadStarted = Date.now();
  		let MasterPlugin = libPlugin.BaseMasterPlugin;
  		try {
  			if (pluginInfo.masterEntrypoint) {
  				({ MasterPlugin } = require(path.posix.join(pluginInfo.requirePath, pluginInfo.masterEntrypoint)));
  			}
  
  			let masterPlugin = new MasterPlugin(
  				pluginInfo, { app, config: masterConfig, db, slaveConnections }, { endpointHitCounter }, logger
  			);
  			await masterPlugin.init();
  			plugins.set(pluginInfo.name, masterPlugin);
  
  		} catch (err) {
  			throw new libErrors.PluginError(pluginInfo.name, err);
  		}
  
  		logger.info(`Loaded plugin ${pluginInfo.name} in ${Date.now() - pluginLoadStarted}ms`);
  	}
  	return plugins;
  }
  
  /**
   * Calls listen on server capturing any errors that occurs
   * binding to the port.  Also adds handler for WebSocket
   * upgrade event.
   *
   * @param {module:net.Server} server - Server to start the listening on.
   * @param {*} args - Arguments to the .listen() call on the server.
   * @returns {Promise} promise that resolves the server is listening.
   */
  function listen(server, ...args) {
  	return new Promise((resolve, reject) => {
  		server.on("upgrade", (req, socket, head) => {
  			logger.verbose("handling WebSocket upgrade");
  
  			// For reasons that defy common sense, the connection event has
  			// to be emitted explictly when using noServer.
  			wss.handleUpgrade(req, socket, head, (ws) => {
  				wss.emit("connection", ws, req);
  			});
  		});
  
  		function wrapError(err) {
  			reject(new libErrors.StartupError(
  				`Server listening failed: ${err.message}`
  			));
  		}
  
  		server.once("error", wrapError);
  		server.listen(...args, () => {
  			server.off("error", wrapError);
  			resolve();
  		});
  	});
  }
  
  function _setConfig(config) {
  	masterConfig = config;
  }
  
  /**
   * Returns the URL needed to connect to the master server.
   *
   * @returns {string} master URL.
   */
  function getMasterUrl() {
  	let url = masterConfig.get("master.external_address");
  	if (!url) {
  		if (masterConfig.get("master.https_port")) {
  			url = `https://localhost:${masterConfig.get("master.https_port")}/`;
  		} else {
  			url = `http://localhost:${masterConfig.get("master.http_port")}/`;
  		}
  	}
  	return url;
  }
  
  async function startServer() {
  	// Set the process title, shows up as the title of the CMD window on windows
  	// and as the process name in ps/top on linux.
  	process.title = "clusterioMaster";
  
  	// argument parsing
  	let args = yargs
  		.scriptName("master")
  		.usage("$0 <command> [options]")
  		.option("log-level", {
  			nargs: 1,
  			describe: "Log level to print to stdout",
  			default: "info",
  			choices: ["none"].concat(Object.keys(levels)),
  			type: "string",
  		})
  		.option("config", {
  			nargs: 1,
  			describe: "master config file to use",
  			default: "config-master.json",
  			type: "string",
  		})
  		.option("plugin-list", {
  			nargs: 1,
  			describe: "File containing list of plugins available with their install path",
  			default: "plugin-list.json",
  			type: "string",
  		})
  		.command("plugin", "Manage available plugins", libSharedCommands.pluginCommand)
  		.command("config", "Manage Master config", libSharedCommands.configCommand)
  		.command("bootstrap", "Bootstrap access to cluster", yargs => {
  			yargs
  				.command("create-admin <name>", "Create a cluster admin")
  				.command("generate-user-token <name>", "Generate authentication token for the given user")
  				.command("create-ctl-config <name>", "Create clusterioctl config for the given user", yargs => {
  					yargs.option("output", {
  						describe: "Path to output config (- for stdout)", type: "string",
  						nargs: 1, default: "config-control.json",
  					});
  				})
  				.demandCommand(1, "You need to specify a command to run");
  		})
  		.command("run", "Run master server", yargs => {
  			yargs.option("dev", { hidden: true, type: "boolean", nargs: 0 });
  		})
  		.demandCommand(1, "You need to specify a command to run")
  		.strict()
  		.argv
  	;
  
  	// Combined log stream of the whole cluster.
  	clusterLogger = winston.createLogger({
  		format: winston.format.json(),
  		level: "verbose",
  		levels,
  	});
  	clusterLogger.add(new winston.transports.File({
  		filename: "cluster.log",
  	}));
  
  	// Log stream for the master server.
  	logger.add(new winston.transports.File({
  		format: winston.format.json(),
  		filename: "master.log",
  	}));
  	logger.add(new winston.transports.Stream({
  		stream: clusterLogger,
  	}));
  	if (args.logLevel !== "none") {
  		logger.add(new ConsoleTransport({
  			level: args.logLevel,
  			format: new libLoggingUtils.TerminalFormat(),
  		}));
  	}
  
  	// add better stack traces on promise rejection
  	process.on("unhandledRejection", err => logger.error(`Unhandled rejection:\n${err.stack}`));
  
  	logger.info(`Loading available plugins from ${args.pluginList}`);
  	let pluginList = new Map();
  	try {
  		pluginList = new Map(JSON.parse(await fs.readFile(args.pluginList)));
  	} catch (err) {
  		if (err.code !== "ENOENT") {
  			throw err;
  		}
  	}
  
  	// If the command is plugin management we don't try to load plugins
  	let command = args._[0];
  	if (command === "plugin") {
  		await libSharedCommands.handlePluginCommand(args, pluginList, args.pluginList);
  		return;
  	}
  
  	logger.info("Loading Plugin info");
  	pluginInfos = await libPluginLoader.loadPluginInfos(pluginList);
  	libConfig.registerPluginConfigGroups(pluginInfos);
  	libConfig.finalizeConfigs();
  
  	masterConfigPath = args.config;
  	logger.info(`Loading config from ${masterConfigPath}`);
  	masterConfig = new libConfig.MasterConfig();
  	try {
  		await masterConfig.load(JSON.parse(await fs.readFile(masterConfigPath)));
  
  	} catch (err) {
  		if (err.code === "ENOENT") {
  			logger.info("Config not found, initializing new config");
  			await masterConfig.init();
  
  		} else {
  			throw err;
  		}
  	}
  
  	if (!masterConfig.get("master.auth_secret")) {
  		logger.info("Generating new master authentication secret");
  		let asyncRandomBytes = util.promisify(crypto.randomBytes);
  		let bytes = await asyncRandomBytes(256);
  		masterConfig.set("master.auth_secret", bytes.toString("base64"));
  		await fs.outputFile(masterConfigPath, JSON.stringify(masterConfig.serialize(), null, 4));
  	}
  
  	if (command === "config") {
  		await libSharedCommands.handleConfigCommand(args, masterConfig, masterConfigPath);
  		return;
  
  	} else if (command === "bootstrap") {
  		let subCommand = args._[1];
  		await loadUsers(masterConfig.get("master.database_directory"), "users.json");
  		if (subCommand === "create-admin") {
  			if (!args.name) {
  				logger.error("name cannot be blank");
  				process.exitCode = 1;
  				return;
  			}
  
  			let admin = db.users.get(args.name);
  			if (!admin) {
  				admin = createUser(args.name);
  			}
  
  			let adminRole = libUsers.ensureDefaultAdminRole(db.roles);
  			admin.roles.add(adminRole);
  			admin.isAdmin = true;
  			await saveUsers(masterConfig.get("master.database_directory"), "users.json");
  
  		} else if (subCommand === "generate-user-token") {
  			let user = db.users.get(args.name);
  			if (!user) {
  				logger.error(`No user named '${args.name}'`);
  				process.exitCode = 1;
  				return;
  			}
  			// eslint-disable-next-line no-console
  			console.log(user.createToken(masterConfig.get("master.auth_secret")));
  
  		} else if (subCommand === "create-ctl-config") {
  			let admin = db.users.get(args.name);
  			if (!admin) {
  				logger.error(`No user named '${args.name}'`);
  				process.exitCode = 1;
  				return;
  			}
  			let controlConfig = new libConfig.ControlConfig();
  			await controlConfig.init();
  
  			controlConfig.set("control.master_url", getMasterUrl());
  			controlConfig.set("control.master_token", admin.createToken(masterConfig.get("master.auth_secret")));
  
  			let content = JSON.stringify(controlConfig.serialize(), null, 4);
  			if (args.output === "-") {
  				// eslint-disable-next-line no-console
  				console.log(content);
  			} else {
  				logger.info(`Writing ${args.output}`);
  				await fs.outputFile(args.output, content);
  			}
  		}
  
  		return;
  	}
  
  	// If we get here the command was run
  
  	// Start webpack development server if enabled
  	if (args.dev) {
  		logger.warn("Webpack development mode enabled");
  		const webpack = require("webpack");
  		const webpackDevMiddleware = require("webpack-dev-middleware");
  		const webpackConfig = require("./webpack.config");
  
  		const compiler = webpack(webpackConfig({}));
  		devMiddleware = webpackDevMiddleware(compiler, {});
  		app.use(devMiddleware);
  	}
  
  	let secondSigint = false;
  	process.on("SIGINT", () => {
  		if (secondSigint) {
  			logger.fatal("Caught second interrupt, terminating immediately");
  			process.exit(1);
  		}
  
  		secondSigint = true;
  		logger.info("Caught interrupt signal, shutting down");
  		shutdown();
  	});
  
  	// terminal closed
  	process.on("SIGHUP", () => {
  		// No graceful cleanup, no warning out (stdout is likely closed.)
  		// Don't close the terminal with the clusterio master in it.
  		process.exit(1);
  	});
  
  	await fs.ensureDir(masterConfig.get("master.database_directory"));
  
  	db.slaves = await loadMap(masterConfig.get("master.database_directory"), "slaves.json");
  	db.instances = await loadInstances(masterConfig.get("master.database_directory"), "instances.json");
  	await loadUsers(masterConfig.get("master.database_directory"), "users.json");
  
  	// Make sure we're actually going to listen on a port
  	let httpPort = masterConfig.get("master.http_port");
  	let httpsPort = masterConfig.get("master.https_port");
  	if (!httpPort && !httpsPort) {
  		logger.fatal("Error: at least one of http_port and https_port must be configured");
  		process.exit(1);
  	}
  
  	let tls_cert = masterConfig.get("master.tls_certificate");
  	let tls_key = masterConfig.get("master.tls_private_key");
  	// Create a self signed certificate if the certificate files doesn't exist
  	if (httpsPort && !await fs.exists(tls_cert) && !await fs.exists(tls_key)) {
  		await generateSslCert({
  			bits: masterConfig.get("master.tls_bits"),
  			sslCertPath: tls_cert,
  			sslPrivKeyPath: tls_key,
  			doLogging: true,
  		});
  	}
  
  	// Add routes for the web interface
  	for (let route of routes) {
  		app.get(route, serveWeb(route));
  	}
  	for (let pluginInfo of pluginInfos) {
  		for (let route of pluginInfo.routes || []) {
  			app.get(route, serveWeb(route));
  		}
  
  		let pluginPackagePath = require.resolve(path.posix.join(pluginInfo.requirePath, "package.json"));
  		let staticPath = path.join(path.dirname(pluginPackagePath), "static");
  		app.use(`/plugin/${pluginInfo.name}`, express.static(staticPath));
  	}
  
  	// Load plugins
  	await pluginManagement();
  
  	// Only start listening for connections after all plugins have loaded
  	if (httpPort) {
  		httpServer = new (require("http").Server)(app);
  		await listen(httpServer, httpPort);
  		logger.info(`Listening for HTTP on port ${httpServer.address().port}`);
  	}
  
  	if (httpsPort) {
  		let certificate, privateKey;
  		try {
  			certificate = await fs.readFile(tls_cert);
  			privateKey = await fs.readFile(tls_key);
  
  		} catch (err) {
  			throw new libErrors.StartupError(
  				`Error loading ssl certificate: ${err.message}`
  			);
  		}
  
  		httpsServer = require("https").createServer({
  			key: privateKey,
  			cert: certificate,
  		}, app);
  		await listen(httpsServer, httpsPort);
  		logger.info(`Listening for HTTPS on port ${httpsServer.address().port}`);
  	}
  }
  
  module.exports = {
  	app,
  
  	// For testing only
  	_db: db,
  	_setConfig,
  	_WebSocketServerConnector: WebSocketServerConnector,
  	_controlConnections: controlConnections,
  	_ControlConnection: ControlConnection,
  	_slaveConnections: slaveConnections,
  	_SlaveConnection: SlaveConnection,
  };
  
  if (module === require.main) {
  	// eslint-disable-next-line no-console
  	console.warn(`
  +==========================================================+
  I WARNING:  This is the development branch for the 2.0     I
  I           version of clusterio.  Expect things to break. I
  +==========================================================+
  `
  	);
  	startServer().catch(err => {
  		if (err instanceof libErrors.StartupError) {
  			logger.fatal(`
  +----------------------------------+
  | Unable to to start master server |
  +----------------------------------+
  ${err.stack}`
  			);
  		} else if (err instanceof libErrors.PluginError) {
  			logger.fatal(`
  ${err.pluginName} plugin threw an unexpected error
  during startup, please report it to the plugin author.
  ------------------------------------------------------
  ${err.original.stack}`
  			);
  		} else {
  			logger.fatal(`
  +---------------------------------------------------------------+
  | Unexpected error occured while starting master, please report |
  | it to https://github.com/clusterio/factorioClusterio/issues   |
  +---------------------------------------------------------------+
  ${err.stack}`
  			);
  		}
  
  		if (masterConfig) {
  			shutdown();
  		}
  	});
  }
  