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 = 'domains.txt'; // 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 }; }); 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 app.post('/api/domains', async (req, res) => { const { domains } = req.body; const fileContent = domains.map(d => `${d.domain} ${d.type}`).join('\n'); const params = { Bucket: BUCKET_NAME, Key: FILE_KEY, Body: fileContent, ContentType: 'text/plain', }; try { await s3.putObject(params).promise(); 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: '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 }; }); 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 } = req.body; // Keep name 'domains' for consistency const fileContent = asns.map(a => `${a.domain} ${a.type}`).join('\n'); const params = { Bucket: BUCKET_NAME, Key: 'asns.txt', Body: fileContent, ContentType: 'text/plain', }; try { await s3.putObject(params).promise(); res.send('File updated successfully'); } catch (error) { console.error(error); res.status(500).send('Error writing 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'); } }); // --- 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 app.get('/api/s3/last-modified', async (req, res) => { try { const [domainsHead, asnsHead, serversHead, filtersHead] = await Promise.all([ s3.headObject({ Bucket: BUCKET_NAME, Key: 'domains.txt' }).promise(), s3.headObject({ Bucket: BUCKET_NAME, Key: 'asns.txt' }).promise(), s3.headObject({ Bucket: BUCKET_NAME, Key: 'servers.json' }).promise(), s3.headObject({ Bucket: BUCKET_NAME, Key: 'filters.json' }).promise() ]); res.json({ domainsLastModified: domainsHead.LastModified ? domainsHead.LastModified.toISOString() : null, asnsLastModified: asnsHead.LastModified ? asnsHead.LastModified.toISOString() : null, serversLastModified: serversHead.LastModified ? serversHead.LastModified.toISOString() : null, filtersLastModified: filtersHead.LastModified ? filtersHead.LastModified.toISOString() : null }); } catch (error) { console.error(error); res.status(500).send('Error fetching last modified dates from S3'); } }); // 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`; // Если есть только один gateway, создаем простую конфигурацию if (Object.keys(gatewayGroups).length === 1) { const [gateway, communities] = Object.entries(gatewayGroups)[0]; config += `if (\n`; communities.forEach((community, index) => { config += `(bgp-communities includes ${community})`; if (index < communities.length - 1) { config += ` \nor `; } }); config += `\n)\n{\n set gw ${gateway}; accept;\n}\nelse\n{\n reject;\n}\n`; } else { // Если несколько gateway, создаем каскадную структуру const gatewayEntries = Object.entries(gatewayGroups); gatewayEntries.forEach(([gateway, communities], gatewayIndex) => { if (gatewayIndex === 0) { config += `if (\n`; } else { config += `else if (\n`; } communities.forEach((community, index) => { config += `(bgp-communities includes ${community})`; if (index < communities.length - 1) { config += ` \nor `; } }); config += `\n)\n{\n set gw ${gateway}; accept;\n}`; if (gatewayIndex === gatewayEntries.length - 1) { config += `\nelse\n{\n reject;\n}\n`; } else { 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`; // Если есть только один gateway, создаем простую конфигурацию if (Object.keys(gatewayGroups).length === 1) { const [gateway, communities] = Object.entries(gatewayGroups)[0]; config += `if (\n`; communities.forEach((community, index) => { config += `(bgp-communities includes ${community})`; if (index < communities.length - 1) { config += ` \nor `; } }); config += `\n)\n{\n set gw ${gateway}; accept;\n}\nelse\n{\n reject;\n}\n`; } else { // Если несколько gateway, создаем каскадную структуру const gatewayEntries = Object.entries(gatewayGroups); gatewayEntries.forEach(([gateway, communities], gatewayIndex) => { if (gatewayIndex === 0) { config += `if (\n`; } else { config += `else if (\n`; } communities.forEach((community, index) => { config += `(bgp-communities includes ${community})`; if (index < communities.length - 1) { config += ` \nor `; } }); config += `\n)\n{\n set gw ${gateway}; accept;\n}`; if (gatewayIndex === gatewayEntries.length - 1) { config += `\nelse\n{\n reject;\n}\n`; } else { 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'); } }); // 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}`); });