monitor/core/globalData.ts
2025-04-16 22:30:27 +07:00

579 lines
17 KiB
TypeScript

import os from 'node:os';
import fsp from 'node:fs/promises';
import path from 'node:path';
import slash from 'slash';
import consoleFactory, { setConsoleEnvData } from '@lib/console';
import { addLocalIpAddress } from '@lib/host/isIpAddressLocal';
import { parseFxserverVersion } from '@lib/fxserver/fxsVersionParser';
import { parseTxDevEnv, TxDevEnvType } from '@shared/txDevEnv';
import { Overwrite } from 'utility-types';
import fatalError from '@lib/fatalError';
import { getNativeVars } from './boot/getNativeVars';
import { getHostVars, hostEnvVarSchemas } from './boot/getHostVars';
import { getZapVars } from './boot/getZapVars';
import { z, ZodSchema } from 'zod';
import { fromZodError } from 'zod-validation-error';
import defaultAds from '../dynamicAds2.json';
import consts from '@shared/consts';
const console = consoleFactory();
/**
* MARK: GETTING VARIABLES
*/
//Get OSType
const osTypeVar = os.type();
let isWindows;
if (osTypeVar === 'Windows_NT') {
isWindows = true;
} else if (osTypeVar === 'Linux') {
isWindows = false;
} else {
fatalError.GlobalData(0, `OS type not supported: ${osTypeVar}`);
}
//Simple env vars
const ignoreDeprecatedConfigs = process.env?.TXHOST_IGNORE_DEPRECATED_CONFIGS === 'true';
/**
* MARK: HELPERS
*/
const cleanPath = (x: string) => slash(path.normalize(x));
const handleMultiVar = <T extends ZodSchema>(
name: string,
schema: T,
procenv: z.infer<T> | undefined,
zapcfg: string | number | undefined,
convar: any,
): z.infer<T> | undefined => {
const alt = zapcfg ?? convar;
if (alt === undefined) {
return procenv;
}
const whichAlt = zapcfg !== undefined ? 'txAdminZapConfig.json' : 'ConVar';
if (procenv !== undefined) {
console.warn(`WARNING: Both the environment variable 'TXHOST_${name}' and the ${whichAlt} equivalent are set. The environment variable will be prioritized.`);
return procenv;
}
const parsed = schema.safeParse(alt);
if (!parsed.success) {
fatalError.GlobalData(20, [
`Invalid value for the TXHOST_${name}-equivalent config in ${whichAlt}.`,
['Value', alt],
'For more information: https://aka.cfx.re/txadmin-env-config',
], fromZodError(parsed.error, { prefix: null }));
}
return parsed.data;
}
/**
* MARK: DEV ENV
*/
type TxDevEnvEnabledType = Overwrite<TxDevEnvType, {
ENABLED: true;
SRC_PATH: string, //required in core/webserver, core/getReactIndex.ts
VITE_URL: string, //required in core/getReactIndex.ts
}>;
type TxDevEnvDisabledType = Overwrite<TxDevEnvType, {
ENABLED: false;
SRC_PATH: undefined;
VITE_URL: undefined;
}>;
let _txDevEnv: TxDevEnvEnabledType | TxDevEnvDisabledType;
const devVars = parseTxDevEnv();
if (devVars.ENABLED) {
console.debug('Starting txAdmin in DEV mode.');
if (!devVars.SRC_PATH || !devVars.VITE_URL) {
fatalError.GlobalData(8, 'Missing TXDEV_VITE_URL and/or TXDEV_SRC_PATH env variables.');
}
_txDevEnv = devVars as TxDevEnvEnabledType;
} else {
_txDevEnv = {
...devVars,
SRC_PATH: undefined,
VITE_URL: undefined,
} as TxDevEnvDisabledType;
}
/**
* MARK: CHECK HOST VARS
*/
const nativeVars = getNativeVars(ignoreDeprecatedConfigs);
//Getting fxserver version
//4380 = GetVehicleType was exposed server-side
//4548 = more or less when node v16 was added
//4574 = add missing PRINT_STRUCTURED_TRACE declaration
//4574 = add resource field to PRINT_STRUCTURED_TRACE
//5894 = CREATE_VEHICLE_SERVER_SETTER
//6185 = added ScanResourceRoot (not yet in use)
//6508 = unhandledRejection is now handlable, we need this due to discord.js's bug
//8495 = changed prometheus::Histogram::BucketBoundaries
//9423 = feat(server): add more infos to playerDropped event
//9655 = Fixed ScanResourceRoot + latent events
const minFxsVersion = 5894;
const fxsVerParsed = parseFxserverVersion(nativeVars.fxsVersion);
const fxsVersion = fxsVerParsed.valid ? fxsVerParsed.build : 99999;
if (!fxsVerParsed.valid) {
console.error('It looks like you are running a custom build of fxserver.');
console.error('And because of that, there is no guarantee that txAdmin will work properly.');
console.error(`Convar: ${nativeVars.fxsVersion}`);
console.error(`Parsed Build: ${fxsVerParsed.build}`);
console.error(`Parsed Branch: ${fxsVerParsed.branch}`);
console.error(`Parsed Platform: ${fxsVerParsed.platform}`);
} else if (fxsVerParsed.build < minFxsVersion) {
fatalError.GlobalData(2, [
'This version of FXServer is too outdated and NOT compatible with txAdmin',
['Current FXServer version', fxsVerParsed.build.toString()],
['Minimum required version', minFxsVersion.toString()],
'Please update your FXServer to a newer version.',
]);
} else if (fxsVerParsed.branch !== 'master') {
console.warn(`You are running a custom branch of FXServer: ${fxsVerParsed.branch}`);
}
//Getting txAdmin version
if (!nativeVars.txaResourceVersion) {
fatalError.GlobalData(3, [
'txAdmin version not set or in the wrong format.',
['Detected version', nativeVars.txaResourceVersion],
]);
}
const txaVersion = nativeVars.txaResourceVersion;
//Get txAdmin Resource Path
if (!nativeVars.txaResourcePath) {
fatalError.GlobalData(4, [
'Could not resolve txAdmin resource path.',
['Convar', nativeVars.txaResourcePath],
]);
}
const txaPath = cleanPath(nativeVars.txaResourcePath);
//Get citizen Root
if (!nativeVars.fxsCitizenRoot) {
fatalError.GlobalData(5, [
'citizen_root convar not set',
['Convar', nativeVars.fxsCitizenRoot],
]);
}
const fxsPath = cleanPath(nativeVars.fxsCitizenRoot as string);
//Check if server is inside WinRar's temp folder
if (isWindows && /Temp[\\/]+Rar\$/i.test(fxsPath)) {
fatalError.GlobalData(12, [
'It looks like you ran FXServer inside WinRAR without extracting it first.',
'Please extract the server files to a proper folder before running it.',
['Server path', fxsPath.replace(/\\/g, '/').replace(/\/$/, '')],
]);
}
//Setting the variables in console without it having to importing from here (circular dependency)
setConsoleEnvData(
txaVersion,
txaPath,
_txDevEnv.ENABLED,
_txDevEnv.VERBOSE
);
/**
* MARK: TXDATA & PROFILE
*/
const hostVars = getHostVars();
//Setting data path
let hasCustomDataPath = false;
let dataPath = cleanPath(path.join(
fxsPath,
isWindows ? '..' : '../../../',
'txData'
));
const dataPathVar = handleMultiVar(
'DATA_PATH',
hostEnvVarSchemas.DATA_PATH,
hostVars.DATA_PATH,
undefined,
nativeVars.txDataPath,
);
if (dataPathVar) {
hasCustomDataPath = true;
dataPath = cleanPath(dataPathVar);
}
//Check paths for non-ASCII characters
//NOTE: Non-ASCII in one of those paths (don't know which) will make NodeJS crash due to a bug in v8 (or something)
// when running localization methods like Date.toLocaleString().
// There was also an issue with the slash() lib and with the +exec on FXServer
const nonASCIIRegex = /[^\x00-\x80]+/;
if (nonASCIIRegex.test(fxsPath) || nonASCIIRegex.test(dataPath)) {
fatalError.GlobalData(7, [
'Due to environmental restrictions, your paths CANNOT contain non-ASCII characters.',
'Example of non-ASCII characters: çâýå, ρέθ, ñäé, ēļæ, глж, เซิร์, 警告.',
'Please make sure FXServer is not in a path contaning those characters.',
`If on windows, we suggest you moving the artifact to "C:/fivemserver/${fxsVersion}/".`,
['FXServer path', fxsPath],
['txData path', dataPath],
]);
}
//Profile - not available as env var
let profileVar = nativeVars.txAdminProfile;
if (profileVar) {
profileVar = profileVar.replace(/[^a-z0-9._-]/gi, '');
if (profileVar.endsWith('.base')) {
fatalError.GlobalData(13, [
['Invalid server profile name', profileVar],
'Profile names cannot end with ".base".',
'It looks like you are trying to point to a server folder instead of a profile.',
]);
}
if (!profileVar.length) {
fatalError.GlobalData(14, [
'Invalid server profile name.',
'If you are using Google Translator on the instructions page,',
'make sure there are no additional spaces in your command.',
]);
}
}
const profileName = profileVar ?? 'default';
const profilePath = cleanPath(path.join(dataPath, profileName));
/**
* MARK: ZAP & NETWORKING
*/
let zapVars: ReturnType<typeof getZapVars> | undefined;
if (!ignoreDeprecatedConfigs) {
//FIXME: ZAP doesn't need this anymore, remove ASAP
const zapCfgFilePath = path.join(dataPath, 'txAdminZapConfig.json');
try {
zapVars = getZapVars(zapCfgFilePath);
if (!_txDevEnv.ENABLED) fsp.unlink(zapCfgFilePath).catch(() => { });
} catch (error) {
fatalError.GlobalData(9, 'Failed to load with ZAP-Hosting configuration.', error);
}
}
//No default, no convar/zap cfg
const txaUrl = hostVars.TXA_URL;
//txAdmin port
const txaPort = handleMultiVar(
'TXA_PORT',
hostEnvVarSchemas.TXA_PORT,
hostVars.TXA_PORT,
zapVars?.txAdminPort,
nativeVars.txAdminPort,
) ?? 40120;
//fxserver port
const fxsPort = handleMultiVar(
'FXS_PORT',
hostEnvVarSchemas.FXS_PORT,
hostVars.FXS_PORT,
zapVars?.forceFXServerPort,
undefined,
);
//Forced interface
const netInterface = handleMultiVar(
'INTERFACE',
hostEnvVarSchemas.INTERFACE,
hostVars.INTERFACE,
zapVars?.forceInterface,
nativeVars.txAdminInterface,
);
if (netInterface) {
addLocalIpAddress(netInterface);
}
/**
* MARK: GENERAL
*/
const forceGameName = hostVars.GAME_NAME;
const hostApiToken = hostVars.API_TOKEN;
const forceMaxClients = handleMultiVar(
'MAX_SLOTS',
hostEnvVarSchemas.MAX_SLOTS,
hostVars.MAX_SLOTS,
zapVars?.deployerDefaults?.maxClients,
undefined,
);
const forceQuietMode = handleMultiVar(
'QUIET_MODE',
hostEnvVarSchemas.QUIET_MODE,
hostVars.QUIET_MODE,
zapVars?.deployerDefaults?.maxClients,
undefined,
) ?? false;
/**
* MARK: PROVIDER
*/
const providerName = handleMultiVar(
'PROVIDER_NAME',
hostEnvVarSchemas.PROVIDER_NAME,
hostVars.PROVIDER_NAME,
zapVars?.providerName,
undefined,
);
const providerLogo = handleMultiVar(
'PROVIDER_LOGO',
hostEnvVarSchemas.PROVIDER_LOGO,
hostVars.PROVIDER_LOGO,
zapVars?.loginPageLogo,
undefined,
);
/**
* MARK: DEFAULTS
*/
const defaultDbHost = handleMultiVar(
'DEFAULT_DBHOST',
hostEnvVarSchemas.DEFAULT_DBHOST,
hostVars.DEFAULT_DBHOST,
zapVars?.deployerDefaults?.mysqlHost,
undefined,
);
const defaultDbPort = handleMultiVar(
'DEFAULT_DBPORT',
hostEnvVarSchemas.DEFAULT_DBPORT,
hostVars.DEFAULT_DBPORT,
zapVars?.deployerDefaults?.mysqlPort,
undefined,
);
const defaultDbUser = handleMultiVar(
'DEFAULT_DBUSER',
hostEnvVarSchemas.DEFAULT_DBUSER,
hostVars.DEFAULT_DBUSER,
zapVars?.deployerDefaults?.mysqlUser,
undefined,
);
const defaultDbPass = handleMultiVar(
'DEFAULT_DBPASS',
hostEnvVarSchemas.DEFAULT_DBPASS,
hostVars.DEFAULT_DBPASS,
zapVars?.deployerDefaults?.mysqlPassword,
undefined,
);
const defaultDbName = handleMultiVar(
'DEFAULT_DBNAME',
hostEnvVarSchemas.DEFAULT_DBNAME,
hostVars.DEFAULT_DBNAME,
zapVars?.deployerDefaults?.mysqlDatabase,
undefined,
);
//Default Master Account
type DefaultMasterAccount = {
username: string;
fivemId?: string;
password?: string;
} | {
username: string;
password: string;
} | undefined;
let defaultMasterAccount: DefaultMasterAccount;
const bcryptRegex = /^\$2[aby]\$[0-9]{2}\$[A-Za-z0-9./]{53}$/;
if (hostVars.DEFAULT_ACCOUNT) {
let [username, fivemId, password] = hostVars.DEFAULT_ACCOUNT.split(':') as (string | undefined)[];
if (username === '') username = undefined;
if (fivemId === '') fivemId = undefined;
if (password === '') password = undefined;
const errArr: [string, any][] = [
['Username', username],
['FiveM ID', fivemId],
['Password', password],
];
if (!username || !consts.regexValidFivemUsername.test(username)) {
fatalError.GlobalData(21, [
'Invalid default account username.',
'It should be a valid FiveM username.',
...errArr,
]);
}
if (fivemId && !consts.validIdentifierParts.fivem.test(fivemId)) {
fatalError.GlobalData(22, [
'Invalid default account FiveM ID.',
'It should match the number in the fivem:0000000 game identifier.',
...errArr,
]);
}
if (password && !bcryptRegex.test(password)) {
fatalError.GlobalData(23, [
'Invalid default account password.',
'Expected bcrypt hash.',
...errArr,
]);
}
if (!fivemId && !password) {
fatalError.GlobalData(24, [
'Invalid default account.',
'Expected at least the FiveM ID or password to be present.',
...errArr,
]);
}
defaultMasterAccount = {
username,
fivemId,
password,
};
} else if (zapVars?.defaultMasterAccount) {
const username = zapVars.defaultMasterAccount?.name;
const password = zapVars.defaultMasterAccount?.password_hash;
if (!consts.regexValidFivemUsername.test(username)) {
fatalError.GlobalData(25, [
'Invalid default account username.',
'It should be a valid FiveM username.',
['Username', username],
]);
}
if (!bcryptRegex.test(password)) {
fatalError.GlobalData(26, [
'Invalid default account password.',
'Expected bcrypt hash.',
['Hash', password],
]);
}
defaultMasterAccount = {
username: username,
password: password,
};
}
//Default cfx key
const defaultCfxKey = handleMultiVar(
'DEFAULT_CFXKEY',
hostEnvVarSchemas.DEFAULT_CFXKEY,
hostVars.DEFAULT_CFXKEY,
zapVars?.deployerDefaults?.license,
undefined,
);
/**
* MARK: FINAL SETUP
*/
if (ignoreDeprecatedConfigs) {
console.verbose.debug('TXHOST_IGNORE_DEPRECATED_CONFIGS is set to true. Ignoring deprecated configs.');
}
const isPterodactyl = !isWindows && process.env?.TXADMIN_ENABLE === '1';
const isZapHosting = providerName === 'ZAP-Hosting';
//Quick config to disable ads
const displayAds = process.env?.TXHOST_TMP_HIDE_ADS !== 'true' || isPterodactyl || isZapHosting;
const adSchema = z.object({
img: z.string(),
url: z.string(),
}).nullable();
const adsDataSchema = z.object({
login: adSchema,
main: adSchema,
});
let adsData: z.infer<typeof adsDataSchema> = {
login: null,
main: null,
};
if (displayAds) {
try {
adsData = adsDataSchema.parse(defaultAds);
} catch (error) {
console.error('Failed to load ads data.', error);
}
}
//FXServer Display Version
let fxsVersionTag = fxsVersion.toString();
if (fxsVerParsed.branch && fxsVerParsed.branch !== 'master') {
fxsVersionTag += '-ft';
}
if (isZapHosting) {
fxsVersionTag += '/ZAP';
} else if (isPterodactyl) {
fxsVersionTag += '/Ptero';
} else if (isWindows && fxsVerParsed.platform === 'windows') {
fxsVersionTag += '/Win';
} else if (!isWindows && fxsVerParsed.platform === 'linux') {
fxsVersionTag += '/Lin';
} else {
fxsVersionTag += '/Unk';
}
/**
* MARK: Exports
*/
export const txDevEnv = Object.freeze(_txDevEnv);
export const txEnv = Object.freeze({
//Calculated
isWindows,
isPterodactyl, //TODO: remove, used only in HB Data
isZapHosting, //TODO: remove, used only in HB Data and authLogic to disable src check
displayAds,
adsData,
//Natives
fxsVersionTag,
fxsVersion,
txaVersion,
txaPath,
fxsPath,
//ConVar
profileName,
profilePath, //FIXME: replace by profileSubPath in most places
profileSubPath: (...parts: string[]) => path.join(profilePath, ...parts),
});
export const txHostConfig = Object.freeze({
//General
dataPath,
dataSubPath: (...parts: string[]) => path.join(dataPath, ...parts),
hasCustomDataPath,
forceGameName,
forceMaxClients,
forceQuietMode,
hostApiToken,
//Networking
txaUrl,
txaPort,
fxsPort,
netInterface,
//Provider
providerName,
providerLogo,
sourceName: providerName ?? 'Host Config',
//Defaults
defaults: {
account: defaultMasterAccount,
cfxKey: defaultCfxKey,
dbHost: defaultDbHost,
dbPort: defaultDbPort,
dbUser: defaultDbUser,
dbPass: defaultDbPass,
dbName: defaultDbName,
},
});
//DEBUG
// console.dir(txEnv, { compact: true });
// console.dir(txDevEnv, { compact: true });
// console.dir(txHostConfig, { compact: true });