From 94bf5ebdfdb2645399b5182c52f31e1149edd674 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sat, 17 Oct 2026 09:06:49 +0200 Subject: [PATCH] Try AJV for all gateway payloads --- src/gateway/events/Message.ts | 1 + src/gateway/opcodes/GuildSubscriptionsBulk.ts | 18 +++++++++++++++-- src/gateway/opcodes/LazyRequest.ts | 20 ++++++++++++++++--- src/gateway/opcodes/PresenceUpdate.ts | 18 +++++++++++++++-- src/gateway/opcodes/RequestGuildMembers.ts | 16 +++++++++++++-- src/gateway/opcodes/StreamCreate.ts | 18 +++++++++++++++-- src/gateway/opcodes/StreamDelete.ts | 18 +++++++++++++++-- src/gateway/opcodes/StreamWatch.ts | 18 +++++++++++++++-- src/gateway/opcodes/VoiceStateUpdate.ts | 18 +++++++++++++++-- src/gateway/opcodes/instanceOf.ts | 1 + 10 files changed, 129 insertions(+), 17 deletions(-) diff --git a/src/gateway/events/Message.ts b/src/gateway/events/Message.ts index b76ab889a..c66c24d10 100644 --- a/src/gateway/events/Message.ts +++ b/src/gateway/events/Message.ts @@ -77,6 +77,7 @@ export async function Message(this: WebSocket, buffer: WS.Data) { if (!this.session_id) console.log(`[Gateway/${this.user_id ?? this.ipAddress}] Unknown session id, dumping to unknown folder`); } + // TODO: somehow make this schema work with AJV check.call(this, PayloadSchema, data); const OPCodeHandler = OPCodeHandlers[data.op]; diff --git a/src/gateway/opcodes/GuildSubscriptionsBulk.ts b/src/gateway/opcodes/GuildSubscriptionsBulk.ts index ec232829b..87de697f6 100644 --- a/src/gateway/opcodes/GuildSubscriptionsBulk.ts +++ b/src/gateway/opcodes/GuildSubscriptionsBulk.ts @@ -1,11 +1,25 @@ import { WebSocket, Payload } from "@spacebar/gateway"; import { onLazyRequest } from "./LazyRequest"; -import { GuildSubscriptionsBulkSchema } from "@spacebar/schemas"; +import { getInvalidSchemaValues, GuildSubscriptionsBulkSchema, IdentifySchema, validateSchema } from "@spacebar/schemas"; import { check } from "./instanceOf"; export async function onGuildSubscriptionsBulk(this: WebSocket, payload: Payload) { const startTime = Date.now(); - check.call(this, GuildSubscriptionsBulkSchema, payload.d); + + // Check payload matches schema + try { + payload.d = validateSchema("GuildSubscriptionsBulkSchema", payload.d); + } catch (e) { + console.error("AJV validation of GuildSubscriptionsBulk schema failed:", e); + console.error("Provided values:"); + if (Array.isArray(e)) + for (const invalidSchemaValue of getInvalidSchemaValues(payload.d, e)) { + console.error(`${invalidSchemaValue.instancePath}:`, invalidSchemaValue.value); + } + + check.call(this, GuildSubscriptionsBulkSchema, payload.d); + } + const body = payload.d as GuildSubscriptionsBulkSchema; let guildId: keyof GuildSubscriptionsBulkSchema["subscriptions"]; diff --git a/src/gateway/opcodes/LazyRequest.ts b/src/gateway/opcodes/LazyRequest.ts index 342401b4a..ca32fd4de 100644 --- a/src/gateway/opcodes/LazyRequest.ts +++ b/src/gateway/opcodes/LazyRequest.ts @@ -20,7 +20,7 @@ import murmur from "murmurhash-js/murmurhash3_gc"; import { getDatabase, Member, Role, Session, User, Channel } from "@spacebar/database"; import { arrayPartition, Stopwatch } from "@spacebar/extensions"; import { WebSocket, Payload, handlePresenceUpdate, OPCODES, Send, handleOffloadedGatewayRequest } from "@spacebar/gateway"; -import { LazyRequestSchema } from "@spacebar/schemas"; +import { getInvalidSchemaValues, IdentifySchema, LazyRequestSchema, validateSchema } from "@spacebar/schemas"; import { getPermission, listenEvent, Presence, Permissions, getMostRelevantSession, Config } from "@spacebar/util"; import { check } from "./instanceOf"; import { start } from "node:repl"; @@ -153,8 +153,22 @@ async function subscribeToMemberEvents(this: WebSocket, user_id: string) { export async function onLazyRequest(this: WebSocket, { d }: Payload) { const sw = Stopwatch.startNew(); - // TODO: check data - check.call(this, LazyRequestSchema, d); + + // TODO: check data - 2026/10/16 is this still relevant? + // Check payload matches schema + try { + d = validateSchema("LazyRequestSchema", d); + } catch (e) { + console.error("AJV validation of LazyRequest schema failed:", e); + console.error("Provided values:"); + if (Array.isArray(e)) + for (const invalidSchemaValue of getInvalidSchemaValues(d, e)) { + console.error(`${invalidSchemaValue.instancePath}:`, invalidSchemaValue.value); + } + + check.call(this, LazyRequestSchema, d); + } + // noinspection JSUnusedLocalSymbols - TODO: implement typing/activities subscriptions const { guild_id, typing, channels, activities, members } = d as LazyRequestSchema; diff --git a/src/gateway/opcodes/PresenceUpdate.ts b/src/gateway/opcodes/PresenceUpdate.ts index abd2f026d..001ebc60c 100644 --- a/src/gateway/opcodes/PresenceUpdate.ts +++ b/src/gateway/opcodes/PresenceUpdate.ts @@ -19,12 +19,26 @@ import { Session, User } from "@spacebar/database"; import { WebSocket, Payload } from "@spacebar/gateway"; import { emitEvent, PresenceUpdateEvent } from "@spacebar/util"; -import { ActivitySchema, InternalStatusOrder, PrivateStatus } from "@spacebar/schemas"; +import { ActivitySchema, getInvalidSchemaValues, IdentifySchema, InternalStatusOrder, PrivateStatus, validateSchema } from "@spacebar/schemas"; import { check } from "./instanceOf"; export async function onPresenceUpdate(this: WebSocket, { d }: Payload) { const startTime = Date.now(); - check.call(this, ActivitySchema, d); + + // Check payload matches schema + try { + d = validateSchema("ActivitySchema", d); + } catch (e) { + console.error("AJV validation of Activity schema failed:", e); + console.error("Provided values:"); + if (Array.isArray(e)) + for (const invalidSchemaValue of getInvalidSchemaValues(d, e)) { + console.error(`${invalidSchemaValue.instancePath}:`, invalidSchemaValue.value); + } + + check.call(this, ActivitySchema, d); + } + const presence = d as ActivitySchema; if (d.status === "unknown") { diff --git a/src/gateway/opcodes/RequestGuildMembers.ts b/src/gateway/opcodes/RequestGuildMembers.ts index ab7f516d0..35b49e96b 100644 --- a/src/gateway/opcodes/RequestGuildMembers.ts +++ b/src/gateway/opcodes/RequestGuildMembers.ts @@ -20,7 +20,7 @@ import { FindManyOptions, ILike, In, MoreThan } from "typeorm"; import { getDatabase, Member, Session } from "@spacebar/database"; import { DateBuilder } from "@spacebar/extensions"; import { WebSocket, Payload, OPCODES, Send, handleOffloadedGatewayRequest } from "@spacebar/gateway"; -import { RequestGuildMembersSchema } from "@spacebar/schemas"; +import { getInvalidSchemaValues, IdentifySchema, RequestGuildMembersSchema, validateSchema } from "@spacebar/schemas"; import { Config, getPermission, GuildMembersChunkEvent, Presence } from "@spacebar/util"; import { check } from "./instanceOf"; @@ -37,7 +37,19 @@ export async function onRequestGuildMembers(this: WebSocket, { d }: Payload) { if (await handleOffloadedGatewayRequest(this, Config.get().offload.gateway.guildMembersUrl!, guildIds)) return; } - check.call(this, RequestGuildMembersSchema, d); + // Check payload matches schema + try { + d = validateSchema("RequestGuildMembersSchema", d); + } catch (e) { + console.error("AJV validation of RequestGuildMembers schema failed:", e); + console.error("Provided values:"); + if (Array.isArray(e)) + for (const invalidSchemaValue of getInvalidSchemaValues(d, e)) { + console.error(`${invalidSchemaValue.instancePath}:`, invalidSchemaValue.value); + } + + check.call(this, RequestGuildMembersSchema, d); + } const { presences, nonce, query: requestQuery } = d as RequestGuildMembersSchema; let { limit, user_ids, guild_id } = d as RequestGuildMembersSchema; diff --git a/src/gateway/opcodes/StreamCreate.ts b/src/gateway/opcodes/StreamCreate.ts index 6fc222abd..66d3e356d 100644 --- a/src/gateway/opcodes/StreamCreate.ts +++ b/src/gateway/opcodes/StreamCreate.ts @@ -20,11 +20,25 @@ import { Channel, Member, Stream, StreamSession, VoiceState } from "@spacebar/da import { genVoiceToken, Payload, WebSocket, generateStreamKey } from "@spacebar/gateway"; import { Config, emitEvent, Snowflake, StreamCreateEvent, StreamServerUpdateEvent, VoiceStateUpdateEvent } from "@spacebar/util"; import { check } from "./instanceOf"; -import { StreamCreateSchema } from "@spacebar/schemas"; +import { getInvalidSchemaValues, IdentifySchema, StreamCreateSchema, validateSchema } from "@spacebar/schemas"; export async function onStreamCreate(this: WebSocket, data: Payload) { const startTime = Date.now(); - check.call(this, StreamCreateSchema, data.d); + + // Check payload matches schema + try { + data.d = validateSchema("StreamCreateSchema", data.d); + } catch (e) { + console.error("AJV validation of StreamCreate schema failed:", e); + console.error("Provided values:"); + if (Array.isArray(e)) + for (const invalidSchemaValue of getInvalidSchemaValues(data.d, e)) { + console.error(`${invalidSchemaValue.instancePath}:`, invalidSchemaValue.value); + } + + check.call(this, StreamCreateSchema, data.d); + } + const body = data.d as StreamCreateSchema; if (body.channel_id.trim().length === 0) return; diff --git a/src/gateway/opcodes/StreamDelete.ts b/src/gateway/opcodes/StreamDelete.ts index 82d095387..012a3eb32 100644 --- a/src/gateway/opcodes/StreamDelete.ts +++ b/src/gateway/opcodes/StreamDelete.ts @@ -20,11 +20,25 @@ import { Member, Stream, VoiceState } from "@spacebar/database"; import { parseStreamKey, Payload, WebSocket } from "@spacebar/gateway"; import { emitEvent, StreamDeleteEvent, VoiceStateUpdateEvent } from "@spacebar/util"; import { check } from "./instanceOf"; -import { StreamDeleteSchema } from "@spacebar/schemas"; +import { getInvalidSchemaValues, IdentifySchema, StreamDeleteSchema, validateSchema } from "@spacebar/schemas"; export async function onStreamDelete(this: WebSocket, data: Payload) { const startTime = Date.now(); - check.call(this, StreamDeleteSchema, data.d); + + // Check payload matches schema + try { + data.d = validateSchema("StreamDeleteSchema", data.d); + } catch (e) { + console.error("AJV validation of StreamDelete schema failed:", e); + console.error("Provided values:"); + if (Array.isArray(e)) + for (const invalidSchemaValue of getInvalidSchemaValues(data.d, e)) { + console.error(`${invalidSchemaValue.instancePath}:`, invalidSchemaValue.value); + } + + check.call(this, StreamDeleteSchema, data.d); + } + const body = data.d as StreamDeleteSchema; let parsedKey: { diff --git a/src/gateway/opcodes/StreamWatch.ts b/src/gateway/opcodes/StreamWatch.ts index f63e4a158..4157bb545 100644 --- a/src/gateway/opcodes/StreamWatch.ts +++ b/src/gateway/opcodes/StreamWatch.ts @@ -20,12 +20,26 @@ import { Not } from "typeorm"; import { Stream, StreamSession } from "@spacebar/database"; import { genVoiceToken, parseStreamKey, Payload, WebSocket } from "@spacebar/gateway"; import { Config, emitEvent, StreamCreateEvent, StreamServerUpdateEvent } from "@spacebar/util"; -import { StreamWatchSchema } from "@spacebar/schemas"; +import { getInvalidSchemaValues, IdentifySchema, StreamWatchSchema, validateSchema } from "@spacebar/schemas"; import { check } from "./instanceOf"; export async function onStreamWatch(this: WebSocket, data: Payload) { const startTime = Date.now(); - check.call(this, StreamWatchSchema, data.d); + + // Check payload matches schema + try { + data.d = validateSchema("StreamWatchSchema", data.d); + } catch (e) { + console.error("AJV validation of StreamWatch schema failed:", e); + console.error("Provided values:"); + if (Array.isArray(e)) + for (const invalidSchemaValue of getInvalidSchemaValues(data.d, e)) { + console.error(`${invalidSchemaValue.instancePath}:`, invalidSchemaValue.value); + } + + check.call(this, StreamWatchSchema, data.d); + } + const body = data.d as StreamWatchSchema; // TODO: apply perms: check if user is allowed to watch diff --git a/src/gateway/opcodes/VoiceStateUpdate.ts b/src/gateway/opcodes/VoiceStateUpdate.ts index bcde7ed6c..49f4be8c6 100644 --- a/src/gateway/opcodes/VoiceStateUpdate.ts +++ b/src/gateway/opcodes/VoiceStateUpdate.ts @@ -19,7 +19,7 @@ import { Guild, Member, VoiceState } from "@spacebar/database"; import { Payload, WebSocket, genVoiceToken } from "@spacebar/gateway"; import { Config, emitEvent, VoiceServerUpdateEvent, VoiceStateUpdateEvent } from "@spacebar/util"; -import { ConfigVoiceRegion, VoiceStateUpdateSchema } from "@spacebar/schemas"; +import { ConfigVoiceRegion, getInvalidSchemaValues, validateSchema, VoiceStateUpdateSchema } from "@spacebar/schemas"; import { check } from "./instanceOf"; // TODO: check if a voice server is setup @@ -29,7 +29,21 @@ import { check } from "./instanceOf"; export async function onVoiceStateUpdate(this: WebSocket, data: Payload) { const startTime = Date.now(); - check.call(this, VoiceStateUpdateSchema, data.d); + + // Check payload matches schema + try { + data.d = validateSchema("VoiceStateUpdateSchema", data.d); + } catch (e) { + console.error("AJV validation of VoiceStateUpdate schema failed:", e); + console.error("Provided values:"); + if (Array.isArray(e)) + for (const invalidSchemaValue of getInvalidSchemaValues(data.d, e)) { + console.error(`${invalidSchemaValue.instancePath}:`, invalidSchemaValue.value); + } + + check.call(this, VoiceStateUpdateSchema, data.d); + } + const body = data.d as VoiceStateUpdateSchema; const isNew = body.channel_id === null && body.guild_id === null; let isChanged = false; diff --git a/src/gateway/opcodes/instanceOf.ts b/src/gateway/opcodes/instanceOf.ts index 7bebf49e2..03a7a81c1 100644 --- a/src/gateway/opcodes/instanceOf.ts +++ b/src/gateway/opcodes/instanceOf.ts @@ -20,6 +20,7 @@ import { instanceOf } from "lambert-server/check"; import { WebSocket } from "@spacebar/gateway"; import { CLOSECODES } from "../util/Constants"; +//@deprecated export function check(this: WebSocket, schema: unknown, data: unknown) { try { const error = instanceOf(schema, data, { path: "body" });