776 lines
20 KiB
TypeScript
776 lines
20 KiB
TypeScript
|
|
/**
|
|||
|
|
* 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`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|