Skip to content

Commit 7bd5f0e

Browse files
committed
feat(core): host status api
1 parent 4f3d301 commit 7bd5f0e

11 files changed

Lines changed: 175 additions & 21 deletions

File tree

core/boot/getHostVars.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ import consts from '@shared/consts';
1010
*/
1111
export const hostEnvVarSchemas = {
1212
//General
13+
API_TOKEN: z.union([
14+
z.literal('disabled'),
15+
z.string().regex(
16+
/^[A-Za-z0-9_-]{16,48}$/,
17+
'Token must be alphanumeric, underscores or dashes only, and between 16 and 48 characters long.'
18+
),
19+
]),
1320
DATA_PATH: z.string().min(1).refine(
1421
(val) => path.isAbsolute(val),
1522
'DATA_PATH must be an absolute path'

core/globalData.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ if (netInterface) {
292292
* MARK: GENERAL
293293
*/
294294
const forceGameName = hostVars.GAME_NAME;
295+
const hostApiToken = hostVars.API_TOKEN;
295296

296297
const forceMaxClients = handleMultiVar(
297298
'MAX_SLOTS',
@@ -545,6 +546,7 @@ export const txHostConfig = Object.freeze({
545546
forceGameName,
546547
forceMaxClients,
547548
forceQuietMode,
549+
hostApiToken,
548550

549551
//Networking
550552
txaUrl,

core/modules/CacheStore.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import fsp from 'node:fs/promises';
33
import throttle from 'lodash-es/throttle.js';
44
import consoleFactory from '@lib/console';
55
import { txDevEnv, txEnv } from '@core/globalData';
6+
import type { z, ZodSchema } from 'zod';
67
const console = consoleFactory(modulename);
78

89

@@ -50,6 +51,15 @@ export default class CacheStore {
5051
return this.#cache.get(key);
5152
}
5253

54+
getTyped<T extends ZodSchema>(key: string, schema: T): z.infer<T> | undefined {
55+
if (!(this.#cache instanceof Map)) return undefined;
56+
const value = this.#cache.get(key);
57+
if (!value) return undefined;
58+
const parsed = schema.safeParse(value);
59+
if (parsed.success) return parsed.data;
60+
return undefined;
61+
}
62+
5363
set(key: string, value: AcceptedCachedTypes) {
5464
if (!(this.#cache instanceof Map)) return false;
5565
if (!isAcceptedType(value)) throw new Error(`Value of type ${typeof value} is not acceptable.`);

core/modules/FxRunner/index.ts

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import StreamValues from 'stream-json/streamers/StreamValues';
44
import { customAlphabet } from 'nanoid/non-secure';
55
import dict49 from 'nanoid-dictionary/nolookalikes';
66
import consoleFactory from '@lib/console';
7-
import { validateFixServerConfig } from '@lib/fxserver/fxsConfigHelper';
7+
import { resolveCFGFilePath, validateFixServerConfig } from '@lib/fxserver/fxsConfigHelper';
88
import { msToShortishDuration } from '@lib/misc';
99
import { SYM_SYSTEM_AUTHOR } from '@lib/symbols';
1010
import { UpdateConfigKeySet } from '@modules/ConfigStore/utils';
@@ -13,6 +13,7 @@ import ProcessManager, { ChildProcessStateInfo } from './ProcessManager';
1313
import handleFd3Messages from './handleFd3Messages';
1414
import ConsoleLineEnum from '@modules/Logger/FXServerLogger/ConsoleLineEnum';
1515
import { txHostConfig } from '@core/globalData';
16+
import path from 'node:path';
1617
const console = consoleFactory('FXRunner');
1718
const genMutex = customAlphabet(dict49, 5);
1819

@@ -469,23 +470,27 @@ export default class FxRunner {
469470
* The resolved paths of the server
470471
* FIXME: check where those paths are needed and only calculate what is relevant
471472
*/
472-
// public get serverPaths() {
473-
// if (!this.isConfigured) return;
474-
// return {
475-
// data: {
476-
// absolute: 'xxx',
477-
// },
478-
// //TODO: cut paste logic from resolveCFGFilePath
479-
// resources: {
480-
// //???
481-
// },
482-
// cfg: {
483-
// fileName: 'xxx',
484-
// relativePath: 'xxx',
485-
// absolutePath: 'xxx',
486-
// }
487-
// };
488-
// }
473+
public get serverPaths() {
474+
if (!this.isConfigured) return;
475+
return {
476+
dataPath: path.normalize(txConfig.server.dataPath!), //to maintain consistency
477+
cfgPath: resolveCFGFilePath(txConfig.server.cfgPath, txConfig.server.dataPath!),
478+
}
479+
// return {
480+
// data: {
481+
// absolute: 'xxx',
482+
// },
483+
// //TODO: cut paste logic from resolveCFGFilePath
484+
// resources: {
485+
// //???
486+
// },
487+
// cfg: {
488+
// fileName: 'xxx',
489+
// relativePath: 'xxx',
490+
// absolutePath: 'xxx',
491+
// }
492+
// };
493+
}
489494

490495

491496
/**

core/modules/Logger/handlers/server.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,15 @@ export default class ServerLogger extends LoggerBase {
228228
if (typeof eventData.data?.projectName === 'string' && eventData.data.projectName.length) {
229229
txCore.cacheStore.set('fxsRuntime:projectName', eventData.data.projectName);
230230
}
231+
if (typeof eventData.data?.gameName === 'string' && eventData.data.gameName.length) {
232+
if(eventData.data.gameName === 'gta5'){
233+
txCore.cacheStore.set('fxsRuntime:gameName', 'fivem');
234+
} else if (eventData.data.gameName === 'rdr3') {
235+
txCore.cacheStore.set('fxsRuntime:gameName', 'redm');
236+
} else {
237+
txCore.cacheStore.delete('fxsRuntime:gameName');
238+
}
239+
}
231240

232241
} else if (eventData.type === 'DebugMessage') {
233242
eventMessage = (typeof eventData.data === 'string')

core/modules/WebServer/middlewares/authMws.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import consoleFactory from '@lib/console';
33
import { checkRequestAuth } from "../authLogic";
44
import { ApiAuthErrorResp, ApiToastResp, GenericApiErrorResp } from "@shared/genericApiTypes";
55
import { InitializedCtx } from '../ctxTypes';
6+
import { txHostConfig } from '@core/globalData';
67
const console = consoleFactory(modulename);
78

89
const webLogoutPage = `<style>
@@ -37,6 +38,62 @@ body {
3738
</script>`;
3839

3940

41+
/**
42+
* For the hosting provider routes
43+
*/
44+
export const hostAuthMw = async (ctx: InitializedCtx, next: Function) => {
45+
const docs = 'https://aka.cfx.re/txadmin-env-config';
46+
47+
//Token disabled
48+
if (txHostConfig.hostApiToken === 'disabled') {
49+
return await next();
50+
}
51+
52+
//Token undefined
53+
if (!txHostConfig.hostApiToken) {
54+
return ctx.send({
55+
error: 'token not configured',
56+
desc: 'need to configure the TXHOST_API_TOKEN environment variable to be able to use the status endpoint',
57+
docs,
58+
});
59+
}
60+
61+
//Token available
62+
let tokenProvided: string | undefined;
63+
const headerToken = ctx.headers['x-txadmin-envtoken'];
64+
if (typeof headerToken === 'string' && headerToken) {
65+
tokenProvided = headerToken;
66+
}
67+
const paramsToken = ctx.query.envtoken;
68+
if (typeof paramsToken === 'string' && paramsToken) {
69+
tokenProvided = paramsToken;
70+
}
71+
if (headerToken && paramsToken) {
72+
return ctx.send({
73+
error: 'token conflict',
74+
desc: 'cannot use both header and query token',
75+
docs,
76+
});
77+
}
78+
if (!tokenProvided) {
79+
return ctx.send({
80+
error: 'token missing',
81+
desc: 'a token needs to be provided in the header or query string',
82+
docs,
83+
});
84+
}
85+
if (tokenProvided !== txHostConfig.hostApiToken) {
86+
return ctx.send({
87+
error: 'invalid token',
88+
desc: 'the token provided does not match the TXHOST_API_TOKEN environment variable',
89+
docs,
90+
});
91+
}
92+
93+
return await next();
94+
};
95+
96+
4097
/**
4198
* Intercom auth middleware
4299
* This does not set ctx.admin and does not use session/cookies whatsoever.

core/modules/WebServer/router.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Router from '@koa/router';
33
import KoaRateLimit from 'koa-ratelimit';
44

55
import * as routes from '@routes/index';
6-
import { apiAuthMw, intercomAuthMw, webAuthMw } from './middlewares/authMws';
6+
import { apiAuthMw, hostAuthMw, intercomAuthMw, webAuthMw } from './middlewares/authMws';
77

88

99
/**
@@ -114,6 +114,9 @@ export default () => {
114114
router.get('/whitelist/:table', apiAuthMw, routes.whitelist_list);
115115
router.post('/whitelist/:table/:action', apiAuthMw, routes.whitelist_actions);
116116

117+
//Host routes
118+
router.get('/host/status', hostAuthMw, routes.host_status);
119+
117120
//DevDebug routes - no auth
118121
if (txDevEnv.ENABLED) {
119122
router.get('/dev/:scope', routes.dev_get);

core/routes/hostStatus.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { txHostConfig } from '@core/globalData';
2+
import type { InitializedCtx } from '@modules/WebServer/ctxTypes';
3+
4+
5+
/**
6+
* Returns host status information
7+
*/
8+
export default async function HostStatus(ctx: InitializedCtx) {
9+
return ctx.send(txManager.hostStatus);
10+
};

core/routes/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,15 @@ export { default as whitelist_page } from './whitelist/page';
6363
export { default as whitelist_list } from './whitelist/list';
6464
export { default as whitelist_actions } from './whitelist/actions';
6565

66-
6766
export { default as advanced_page } from './advanced/get';
6867
export { default as advanced_actions } from './advanced/actions';
6968

7069
//FIXME: reorganizar TODAS rotas de logs, incluindo listagem e download
7170
export { default as serverLog } from './serverLog.js';
7271
export { default as serverLogPartial } from './serverLogPartial.js';
7372

73+
export { default as host_status } from './hostStatus';
74+
7475
export {
7576
get as dev_get,
7677
post as dev_post,

core/txManager.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,27 @@ import { TxConfigState } from "@shared/enums";
66
import type { GlobalStatusType } from "@shared/socketioTypes";
77
import quitProcess from "@lib/quitProcess";
88
import consoleFactory, { processStdioEnsureEol, setTTYTitle } from "@lib/console";
9+
import { z } from "zod";
910
const console = consoleFactory('Manager');
1011

12+
//Types
13+
type HostStatusType = {
14+
//txAdmin state
15+
cfgPath: string | null;
16+
dataPath: string | null;
17+
isConfigured: boolean;
18+
playerCount: number;
19+
status: typeof txCore.fxMonitor.currentStatus;
20+
21+
//Detected at runtime
22+
cfxId: string | null;
23+
gameName: 'fivem' | 'redm' | null;
24+
joinLink: string | null;
25+
joinDeepLink: string | null;
26+
playerSlots: number | null;
27+
projectName: string | null;
28+
}
29+
1130

1231
/**
1332
* This class is for "high order" logic and methods that shouldn't live inside any specific component.
@@ -125,6 +144,31 @@ export default class TxManager {
125144
}
126145

127146

147+
/**
148+
* Returns the status object that is sent to the host status endpoint
149+
*/
150+
get hostStatus(): HostStatusType {
151+
const serverPaths = txCore.fxRunner.serverPaths;
152+
const cfxId = txCore.cacheStore.getTyped('fxsRuntime:cfxId', z.string()) ?? null;
153+
return {
154+
//txAdmin state
155+
isConfigured: this.configState === TxConfigState.Ready,
156+
dataPath: serverPaths?.dataPath ?? null,
157+
cfgPath: serverPaths?.cfgPath ?? null,
158+
playerCount: txCore.fxPlayerlist.onlineCount,
159+
status: txCore.fxMonitor.currentStatus,
160+
161+
//Detected at runtime
162+
cfxId,
163+
gameName: txCore.cacheStore.getTyped('fxsRuntime:gameName', z.enum(['fivem', 'redm'])) ?? null,
164+
joinDeepLink: cfxId ? `fivem://connect/cfx.re/join/${cfxId}` : null,
165+
joinLink: cfxId ? `https://cfx.re/join/${cfxId}` : null,
166+
playerSlots: txCore.cacheStore.getTyped('fxsRuntime:maxClients', z.number()) ?? null,
167+
projectName: txCore.cacheStore.getTyped('fxsRuntime:projectName', z.string()) ?? null,
168+
}
169+
}
170+
171+
128172
/**
129173
* Returns the global status object that is sent to the clients
130174
*/
@@ -141,7 +185,6 @@ export default class TxManager {
141185
name: txConfig.general.serverName,
142186
whitelist: txConfig.whitelist.mode,
143187
},
144-
// @ts-ignore scheduler type narrowing is wrong because cant use "as const" in javascript
145188
scheduler: txCore.fxScheduler.getStatus(), //no push events, updated every Scheduler.checkSchedule()
146189
}
147190
}

0 commit comments

Comments
 (0)