// ============================================================ // data_gateway/auth.js - 数据网关鉴权与内网守卫模块 // 职责: // - 校验 X-API-Id + X-API-Key // - 校验内部读取 token // - 统一提供 localOnly / internalOnly 语义 // ============================================================ const crypto = require('crypto') const fs = require('fs') const path = require('path') const SKILLS_DIR = path.join(__dirname, 'skills') // 缓存已加载的 skill 配置 const skillConfigCache = new Map() const loadSkillConfig = (skillId) => { if (skillConfigCache.has(skillId)) return skillConfigCache.get(skillId) const cfgPath = path.join(SKILLS_DIR, skillId, 'config.json') if (!fs.existsSync(cfgPath)) return null try { const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8')) skillConfigCache.set(skillId, cfg) return cfg } catch { return null } } // 刷新缓存(用于配置热更新) const refreshCache = (skillId) => { skillConfigCache.delete(skillId) return loadSkillConfig(skillId) } // 枚举所有已启用的 skill const listEnabledSkills = () => { const result = [] if (!fs.existsSync(SKILLS_DIR)) return result const dirs = fs.readdirSync(SKILLS_DIR, { withFileTypes: true }) for (const d of dirs) { if (!d.isDirectory()) continue const cfg = loadSkillConfig(d.name) if (cfg && cfg.enabled !== false) { result.push({ id: cfg.id, name: cfg.name }) } } return result } const normalizeIp = (value) => { const raw = String(value || '').trim() if (!raw) return '' if (raw.startsWith('::ffff:')) return raw.slice(7) return raw } const getRequestIp = (req) => { const forwarded = String(req.headers['x-forwarded-for'] || '') .split(',') .map(item => normalizeIp(item)) .find(Boolean) if (forwarded) return forwarded return normalizeIp( req.ip || req.socket?.remoteAddress || req.connection?.remoteAddress || req.connection?.socket?.remoteAddress ) } const isLoopbackIp = (ip) => { const normalized = normalizeIp(ip) return normalized === '127.0.0.1' || normalized === '::1' } const isLocalRequest = (req) => isLoopbackIp(getRequestIp(req)) // 校验外部 API 请求 const verifyApiKey = (req) => { const apiId = String(req.headers['x-api-id'] || '').trim() const apiKey = String(req.headers['x-api-key'] || '').trim() if (!apiId || !apiKey) return { ok: false, error: 'missing api id or key' } const cfg = loadSkillConfig(apiId) if (!cfg) return { ok: false, error: 'skill not found' } if (cfg.enabled === false) return { ok: false, error: 'skill disabled' } const expectedHash = String(cfg.api_key_hash || '') if (!expectedHash) return { ok: false, error: 'skill not configured' } const actualHash = 'sha256:' + crypto.createHash('sha256').update(apiKey).digest('hex') if (actualHash !== expectedHash) return { ok: false, error: 'invalid api key' } return { ok: true, skill: cfg } } // 校验内部 Tool 读取请求 const verifyInternalToken = (req) => { const token = String(req.headers['x-internal-token'] || '').trim() const skillId = String(req.headers['x-skill-id'] || req.params?.skillId || '').trim() if (!token || !skillId) return { ok: false, error: 'missing token or skill id' } const cfg = loadSkillConfig(skillId) if (!cfg) return { ok: false, error: 'skill not found' } const expectedHash = String(cfg.read_token_hash || '') if (!expectedHash) return { ok: false, error: 'skill not configured for internal read' } const actualHash = 'sha256:' + crypto.createHash('sha256').update(token).digest('hex') if (actualHash !== expectedHash) return { ok: false, error: 'invalid internal token' } return { ok: true, skill: cfg } } const requireLocalOnly = (req, res, next) => { if (isLocalRequest(req)) return next() return res.status(403).json({ ok: false, error: 'local_only' }) } const requireInternalOnly = (req, res, next) => { if (isLocalRequest(req)) return next() const auth = verifyInternalToken(req) if (auth.ok) return next() return res.status(401).json({ ok: false, error: auth.error }) } module.exports = { loadSkillConfig, refreshCache, listEnabledSkills, verifyApiKey, verifyInternalToken, getRequestIp, isLocalRequest, requireLocalOnly, requireInternalOnly }