From b48d3f5240ed56d1f7b04f9bcf00f4b4cbe56ebd Mon Sep 17 00:00:00 2001 From: Danya H Date: Mon, 28 Sep 2026 00:22:45 +0100 Subject: [PATCH] fix(rpc): improve handlers --- rpc/src/core/base.ts | 14 +++----------- rpc/src/core/client.ts | 27 +++++++++------------------ rpc/src/core/server.ts | 26 +++++--------------------- rpc/src/core/webview.ts | 40 +++++++++++++++++++++++----------------- rpc/src/utils/funcs.ts | 37 ++++++++++++++++++++++++++++++++++--- rpc/src/utils/types.ts | 1 - 6 files changed, 74 insertions(+), 71 deletions(-) diff --git a/rpc/src/core/base.ts b/rpc/src/core/base.ts index 5bd4dc6..3330db8 100644 --- a/rpc/src/core/base.ts +++ b/rpc/src/core/base.ts @@ -13,7 +13,6 @@ import { type RPCState, } from '../utils/types' -/** Shared plumbing of the server, client and webview instances */ export class RPCInstanceBase { protected readonly env: RPCEnvironment protected readonly debug: boolean @@ -26,9 +25,7 @@ export class RPCInstanceBase { this._pending = new Pending(cfg.timeout ?? 5000) } - // ===== LISTENERS ===== - - /** Registers `cb` for `event` on `emitter`; `method` is only used for logs */ + /** Registers `cb` for `event` on `emitter`; `method` is only for logs */ protected listen( emitter: Emitter, method: string, @@ -39,15 +36,14 @@ export class RPCInstanceBase { emitter.on(event, cb) return this } - + + /** Unregisters `cb` for `event` on `emitter`; `method` is only for logs */ protected unlisten(emitter: Emitter, method: string, event: string): this { this.log(`${method} ${event}`) emitter.off(event) return this } - // ===== OUTGOING ===== - /** Builds an event payload sent from this environment to `to` */ protected request( event: string, @@ -89,8 +85,6 @@ export class RPCInstanceBase { if (!found) this.logIgnored(response) } - // ===== INCOMING ===== - /** * Runs the listener for `request` and builds the response to send back. * Never throws: a missing listener or a thrown error ends up in `error`. @@ -147,8 +141,6 @@ export class RPCInstanceBase { }) } - // ===== LOGS ===== - /** Debug-only log, prefixed with the environment */ protected log(message: string): void { if (this.debug) console.log(`[RPC]:${this.env}:${message}`) diff --git a/rpc/src/core/client.ts b/rpc/src/core/client.ts index fde135b..98bdd39 100644 --- a/rpc/src/core/client.ts +++ b/rpc/src/core/client.ts @@ -2,7 +2,7 @@ import type * as s from '@entityseven/fivem-rpc-shared-types' import { Emitter } from '../utils/emitter' import { RPCError } from '../utils/errors' -import { parse, stringify, stringifyWeb } from '../utils/funcs' +import { stringify, stringifyWeb } from '../utils/funcs' import { NATIVE_CLIENT_EVENTS, NATIVE_CLIENT_NETWORK_EVENTS, @@ -35,11 +35,14 @@ export class RPCInstanceClient extends RPCInstanceBase { RegisterNuiCallbackType(RPCEvents.LISTENER_WEB) on( `__cfx_nui:${RPCEvents.LISTENER_WEB}`, - async (data: RPCState, callback: (res: unknown) => void) => { + async (data: unknown, callback: (res: unknown) => void) => { + const payload = this.accept(data) + if (!payload) return callback({ status: 'invalid' }) + try { - callback(await this._handleWeb(data)) + callback(await this._handleWeb(payload)) } catch (e) { - callback(this.errorResponse(data, e)) + callback(this.errorResponse(payload, e)) } }, ) @@ -48,16 +51,8 @@ export class RPCInstanceClient extends RPCInstanceBase { // ===== HANDLERS ===== private async _handleServer(payloadRaw: RPCStateRaw) { - try { - parse(payloadRaw) - } catch { - throw new RPCError(RPCErrors.INVALID_DATA, RPCErrors.INVALID_DATA) - } - const payload = parse(payloadRaw) - - this.log( - `accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`, - ) + const payload = this.accept(payloadRaw) + if (!payload) return if (payload.type === 'event') { if (payload.calledTo === 'client') { @@ -86,10 +81,6 @@ export class RPCInstanceClient extends RPCInstanceBase { } private async _handleWeb(payload: RPCState): Promise { - this.log( - `accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`, - ) - if (payload.type === 'event') { if (payload.calledTo === 'client') { return this.dispatch(this._emitterWeb, payload) diff --git a/rpc/src/core/server.ts b/rpc/src/core/server.ts index cb7f103..b7ea9f3 100644 --- a/rpc/src/core/server.ts +++ b/rpc/src/core/server.ts @@ -2,7 +2,7 @@ import type * as s from '@entityseven/fivem-rpc-shared-types' import { Emitter } from '../utils/emitter' import { RPCError } from '../utils/errors' -import { parse, stringify } from '../utils/funcs' +import { stringify } from '../utils/funcs' import { NATIVE_SERVER_EVENTS } from '../utils/native' import { type RPCConfig, @@ -32,16 +32,8 @@ export class RPCInstanceServer extends RPCInstanceBase { // ===== HANDLERS ===== private async _handleClient(payloadRaw: RPCStateRaw) { - try { - parse(payloadRaw) - } catch { - throw new RPCError(RPCErrors.INVALID_DATA, RPCErrors.INVALID_DATA) - } - const payload = parse(payloadRaw) - - this.log( - `accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`, - ) + const payload = this.accept(payloadRaw) + if (!payload) return if (payload.calledFrom === 'client') { if (payload.type === 'event') { @@ -71,16 +63,8 @@ export class RPCInstanceServer extends RPCInstanceBase { } private async _handleWeb(payloadRaw: RPCStateRaw) { - try { - parse(payloadRaw) - } catch { - throw new RPCError(RPCErrors.INVALID_DATA, RPCErrors.INVALID_DATA) - } - const payload = parse(payloadRaw) - - this.log( - `accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`, - ) + const payload = this.accept(payloadRaw) + if (!payload) return if (payload.calledFrom === 'webview') { if (payload.type === 'event') { diff --git a/rpc/src/core/webview.ts b/rpc/src/core/webview.ts index b3ff380..10ebeb2 100644 --- a/rpc/src/core/webview.ts +++ b/rpc/src/core/webview.ts @@ -23,23 +23,30 @@ export class RPCInstanceWebview extends RPCInstanceBase { console.log('[RPC] Initialized Webview') - window.addEventListener('message', (e: MessageEvent) => { - if (e.data.origin === RPCEvents.LISTENER_CLIENT) { - this._handleClient(e.data.data) - } - if (e.data.origin === RPCEvents.LISTENER_SERVER) { - this._handleServer(e.data.data) - } - }) + window.addEventListener( + 'message', + (e: MessageEvent | null>) => { + const origin = e.data?.origin + // not ours, e.g. the resource's own SendNUIMessage calls + if ( + origin !== RPCEvents.LISTENER_CLIENT && + origin !== RPCEvents.LISTENER_SERVER + ) { + return + } + + const payload = this.accept(e.data?.data) + if (!payload) return + + if (origin === RPCEvents.LISTENER_CLIENT) this._handleClient(payload) + else this._handleServer(payload) + }, + ) } // ===== HANDLERS ===== private async _handleClient(payload: RPCState) { - this.log( - `accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`, - ) - if (payload.calledFrom === 'client' && payload.type === 'event') { const response = await this.dispatch(this._emitterClient, payload) @@ -48,10 +55,6 @@ export class RPCInstanceWebview extends RPCInstanceBase { } private async _handleServer(payload: RPCState) { - this.log( - `accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`, - ) - if (payload.calledFrom === 'server' && payload.type === 'event') { const response = await this.dispatch(this._emitterServer, payload) @@ -156,7 +159,10 @@ export class RPCInstanceWebview extends RPCInstanceBase { private _request(payload: RPCState): Promise { const response = this._pending.wait(payload) this._createHttpClientRequest(payload).then( - res => this.settle(res), + res => { + const reply = this.accept(res) + if (reply) this.settle(reply) + }, (error: Error) => this._pending.reject(payload.uuid, error), ) return response diff --git a/rpc/src/utils/funcs.ts b/rpc/src/utils/funcs.ts index 56c81c9..e2425a3 100644 --- a/rpc/src/utils/funcs.ts +++ b/rpc/src/utils/funcs.ts @@ -1,17 +1,48 @@ import type { + RPCEnvironment, RPCState, RPCStateRaw, RPCStateWeb, RPCStateWebRaw, } from './types' +const ENVIRONMENTS: readonly unknown[] = [ + 'server', + 'client', + 'webview', +] satisfies RPCEnvironment[] + /** * **Internal** * - * Typed data parser + * Checks the shape of an incoming payload. Payloads come from the network or + * another runtime, so nothing about them is trusted. */ -export function parse(data: RPCStateRaw): RPCState { - return JSON.parse(data) +export function isRPCState(value: unknown): value is RPCState { + if (typeof value !== 'object' || value === null) return false + const v = value as Record + return ( + typeof v.event === 'string' && + typeof v.uuid === 'string' && + (v.type === 'event' || v.type === 'response') && + ENVIRONMENTS.includes(v.calledFrom) && + ENVIRONMENTS.includes(v.calledTo) && + (v.data === null || Array.isArray(v.data)) + ) +} + +/** + * **Internal** + * + * Parses a raw payload, `null` if it is not JSON or not an RPC payload + */ +export function parse(data: RPCStateRaw): RPCState | null { + try { + const value: unknown = JSON.parse(data) + return isRPCState(value) ? value : null + } catch { + return null + } } /** diff --git a/rpc/src/utils/types.ts b/rpc/src/utils/types.ts index 3e793df..d0fc19c 100644 --- a/rpc/src/utils/types.ts +++ b/rpc/src/utils/types.ts @@ -94,7 +94,6 @@ export enum RPCEvents { */ export enum RPCErrors { EVENT_NOT_REGISTERED = 'Event not registered', - INVALID_DATA = 'Invalid data (possibly broken JSON)', NO_PLAYER = 'No player (failed to resolve from local index)', UNKNOWN_NATIVE = 'Unknown native event (if you are sure this exists - use native handler)', UNKNOWN_ENVIRONMENT = 'Unknown environment (must be either "server", "client" or "webview")',