Backend: SQLite storage, EvoBGP integration, filters in SQL
Made-with: Cursor
This commit is contained in:
@@ -1,38 +1,52 @@
|
||||
/**
|
||||
* Роуты для работы с communities (справочник BGP Community)
|
||||
* Роуты для работы с communities (справочник BGP Community) — данные из EvoBGP API
|
||||
*/
|
||||
|
||||
const { s3, BUCKET_NAME, writeS3JsonObject, invalidateCacheForKey } = require('../services/s3Service');
|
||||
const evobgpClient = require('../services/evobgpClient');
|
||||
const filterRulesStorage = require('../services/filterRulesStorage');
|
||||
const { sendError, sendOk } = require('../middleware/errorHandler');
|
||||
const { GetObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3');
|
||||
const { streamToString } = require('../services/s3Service');
|
||||
const validators = require('../lib/validators');
|
||||
const { getS3JsonWithHeaders } = require('../utils/s3Helpers');
|
||||
|
||||
const S3_KEY = 'bgp_data/communities.json';
|
||||
function requireEvobgp(res) {
|
||||
if (!evobgpClient.isConfigured()) {
|
||||
return sendError(
|
||||
res,
|
||||
503,
|
||||
'EvoBGP не настроен: задайте EVOBGP_API_URL и EVOBGP_API_TOKEN',
|
||||
'E_EVOBGP_NOT_CONFIGURED',
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// GET /api/communities
|
||||
async function getCommunities(req, res) {
|
||||
await getS3JsonWithHeaders(S3_KEY, req, res, {
|
||||
transform: (communities) => {
|
||||
// Нормализация
|
||||
return communities
|
||||
.filter((c) => c && typeof c.value === 'string' && c.value.trim().length > 0)
|
||||
.map((c) => ({
|
||||
value: String(c.value).trim(),
|
||||
name: c.name ? String(c.name) : '',
|
||||
description: c.description ? String(c.description) : '',
|
||||
tags: Array.isArray(c.tags) ? c.tags.map(String) : [],
|
||||
color: c.color ? String(c.color) : '',
|
||||
icon: c.icon ? String(c.icon) : ''
|
||||
}));
|
||||
},
|
||||
defaultValue: []
|
||||
});
|
||||
const err = requireEvobgp(res);
|
||||
if (err) return err;
|
||||
try {
|
||||
const communities = await evobgpClient.listCommunities();
|
||||
const normalized = communities
|
||||
.filter((c) => c && typeof c.value === 'string' && c.value.trim().length > 0)
|
||||
.map((c) => ({
|
||||
value: String(c.value).trim(),
|
||||
name: c.name ? String(c.name) : '',
|
||||
description: c.description ? String(c.description) : '',
|
||||
tags: Array.isArray(c.tags) ? c.tags.map(String) : [],
|
||||
color: c.color ? String(c.color) : '',
|
||||
icon: c.icon ? String(c.icon) : '',
|
||||
}));
|
||||
res.json(normalized);
|
||||
} catch (e) {
|
||||
console.error('getCommunities', e);
|
||||
return sendError(res, 500, e.message || 'EvoBGP error', 'E_EVOBGP');
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/communities
|
||||
async function postCommunities(req, res) {
|
||||
const err = requireEvobgp(res);
|
||||
if (err) return err;
|
||||
|
||||
const { communities } = req.body;
|
||||
|
||||
if (!Array.isArray(communities)) {
|
||||
@@ -42,26 +56,26 @@ async function postCommunities(req, res) {
|
||||
const seen = new Set();
|
||||
const normalized = [];
|
||||
const validationErrors = [];
|
||||
|
||||
|
||||
for (let i = 0; i < communities.length; i++) {
|
||||
const entry = communities[i] || {};
|
||||
const value = typeof entry.value === 'string' ? entry.value.trim() : '';
|
||||
|
||||
|
||||
if (!value) {
|
||||
validationErrors.push(`Community at index ${i} is missing required field: value`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if (!validators.isValidCommunity(value)) {
|
||||
validationErrors.push(`Community at index ${i} has invalid value: ${value}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if (seen.has(value)) {
|
||||
validationErrors.push(`Duplicate community value at index ${i}: ${value}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
seen.add(value);
|
||||
normalized.push({
|
||||
value,
|
||||
@@ -75,97 +89,78 @@ async function postCommunities(req, res) {
|
||||
enabled: typeof entry.enabled === 'boolean' ? entry.enabled : true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (validationErrors.length > 0) {
|
||||
return sendError(res, 400, 'Validation errors', 'E_VALIDATION', { errors: validationErrors });
|
||||
}
|
||||
|
||||
try {
|
||||
await s3.send(new PutObjectCommand({
|
||||
Bucket: BUCKET_NAME,
|
||||
Key: S3_KEY,
|
||||
Body: JSON.stringify(normalized, null, 2),
|
||||
ContentType: 'application/json',
|
||||
}));
|
||||
invalidateCacheForKey(S3_KEY);
|
||||
const { headMeta } = require('../services/s3Service');
|
||||
const meta = await headMeta(S3_KEY);
|
||||
return sendOk(res, meta);
|
||||
await evobgpClient.saveCommunitiesData(normalized);
|
||||
return sendOk(res, {
|
||||
etag: null,
|
||||
lastModified: new Date().toISOString(),
|
||||
contentLength: null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error writing communities to S3:', error);
|
||||
return sendError(res, 500, 'Error writing communities to S3', 'E_S3');
|
||||
console.error('Error writing communities to EvoBGP:', error);
|
||||
return sendError(res, 500, error.message || 'EvoBGP write failed', 'E_EVOBGP');
|
||||
}
|
||||
}
|
||||
|
||||
function countCommunityInLines(rows, getCommunity) {
|
||||
const usage = new Map();
|
||||
for (const row of rows || []) {
|
||||
const c = getCommunity(row);
|
||||
if (c) usage.set(c, (usage.get(c) || 0) + 1);
|
||||
}
|
||||
return usage;
|
||||
}
|
||||
|
||||
// GET /api/communities/stats
|
||||
async function getCommunityStats(req, res) {
|
||||
const err = requireEvobgp(res);
|
||||
if (err) return err;
|
||||
try {
|
||||
const [domainsRes, ipRangesRes, asnsRes, filtersRes] = await Promise.allSettled([
|
||||
s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: 'bgp_data/domains_community.txt' })),
|
||||
s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: 'bgp_data/ips.txt' })),
|
||||
s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: 'bgp_data/asns.txt' })),
|
||||
s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: 'filter-manager/simple-filters.json' })),
|
||||
const [domains, ipRanges, asns] = await Promise.allSettled([
|
||||
evobgpClient.fetchDomainsData(),
|
||||
evobgpClient.fetchIpRangesData(),
|
||||
evobgpClient.fetchAsnsData(),
|
||||
]);
|
||||
|
||||
|
||||
const communityUsage = new Map();
|
||||
|
||||
// Подсчет использования в доменах
|
||||
if (domainsRes.status === 'fulfilled') {
|
||||
const text = await streamToString(domainsRes.value.Body);
|
||||
text.split('\n').filter(Boolean).forEach(line => {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts[1]) {
|
||||
communityUsage.set(parts[1], (communityUsage.get(parts[1]) || 0) + 1);
|
||||
}
|
||||
});
|
||||
|
||||
if (domains.status === 'fulfilled') {
|
||||
const m = countCommunityInLines(domains.value, (r) => r.community);
|
||||
for (const [k, v] of m) communityUsage.set(k, (communityUsage.get(k) || 0) + v);
|
||||
}
|
||||
|
||||
// Подсчет в IP ranges
|
||||
if (ipRangesRes.status === 'fulfilled') {
|
||||
const text = await streamToString(ipRangesRes.value.Body);
|
||||
text.split('\n').filter(Boolean).forEach(line => {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts[1]) {
|
||||
communityUsage.set(parts[1], (communityUsage.get(parts[1]) || 0) + 1);
|
||||
}
|
||||
});
|
||||
|
||||
if (ipRanges.status === 'fulfilled') {
|
||||
const m = countCommunityInLines(ipRanges.value, (r) => r.community);
|
||||
for (const [k, v] of m) communityUsage.set(k, (communityUsage.get(k) || 0) + v);
|
||||
}
|
||||
|
||||
// Подсчет в ASNs
|
||||
if (asnsRes.status === 'fulfilled') {
|
||||
const text = await streamToString(asnsRes.value.Body);
|
||||
text.split('\n').filter(Boolean).forEach(line => {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts[1]) {
|
||||
communityUsage.set(parts[1], (communityUsage.get(parts[1]) || 0) + 1);
|
||||
}
|
||||
});
|
||||
|
||||
if (asns.status === 'fulfilled') {
|
||||
const m = countCommunityInLines(asns.value, (r) => r.community);
|
||||
for (const [k, v] of m) communityUsage.set(k, (communityUsage.get(k) || 0) + v);
|
||||
}
|
||||
|
||||
// Подсчет в фильтрах
|
||||
if (filtersRes.status === 'fulfilled') {
|
||||
try {
|
||||
const text = await streamToString(filtersRes.value.Body);
|
||||
const filters = JSON.parse(text);
|
||||
if (Array.isArray(filters)) {
|
||||
filters.forEach(f => {
|
||||
if (f.community) {
|
||||
communityUsage.set(f.community, (communityUsage.get(f.community) || 0) + 1);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const simpleFilters = filterRulesStorage.listRules('simple');
|
||||
for (const f of simpleFilters) {
|
||||
if (f.community) {
|
||||
communityUsage.set(f.community, (communityUsage.get(f.community) || 0) + 1);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const stats = Array.from(communityUsage.entries()).map(([community, count]) => ({
|
||||
community,
|
||||
count,
|
||||
})).sort((a, b) => b.count - a.count);
|
||||
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
const stats = Array.from(communityUsage.entries())
|
||||
.map(([community, count]) => ({ community, count }))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
|
||||
res.json({ stats, total: stats.reduce((sum, s) => sum + s.count, 0) });
|
||||
} catch (error) {
|
||||
console.error('Error getting community stats:', error);
|
||||
return sendError(res, 500, 'Error getting community stats', 'E_S3');
|
||||
return sendError(res, 500, 'Error getting community stats', 'E_EVOBGP');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,4 +169,3 @@ module.exports = {
|
||||
postCommunities,
|
||||
getCommunityStats,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user