diff --git a/src/server/data_gateway/auth.js b/src/server/data_gateway/auth.js index 9c208b1..4b99431 100644 --- a/src/server/data_gateway/auth.js +++ b/src/server/data_gateway/auth.js @@ -1,6 +1,9 @@ // ============================================================ -// data_gateway/auth.js - 对外 API 鉴权模块 -// 职责:校验 X-API-Id + X-API-Key,匹配 skill config.json 中的哈希 +// data_gateway/auth.js - 数据网关鉴权与内网守卫模块 +// 职责: +// - 校验 X-API-Id + X-API-Key +// - 校验内部读取 token +// - 统一提供 localOnly / internalOnly 语义 // ============================================================ const crypto = require('crypto') const fs = require('fs') @@ -45,6 +48,34 @@ const listEnabledSkills = () => { 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() @@ -82,4 +113,26 @@ const verifyInternalToken = (req) => { return { ok: true, skill: cfg } } -module.exports = { loadSkillConfig, refreshCache, listEnabledSkills, verifyApiKey, verifyInternalToken } \ No newline at end of file +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 +} diff --git a/src/server/data_gateway/index.js b/src/server/data_gateway/index.js index 0b64073..cc63987 100644 --- a/src/server/data_gateway/index.js +++ b/src/server/data_gateway/index.js @@ -2,10 +2,10 @@ // data_gateway/index.js - 对外 API 数据网关主入口 // 暴露两个路由: // POST /api/v1/ingest/:skillId 外部数据写入(公网,需 API Key) -// GET /api/v1/data/:skillId/query 内部数据读取(仅 localhost,需 Internal Token) +// GET /api/v1/data/:skillId/query 内部数据读取(localhost 或 Internal Token) // ============================================================ const { logJSON } = require('../logger') -const { verifyApiKey, verifyInternalToken, listEnabledSkills } = require('./auth') +const { verifyApiKey, listEnabledSkills, requireLocalOnly, requireInternalOnly } = require('./auth') const { insert, upsertLatest, queryLatest, queryList, queryByTimeRange, count } = require('./store') const { loadSkillConfig } = require('./auth') const { bindRoutes: bindExternalStorageRoutes } = require('./skills/external_storage') @@ -165,18 +165,15 @@ const bindRoutes = (app) => { }) // ============================================================ - // 内部读取接口(仅 localhost) + // 内部读取接口(本机直连,或带 Internal Token 的受保护读取) // GET /api/v1/data/:skillId/query?mode=latest|list&limit=100&offset=0 // Headers: X-Internal-Token, X-Skill-Id // ============================================================ - app.get('/api/v1/data/:skillId/query', (req, res) => { + app.get('/api/v1/data/:skillId/query', requireInternalOnly, (req, res) => { setNoCache(res) const skillId = String(req.params.skillId || '').trim() if (!skillId) return res.status(400).json({ ok: false, error: 'missing skill id' }) - const auth = verifyInternalToken(req) - if (!auth.ok) return res.status(401).json({ ok: false, error: auth.error }) - const mode = String(req.query.mode || 'latest').trim() const limit = parseInt(req.query.limit, 10) || 100 const offset = parseInt(req.query.offset, 10) || 0 @@ -208,7 +205,7 @@ const bindRoutes = (app) => { // 管理接口:列出所有已启用的 skill(仅 localhost) // GET /api/v1/data/skills // ============================================================ - app.get('/api/v1/data/skills', (req, res) => { + app.get('/api/v1/data/skills', requireLocalOnly, (req, res) => { setNoCache(res) const skills = listEnabledSkills() const result = skills.map(s => { @@ -227,4 +224,4 @@ const bindRoutes = (app) => { app.use(errorHandler) } -module.exports = { bindRoutes, listEnabledSkills, loadSkillConfig } \ No newline at end of file +module.exports = { bindRoutes, listEnabledSkills, loadSkillConfig }