fix(protocol): reject invalid request contract values
This commit is contained in:
@@ -2,10 +2,12 @@ export {
|
|||||||
hasOwn,
|
hasOwn,
|
||||||
optionalField,
|
optionalField,
|
||||||
requireArray,
|
requireArray,
|
||||||
|
requireBoolean,
|
||||||
requireInteger,
|
requireInteger,
|
||||||
requireNumber,
|
requireNumber,
|
||||||
requireRecord,
|
requireRecord,
|
||||||
requireString,
|
requireString,
|
||||||
|
requireStringOrFiniteNumber,
|
||||||
type JsonRecord,
|
type JsonRecord,
|
||||||
} from './validation.ts';
|
} from './validation.ts';
|
||||||
export {
|
export {
|
||||||
|
|||||||
+12
-20
@@ -5,10 +5,12 @@ import {
|
|||||||
import {
|
import {
|
||||||
hasOwn,
|
hasOwn,
|
||||||
optionalField,
|
optionalField,
|
||||||
|
requireBoolean,
|
||||||
requireInteger,
|
requireInteger,
|
||||||
requireNumber,
|
requireNumber,
|
||||||
requireRecord,
|
requireRecord,
|
||||||
requireString,
|
requireString,
|
||||||
|
requireStringOrFiniteNumber,
|
||||||
type JsonRecord,
|
type JsonRecord,
|
||||||
} from './validation.ts';
|
} from './validation.ts';
|
||||||
import {
|
import {
|
||||||
@@ -73,18 +75,6 @@ export interface LoginRequestPayload extends Readonly<Record<string, unknown>> {
|
|||||||
readonly machineroom: string;
|
readonly machineroom: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function requireStringOrNumber(value: unknown, path: string): string | number {
|
|
||||||
if (typeof value === 'string') return value;
|
|
||||||
return requireNumber(value, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
function requireBoolean(value: unknown, path: string): boolean {
|
|
||||||
if (typeof value !== 'boolean') {
|
|
||||||
throw new TypeError(`${path}: expected boolean, received ${value === null ? 'null' : typeof value}`);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildLoginRequest(
|
export function buildLoginRequest(
|
||||||
runtimeConfig: FirstSliceRuntimeConfig,
|
runtimeConfig: FirstSliceRuntimeConfig,
|
||||||
account: LoginAccountIdentity,
|
account: LoginAccountIdentity,
|
||||||
@@ -99,18 +89,18 @@ export function buildLoginRequest(
|
|||||||
if (location !== null) requireRecord(location, '$.device.location');
|
if (location !== null) requireRecord(location, '$.device.location');
|
||||||
|
|
||||||
const data: Record<string, unknown> = {
|
const data: Record<string, unknown> = {
|
||||||
agentid: requireStringOrNumber(identity.agentid, '$.runtimeConfig.identity.agentid'),
|
agentid: requireStringOrFiniteNumber(identity.agentid, '$.runtimeConfig.identity.agentid'),
|
||||||
gameid: requireStringOrNumber(identity.gameid, '$.runtimeConfig.identity.gameid'),
|
gameid: requireStringOrFiniteNumber(identity.gameid, '$.runtimeConfig.identity.gameid'),
|
||||||
openid: requireString(accountRecord.openid, '$.account.openid'),
|
openid: requireString(accountRecord.openid, '$.account.openid'),
|
||||||
nickname: requireString(accountRecord.nickname, '$.account.nickname'),
|
nickname: requireString(accountRecord.nickname, '$.account.nickname'),
|
||||||
avatar: requireString(accountRecord.avatar, '$.account.avatar'),
|
avatar: requireString(accountRecord.avatar, '$.account.avatar'),
|
||||||
sex: requireInteger(accountRecord.sex, '$.account.sex'),
|
sex: requireInteger(accountRecord.sex, '$.account.sex'),
|
||||||
province: requireString(accountRecord.province, '$.account.province'),
|
province: requireString(accountRecord.province, '$.account.province'),
|
||||||
city: requireString(accountRecord.city, '$.account.city'),
|
city: requireString(accountRecord.city, '$.account.city'),
|
||||||
unionid: requireStringOrNumber(accountRecord.unionid, '$.account.unionid'),
|
unionid: requireStringOrFiniteNumber(accountRecord.unionid, '$.account.unionid'),
|
||||||
version: requireNumber(identity.version, '$.runtimeConfig.identity.version'),
|
version: requireNumber(identity.version, '$.runtimeConfig.identity.version'),
|
||||||
channelid: requireStringOrNumber(identity.channelid, '$.runtimeConfig.identity.channelid'),
|
channelid: requireStringOrFiniteNumber(identity.channelid, '$.runtimeConfig.identity.channelid'),
|
||||||
marketid: requireStringOrNumber(identity.marketid, '$.runtimeConfig.identity.marketid'),
|
marketid: requireStringOrFiniteNumber(identity.marketid, '$.runtimeConfig.identity.marketid'),
|
||||||
location,
|
location,
|
||||||
machineid: requireString(deviceRecord.machineid, '$.device.machineid'),
|
machineid: requireString(deviceRecord.machineid, '$.device.machineid'),
|
||||||
machineroom: requireString(deviceRecord.machineroom, '$.device.machineroom'),
|
machineroom: requireString(deviceRecord.machineroom, '$.device.machineroom'),
|
||||||
@@ -127,10 +117,12 @@ export function buildLoginRequest(
|
|||||||
|
|
||||||
const loginPlayerId = requireRecord(deviceRecord.loginPlayerId, '$.device.loginPlayerId');
|
const loginPlayerId = requireRecord(deviceRecord.loginPlayerId, '$.device.loginPlayerId');
|
||||||
if (requireBoolean(loginPlayerId.enabled, '$.device.loginPlayerId.enabled')) {
|
if (requireBoolean(loginPlayerId.enabled, '$.device.loginPlayerId.enabled')) {
|
||||||
const cached = loginPlayerId.cachedPlayerId;
|
const path = '$.device.loginPlayerId.cachedPlayerId';
|
||||||
if (typeof cached === 'number' && Number.isInteger(cached) && cached > 0) {
|
const cachedPlayerId = requireInteger(loginPlayerId.cachedPlayerId, path);
|
||||||
data.playerid = cached;
|
if (cachedPlayerId <= 0) {
|
||||||
|
throw new RangeError(`${path}: expected positive integer, received number`);
|
||||||
}
|
}
|
||||||
|
data.playerid = cachedPlayerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildFirstSliceEnvelope('player_login', data as LoginRequestPayload);
|
return buildFirstSliceEnvelope('player_login', data as LoginRequestPayload);
|
||||||
|
|||||||
+29
-12
@@ -4,10 +4,13 @@ import {
|
|||||||
} from '../first-slice-routes.ts';
|
} from '../first-slice-routes.ts';
|
||||||
import {
|
import {
|
||||||
hasOwn,
|
hasOwn,
|
||||||
|
optionalField,
|
||||||
requireArray,
|
requireArray,
|
||||||
requireInteger,
|
requireInteger,
|
||||||
|
requireNumber,
|
||||||
requireRecord,
|
requireRecord,
|
||||||
requireString,
|
requireString,
|
||||||
|
requireStringOrFiniteNumber,
|
||||||
type JsonRecord,
|
type JsonRecord,
|
||||||
} from './validation.ts';
|
} from './validation.ts';
|
||||||
import type { FirstSliceRuntimeConfig } from './login-contract.ts';
|
import type { FirstSliceRuntimeConfig } from './login-contract.ts';
|
||||||
@@ -63,18 +66,13 @@ function baseRoomAction(
|
|||||||
): RoomActionPayload {
|
): RoomActionPayload {
|
||||||
const identity = requireRecord(runtimeConfig?.identity, '$.runtimeConfig.identity');
|
const identity = requireRecord(runtimeConfig?.identity, '$.runtimeConfig.identity');
|
||||||
const roomRecord = requireRecord(room, '$.room');
|
const roomRecord = requireRecord(room, '$.room');
|
||||||
const agentid = identity.agentid;
|
|
||||||
const gameid = identity.gameid;
|
|
||||||
if (typeof agentid !== 'string' && typeof agentid !== 'number') {
|
|
||||||
throw new TypeError(`$.runtimeConfig.identity.agentid: expected string or number, received ${typeof agentid}`);
|
|
||||||
}
|
|
||||||
if (typeof gameid !== 'string' && typeof gameid !== 'number') {
|
|
||||||
throw new TypeError(`$.runtimeConfig.identity.gameid: expected string or number, received ${typeof gameid}`);
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
agentid,
|
agentid: requireStringOrFiniteNumber(
|
||||||
|
identity.agentid,
|
||||||
|
'$.runtimeConfig.identity.agentid',
|
||||||
|
),
|
||||||
playerid: requireInteger(roomRecord.playerid, '$.room.playerid'),
|
playerid: requireInteger(roomRecord.playerid, '$.room.playerid'),
|
||||||
gameid,
|
gameid: requireStringOrFiniteNumber(identity.gameid, '$.runtimeConfig.identity.gameid'),
|
||||||
roomcode: requireString(roomRecord.roomcode, '$.room.roomcode'),
|
roomcode: requireString(roomRecord.roomcode, '$.room.roomcode'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -124,8 +122,27 @@ export function buildJoinRoomRequest(
|
|||||||
location: environmentRecord.location,
|
location: environmentRecord.location,
|
||||||
ip: requireString(environmentRecord.ip, '$.environment.ip'),
|
ip: requireString(environmentRecord.ip, '$.environment.ip'),
|
||||||
};
|
};
|
||||||
if (hasOwn(environmentRecord, 'vipMatch')) data.vipMatch = environmentRecord.vipMatch;
|
const vipMatch = optionalField(
|
||||||
if (hasOwn(environmentRecord, 'match_id')) data.match_id = environmentRecord.match_id;
|
environmentRecord,
|
||||||
|
'vipMatch',
|
||||||
|
'$.environment',
|
||||||
|
(value, path): 1 => {
|
||||||
|
const parsed = requireNumber(value, path);
|
||||||
|
if (parsed !== 1) {
|
||||||
|
throw new RangeError(`${path}: expected number 1, received number`);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (vipMatch !== undefined) data.vipMatch = vipMatch;
|
||||||
|
|
||||||
|
const matchId = optionalField(
|
||||||
|
environmentRecord,
|
||||||
|
'match_id',
|
||||||
|
'$.environment',
|
||||||
|
requireStringOrFiniteNumber,
|
||||||
|
);
|
||||||
|
if (matchId !== undefined) data.match_id = matchId;
|
||||||
return buildFirstSliceEnvelope('self_join_room', data as JoinRoomPayload);
|
return buildFirstSliceEnvelope('self_join_room', data as JoinRoomPayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ export function requireString(value: unknown, path: string): string {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function requireBoolean(value: unknown, path: string): boolean {
|
||||||
|
if (typeof value !== 'boolean') return invalid(path, 'boolean', value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
export function requireNumber(value: unknown, path: string): number {
|
export function requireNumber(value: unknown, path: string): number {
|
||||||
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
||||||
return invalid(path, 'finite number', value);
|
return invalid(path, 'finite number', value);
|
||||||
@@ -39,6 +44,14 @@ export function requireInteger(value: unknown, path: string): number {
|
|||||||
return number;
|
return number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function requireStringOrFiniteNumber(value: unknown, path: string): string | number {
|
||||||
|
if (typeof value === 'string') return value;
|
||||||
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
||||||
|
return invalid(path, 'string or finite number', value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
export function requireArray(value: unknown, path: string): readonly unknown[] {
|
export function requireArray(value: unknown, path: string): readonly unknown[] {
|
||||||
if (!Array.isArray(value)) return invalid(path, 'array', value);
|
if (!Array.isArray(value)) return invalid(path, 'array', value);
|
||||||
return value;
|
return value;
|
||||||
|
|||||||
@@ -97,12 +97,15 @@ test('buildLoginRequest 只在显式复用模式且缓存 ID 有效时加入 pla
|
|||||||
});
|
});
|
||||||
assert.equal(enabled.data.playerid, 430511);
|
assert.equal(enabled.data.playerid, 430511);
|
||||||
|
|
||||||
for (const cachedPlayerId of [-1, 1.5, '430511', null]) {
|
for (const cachedPlayerId of [-1, 0, 1.5, Number.NaN, Infinity, '430511', null, undefined]) {
|
||||||
const invalid = buildLoginRequest(runtimeConfig, account, {
|
const type = cachedPlayerId === null ? 'null' : typeof cachedPlayerId;
|
||||||
...device,
|
assert.throws(
|
||||||
loginPlayerId: { enabled: true, cachedPlayerId },
|
() => buildLoginRequest(runtimeConfig, account, {
|
||||||
});
|
...device,
|
||||||
assert.equal(owns(invalid.data, 'playerid'), false);
|
loginPlayerId: { enabled: true, cachedPlayerId },
|
||||||
|
}),
|
||||||
|
new RegExp(`\\$\\.device\\.loginPlayerId\\.cachedPlayerId.*${type}`),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -235,6 +238,20 @@ test('房间请求 builder 使用唯一 route,并仅发协议定义字段', ()
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('房间请求拒绝 agentid/gameid 的非有限数值并报告来源路径', () => {
|
||||||
|
for (const field of ['agentid', 'gameid'] as const) {
|
||||||
|
for (const value of [Number.NaN, Infinity, -Infinity]) {
|
||||||
|
assert.throws(
|
||||||
|
() => buildPrepareRequest(
|
||||||
|
{ identity: { ...runtimeConfig.identity, [field]: value } },
|
||||||
|
{ playerid: 430511, roomcode: '100001' },
|
||||||
|
),
|
||||||
|
new RegExp(`\\$\\.runtimeConfig\\.identity\\.${field}.*number`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('buildJoinRoomRequest 不凭空补 vipMatch 或 match_id', () => {
|
test('buildJoinRoomRequest 不凭空补 vipMatch 或 match_id', () => {
|
||||||
const request = buildJoinRoomRequest(
|
const request = buildJoinRoomRequest(
|
||||||
runtimeConfig,
|
runtimeConfig,
|
||||||
@@ -245,6 +262,40 @@ test('buildJoinRoomRequest 不凭空补 vipMatch 或 match_id', () => {
|
|||||||
assert.equal(owns(request.data, 'match_id'), false);
|
assert.equal(owns(request.data, 'match_id'), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('buildJoinRoomRequest 拒绝非法或 undefined vipMatch', () => {
|
||||||
|
for (const vipMatch of [undefined, 0, 2, '1', Number.NaN, Infinity]) {
|
||||||
|
const type = typeof vipMatch;
|
||||||
|
assert.throws(
|
||||||
|
() => buildJoinRoomRequest(
|
||||||
|
runtimeConfig,
|
||||||
|
{ playerid: 430511, roomcode: '100001' },
|
||||||
|
{ location: null, ip: '127.0.0.1', vipMatch } as never,
|
||||||
|
),
|
||||||
|
new RegExp(`\\$\\.environment\\.vipMatch.*${type}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildJoinRoomRequest 拒绝非法、undefined 或非有限 match_id', () => {
|
||||||
|
for (const match_id of [undefined, null, false, {}, Number.NaN, Infinity]) {
|
||||||
|
const type = match_id === null ? 'null' : Array.isArray(match_id) ? 'array' : typeof match_id;
|
||||||
|
assert.throws(
|
||||||
|
() => buildJoinRoomRequest(
|
||||||
|
runtimeConfig,
|
||||||
|
{ playerid: 430511, roomcode: '100001' },
|
||||||
|
{ location: null, ip: '127.0.0.1', match_id } as never,
|
||||||
|
),
|
||||||
|
new RegExp(`\\$\\.environment\\.match_id.*${type}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.equal(buildJoinRoomRequest(
|
||||||
|
runtimeConfig,
|
||||||
|
{ playerid: 430511, roomcode: '100001' },
|
||||||
|
{ location: null, ip: '127.0.0.1', match_id: 7 },
|
||||||
|
).data.match_id, 7);
|
||||||
|
});
|
||||||
|
|
||||||
test('座位事件 parser 校验实际消费字段并保留 raw', () => {
|
test('座位事件 parser 校验实际消费字段并保留 raw', () => {
|
||||||
const prepare = { seat: 1, deskwar: 1, extension: 'kept' };
|
const prepare = { seat: 1, deskwar: 1, extension: 'kept' };
|
||||||
assert.equal(parsePlayerPreparePayload(prepare).raw, prepare);
|
assert.equal(parsePlayerPreparePayload(prepare).raw, prepare);
|
||||||
|
|||||||
Reference in New Issue
Block a user