287 lines
10 KiB
JavaScript
287 lines
10 KiB
JavaScript
const modulename = 'WebServer:DeployerActions';
|
|
import path from 'node:path';
|
|
import { cloneDeep } from 'lodash-es';
|
|
import slash from 'slash';
|
|
import mysql from 'mysql2/promise';
|
|
import consts from '@shared/consts';
|
|
import { txEnv, txHostConfig } from '@core/globalData';
|
|
import { validateModifyServerConfig } from '@lib/fxserver/fxsConfigHelper';
|
|
import consoleFactory from '@lib/console';
|
|
import { SYM_RESET_CONFIG } from '@lib/symbols';
|
|
const console = consoleFactory(modulename);
|
|
|
|
//Helper functions
|
|
const isUndefined = (x) => (x === undefined);
|
|
|
|
|
|
/**
|
|
* Handle all the server control actions
|
|
* @param {object} ctx
|
|
*/
|
|
export default async function DeployerActions(ctx) {
|
|
//Sanity check
|
|
if (isUndefined(ctx.params.action)) {
|
|
return ctx.utils.error(400, 'Invalid Request');
|
|
}
|
|
const action = ctx.params.action;
|
|
|
|
//Check permissions
|
|
if (!ctx.admin.testPermission('master', modulename)) {
|
|
return ctx.send({ success: false, refresh: true });
|
|
}
|
|
|
|
//Check if this is the correct state for the deployer
|
|
if (txManager.deployer == null) {
|
|
return ctx.send({ success: false, refresh: true });
|
|
}
|
|
|
|
//Delegate to the specific action functions
|
|
if (action == 'confirmRecipe') {
|
|
return await handleConfirmRecipe(ctx);
|
|
} else if (action == 'setVariables') {
|
|
return await handleSetVariables(ctx);
|
|
} else if (action == 'commit') {
|
|
return await handleSaveConfig(ctx);
|
|
} else if (action == 'cancel') {
|
|
return await handleCancel(ctx);
|
|
} else {
|
|
return ctx.send({
|
|
type: 'danger',
|
|
message: 'Unknown setup action.',
|
|
});
|
|
}
|
|
};
|
|
|
|
|
|
//================================================================
|
|
/**
|
|
* Handle submition of user-edited recipe (record to deployer, starts the process)
|
|
* @param {object} ctx
|
|
*/
|
|
async function handleConfirmRecipe(ctx) {
|
|
//Sanity check
|
|
if (isUndefined(ctx.request.body.recipe)) {
|
|
return ctx.utils.error(400, 'Invalid Request - missing parameters');
|
|
}
|
|
const userEditedRecipe = ctx.request.body.recipe;
|
|
|
|
try {
|
|
ctx.admin.logAction('Setting recipe.');
|
|
await txManager.deployer.confirmRecipe(userEditedRecipe);
|
|
} catch (error) {
|
|
return ctx.send({ type: 'danger', message: error.message });
|
|
}
|
|
|
|
return ctx.send({ success: true });
|
|
}
|
|
|
|
|
|
//================================================================
|
|
/**
|
|
* Handle submition of the input variables/parameters
|
|
* @param {object} ctx
|
|
*/
|
|
async function handleSetVariables(ctx) {
|
|
//Sanity check
|
|
if (isUndefined(ctx.request.body.svLicense)) {
|
|
return ctx.utils.error(400, 'Invalid Request - missing parameters');
|
|
}
|
|
const userVars = cloneDeep(ctx.request.body);
|
|
|
|
//Validating sv_licenseKey
|
|
if (
|
|
!consts.regexSvLicenseNew.test(userVars.svLicense)
|
|
&& !consts.regexSvLicenseOld.test(userVars.svLicense)
|
|
) {
|
|
return ctx.send({ type: 'danger', message: 'The Server License does not appear to be valid.' });
|
|
}
|
|
|
|
//Validating steam api key requirement
|
|
if (
|
|
txManager.deployer.recipe.steamRequired
|
|
&& (typeof userVars.steam_webApiKey !== 'string' || userVars.steam_webApiKey.length < 24)
|
|
) {
|
|
return ctx.send({
|
|
type: 'danger',
|
|
message: 'This recipe requires steam_webApiKey to be set and valid.',
|
|
});
|
|
}
|
|
|
|
//DB Stuff
|
|
if (typeof userVars.dbDelete !== 'undefined') {
|
|
//Testing the db config
|
|
try {
|
|
userVars.dbPort = parseInt(userVars.dbPort);
|
|
if (isNaN(userVars.dbPort)) {
|
|
return ctx.send({ type: 'danger', message: 'The database port is invalid (non-integer). The default is 3306.' });
|
|
}
|
|
|
|
const mysqlOptions = {
|
|
host: userVars.dbHost,
|
|
port: userVars.dbPort,
|
|
user: userVars.dbUsername,
|
|
password: userVars.dbPassword,
|
|
connectTimeout: 5000,
|
|
};
|
|
await mysql.createConnection(mysqlOptions);
|
|
} catch (error) {
|
|
let outMessage = error?.message ?? 'Unknown error occurred.';
|
|
if (error?.code === 'ECONNREFUSED') {
|
|
let specificError = (txEnv.isWindows)
|
|
? 'If you do not have a database installed, you can download and run XAMPP.'
|
|
: 'If you do not have a database installed, you must download and run MySQL or MariaDB.';
|
|
if (userVars.dbPort !== 3306) {
|
|
specificError += '<br>\n<b>You are not using the default DB port 3306, make sure it is correct!</b>';
|
|
}
|
|
outMessage = `${error?.message}<br>\n${specificError}`;
|
|
} else if (error.message?.includes('auth_gssapi_client')) {
|
|
outMessage = `Your database does not accept the required authentication method. Please update your MySQL/MariaDB server and try again.`;
|
|
}
|
|
|
|
return ctx.send({ type: 'danger', message: `<b>Database connection failed:</b> ${outMessage}` });
|
|
}
|
|
|
|
//Setting connection string
|
|
userVars.dbDelete = (userVars.dbDelete === 'true');
|
|
const dbFullHost = (userVars.dbPort === 3306)
|
|
? userVars.dbHost
|
|
: `${userVars.dbHost}:${userVars.dbPort}`;
|
|
userVars.dbConnectionString = (userVars.dbPassword.length)
|
|
? `mysql://${userVars.dbUsername}:${userVars.dbPassword}@${dbFullHost}/${userVars.dbName}?charset=utf8mb4`
|
|
: `mysql://${userVars.dbUsername}@${dbFullHost}/${userVars.dbName}?charset=utf8mb4`;
|
|
}
|
|
|
|
//Max Clients & Server Endpoints
|
|
userVars.maxClients = (txHostConfig.forceMaxClients) ? txHostConfig.forceMaxClients : 48;
|
|
if (txHostConfig.netInterface || txHostConfig.fxsPort) {
|
|
const comment = `# ${txHostConfig.sourceName}: do not modify!`;
|
|
const endpointIface = txHostConfig.netInterface ?? '0.0.0.0';
|
|
const endpointPort = txHostConfig.fxsPort ?? 30120;
|
|
userVars.serverEndpoints = [
|
|
`endpoint_add_tcp "${endpointIface}:${endpointPort}" ${comment}`,
|
|
`endpoint_add_udp "${endpointIface}:${endpointPort}" ${comment}`,
|
|
].join('\n');
|
|
} else {
|
|
userVars.serverEndpoints = [
|
|
'endpoint_add_tcp "0.0.0.0:30120"',
|
|
'endpoint_add_udp "0.0.0.0:30120"',
|
|
].join('\n');
|
|
}
|
|
|
|
//Setting identifiers array
|
|
const admin = txCore.adminStore.getAdminByName(ctx.admin.name);
|
|
if (!admin) return ctx.send({ type: 'danger', message: 'Admin not found.' });
|
|
const addPrincipalLines = [];
|
|
Object.keys(admin.providers).forEach((providerName) => {
|
|
if (admin.providers[providerName].identifier) {
|
|
addPrincipalLines.push(`add_principal identifier.${admin.providers[providerName].identifier} group.admin #${ctx.admin.name}`);
|
|
}
|
|
});
|
|
userVars.addPrincipalsMaster = (addPrincipalLines.length)
|
|
? addPrincipalLines.join('\n')
|
|
: '# Deployer Note: this admin master has no identifiers to be automatically added.\n# add_principal identifier.discord:111111111111111111 group.admin #example';
|
|
|
|
//Start deployer
|
|
try {
|
|
ctx.admin.logAction('Running recipe.');
|
|
txManager.deployer.start(userVars);
|
|
} catch (error) {
|
|
return ctx.send({ type: 'danger', message: error.message });
|
|
}
|
|
|
|
return ctx.send({ success: true });
|
|
}
|
|
|
|
|
|
//================================================================
|
|
/**
|
|
* Handle the commit of a Recipe by receiving the user edited server.cfg
|
|
* @param {object} ctx
|
|
*/
|
|
async function handleSaveConfig(ctx) {
|
|
//Sanity check
|
|
if (isUndefined(ctx.request.body.serverCFG)) {
|
|
return ctx.utils.error(400, 'Invalid Request - missing parameters');
|
|
}
|
|
const serverCFG = ctx.request.body.serverCFG;
|
|
const cfgFilePath = path.join(txManager.deployer.deployPath, 'server.cfg');
|
|
txCore.cacheStore.set('deployer:recipe', txManager.deployer?.recipe?.name ?? 'unknown');
|
|
|
|
//Validating config contents + saving file and backup
|
|
try {
|
|
const result = await validateModifyServerConfig(serverCFG, cfgFilePath, txManager.deployer.deployPath);
|
|
if (result.errors) {
|
|
return ctx.send({
|
|
type: 'danger',
|
|
success: false,
|
|
markdown: true,
|
|
message: `**Cannot save \`server.cfg\` due to error(s) in your config file(s):**\n${result.errors}`,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
return ctx.send({
|
|
type: 'danger',
|
|
success: false,
|
|
markdown: true,
|
|
message: `**Failed to save \`server.cfg\` with error:**\n${error.message}`,
|
|
});
|
|
}
|
|
|
|
//Preparing & saving config
|
|
let onesync = SYM_RESET_CONFIG;
|
|
if (typeof txManager.deployer?.recipe?.onesync === 'string' && txManager.deployer.recipe.onesync.length) {
|
|
onesync = txManager.deployer.recipe.onesync;
|
|
}
|
|
try {
|
|
txCore.configStore.saveConfigs({
|
|
server: {
|
|
dataPath: slash(path.normalize(txManager.deployer.deployPath)),
|
|
cfgPath: 'server.cfg',
|
|
onesync,
|
|
}
|
|
}, ctx.admin.name);
|
|
} catch (error) {
|
|
console.warn(`[${ctx.admin.name}] Error changing fxserver settings via deployer.`);
|
|
console.verbose.dir(error);
|
|
return ctx.send({
|
|
type: 'danger',
|
|
markdown: true,
|
|
message: `**Error saving the configuration file:** ${error.message}`
|
|
});
|
|
}
|
|
|
|
ctx.admin.logAction('Completed and committed server deploy.');
|
|
|
|
//If running (for some reason), kill it first
|
|
if (!txCore.fxRunner.isIdle) {
|
|
ctx.admin.logCommand('STOP SERVER');
|
|
await txCore.fxRunner.killServer('new server deployed', ctx.admin.name, true);
|
|
}
|
|
|
|
//Starting server
|
|
const spawnError = await txCore.fxRunner.spawnServer(false);
|
|
if (spawnError !== null) {
|
|
return ctx.send({
|
|
type: 'danger',
|
|
markdown: true,
|
|
message: `Config file saved, but failed to start server with error:\n${spawnError}`,
|
|
});
|
|
} else {
|
|
txManager.deployer = null;
|
|
txCore.webServer.webSocket.pushRefresh('status');
|
|
return ctx.send({ success: true });
|
|
}
|
|
}
|
|
|
|
|
|
//================================================================
|
|
/**
|
|
* Handle the cancellation of the deployer proguess
|
|
* @param {object} ctx
|
|
*/
|
|
async function handleCancel(ctx) {
|
|
txManager.deployer = null;
|
|
txCore.webServer.webSocket.pushRefresh('status');
|
|
return ctx.send({ success: true });
|
|
}
|