Files
esengine/packages/network-server/src/validation/RpcValidator.ts
2025-08-12 09:39:07 +08:00

776 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* RPC 验证器
*
* 专门用于验证 RPC 调用的参数、权限、频率等
*/
import { NetworkValue, RpcMetadata } from '@esengine/ecs-framework-network-shared';
import { ClientConnection } from '../core/ClientConnection';
import { ValidationResult } from './MessageValidator';
/**
* RPC 验证配置
*/
export interface RpcValidationConfig {
/** 最大参数数量 */
maxParameterCount?: number;
/** 单个参数最大大小(字节) */
maxParameterSize?: number;
/** 允许的参数类型 */
allowedParameterTypes?: string[];
/** 方法名黑名单 */
blacklistedMethods?: string[];
/** 方法名白名单 */
whitelistedMethods?: string[];
/** 是否启用参数类型检查 */
enableTypeCheck?: boolean;
/** 是否启用参数内容过滤 */
enableContentFilter?: boolean;
}
/**
* RPC 调用上下文
*/
export interface RpcCallContext {
/** 客户端连接 */
client: ClientConnection;
/** 网络对象ID */
networkId: number;
/** 组件类型 */
componentType: string;
/** 方法名 */
methodName: string;
/** 参数列表 */
parameters: NetworkValue[];
/** RPC 元数据 */
metadata: RpcMetadata;
/** RPC 类型 */
rpcType: 'client-rpc' | 'server-rpc';
}
/**
* 参数类型定义
*/
export interface ParameterTypeDefinition {
/** 参数名 */
name: string;
/** 参数类型 */
type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'any';
/** 是否必需 */
required?: boolean;
/** 最小值/长度 */
min?: number;
/** 最大值/长度 */
max?: number;
/** 允许的值列表 */
allowedValues?: NetworkValue[];
/** 正则表达式(仅用于字符串) */
pattern?: RegExp;
/** 自定义验证函数 */
customValidator?: (value: NetworkValue) => ValidationResult;
}
/**
* 方法签名定义
*/
export interface MethodSignature {
/** 方法名 */
methodName: string;
/** 组件类型 */
componentType: string;
/** 参数定义 */
parameters: ParameterTypeDefinition[];
/** 返回值类型 */
returnType?: string;
/** 是否需要权限验证 */
requiresAuth?: boolean;
/** 所需权限 */
requiredPermissions?: string[];
/** 频率限制(调用/分钟) */
rateLimit?: number;
/** 自定义验证函数 */
customValidator?: (context: RpcCallContext) => ValidationResult;
}
/**
* RPC 频率跟踪
*/
interface RpcRateTracker {
/** 客户端ID */
clientId: string;
/** 方法调用计数 */
methodCalls: Map<string, { count: number; resetTime: Date }>;
/** 最后更新时间 */
lastUpdate: Date;
}
/**
* RPC 验证器
*/
export class RpcValidator {
private config: RpcValidationConfig;
private methodSignatures = new Map<string, MethodSignature>();
private rateTrackers = new Map<string, RpcRateTracker>();
private cleanupTimer: NodeJS.Timeout | null = null;
constructor(config: RpcValidationConfig = {}) {
this.config = {
maxParameterCount: 10,
maxParameterSize: 65536, // 64KB
allowedParameterTypes: ['string', 'number', 'boolean', 'object', 'array'],
blacklistedMethods: [],
whitelistedMethods: [],
enableTypeCheck: true,
enableContentFilter: true,
...config
};
this.initialize();
}
/**
* 验证 RPC 调用
*/
validateRpcCall(context: RpcCallContext): ValidationResult {
try {
// 基本验证
const basicResult = this.validateBasicRpcCall(context);
if (!basicResult.valid) {
return basicResult;
}
// 方法名验证
const methodResult = this.validateMethodName(context);
if (!methodResult.valid) {
return methodResult;
}
// 权限验证
const permissionResult = this.validateRpcPermissions(context);
if (!permissionResult.valid) {
return permissionResult;
}
// 参数验证
const parameterResult = this.validateParameters(context);
if (!parameterResult.valid) {
return parameterResult;
}
// 频率限制验证
const rateResult = this.validateRateLimit(context);
if (!rateResult.valid) {
return rateResult;
}
// 签名验证(如果有定义)
const signatureResult = this.validateMethodSignature(context);
if (!signatureResult.valid) {
return signatureResult;
}
return { valid: true };
} catch (error) {
return {
valid: false,
error: (error as Error).message,
errorCode: 'RPC_VALIDATION_ERROR'
};
}
}
/**
* 注册方法签名
*/
registerMethodSignature(signature: MethodSignature): void {
const key = `${signature.componentType}.${signature.methodName}`;
this.methodSignatures.set(key, signature);
}
/**
* 移除方法签名
*/
removeMethodSignature(componentType: string, methodName: string): boolean {
const key = `${componentType}.${methodName}`;
return this.methodSignatures.delete(key);
}
/**
* 获取方法签名
*/
getMethodSignature(componentType: string, methodName: string): MethodSignature | undefined {
const key = `${componentType}.${methodName}`;
return this.methodSignatures.get(key);
}
/**
* 添加方法到黑名单
*/
addToBlacklist(methodName: string): void {
if (!this.config.blacklistedMethods!.includes(methodName)) {
this.config.blacklistedMethods!.push(methodName);
}
}
/**
* 从黑名单移除方法
*/
removeFromBlacklist(methodName: string): boolean {
const index = this.config.blacklistedMethods!.indexOf(methodName);
if (index !== -1) {
this.config.blacklistedMethods!.splice(index, 1);
return true;
}
return false;
}
/**
* 添加方法到白名单
*/
addToWhitelist(methodName: string): void {
if (!this.config.whitelistedMethods!.includes(methodName)) {
this.config.whitelistedMethods!.push(methodName);
}
}
/**
* 获取客户端的 RPC 统计
*/
getClientRpcStats(clientId: string): {
totalCalls: number;
methodStats: Record<string, number>;
} {
const tracker = this.rateTrackers.get(clientId);
if (!tracker) {
return { totalCalls: 0, methodStats: {} };
}
let totalCalls = 0;
const methodStats: Record<string, number> = {};
for (const [method, data] of tracker.methodCalls) {
totalCalls += data.count;
methodStats[method] = data.count;
}
return { totalCalls, methodStats };
}
/**
* 重置客户端的频率限制
*/
resetClientRateLimit(clientId: string): boolean {
const tracker = this.rateTrackers.get(clientId);
if (!tracker) {
return false;
}
tracker.methodCalls.clear();
tracker.lastUpdate = new Date();
return true;
}
/**
* 销毁验证器
*/
destroy(): void {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
this.cleanupTimer = null;
}
this.methodSignatures.clear();
this.rateTrackers.clear();
}
/**
* 初始化验证器
*/
private initialize(): void {
// 启动清理定时器每5分钟清理一次
this.cleanupTimer = setInterval(() => {
this.cleanupRateTrackers();
}, 5 * 60 * 1000);
}
/**
* 基本 RPC 调用验证
*/
private validateBasicRpcCall(context: RpcCallContext): ValidationResult {
// 网络对象ID验证
if (!Number.isInteger(context.networkId) || context.networkId <= 0) {
return {
valid: false,
error: 'Invalid network object ID',
errorCode: 'INVALID_NETWORK_ID'
};
}
// 组件类型验证
if (!context.componentType || typeof context.componentType !== 'string') {
return {
valid: false,
error: 'Invalid component type',
errorCode: 'INVALID_COMPONENT_TYPE'
};
}
// 方法名验证
if (!context.methodName || typeof context.methodName !== 'string') {
return {
valid: false,
error: 'Invalid method name',
errorCode: 'INVALID_METHOD_NAME'
};
}
// 参数数组验证
if (!Array.isArray(context.parameters)) {
return {
valid: false,
error: 'Parameters must be an array',
errorCode: 'INVALID_PARAMETERS_FORMAT'
};
}
// 参数数量检查
if (context.parameters.length > this.config.maxParameterCount!) {
return {
valid: false,
error: `Too many parameters: ${context.parameters.length} (max: ${this.config.maxParameterCount})`,
errorCode: 'TOO_MANY_PARAMETERS'
};
}
return { valid: true };
}
/**
* 方法名验证
*/
private validateMethodName(context: RpcCallContext): ValidationResult {
const methodName = context.methodName;
// 黑名单检查
if (this.config.blacklistedMethods!.length > 0) {
if (this.config.blacklistedMethods!.includes(methodName)) {
return {
valid: false,
error: `Method '${methodName}' is blacklisted`,
errorCode: 'METHOD_BLACKLISTED'
};
}
}
// 白名单检查
if (this.config.whitelistedMethods!.length > 0) {
if (!this.config.whitelistedMethods!.includes(methodName)) {
return {
valid: false,
error: `Method '${methodName}' is not whitelisted`,
errorCode: 'METHOD_NOT_WHITELISTED'
};
}
}
// 危险方法名检查
const dangerousPatterns = [
/^__/, // 私有方法
/constructor/i,
/prototype/i,
/eval/i,
/function/i
];
for (const pattern of dangerousPatterns) {
if (pattern.test(methodName)) {
return {
valid: false,
error: `Potentially dangerous method name: '${methodName}'`,
errorCode: 'DANGEROUS_METHOD_NAME'
};
}
}
return { valid: true };
}
/**
* RPC 权限验证
*/
private validateRpcPermissions(context: RpcCallContext): ValidationResult {
// 基本 RPC 权限检查
if (!context.client.hasPermission('canSendRpc')) {
return {
valid: false,
error: 'Client does not have RPC permission',
errorCode: 'RPC_PERMISSION_DENIED'
};
}
// ServerRpc 特殊权限检查
if (context.rpcType === 'server-rpc') {
if (context.metadata.requiresAuth && !context.client.isAuthenticated) {
return {
valid: false,
error: 'Authentication required for this RPC',
errorCode: 'AUTHENTICATION_REQUIRED'
};
}
}
// 检查方法签名中的权限要求
const signature = this.getMethodSignature(context.componentType, context.methodName);
if (signature && signature.requiredPermissions) {
for (const permission of signature.requiredPermissions) {
if (!context.client.hasCustomPermission(permission)) {
return {
valid: false,
error: `Required permission '${permission}' not found`,
errorCode: 'INSUFFICIENT_PERMISSIONS'
};
}
}
}
return { valid: true };
}
/**
* 参数验证
*/
private validateParameters(context: RpcCallContext): ValidationResult {
// 参数大小检查
for (let i = 0; i < context.parameters.length; i++) {
const param = context.parameters[i];
try {
const serialized = JSON.stringify(param);
const size = new TextEncoder().encode(serialized).length;
if (size > this.config.maxParameterSize!) {
return {
valid: false,
error: `Parameter ${i} is too large: ${size} bytes (max: ${this.config.maxParameterSize})`,
errorCode: 'PARAMETER_TOO_LARGE'
};
}
} catch (error) {
return {
valid: false,
error: `Parameter ${i} is not serializable`,
errorCode: 'PARAMETER_NOT_SERIALIZABLE'
};
}
}
// 参数类型检查
if (this.config.enableTypeCheck) {
const typeResult = this.validateParameterTypes(context);
if (!typeResult.valid) {
return typeResult;
}
}
// 参数内容过滤
if (this.config.enableContentFilter) {
const contentResult = this.validateParameterContent(context);
if (!contentResult.valid) {
return contentResult;
}
}
return { valid: true };
}
/**
* 参数类型验证
*/
private validateParameterTypes(context: RpcCallContext): ValidationResult {
for (let i = 0; i < context.parameters.length; i++) {
const param = context.parameters[i];
const paramType = this.getParameterType(param);
if (!this.config.allowedParameterTypes!.includes(paramType)) {
return {
valid: false,
error: `Parameter ${i} type '${paramType}' is not allowed`,
errorCode: 'INVALID_PARAMETER_TYPE'
};
}
}
return { valid: true };
}
/**
* 参数内容验证
*/
private validateParameterContent(context: RpcCallContext): ValidationResult {
for (let i = 0; i < context.parameters.length; i++) {
const param = context.parameters[i];
// 检查危险内容
if (typeof param === 'string') {
const dangerousPatterns = [
/<script/i,
/javascript:/i,
/eval\(/i,
/function\(/i,
/__proto__/i
];
for (const pattern of dangerousPatterns) {
if (pattern.test(param)) {
return {
valid: false,
error: `Parameter ${i} contains potentially dangerous content`,
errorCode: 'DANGEROUS_PARAMETER_CONTENT'
};
}
}
}
}
return { valid: true };
}
/**
* 频率限制验证
*/
private validateRateLimit(context: RpcCallContext): ValidationResult {
const signature = this.getMethodSignature(context.componentType, context.methodName);
if (!signature || !signature.rateLimit) {
return { valid: true }; // 没有频率限制
}
const clientId = context.client.id;
const methodKey = `${context.componentType}.${context.methodName}`;
let tracker = this.rateTrackers.get(clientId);
if (!tracker) {
tracker = {
clientId,
methodCalls: new Map(),
lastUpdate: new Date()
};
this.rateTrackers.set(clientId, tracker);
}
const now = new Date();
let methodData = tracker.methodCalls.get(methodKey);
if (!methodData) {
methodData = {
count: 1,
resetTime: new Date(now.getTime() + 60000) // 1分钟后重置
};
tracker.methodCalls.set(methodKey, methodData);
return { valid: true };
}
// 检查是否需要重置
if (now >= methodData.resetTime) {
methodData.count = 1;
methodData.resetTime = new Date(now.getTime() + 60000);
return { valid: true };
}
// 检查频率限制
if (methodData.count >= signature.rateLimit) {
return {
valid: false,
error: `Rate limit exceeded for method '${methodKey}': ${methodData.count}/${signature.rateLimit} per minute`,
errorCode: 'RATE_LIMIT_EXCEEDED'
};
}
methodData.count++;
tracker.lastUpdate = now;
return { valid: true };
}
/**
* 方法签名验证
*/
private validateMethodSignature(context: RpcCallContext): ValidationResult {
const signature = this.getMethodSignature(context.componentType, context.methodName);
if (!signature) {
return { valid: true }; // 没有定义签名,跳过验证
}
// 参数数量检查
const requiredParams = signature.parameters.filter(p => p.required !== false);
if (context.parameters.length < requiredParams.length) {
return {
valid: false,
error: `Not enough parameters: expected at least ${requiredParams.length}, got ${context.parameters.length}`,
errorCode: 'INSUFFICIENT_PARAMETERS'
};
}
if (context.parameters.length > signature.parameters.length) {
return {
valid: false,
error: `Too many parameters: expected at most ${signature.parameters.length}, got ${context.parameters.length}`,
errorCode: 'EXCESS_PARAMETERS'
};
}
// 参数类型和值验证
for (let i = 0; i < Math.min(context.parameters.length, signature.parameters.length); i++) {
const param = context.parameters[i];
const paramDef = signature.parameters[i];
const paramResult = this.validateParameterDefinition(param, paramDef, i);
if (!paramResult.valid) {
return paramResult;
}
}
// 自定义验证
if (signature.customValidator) {
const customResult = signature.customValidator(context);
if (!customResult.valid) {
return customResult;
}
}
return { valid: true };
}
/**
* 验证参数定义
*/
private validateParameterDefinition(
value: NetworkValue,
definition: ParameterTypeDefinition,
index: number
): ValidationResult {
// 类型检查
const actualType = this.getParameterType(value);
if (definition.type !== 'any' && actualType !== definition.type) {
return {
valid: false,
error: `Parameter ${index} type mismatch: expected '${definition.type}', got '${actualType}'`,
errorCode: 'PARAMETER_TYPE_MISMATCH'
};
}
// 值范围检查
if (typeof value === 'number' && (definition.min !== undefined || definition.max !== undefined)) {
if (definition.min !== undefined && value < definition.min) {
return {
valid: false,
error: `Parameter ${index} value ${value} is less than minimum ${definition.min}`,
errorCode: 'PARAMETER_BELOW_MINIMUM'
};
}
if (definition.max !== undefined && value > definition.max) {
return {
valid: false,
error: `Parameter ${index} value ${value} is greater than maximum ${definition.max}`,
errorCode: 'PARAMETER_ABOVE_MAXIMUM'
};
}
}
// 字符串长度检查
if (typeof value === 'string' && (definition.min !== undefined || definition.max !== undefined)) {
if (definition.min !== undefined && value.length < definition.min) {
return {
valid: false,
error: `Parameter ${index} string length ${value.length} is less than minimum ${definition.min}`,
errorCode: 'STRING_TOO_SHORT'
};
}
if (definition.max !== undefined && value.length > definition.max) {
return {
valid: false,
error: `Parameter ${index} string length ${value.length} is greater than maximum ${definition.max}`,
errorCode: 'STRING_TOO_LONG'
};
}
}
// 允许值检查
if (definition.allowedValues && definition.allowedValues.length > 0) {
if (!definition.allowedValues.includes(value)) {
return {
valid: false,
error: `Parameter ${index} value '${value}' is not in allowed values: ${definition.allowedValues.join(', ')}`,
errorCode: 'VALUE_NOT_ALLOWED'
};
}
}
// 正则表达式检查(字符串)
if (typeof value === 'string' && definition.pattern) {
if (!definition.pattern.test(value)) {
return {
valid: false,
error: `Parameter ${index} string '${value}' does not match required pattern`,
errorCode: 'PATTERN_MISMATCH'
};
}
}
// 自定义验证
if (definition.customValidator) {
const customResult = definition.customValidator(value);
if (!customResult.valid) {
return {
...customResult,
error: `Parameter ${index} validation failed: ${customResult.error}`
};
}
}
return { valid: true };
}
/**
* 获取参数类型
*/
private getParameterType(value: any): string {
if (value === null || value === undefined) {
return 'null';
}
if (Array.isArray(value)) {
return 'array';
}
return typeof value;
}
/**
* 清理过期的频率跟踪器
*/
private cleanupRateTrackers(): void {
const now = new Date();
const expireTime = 10 * 60 * 1000; // 10分钟
let cleanedCount = 0;
for (const [clientId, tracker] of this.rateTrackers.entries()) {
if (now.getTime() - tracker.lastUpdate.getTime() > expireTime) {
this.rateTrackers.delete(clientId);
cleanedCount++;
} else {
// 清理过期的方法调用记录
for (const [methodKey, methodData] of tracker.methodCalls.entries()) {
if (now >= methodData.resetTime) {
tracker.methodCalls.delete(methodKey);
}
}
}
}
if (cleanedCount > 0) {
console.log(`RPC validator cleanup: ${cleanedCount} rate trackers removed`);
}
}
}