require('dotenv').config(); const express = require('express'); const AWS = require('aws-sdk'); const cors = require('cors'); const path = require('path'); const app = express(); const port = 3001; app.use(cors()); app.use(express.json()); // Serve static files from the React app app.use(express.static(path.join(__dirname, 'public'))); // Configure AWS S3 const s3 = new AWS.S3({ endpoint: 'https://storage.yandexcloud.net', region: process.env.AWS_REGION, credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, } }); const BUCKET_NAME = process.env.S3_BUCKET_NAME; const FILE_KEY = 'bgp_data/domains.txt'; // Helper: build MikroTik nested if/else blocks (RouterOS v7 filter language does not support 'else if') function buildNestedGatewayBlocks(gatewayGroups, baseIndentSpaces = 4) { const indent = (n) => ' '.repeat(n); const entries = Object.entries(gatewayGroups); if (entries.length === 0) return ''; function buildAt(index, pad) { const [gateway, communities] = entries[index]; let s = ''; s += `${indent(pad)}if (\n`; communities.forEach((community, i) => { s += `${indent(pad + 4)}(bgp-communities includes ${community})`; if (i < communities.length - 1) s += ' or \n'; }); s += `\n${indent(pad)})\n`; s += `${indent(pad)}{\n${indent(pad + 8 - 4)}set gw ${gateway}; accept;\n${indent(pad)}}\n`; if (index < entries.length - 1) { s += `${indent(pad)}else\n${indent(pad)}{\n`; s += buildAt(index + 1, pad + 4); s += `\n${indent(pad)}}`; } else { s += `${indent(pad)}else\n${indent(pad)}{\n${indent(pad + 4)}reject;\n${indent(pad)}}`; } return s; } return buildAt(0, baseIndentSpaces); } // Helper: read text file from S3 and return { body, etag, lastModified, contentLength } async function readS3TextObject(key) { const params = { Bucket: BUCKET_NAME, Key: key }; const data = await s3.getObject(params).promise(); return { body: data.Body.toString('utf-8'), etag: data.ETag ? String(data.ETag).replace(/\"/g, '"') : undefined, lastModified: data.LastModified ? data.LastModified.toISOString() : undefined, contentLength: typeof data.ContentLength === 'number' ? data.ContentLength : undefined }; } // Helper: head object and return current ETag async function headS3ObjectEtag(key) { const head = await s3.headObject({ Bucket: BUCKET_NAME, Key: key }).promise(); return head.ETag ? String(head.ETag).replace(/\"/g, '"') : undefined; } // Simple in-memory soft locks with TTL const locks = new Map(); // key -> { owner, expiresAt } function cleanupExpiredLocks() { const now = Date.now(); for (const [k, v] of locks.entries()) { if (!v || typeof v.expiresAt !== 'number' || v.expiresAt <= now) { locks.delete(k); } } } setInterval(cleanupExpiredLocks, 30_000); // Get domains from S3 app.get('/api/domains', async (req, res) => { const params = { Bucket: BUCKET_NAME, Key: FILE_KEY, }; try { const data = await s3.getObject(params).promise(); const fileContent = data.Body.toString('utf-8'); const domains = fileContent.split('\n').filter(line => line).map(line => { // Trim the line to remove any leading/trailing whitespace, including \r // And split by any whitespace to be more robust const parts = line.trim().split(/\s+/); const domain = parts[0] || ''; const type = parts[1] || ''; return { domain, type }; }); if (data.ETag) { res.set('ETag', String(data.ETag)); } if (data.LastModified) { res.set('Last-Modified', new Date(data.LastModified).toUTCString()); } if (typeof data.ContentLength === 'number') { res.set('Content-Length-Source', String(data.ContentLength)); } res.json(domains); } catch (error) { if (error.code === 'NoSuchKey') { res.json([]); // Return empty array if file does not exist } else { console.error(error); res.status(500).send('Error reading from S3'); } } }); // Update domains in S3 with optimistic concurrency via ETag check app.post('/api/domains', async (req, res) => { const { domains, etag } = req.body; const fileContent = (domains || []).map(d => `${String(d.domain || '').trim()} ${String(d.type || '').trim()}`.trim()).filter(Boolean).join('\n'); // Concurrency guard: if client sent etag, ensure current ETag matches try { if (etag) { const current = await headS3ObjectEtag(FILE_KEY).catch(() => undefined); if (current && current.replace(/\"/g, '"') !== String(etag)) { return res.status(412).json({ message: 'Precondition Failed: ETag mismatch' }); } } } catch (e) { // ignore if head fails due to NoSuchKey; proceed to create } const params = { Bucket: BUCKET_NAME, Key: FILE_KEY, Body: fileContent, ContentType: 'text/plain', }; try { const put = await s3.putObject(params).promise(); res.set('ETag', put.ETag || ''); res.send('File updated successfully'); } catch (error) { console.error(error); res.status(500).send('Error writing to S3'); } }); // --- ASNs Routes --- // Get ASNs from S3 app.get('/api/asns', async (req, res) => { const params = { Bucket: BUCKET_NAME, Key: 'bgp_data/asns.txt', }; try { const data = await s3.getObject(params).promise(); const fileContent = data.Body.toString('utf-8'); const asns = fileContent.split('\n').filter(line => line).map(line => { const parts = line.trim().split(/\s+/); const domain = parts[0] || ''; // Keep name 'domain' for consistency in component const type = parts[1] || ''; return { domain, type }; }); if (data.ETag) res.set('ETag', String(data.ETag)); if (data.LastModified) res.set('Last-Modified', new Date(data.LastModified).toUTCString()); if (typeof data.ContentLength === 'number') res.set('Content-Length-Source', String(data.ContentLength)); res.json(asns); } catch (error) { if (error.code === 'NoSuchKey') { res.json([]); } else { console.error(error); res.status(500).send('Error reading from S3'); } } }); // Update ASNs in S3 app.post('/api/asns', async (req, res) => { const { domains: asns, etag } = req.body; // Keep name 'domains' for consistency const fileContent = (asns || []).map(a => `${String(a.domain || '').trim()} ${String(a.type || '').trim()}`.trim()).filter(Boolean).join('\n'); try { if (etag) { const current = await headS3ObjectEtag('bgp_data/asns.txt').catch(() => undefined); if (current && current.replace(/\"/g, '"') !== String(etag)) { return res.status(412).json({ message: 'Precondition Failed: ETag mismatch' }); } } } catch {} const params = { Bucket: BUCKET_NAME, Key: 'bgp_data/asns.txt', Body: fileContent, ContentType: 'text/plain', }; try { const put = await s3.putObject(params).promise(); if (put.ETag) res.set('ETag', String(put.ETag)); res.send('File updated successfully'); } catch (error) { console.error(error); res.status(500).send('Error writing to S3'); } }); // --- Domains New Routes --- // Get domains-new from S3 app.get('/api/domains-new', async (req, res) => { const params = { Bucket: BUCKET_NAME, Key: 'bgp_data/domains_community.txt', }; try { const data = await s3.getObject(params).promise(); const fileContent = data.Body.toString('utf-8'); const domains = fileContent.split('\n').filter(line => line).map(line => { const parts = line.trim().split(/\s+/); const domain = parts[0] || ''; const community = parts[1] || ''; return { domain, community }; }); if (data.ETag) res.set('ETag', String(data.ETag)); if (data.LastModified) res.set('Last-Modified', new Date(data.LastModified).toUTCString()); if (typeof data.ContentLength === 'number') res.set('Content-Length-Source', String(data.ContentLength)); res.json(domains); } catch (error) { if (error.code === 'NoSuchKey') { res.json([]); // Return empty array if file does not exist } else { console.error(error); res.status(500).send('Error reading from S3'); } } }); // Update domains-new in S3 app.post('/api/domains-new', async (req, res) => { const { domains, etag } = req.body; const fileContent = (domains || []).map(d => `${String(d.domain || '').trim()} ${String(d.community || '').trim()}`.trim()).filter(Boolean).join('\n'); try { if (etag) { const current = await headS3ObjectEtag('bgp_data/domains_community.txt').catch(() => undefined); if (current && current.replace(/\"/g, '"') !== String(etag)) { return res.status(412).json({ message: 'Precondition Failed: ETag mismatch' }); } } } catch {} const params = { Bucket: BUCKET_NAME, Key: 'bgp_data/domains_community.txt', Body: fileContent, ContentType: 'text/plain', }; try { const put = await s3.putObject(params).promise(); if (put.ETag) res.set('ETag', String(put.ETag)); res.send('File updated successfully'); } catch (error) { console.error(error); res.status(500).send('Error writing to S3'); } }); // --- IP Ranges Routes --- // Get IP ranges from S3 app.get('/api/ip-ranges', async (req, res) => { const params = { Bucket: BUCKET_NAME, Key: 'bgp_data/ips.txt', }; try { const data = await s3.getObject(params).promise(); const fileContent = data.Body.toString('utf-8'); const ipRanges = fileContent.split('\n').filter(line => line).map(line => { const parts = line.trim().split(/\s+/); const ipRange = parts[0] || ''; const community = parts[1] || ''; return { ipRange, community }; }); if (data.ETag) res.set('ETag', String(data.ETag)); if (data.LastModified) res.set('Last-Modified', new Date(data.LastModified).toUTCString()); if (typeof data.ContentLength === 'number') res.set('Content-Length-Source', String(data.ContentLength)); res.json(ipRanges); } catch (error) { if (error.code === 'NoSuchKey') { res.json([]); // Return empty array if file does not exist } else { console.error(error); res.status(500).send('Error reading from S3'); } } }); // Update IP ranges in S3 app.post('/api/ip-ranges', async (req, res) => { const { ipRanges, etag } = req.body; const fileContent = (ipRanges || []).map(ip => `${String(ip.ipRange || '').trim()} ${String(ip.community || '').trim()}`.trim()).filter(Boolean).join('\n'); try { if (etag) { const current = await headS3ObjectEtag('bgp_data/ips.txt').catch(() => undefined); if (current && current.replace(/\"/g, '"') !== String(etag)) { return res.status(412).json({ message: 'Precondition Failed: ETag mismatch' }); } } } catch {} const params = { Bucket: BUCKET_NAME, Key: 'bgp_data/ips.txt', Body: fileContent, ContentType: 'text/plain', }; try { const put = await s3.putObject(params).promise(); if (put.ETag) res.set('ETag', String(put.ETag)); res.send('File updated successfully'); } catch (error) { console.error(error); res.status(500).send('Error writing to S3'); } }); // --- Communities Directory Routes --- // Get communities from S3 app.get('/api/communities', async (req, res) => { const params = { Bucket: BUCKET_NAME, Key: 'bgp_data/communities.json', }; try { const data = await s3.getObject(params).promise(); const fileContent = data.Body.toString('utf-8'); let communities = []; try { const parsed = JSON.parse(fileContent); communities = Array.isArray(parsed) ? parsed : []; } catch (parseError) { console.error('Error parsing communities.json:', parseError); communities = []; } // Basic normalization communities = 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) : [], gatewayDefault: c.gatewayDefault ? String(c.gatewayDefault) : '', color: c.color ? String(c.color) : '' })); res.json(communities); } catch (error) { if (error.code === 'NoSuchKey') { // If file missing, start with empty list return res.json([]); } console.error('Error reading communities from S3:', error); res.status(500).send('Error reading communities from S3'); } }); // Update communities in S3 app.post('/api/communities', async (req, res) => { const { communities } = req.body; if (!Array.isArray(communities)) { return res.status(400).send('communities must be an array'); } // Validate entries and ensure unique values const seen = new Set(); const normalized = []; for (let i = 0; i < communities.length; i++) { const entry = communities[i] || {}; const value = typeof entry.value === 'string' ? entry.value.trim() : ''; if (!value) { return res.status(400).send(`Community at index ${i} is missing required field: value`); } if (seen.has(value)) { return res.status(400).send(`Duplicate community value at index ${i}: ${value}`); } seen.add(value); normalized.push({ value, name: entry.name ? String(entry.name) : '', description: entry.description ? String(entry.description) : '', tags: Array.isArray(entry.tags) ? entry.tags.map(String) : [], gatewayDefault: entry.gatewayDefault ? String(entry.gatewayDefault) : '', color: entry.color ? String(entry.color) : '' }); } const params = { Bucket: BUCKET_NAME, Key: 'bgp_data/communities.json', Body: JSON.stringify(normalized, null, 2), ContentType: 'application/json', }; try { await s3.putObject(params).promise(); res.send('Communities updated successfully'); } catch (error) { console.error('Error writing communities to S3:', error); res.status(500).send('Error writing communities to S3'); } }); // --- Servers Routes (JSON format) --- // Get servers from S3 app.get('/api/servers', async (req, res) => { const params = { Bucket: BUCKET_NAME, Key: 'servers.json', }; try { const data = await s3.getObject(params).promise(); const fileContent = data.Body.toString('utf-8'); let servers = []; try { servers = JSON.parse(fileContent); // Ensure it's an array if (!Array.isArray(servers)) { servers = []; } } catch (parseError) { console.error('Error parsing servers.json:', parseError); servers = []; } res.json(servers); } catch (error) { if (error.code === 'NoSuchKey') { res.json([]); // Return empty array if file does not exist } else { console.error(error); res.status(500).send('Error reading from S3'); } } }); // Update servers in S3 app.post('/api/servers', async (req, res) => { const { domains: servers } = req.body; // Keep name 'domains' for consistency // Validate servers structure if (!Array.isArray(servers)) { return res.status(400).send('Servers must be an array'); } // Validate each server has required fields for (let i = 0; i < servers.length; i++) { const server = servers[i]; if (!server.ip || !server.dns || !server.country || !server.provider || !server.tunnel) { return res.status(400).send(`Server at index ${i} is missing required fields`); } } const params = { Bucket: BUCKET_NAME, Key: 'servers.json', Body: JSON.stringify(servers, null, 2), // Pretty print JSON ContentType: 'application/json', }; try { await s3.putObject(params).promise(); res.send('File updated successfully'); } catch (error) { console.error(error); res.status(500).send('Error writing to S3'); } }); // --- Billing Routes --- // Get billing data from S3 app.get('/api/billing', async (req, res) => { const params = { Bucket: BUCKET_NAME, Key: 'servers-billing.json', }; try { const data = await s3.getObject(params).promise(); const fileContent = data.Body.toString('utf-8'); let billingData = []; try { billingData = JSON.parse(fileContent); // Ensure it's an array if (!Array.isArray(billingData)) { billingData = []; } } catch (parseError) { console.error('Error parsing servers-billing.json:', parseError); billingData = []; } res.json(billingData); } catch (error) { if (error.code === 'NoSuchKey') { res.json([]); // Return empty array if file does not exist } else { console.error(error); res.status(500).send('Error reading from S3'); } } }); // Update billing data in S3 app.post('/api/billing', async (req, res) => { const { domains: billingData } = req.body; // Keep name 'domains' for consistency // Validate billing data structure if (!Array.isArray(billingData)) { return res.status(400).send('Billing data must be an array'); } // Validate each billing item has required fields for (let i = 0; i < billingData.length; i++) { const item = billingData[i]; if (!item.hostName || !item.country || !item.provider) { return res.status(400).send(`Billing item at index ${i} is missing required fields: hostName, country, provider`); } } const params = { Bucket: BUCKET_NAME, Key: 'servers-billing.json', Body: JSON.stringify(billingData, null, 2), // Pretty print JSON ContentType: 'application/json', }; try { await s3.putObject(params).promise(); res.send('File updated successfully'); } catch (error) { console.error(error); res.status(500).send('Error writing to S3'); } }); // --- Filters Routes (JSON format) --- // Get filters from S3 app.get('/api/filters', async (req, res) => { const params = { Bucket: BUCKET_NAME, Key: 'filters.json', }; try { const data = await s3.getObject(params).promise(); const fileContent = data.Body.toString('utf-8'); let filters = []; try { filters = JSON.parse(fileContent); // Ensure it's an array if (!Array.isArray(filters)) { filters = []; } } catch (parseError) { console.error('Error parsing filters.json:', parseError); filters = []; } res.json(filters); } catch (error) { if (error.code === 'NoSuchKey') { res.json([]); // Return empty array if file does not exist } else { console.error(error); res.status(500).send('Error reading from S3'); } } }); // Update filters in S3 app.post('/api/filters', async (req, res) => { const { domains: filters } = req.body; // Keep name 'domains' for consistency // Validate filters structure if (!Array.isArray(filters)) { return res.status(400).send('Filters must be an array'); } // Validate each filter has required fields for (let i = 0; i < filters.length; i++) { const filter = filters[i]; if (!filter.community || !filter.gateway) { return res.status(400).send(`Filter at index ${i} is missing required fields`); } } const params = { Bucket: BUCKET_NAME, Key: 'filters.json', Body: JSON.stringify(filters, null, 2), // Pretty print JSON ContentType: 'application/json', }; try { await s3.putObject(params).promise(); res.send('File updated successfully'); } catch (error) { console.error(error); res.status(500).send('Error writing to S3'); } }); // Эндпоинт метаданных S3 по ключевым файлам (Last-Modified, ETag, Content-Length) app.get('/api/s3/last-modified', async (req, res) => { try { const keys = [ { name: 'domainsNew', key: 'bgp_data/domains_community.txt' }, { name: 'asns', key: 'bgp_data/asns.txt' }, { name: 'servers', key: 'servers.json' }, { name: 'filters', key: 'filters.json' }, { name: 'ipRanges', key: 'bgp_data/ips.txt' } ]; const results = await Promise.allSettled( keys.map(k => s3.headObject({ Bucket: BUCKET_NAME, Key: k.key }).promise()) ); const out = {}; results.forEach((r, idx) => { const name = keys[idx].name; if (r.status === 'fulfilled') { out[name] = { lastModified: r.value.LastModified ? r.value.LastModified.toISOString() : null, etag: r.value.ETag || null, contentLength: typeof r.value.ContentLength === 'number' ? r.value.ContentLength : null }; } else { out[name] = null; } }); res.json(out); } catch (error) { console.error('Error fetching last modified dates from S3:', error); res.status(500).send('Error fetching last modified dates from S3'); } }); // Soft-lock endpoints // GET lock status app.get('/api/locks/:resource', (req, res) => { cleanupExpiredLocks(); const { resource } = req.params; const info = locks.get(resource); if (!info) return res.json({ locked: false }); res.json({ locked: true, owner: info.owner, expiresAt: info.expiresAt }); }); // POST acquire/refresh lock app.post('/api/locks/:resource', (req, res) => { cleanupExpiredLocks(); const { resource } = req.params; const { owner = 'anonymous', ttlSeconds = 120 } = req.body || {}; const now = Date.now(); const existing = locks.get(resource); if (existing && existing.expiresAt > now && existing.owner !== owner) { return res.status(423).json({ message: 'Resource is locked by another user', owner: existing.owner, expiresAt: existing.expiresAt }); } const expiresAt = now + Math.max(30, Math.min(600, Number(ttlSeconds) || 120)) * 1000; locks.set(resource, { owner, expiresAt }); res.json({ locked: true, owner, expiresAt }); }); // DELETE release lock app.delete('/api/locks/:resource', (req, res) => { const { resource } = req.params; locks.delete(resource); res.json({ released: true }); }); // Generate MikroTik configuration from filters app.get('/api/filters/generate-config', async (req, res) => { const params = { Bucket: BUCKET_NAME, Key: 'filters.json', }; try { const data = await s3.getObject(params).promise(); const fileContent = data.Body.toString('utf-8'); let filters = []; try { filters = JSON.parse(fileContent); if (!Array.isArray(filters)) { filters = []; } } catch (parseError) { console.error('Error parsing filters.json:', parseError); filters = []; } if (filters.length === 0) { return res.json({ config: '// No filters to generate configuration' }); } // Group filters by gateway const gatewayGroups = {}; filters.forEach(filter => { if (!gatewayGroups[filter.gateway]) { gatewayGroups[filter.gateway] = []; } gatewayGroups[filter.gateway].push(filter.community); }); let config = '// Frouting filter configuration for MikroTik 7.14+\n'; config += '// Generated automatically\n'; config += `// Date: ${new Date().toISOString()}\n\n`; config += '/routing filter bgp-in-tmp {\n'; // Строим вложенные if/else (RouterOS не поддерживает else if в данном контексте) config += buildNestedGatewayBlocks(gatewayGroups, 4); config += '}\n'; res.json({ config }); } catch (error) { if (error.code === 'NoSuchKey') { res.json({ config: '// filters.json file not found' }); } else { console.error(error); res.status(500).send('Error generating configuration'); } } }); // Export MikroTik configuration to S3 app.post('/api/filters/export-config', async (req, res) => { const params = { Bucket: BUCKET_NAME, Key: 'filters.json', }; try { const data = await s3.getObject(params).promise(); const fileContent = data.Body.toString('utf-8'); let filters = []; try { filters = JSON.parse(fileContent); if (!Array.isArray(filters)) { filters = []; } } catch (parseError) { console.error('Error parsing filters.json:', parseError); filters = []; } if (filters.length === 0) { return res.json({ success: false, message: 'Нет фильтров для экспорта' }); } // Group filters by gateway const gatewayGroups = {}; filters.forEach(filter => { if (!gatewayGroups[filter.gateway]) { gatewayGroups[filter.gateway] = []; } gatewayGroups[filter.gateway].push(filter.community); }); let config = '// Frouting filter configuration for MikroTik 7.14+\n'; config += '// Generated automatically\n'; config += `// Date: ${new Date().toISOString()}\n\n`; config += '/routing filter bgp-in-tmp {\n'; // Строим вложенные if/else (RouterOS не поддерживает else if в данном контексте) config += buildNestedGatewayBlocks(gatewayGroups, 4); config += '}\n'; // Save configuration to S3 const exportParams = { Bucket: BUCKET_NAME, Key: 'mikrotik-frouting-config.txt', Body: config, ContentType: 'text/plain', }; await s3.putObject(exportParams).promise(); res.json({ success: true, message: 'Конфигурация экспортирована в S3' }); } catch (error) { console.error(error); res.status(500).send('Error exporting configuration'); } }); // --- Server Configs Routes --- // Get server configs list app.get('/api/server-configs', async (req, res) => { const params = { Bucket: BUCKET_NAME, Key: 'server-configs.json', }; try { const data = await s3.getObject(params).promise(); const fileContent = data.Body.toString('utf-8'); let servers = []; try { servers = JSON.parse(fileContent); if (!Array.isArray(servers)) { servers = []; } } catch (parseError) { console.error('Error parsing server-configs.json:', parseError); servers = []; } res.json(servers); } catch (error) { if (error.code === 'NoSuchKey') { res.json([]); // Return empty array if file does not exist } else { console.error(error); res.status(500).send('Error reading server configs from S3'); } } }); // Update server configs list app.post('/api/server-configs', async (req, res) => { const { servers } = req.body; // Validate servers structure if (!Array.isArray(servers)) { return res.status(400).send('Servers must be an array'); } // Validate each server has required fields for (let i = 0; i < servers.length; i++) { const server = servers[i]; if (!server.id || !server.name) { return res.status(400).send(`Server at index ${i} is missing required fields`); } } const params = { Bucket: BUCKET_NAME, Key: 'server-configs.json', Body: JSON.stringify(servers, null, 2), // Pretty print JSON ContentType: 'application/json', }; try { await s3.putObject(params).promise(); res.send('Server configs updated successfully'); } catch (error) { console.error(error); res.status(500).send('Error writing server configs to S3'); } }); // Get specific server config app.get('/api/server-configs/:serverId', async (req, res) => { const { serverId } = req.params; const params = { Bucket: BUCKET_NAME, Key: `filter-manager/config-${serverId}.txt`, }; try { const data = await s3.getObject(params).promise(); const config = data.Body.toString('utf-8'); res.json({ config }); } catch (error) { if (error.code === 'NoSuchKey') { res.json({ config: '// Конфигурация не найдена' }); } else { console.error(error); res.status(500).send('Error reading server config from S3'); } } }); // Save specific server config app.post('/api/server-configs/:serverId', async (req, res) => { const { serverId } = req.params; const { config } = req.body; if (!config) { return res.status(400).send('Config is required'); } const params = { Bucket: BUCKET_NAME, Key: `filter-manager/config-${serverId}.txt`, Body: config, ContentType: 'text/plain', }; try { await s3.putObject(params).promise(); res.send('Server config saved successfully'); } catch (error) { console.error(error); res.status(500).send('Error writing server config to S3'); } }); // Delete specific server config app.delete('/api/server-configs/:serverId', async (req, res) => { const { serverId } = req.params; const params = { Bucket: BUCKET_NAME, Key: `filter-manager/config-${serverId}.txt`, }; try { await s3.deleteObject(params).promise(); res.send('Server config deleted successfully'); } catch (error) { console.error(error); res.status(500).send('Error deleting server config from S3'); } }); // Delete server completely (config + filters) app.delete('/api/server-configs/:serverId/complete', async (req, res) => { const { serverId } = req.params; try { // Удаляем конфигурацию сервера const configParams = { Bucket: BUCKET_NAME, Key: `filter-manager/config-${serverId}.txt`, }; // Удаляем фильтры сервера const filtersParams = { Bucket: BUCKET_NAME, Key: `filter-manager/server-filters-${serverId}.json`, }; // Удаляем оба файла параллельно await Promise.allSettled([ s3.deleteObject(configParams).promise(), s3.deleteObject(filtersParams).promise() ]); res.send('Server and all associated files deleted successfully'); } catch (error) { console.error(error); res.status(500).send('Error deleting server files from S3'); } }); // --- Server Filters Routes --- // Get server filters app.get('/api/server-filters/:serverId', async (req, res) => { const { serverId } = req.params; const params = { Bucket: BUCKET_NAME, Key: `filter-manager/server-filters-${serverId}.json`, }; try { const data = await s3.getObject(params).promise(); const fileContent = data.Body.toString('utf-8'); let filters = []; try { filters = JSON.parse(fileContent); if (!Array.isArray(filters)) { filters = []; } } catch (parseError) { console.error('Error parsing server filters:', parseError); filters = []; } res.json(filters); } catch (error) { if (error.code === 'NoSuchKey') { res.json([]); // Return empty array if file does not exist } else { console.error(error); res.status(500).send('Error reading server filters from S3'); } } }); // Generate MikroTik configuration from server filters app.post('/api/server-filters/generate-config', async (req, res) => { console.log('Received generate-config request with filters:', req.body); const { filters } = req.body; if (!Array.isArray(filters) || filters.length === 0) { console.log('No filters provided, returning empty config'); return res.json({ config: '// No filters to generate configuration' }); } // Group filters by gateway const gatewayGroups = {}; filters.forEach(filter => { if (!gatewayGroups[filter.gateway]) { gatewayGroups[filter.gateway] = []; } gatewayGroups[filter.gateway].push(filter.community); }); console.log('Grouped filters by gateway:', gatewayGroups); let config = '// Frouting filter configuration for MikroTik 7.14+\n'; config += '// Generated automatically\n'; config += `// Date: ${new Date().toISOString()}\n\n`; config += '/routing filter bgp-in-tmp {\n'; // Строим вложенные if/else (RouterOS не поддерживает else if в данном контексте) config += buildNestedGatewayBlocks(gatewayGroups, 4); config += '}\n'; console.log('Generated config:', config); console.log('Sending response:', { config }); res.json({ config }); }); // Update server filters app.post('/api/server-filters/:serverId', async (req, res) => { const { serverId } = req.params; const { filters } = req.body; // Validate filters structure if (!Array.isArray(filters)) { return res.status(400).send('Filters must be an array'); } // Validate each filter has required fields for (let i = 0; i < filters.length; i++) { const filter = filters[i]; if (!filter.community || !filter.gateway) { return res.status(400).send(`Filter at index ${i} is missing required fields`); } } const params = { Bucket: BUCKET_NAME, Key: `filter-manager/server-filters-${serverId}.json`, Body: JSON.stringify(filters, null, 2), // Pretty print JSON ContentType: 'application/json', }; try { await s3.putObject(params).promise(); res.send('Server filters updated successfully'); } catch (error) { console.error(error); res.status(500).send('Error writing server filters to S3'); } }); // --- Simple Filters Routes --- // Get simple filters app.get('/api/simple-filters', async (req, res) => { const params = { Bucket: BUCKET_NAME, Key: 'filter-manager/simple-filters.json', }; try { const data = await s3.getObject(params).promise(); const fileContent = data.Body.toString('utf-8'); let filters = []; try { filters = JSON.parse(fileContent); if (!Array.isArray(filters)) { filters = []; } } catch (parseError) { console.error('Error parsing simple filters:', parseError); filters = []; } res.json(filters); } catch (error) { if (error.code === 'NoSuchKey') { res.json([]); // Return empty array if file does not exist } else { console.error(error); res.status(500).send('Error reading simple filters from S3'); } } }); // Update simple filters app.post('/api/simple-filters', async (req, res) => { const { filters } = req.body; // Validate filters structure if (!Array.isArray(filters)) { return res.status(400).send('Filters must be an array'); } // Validate each filter has required fields for (let i = 0; i < filters.length; i++) { const filter = filters[i]; if (!filter.community || !filter.gateway) { return res.status(400).send(`Filter at index ${i} is missing required fields`); } } const params = { Bucket: BUCKET_NAME, Key: 'filter-manager/simple-filters.json', Body: JSON.stringify(filters, null, 2), // Pretty print JSON ContentType: 'application/json', }; try { await s3.putObject(params).promise(); res.send('Simple filters updated successfully'); } catch (error) { console.error(error); res.status(500).send('Error writing simple filters to S3'); } }); // --- Auto URL Routes --- // Get auto URLs from S3 app.get('/api/auto-urls', async (req, res) => { const params = { Bucket: BUCKET_NAME, Key: 'bgp_data/auto_url/urls.txt', }; try { const data = await s3.getObject(params).promise(); const fileContent = data.Body.toString('utf-8'); const urls = fileContent.split('\n').filter(line => line).map(line => { const parts = line.trim().split(/\s+/); const url = parts[0] || ''; const community = parts[1] || ''; return { url, community }; }); res.json(urls); } catch (error) { if (error.code === 'NoSuchKey') { res.json([]); // Return empty array if file does not exist } else { console.error(error); res.status(500).send('Error reading auto URLs from S3'); } } }); // Update auto URLs in S3 app.post('/api/auto-urls', async (req, res) => { const { urls } = req.body; const fileContent = urls.map(u => `${u.url} ${u.community}`).join('\n'); const params = { Bucket: BUCKET_NAME, Key: 'bgp_data/auto_url/urls.txt', Body: fileContent, ContentType: 'text/plain', }; try { await s3.putObject(params).promise(); res.send('Auto URLs updated successfully'); } catch (error) { console.error(error); res.status(500).send('Error writing auto URLs to S3'); } }); // Process auto URLs and update IPs app.post('/api/auto-urls/process', async (req, res) => { const https = require('https'); const http = require('http'); try { // Get current auto URLs const urlsParams = { Bucket: BUCKET_NAME, Key: 'bgp_data/auto_url/urls.txt', }; let urls = []; try { const urlsData = await s3.getObject(urlsParams).promise(); const urlsContent = urlsData.Body.toString('utf-8'); urls = urlsContent.split('\n').filter(line => line).map(line => { const parts = line.trim().split(/\s+/); return { url: parts[0] || '', community: parts[1] || '' }; }); } catch (error) { if (error.code !== 'NoSuchKey') { throw error; } } if (urls.length === 0) { return res.status(400).send('No URLs to process'); } // Get current IPs const ipsParams = { Bucket: BUCKET_NAME, Key: 'bgp_data/ips.txt', }; let currentIps = []; try { const ipsData = await s3.getObject(ipsParams).promise(); const ipsContent = ipsData.Body.toString('utf-8'); currentIps = ipsContent.split('\n').filter(line => line).map(line => { const parts = line.trim().split(/\s+/); return { ipRange: parts[0] || '', community: parts[1] || '' }; }); } catch (error) { if (error.code !== 'NoSuchKey') { throw error; } } // Process each URL const newIps = []; for (const urlData of urls) { try { const url = urlData.url.trim(); const community = urlData.community.trim(); if (!url || !community) continue; // Download content from URL const content = await new Promise((resolve, reject) => { const protocol = url.startsWith('https:') ? https : http; const req = protocol.get(url, (res) => { let data = ''; res.on('data', (chunk) => data += chunk); res.on('end', () => resolve(data)); }); req.on('error', reject); req.setTimeout(10000, () => req.destroy()); }); // Parse IPs from content const lines = content.split('\n'); for (const line of lines) { const ip = line.trim(); if (ip && (ip.includes('.') || ip.includes(':'))) { newIps.push({ ipRange: ip, community }); } } } catch (error) { console.error(`Error processing URL ${urlData.url}:`, error); } } // Merge with existing IPs (avoid duplicates) const existingIpRanges = new Set(currentIps.map(ip => ip.ipRange)); const uniqueNewIps = newIps.filter(ip => !existingIpRanges.has(ip.ipRange)); const allIps = [...currentIps, ...uniqueNewIps]; // Save updated IPs const updatedIpsContent = allIps.map(ip => `${ip.ipRange} ${ip.community}`).join('\n'); const updateParams = { Bucket: BUCKET_NAME, Key: 'bgp_data/ips.txt', Body: updatedIpsContent, ContentType: 'text/plain', }; await s3.putObject(updateParams).promise(); res.json({ success: true, message: `Обработано ${urls.length} URL, добавлено ${uniqueNewIps.length} новых IP-адресов`, processedUrls: urls.length, newIpsCount: uniqueNewIps.length, totalIpsCount: allIps.length }); } catch (error) { console.error('Error processing auto URLs:', error); res.status(500).send('Error processing auto URLs'); } }); // The "catchall" handler: for any request that doesn't // match one above, send back React's index.html file. app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); app.listen(port, () => { console.log(`Server is running on http://localhost:${port}`); });