/** * GET/POST для справочников из EvoBGP (ранее textDataRoutes + S3). */ const crypto = require('crypto'); const evobgpClient = require('../services/evobgpClient'); const { sendError, sendOk, checkIfNoneMatch } = require('../middleware/errorHandler'); const countOnlyCache = { map: new Map(), ttlMs: 10_000 }; function getCountOnlyCache(cacheKey) { const v = countOnlyCache.map.get(cacheKey); if (!v) return null; if (Date.now() > v.at + countOnlyCache.ttlMs) { countOnlyCache.map.delete(cacheKey); return null; } return v.value; } function setCountOnlyCache(cacheKey, value) { countOnlyCache.map.set(cacheKey, { value, at: Date.now() }); } function etagForJson(obj) { const s = JSON.stringify(obj); const h = crypto.createHash('sha256').update(Buffer.from(s, 'utf8')).digest('hex'); return `"${h}"`; } function filterPage(items, q, offset, limit) { const qstr = q ? String(q).toLowerCase() : ''; const filtered = !qstr ? items : items.filter((it) => JSON.stringify(it).toLowerCase().includes(qstr)); const total = filtered.length; if (limit > 0) { return { items: filtered.slice(offset, offset + limit), total }; } return { items: filtered, total }; } function createEvobgpListGET({ fetchItems, cachePrefix, validate }) { return async (req, res) => { const { q = '', offset, limit, countOnly, format } = req.query || {}; if (!evobgpClient.isConfigured()) { return sendError(res, 503, 'EvoBGP не настроен', 'E_EVOBGP_NOT_CONFIGURED'); } try { if (countOnly === 'true') { const cacheKey = `${cachePrefix}:count:${q}`; const cached = getCountOnlyCache(cacheKey); if (cached != null) { return res.json(format === 'std' ? { items: [], total: cached, meta: {} } : { total: cached }); } const all = await fetchItems(); const { total } = filterPage(all, q, 0, 0); setCountOnlyCache(cacheKey, total); return res.json(format === 'std' ? { items: [], total, meta: {} } : { total }); } if (Number(limit) > 0) { const all = await fetchItems(); if (!validate(all)) { return sendError(res, 500, 'Invalid data format', 'E_SCHEMA'); } const { items, total } = filterPage(all, q, Number(offset) || 0, Number(limit) || 0); return res.json(format === 'std' ? { items, total, meta: {} } : { items, total }); } const all = await fetchItems(); if (!validate(all)) { return sendError(res, 500, 'Invalid data format', 'E_SCHEMA'); } const etag = etagForJson(all); res.set('ETag', etag); res.set('Last-Modified', new Date().toUTCString()); res.set('Content-Length-Source', String(Buffer.byteLength(JSON.stringify(all), 'utf8'))); if (checkIfNoneMatch(req, res, etag)) return; if (format === 'std') { return res.json({ items: all, total: all.length, meta: {} }); } return res.json(all); } catch (e) { console.error(cachePrefix, e); return sendError(res, 500, e.message || 'EvoBGP error', 'E_EVOBGP'); } }; } function readIfMatch(req) { const raw = req.headers['if-match'] || req.headers['If-Match']; if (!raw) return null; return String(raw).trim(); } function createEvobgpDomainsNewPOST({ validate, validateItem }) { return async (req, res) => { const { domains: items, etag: bodyEtag } = req.body || {}; if (!Array.isArray(items)) { return sendError(res, 400, 'Data must be an array', 'E_BAD_REQUEST'); } if (!validate(items)) { return sendError(res, 400, 'Invalid payload format', 'E_SCHEMA'); } if (validateItem) { const validationErrors = []; for (let i = 0; i < items.length; i++) { const errors = validateItem(items[i], i); if (errors.length > 0) validationErrors.push(...errors); } if (validationErrors.length > 0) { return sendError(res, 400, 'Validation errors', 'E_VALIDATION', { errors: validationErrors.slice(0, 10), }); } } if (!evobgpClient.isConfigured()) { return sendError(res, 503, 'EvoBGP не настроен', 'E_EVOBGP_NOT_CONFIGURED'); } try { const current = await evobgpClient.fetchDomainsData(); const currentEtag = etagForJson(current); const ifMatch = readIfMatch(req); const expected = ifMatch || bodyEtag; if (expected && String(expected).trim() !== currentEtag) { return sendError(res, 412, 'ETag mismatch', 'E_ETAG_MISMATCH'); } const normalized = items.map((d) => ({ domain: String(d.domain || '').trim().toLowerCase(), community: String(d.community || '').trim(), })); await evobgpClient.saveDomainsData(normalized, current); return sendOk(res, { etag: null, lastModified: new Date().toISOString(), contentLength: null }); } catch (e) { console.error(e); return sendError(res, 500, e.message || 'EvoBGP write failed', 'E_EVOBGP'); } }; } function createEvobgpAsnsPOST({ validate, validateItem }) { return async (req, res) => { const { domains: items, etag: bodyEtag } = req.body || {}; if (!Array.isArray(items)) { return sendError(res, 400, 'Data must be an array', 'E_BAD_REQUEST'); } if (!validate(items)) { return sendError(res, 400, 'Invalid payload format', 'E_SCHEMA'); } if (validateItem) { const validationErrors = []; for (let i = 0; i < items.length; i++) { const errors = validateItem(items[i], i); if (errors.length > 0) validationErrors.push(...errors); } if (validationErrors.length > 0) { return sendError(res, 400, 'Validation errors', 'E_VALIDATION', { errors: validationErrors.slice(0, 10), }); } } if (!evobgpClient.isConfigured()) { return sendError(res, 503, 'EvoBGP не настроен', 'E_EVOBGP_NOT_CONFIGURED'); } try { const current = await evobgpClient.fetchAsnsData(); const currentEtag = etagForJson( current.map((a) => ({ domain: a.asn, type: a.community })), ); const ifMatch = readIfMatch(req); const expected = ifMatch || bodyEtag; if (expected && String(expected).trim() !== currentEtag) { return sendError(res, 412, 'ETag mismatch', 'E_ETAG_MISMATCH'); } const normalized = items.map((a) => ({ asn: String(a.domain || a.asn || '').trim(), community: String(a.type || a.community || '').trim(), })); await evobgpClient.saveAsnsData(normalized, current); return sendOk(res, { etag: null, lastModified: new Date().toISOString(), contentLength: null }); } catch (e) { console.error(e); return sendError(res, 500, e.message || 'EvoBGP write failed', 'E_EVOBGP'); } }; } function createEvobgpIpRangesPOST({ validate, validateItem }) { return async (req, res) => { const payload = Array.isArray(req.body?.domains) ? req.body.domains : Array.isArray(req.body?.ipRanges) ? req.body.ipRanges : null; const { etag: bodyEtag } = req.body || {}; if (!Array.isArray(payload)) { return sendError(res, 400, 'Data must be an array', 'E_BAD_REQUEST'); } if (!validate(payload)) { return sendError(res, 400, 'Invalid payload format', 'E_SCHEMA'); } if (validateItem) { const validationErrors = []; for (let i = 0; i < payload.length; i++) { const errors = validateItem(payload[i], i); if (errors.length > 0) validationErrors.push(...errors); } if (validationErrors.length > 0) { return sendError(res, 400, 'Validation errors', 'E_VALIDATION', { errors: validationErrors.slice(0, 10), }); } } if (!evobgpClient.isConfigured()) { return sendError(res, 503, 'EvoBGP не настроен', 'E_EVOBGP_NOT_CONFIGURED'); } try { const current = await evobgpClient.fetchIpRangesData(); const currentEtag = etagForJson(current); const ifMatch = readIfMatch(req); const expected = ifMatch || bodyEtag; if (expected && String(expected).trim() !== currentEtag) { return sendError(res, 412, 'ETag mismatch', 'E_ETAG_MISMATCH'); } const normalized = payload.map((ip) => ({ ipRange: String(ip.ipRange || '').trim(), community: String(ip.community || '').trim(), })); await evobgpClient.saveIpRangesData(normalized, current); return sendOk(res, { etag: null, lastModified: new Date().toISOString(), contentLength: null }); } catch (e) { console.error(e); return sendError(res, 500, e.message || 'EvoBGP write failed', 'E_EVOBGP'); } }; } /** Legacy /api/domains: тот же модуль ASN, ответ в полях domain + type. */ function createEvobgpLegacyDomainsGET({ validate }) { return createEvobgpListGET({ cachePrefix: 'domains', validate, fetchItems: async () => { const rows = await evobgpClient.fetchAsnsData(); return rows.map((r) => ({ domain: r.asn, type: r.community })); }, }); } module.exports = { createEvobgpListGET, createEvobgpDomainsNewPOST, createEvobgpAsnsPOST, createEvobgpIpRangesPOST, createEvobgpLegacyDomainsGET, };