UNPKG

unicorn-rpa

Version:

🦄 Unicorn RPA - 强大的跨域iframe自动化控制工具

426 lines (397 loc) 14.3 kB
/** * Unicorn RPA 安全策略模块 * * 提供来源域名白名单验证和消息频率限制功能,确保只有来自可信域名的消息才能执行自动化操作, * 并防止消息发送过于频繁。 * 支持精确匹配和通配符匹配两种模式。 * * @module security * @version 1.0.20 */ const CryptoJS = require('crypto-js'); /** * 安全配置对象 * * 定义允许的来源域名白名单和消息频率限制配置。 * 只有来自这些域名的消息才会被接受,且每个来源每10秒只能发送一次消息。 * 支持通配符匹配,例如 'https://*.example.com' 可以匹配所有子域名。 * * @type {Object} * @property {string[]} ALLOWED_ORIGINS - 允许的来源域名列表 * @property {Object} RATE_LIMIT - 频率限制配置 * @property {number} RATE_LIMIT.INTERVAL - 消息发送间隔(毫秒),默认10秒 */ const SECURITY_CONFIG = { ALLOWED_ORIGINS: [ 'http://localhost:5000', // Vite 默认端口 'http://localhost:5173', // Vite 默认端口 'http://localhost:8080', // 常用开发端口 'http://127.0.0.1:5000', // 本地IP地址 'http://127.0.0.1:5173', // 本地IP地址 'http://127.0.0.1:8080' // 本地IP地址 ], RATE_LIMIT: { INTERVAL: 10000 // 10秒间隔(毫秒) } }; /** * 消息发送记录存储 * * 用于记录每个来源域名的最后发送时间,实现频率限制功能。 * 使用 Map 结构存储,key 为来源域名,value 为最后发送时间戳。 * * @type {Map<string, number>} */ const messageHistory = new Map(); /** * 支持的动作类型列表 * * 定义所有支持的动作类型,用于验证接收到的动作是否可以被处理。 * 如果包含不支持的动作类型,整个请求将被拒绝。 * * @type {string[]} */ const SUPPORTED_ACTION_TYPES = [ 'fill', // 文本输入 'select', // 下拉选择 'click', // 点击 'doubleClick', // 双击 'rightClick', // 右键点击 'hover', // 悬停 'scroll', // 滚动 'screenshot', // 截图 'waitFor', // 等待元素出现 'wait', // 等待固定时长 'treeSelect', // 树形选择 'checkAllCheckboxes', // 批量勾选复选框 'getText', // 获取文本 'getValue', // 获取值 'timeRange', // 时间范围选择 'antCalendar', // ant-calendar 时间范围选择 'interfaceReturn', // 接口返回拦截 'waitForNext' // 智能等待下一个元素 ]; /** * 清理过期的消息记录 * * 定期清理超过限制时间的消息记录,防止内存泄漏。 * 清理间隔为频率限制时间的2倍。 */ function cleanupExpiredRecords() { const now = Date.now(); const cutoffTime = now - SECURITY_CONFIG.RATE_LIMIT.INTERVAL; const entries = messageHistory.entries(); for (let i = 0; i < entries.length; i++) { const entry = entries[i]; const origin = entry[0]; const lastTime = entry[1]; if (lastTime < cutoffTime) { messageHistory.delete(origin); } } } // 启动定期清理任务 setInterval(cleanupExpiredRecords, SECURITY_CONFIG.RATE_LIMIT.INTERVAL * 2); /** * 验证动作类型是否支持 * * 检查动作数组中的所有动作类型是否都在支持列表中。 * 如果有一个动作类型不支持,整个请求将被拒绝。 * * @param {Array} actions - 动作数组 * @returns {Object} 验证结果 * @returns {boolean} returns.valid - 验证是否通过 * @returns {string} [returns.error] - 验证失败时的错误信息 * @returns {string} [returns.unsupportedType] - 不支持的动作类型 * * @example * // 所有动作类型都支持 * validateActionTypes([ * { type: 'click', selector: '.btn' }, * { type: 'fill', selector: '.input', value: 'test' } * ]) // { valid: true } * * // 包含不支持的动作类型 * validateActionTypes([ * { type: 'click', selector: '.btn' }, * { type: 'unsupported', selector: '.element' } * ]) // { valid: false, error: '不支持的动作类型', unsupportedType: 'unsupported' } */ function validateActionTypes(actions) { // 检查动作数组是否存在且为数组 if (!Array.isArray(actions)) { return { valid: false, error: 'actions必须是数组' }; } // 检查每个动作的类型 for (let i = 0; i < actions.length; i++) { const action = actions[i]; // 检查动作对象是否有效 if (!action || typeof action !== 'object') { return { valid: false, error: `第${i + 1}个动作无效` }; } // 检查动作类型是否存在 if (!action.type || typeof action.type !== 'string') { return { valid: false, error: `第${i + 1}个动作缺少type属性` }; } // 检查动作类型是否支持 if (!SUPPORTED_ACTION_TYPES.includes(action.type)) { return { valid: false, error: '不支持的动作类型', unsupportedType: action.type }; } } return { valid: true }; } /** * 检查消息发送频率是否超过限制 * * 验证指定来源是否在允许的时间间隔内发送过消息。 * 如果距离上次发送时间不足配置的间隔,则拒绝当前消息。 * * @param {string} origin - 消息来源域名 * @returns {boolean} 如果允许发送返回 true,否则返回 false * * @example * // 首次发送 * checkRateLimit('http://localhost:3000') // true * * // 5秒后再次发送(间隔不足10秒) * checkRateLimit('http://localhost:3000') // false * * // 15秒后再次发送(间隔超过10秒) * checkRateLimit('http://localhost:3000') // true */ function checkRateLimit(origin) { const now = Date.now(); const lastTime = messageHistory.get(origin); // 如果没有发送记录或距离上次发送已超过限制时间 if (!lastTime || (now - lastTime) >= SECURITY_CONFIG.RATE_LIMIT.INTERVAL) { // 更新发送时间并允许发送 messageHistory.set(origin, now); return true; } // 发送过于频繁,拒绝发送 const remainingTime = SECURITY_CONFIG.RATE_LIMIT.INTERVAL - (now - lastTime); console.warn(`[安全警告] 消息发送过于频繁: ${origin},请等待 ${Math.ceil(remainingTime / 1000)} 秒后重试`); return false; } /** * 验证消息来源是否在白名单中 * * 检查 postMessage 事件的来源域名是否在允许的白名单中。 * 支持精确匹配和通配符匹配(以 * 结尾的域名)。 * * @param {MessageEvent} event - postMessage 事件对象 * @param {string} event.origin - 消息来源的完整域名(协议+域名+端口) * @returns {boolean} 如果来源在白名单中返回 true,否则返回 false * * @example * // 精确匹配 * validateOrigin({ origin: 'http://localhost:3000' }) // true * * // 通配符匹配 * validateOrigin({ origin: 'https://app.example.com' }) // 如果配置了 'https://*.example.com' * * // 不匹配 * validateOrigin({ origin: 'http://malicious.com' }) // false */ function validateOrigin(event) { // 检查来源是否在白名单中,支持精确匹配和通配符匹配 let isOriginAllowed = false; for (let i = 0; i < SECURITY_CONFIG.ALLOWED_ORIGINS.length; i++) { const allowedOrigin = SECURITY_CONFIG.ALLOWED_ORIGINS[i]; if (event.origin === allowedOrigin || // 精确匹配 (allowedOrigin.endsWith('*') && event.origin.startsWith(allowedOrigin.slice(0, -1)))) { // 通配符匹配 isOriginAllowed = true; break; } } // 如果来源不在白名单中,记录警告并返回 false if (!isOriginAllowed) { console.warn(`[安全警告] 拒绝来自未授权来源的消息: ${event.origin}`); return false; } return true; } /** * 生成标准化的安全响应对象 * * 创建统一格式的响应对象,包含成功状态、会话ID、时间戳等信息。 * 支持成功响应和错误响应两种模式。 * * @param {boolean} success - 操作是否成功 * @param {string} sessionId - 会话标识符,用于追踪请求 * @param {*} [data=null] - 成功时的响应数据,可选 * @param {string} [error=null] - 失败时的错误信息,可选 * @returns {Object} 标准化的响应对象 * * @example * // 成功响应 * createSecureResponse(true, 'session123', { result: 'success' }) * // 返回: { type: 'automationResult', success: true, sessionId: 'session123', ... } * * // 错误响应 * createSecureResponse(false, 'session123', null, '验证失败') * // 返回: { type: 'automationResult', success: false, sessionId: 'session123', error: '验证失败', ... } */ function createSecureResponse(success, sessionId, data = null, error = null) { const response = { type: 'automationResult', // 标识响应类型 success, // 操作结果 sessionId, // 会话ID timestamp: new Date().toISOString() // ISO格式的时间戳 }; // 成功时包含结果数据 if (data) { response.results = data; } // 失败时包含错误信息 if (error) { response.error = error; } return response; } // AUTH校验相关配置 const AUTH_AES_KEY = 'unicorn-rpa-2024-very-secret-key-1234'; const AUTH_HASH = 'cedf55c3046fc919047fcc7b4f4ada8b823195d5e97e1ebda0bff3c11d313f7a'; /** * 解密auth字段 * @param {string} encrypted - 加密后的auth字符串 * @returns {string} 解密后的明文 */ function decryptAuth(encrypted) { try { const bytes = CryptoJS.AES.decrypt(encrypted, AUTH_AES_KEY); return bytes.toString(CryptoJS.enc.Utf8); } catch (e) { return ''; } } /** * 校验auth字段 * @param {string} auth - 加密后的auth字符串 * @returns {boolean} 是否通过校验 */ function validateAuth(auth) { if (!auth) return false; const decrypted = decryptAuth(auth); if (!decrypted) return false; const hash = CryptoJS.SHA256(decrypted).toString(); return hash === AUTH_HASH; } /** * 执行完整的安全验证流程 * * 对接收到的 postMessage 事件进行安全验证,包括来源域名验证、频率限制检查和动作类型验证。 * 返回验证结果对象,包含验证状态和错误信息。 * * @param {MessageEvent} event - postMessage 事件对象 * @param {Array} [actions] - 动作数组,用于验证动作类型 * @returns {Object} 验证结果对象 * @returns {boolean} returns.valid - 验证是否通过 * @returns {string} [returns.error] - 验证失败时的错误信息 * @returns {string} [returns.unsupportedType] - 不支持的动作类型(如果有) * * @example * const result = performSecurityValidation(event, actions); * if (result.valid) { * // 验证通过,继续处理 * } else { * // 验证失败,处理错误 * console.error(result.error); * if (result.unsupportedType) { * console.error('不支持的动作类型:', result.unsupportedType); * } * } */ function performSecurityValidation(event, actions = null) { // 1. 先校验auth字段 const auth = event.data.auth; if (!validateAuth(auth)) { return { valid: false, error: 'auth校验失败' }; } // 2. 来源白名单 - 已关闭 // if (!validateOrigin(event)) { // return { valid: false, error: '来源验证失败' }; // } // 3. 频率限制 - 已关闭 // if (!checkRateLimit(event.origin)) { // return { valid: false, error: '消息发送过于频繁' }; // } // 4. 动作类型 if (actions) { const actionValidation = validateActionTypes(actions); if (!actionValidation.valid) { return { valid: false, error: actionValidation.error, unsupportedType: actionValidation.unsupportedType }; } } // 5. 所有验证通过 return { valid: true }; } /** * 获取安全统计信息 * * 返回当前的安全状态统计信息,包括活跃的来源数量、配置信息和支持的动作类型。 * * @returns {Object} 安全统计信息 * @returns {number} returns.activeOrigins - 当前活跃的来源数量 * @returns {number} returns.allowedOrigins - 允许的来源总数 * @returns {number} returns.rateLimitInterval - 频率限制间隔(秒) * @returns {Array} returns.supportedActionTypes - 支持的动作类型列表 * @returns {number} returns.supportedActionTypesCount - 支持的动作类型数量 * * @example * const stats = getSecurityStats(); * console.log(`活跃来源: ${stats.activeOrigins}/${stats.allowedOrigins}`); * console.log(`频率限制: ${stats.rateLimitInterval}秒`); * console.log(`支持的动作类型: ${stats.supportedActionTypesCount}种`); */ function getSecurityStats() { return { activeOrigins: messageHistory.size, allowedOrigins: SECURITY_CONFIG.ALLOWED_ORIGINS.length, rateLimitInterval: SECURITY_CONFIG.RATE_LIMIT.INTERVAL / 1000, supportedActionTypes: SUPPORTED_ACTION_TYPES, supportedActionTypesCount: SUPPORTED_ACTION_TYPES.length }; } /** * 模块导出 * * 导出安全模块的公共接口,供其他模块使用。 * * @exports {Object} 安全模块的公共接口 * @exports {Function} validateOrigin - 来源验证函数 * @exports {Function} checkRateLimit - 频率限制检查函数 * @exports {Function} validateActionTypes - 动作类型验证函数 * @exports {Function} performSecurityValidation - 安全验证流程函数 * @exports {Function} createSecureResponse - 响应生成函数 * @exports {Function} getSecurityStats - 安全统计函数 * @exports {Object} SECURITY_CONFIG - 安全配置对象 * @exports {Array} SUPPORTED_ACTION_TYPES - 支持的动作类型列表 */ module.exports = { validateOrigin, // 来源验证函数 checkRateLimit, // 频率限制检查函数 validateActionTypes, // 动作类型验证函数 performSecurityValidation, // 安全验证流程函数 createSecureResponse, // 响应生成函数 getSecurityStats, // 安全统计函数 SECURITY_CONFIG, // 安全配置对象 SUPPORTED_ACTION_TYPES // 支持的动作类型列表 };