Files
webclone/web-interface/server.js
T

523 lines
19 KiB
JavaScript

const express = require('express');
const path = require('path');
const cors = require('cors');
const axios = require('axios');
const { exec, spawn } = require('child_process');
const fs = require('fs');
const app = express();
const PORT = process.env.PORT || 3000;
// Storage manager routes
const storageManager = require('./storage-manager');
app.use('/api/clones', storageManager);
// Security headers
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
next();
});
// Middleware
app.use(cors());
app.use(express.json());
// Serve static files with proper MIME types
app.use(express.static(path.join(__dirname, 'public'), {
setHeaders: (res, path) => {
if (path.endsWith('.css')) res.setHeader('Content-Type', 'text/css');
if (path.endsWith('.js')) res.setHeader('Content-Type', 'application/javascript');
if (path.endsWith('.html')) res.setHeader('Content-Type', 'text/html');
}
}));
// Middleware para injetar fix de roteador em sites clonados (SPAs como React, Vue, Angular, Vite)
app.use((req, res, next) => {
if (!req.path.startsWith('/api') && req.path !== '/' && req.path !== '/old') {
let urlPath = req.path;
// Remover prefixo /cloned-sites se existir para buscar corretamente no disco
if (urlPath.startsWith('/cloned-sites/')) {
urlPath = urlPath.replace('/cloned-sites/', '/');
}
if (!urlPath.match(/\.[a-zA-Z0-9]+$/)) {
if (!urlPath.endsWith('/')) urlPath += '/';
urlPath += 'index.html';
}
if (urlPath.endsWith('.html')) {
const filePath = path.join(__dirname, '..', 'cloned-sites', urlPath);
if (fs.existsSync(filePath)) {
try {
let html = fs.readFileSync(filePath, 'utf8');
if (!html.includes('__CLONEWEB_ROUTER_FIX__')) {
const injectScript = `
<script id="__CLONEWEB_ROUTER_FIX__">
(function() {
try {
const originalPathname = Object.getOwnPropertyDescriptor(Location.prototype, 'pathname').get;
const originalHref = Object.getOwnPropertyDescriptor(Location.prototype, 'href').get;
Object.defineProperty(Location.prototype, 'pathname', {
get: function() {
const path = originalPathname.call(this);
if (path !== '/' && path !== '/index.html' && (path.includes('_202') || path.includes('/cloned-sites/'))) {
return '/';
}
return path;
},
set: function(val) {
window.location.assign(val);
}
});
Object.defineProperty(Location.prototype, 'href', {
get: function() {
const href = originalHref.call(this);
if (href.includes('_202') || href.includes('/cloned-sites/')) {
const url = new URL(href);
return url.protocol + '//' + url.host + '/';
}
return href;
}
});
const OriginalURL = window.URL;
window.URL = function(url, base) {
if (typeof url === 'string' && (url.includes('_202') || url.includes('/cloned-sites/'))) {
url = url.replace(/\\/cloned-sites\\/[^/]+/gi, '').replace(/\\/[^/]+_202[0-9-_T:.]+(\\/index\\.html)?/gi, '/');
}
return new OriginalURL(url, base);
};
window.URL.prototype = OriginalURL.prototype;
Object.assign(window.URL, OriginalURL);
console.log('🚀 [CloneWeb] Ultimate SPA Router fix initialized. Simulating root pathname /');
} catch(e) {
console.error('❌ [CloneWeb] Failed to initialize router fix:', e);
}
})();
</script>
`;
if (html.includes('<head>')) {
html = html.replace('<head>', '<head>' + injectScript);
} else {
html = injectScript + html;
}
}
res.setHeader('Content-Type', 'text/html');
return res.send(html);
} catch (err) {
console.error('Erro ao injetar fix no HTML do clone:', err);
}
}
}
}
next();
});
// Serve cloned sites directory
app.use(express.static(path.join(__dirname, '..', 'cloned-sites'), {
setHeaders: (res, filePath) => {
if (filePath.endsWith('.css')) res.setHeader('Content-Type', 'text/css');
if (filePath.endsWith('.js')) res.setHeader('Content-Type', 'application/javascript');
}
}));
// Proxy para o crawler service - SEM TIMEOUT
app.post('/api/crawl', async (req, res) => {
try {
console.log('🚀 Proxying crawl request to crawler service...');
console.log('⏳ Clonagem pode demorar vários minutos para sites grandes...');
// SEM TIMEOUT - permite clonagens longas
const response = await axios.post('http://localhost:3001/crawl', req.body, {
timeout: 0 // Sem limite de tempo
});
console.log('✅ Crawl completed successfully');
res.json(response.data);
} catch (error) {
console.error('❌ Crawl error:', error.message);
res.status(500).json({
success: false,
error: error.response?.data?.error || error.message
});
}
});
// Proxy para o wget cloner - SEM TIMEOUT para clonagens grandes
app.post('/api/wget-clone', async (req, res) => {
try {
console.log('🔧 Proxying wget clone request...');
console.log('⏳ Clonagem pode demorar vários minutos para sites grandes...');
// SEM TIMEOUT - permite clonagens que demoram mais de 10 minutos
const response = await axios.post('http://localhost:3002/wget-clone', req.body, {
timeout: 0 // Sem limite de tempo
});
console.log('✅ Wget clone completed successfully');
// Sync to host /root/Desktop/Clonados
const directory = response.data.data?.directory || response.data.directory;
if (response.data && response.data.success && directory) {
const { exec } = require('child_process');
const containerId = require('os').hostname();
const cloneDir = directory.split('/').pop();
const hostDir = '/root/Desktop/Clonados';
exec(`mkdir -p ${hostDir} && cp -r ${directory} ${hostDir}/ 2>/dev/null || docker cp ${containerId}:${directory} ${hostDir}/ 2>/dev/null`, (err) => {
if (err) console.log('⚠️ Sync to host failed:', err.message);
else console.log('✅ Clone synced to host Desktop:', cloneDir);
});
}
res.json(response.data);
} catch (error) {
console.error('❌ Wget clone error:', error.message);
res.status(500).json({
success: false,
error: error.response?.data?.error || error.message
});
}
});
// Smart Clone Endpoint (Local Spawn)
app.post('/api/smart-clone', (req, res) => {
const { url } = req.body;
if (!url) {
return res.status(400).json({ error: 'URL is required' });
}
console.log('🚀 Starting Smart Clone for:', url);
console.log('⏳ This uses Puppeteer and may take a while...');
// Spawn the smart cloner process
const scriptPath = path.join(__dirname, '..', 'simple-crawler', 'smart-cloner.js');
console.log(`[SmartClone] Executing: node "${scriptPath}" --url="${url}"`);
const child = spawn('node', [scriptPath, `--url=${url}`], {
cwd: path.join(__dirname, '..'),
shell: true
});
let output = '';
let errorOutput = '';
child.stdout.on('data', (data) => {
const text = data.toString();
output += text;
console.log(`[SmartClone STDOUT] ${text.trim()}`);
});
child.stderr.on('data', (data) => {
const text = data.toString();
errorOutput += text;
console.error(`[SmartClone STDERR] ${text.trim()}`);
});
child.on('error', (err) => {
console.error('[SmartClone SPAWN ERROR]', err);
if (!res.headersSent) {
res.status(500).json({ success: false, error: 'Failed to start cloning process', details: err.message });
}
});
child.on('close', (code) => {
console.log(`[SmartClone] Process exited with code ${code}`);
if (res.headersSent) return;
try {
// Find the last JSON object in output
const matches = output.match(/\{[\s\S]*\}/g);
if (matches && matches.length > 0) {
const lastMatch = matches[matches.length - 1];
const result = JSON.parse(lastMatch);
// Sync to host /root/Desktop/Clonados
const directory = result.data?.directory || result.directory || result.path;
if (result.success && directory) {
const { exec } = require('child_process');
const containerId = require('os').hostname();
const cloneDir = directory.split('/').pop();
const hostDir = '/root/Desktop/Clonados';
exec(`mkdir -p ${hostDir} && cp -r ${directory} ${hostDir}/ 2>/dev/null || docker cp ${containerId}:${directory} ${hostDir}/ 2>/dev/null`, (err) => {
if (err) console.log('⚠️ SmartClone sync to host failed:', err.message);
else console.log('✅ SmartClone synced to host Desktop:', cloneDir);
});
}
res.json({
success: result.success,
data: result,
error: result.error
});
} else {
throw new Error('No valid JSON output found from cloner');
}
} catch (e) {
console.error('[SmartClone PARSE ERROR]', e.message);
if (code === 0) {
res.json({ success: true, message: 'Process finished but output parsing failed', fullOutput: output });
} else {
res.status(500).json({ success: false, error: 'Clone failed or output was invalid', details: errorOutput });
}
}
});
});
// Health check do wget cloner (porta 3002)
app.get('/api/health', async (req, res) => {
try {
const response = await axios.get('http://localhost:3002/health', {
timeout: 5000
});
res.json(response.data);
} catch (error) {
res.status(500).json({
status: 'error',
message: 'WGET Cloner service unavailable'
});
}
});
// Rota de visualização de arquivos da pasta clonada (Gerenciador de Arquivos Web Embutido)
app.get('/api/browse/:folder', (req, res) => {
try {
const folderName = req.params.folder;
const folderPath = path.join(__dirname, '..', 'cloned-sites', folderName);
if (!fs.existsSync(folderPath)) {
return res.status(404).send('<h1>Pasta não encontrada</h1><p>A pasta solicitada não existe no servidor.</p>');
}
const items = fs.readdirSync(folderPath);
let filesHtml = '';
items.forEach(item => {
const itemPath = path.join(folderPath, item);
const stat = fs.statSync(itemPath);
const isDir = stat.isDirectory();
const size = isDir ? '-' : (stat.size / 1024).toFixed(2) + ' KB';
const date = new Date(stat.mtime).toLocaleString('pt-BR');
const icon = isDir ? 'fa-folder' : (item.endsWith('.html') ? 'fa-file-code' : (item.endsWith('.json') ? 'fa-file-alt' : 'fa-file'));
const link = isDir ? `#` : `/${folderName}/${item}`;
filesHtml += `
<tr>
<td><i class="fas ${icon}" style="color: #4f46e5; margin-right: 10px;"></i> <a href="${link}" target="_blank" style="color: #1f2937; text-decoration: none; font-weight: 500;">${item}</a></td>
<td style="color: #6b7280;">${size}</td>
<td style="color: #6b7280;">${date}</td>
<td>
${!isDir ? `<a href="${link}" target="_blank" class="btn btn-secondary btn-sm" style="background: #e0e7ff; color: #4f46e5; padding: 5px 10px; border-radius: 6px; text-decoration: none; font-size: 14px;">Visualizar</a>` : ''}
${!isDir ? `<a href="${link}" download="${item}" class="btn btn-secondary btn-sm" style="background: #fee2e2; color: #ef4444; padding: 5px 10px; border-radius: 6px; text-decoration: none; font-size: 14px; margin-left: 5px;">Baixar</a>` : ''}
</td>
</tr>
`;
});
const html = `
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Arquivos Clonados - ${folderName}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; background: #f3f4f6; margin: 0; padding: 40px 20px; color: #1f2937; }
.container { max-width: 1000px; margin: 0 auto; background: white; border-radius: 16px; box-shadow: 0 10px 25px rgba(0,0,0,0.05); overflow: hidden; padding: 30px; }
.header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #e5e7eb; padding-bottom: 20px; margin-bottom: 25px; }
.header h1 { font-size: 24px; margin: 0; color: #111827; display: flex; align-items: center; gap: 12px; }
.actions { display: flex; gap: 12px; }
.btn { padding: 10px 20px; border-radius: 8px; font-weight: 600; text-decoration: none; display: inline-flex; align-items: center; gap: 8px; font-size: 14px; transition: all 0.2s; }
.btn-primary { background: #4f46e5; color: white; }
.btn-primary:hover { background: #4338ca; }
.btn-secondary { background: #f3f4f6; color: #374151; }
.btn-secondary:hover { background: #e5e7eb; }
table { width: 100%; border-collapse: collapse; text-align: left; }
th { padding: 16px; background: #f9fafb; font-weight: 600; color: #374151; border-bottom: 1px solid #e5e7eb; font-size: 14px; }
td { padding: 16px; border-bottom: 1px solid #e5e7eb; font-size: 15px; }
tr:hover { background: #f9fafb; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1><i class="fas fa-folder-open" style="color: #4f46e5;"></i> ${folderName}</h1>
<div class="actions">
<a href="/${folderName}/index.html" target="_blank" class="btn btn-primary"><i class="fas fa-external-link-alt"></i> Abrir Site Clonado</a>
<a href="/" class="btn btn-secondary"><i class="fas fa-arrow-left"></i> Voltar ao Dashboard</a>
</div>
</div>
<table>
<thead>
<tr>
<th>Nome do Ficheiro</th>
<th>Tamanho</th>
<th>Modificado em</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
${filesHtml}
</tbody>
</table>
</div>
</body>
</html>
`;
res.send(html);
} catch (error) {
console.error('Erro ao listar pasta:', error);
res.status(500).send('<h1>Erro interno do servidor</h1>');
}
});
// Abrir pasta do clone (mantém compatibilidade legada e retorna a URL do Web Browse)
app.post('/api/open-folder', (req, res) => {
try {
const { path: folderPath } = req.body;
if (!folderPath) {
return res.status(400).json({ error: 'Caminho da pasta é obrigatório' });
}
const folderName = path.basename(folderPath);
const browseUrl = `/api/browse/${folderName}`;
res.json({ success: true, message: 'Pasta acessível via Web', url: browseUrl });
} catch (error) {
console.error('Erro ao abrir pasta:', error);
res.status(500).json({ error: 'Erro interno do servidor' });
}
});
// Abrir site clonado via HTTP para evitar problemas de origem/CORS
app.post('/api/open-site', (req, res) => {
try {
const { path: sitePath } = req.body;
if (!sitePath) {
return res.status(400).json({ error: 'Caminho do site é obrigatório' });
}
const folderName = path.basename(sitePath);
const siteUrl = `/${folderName}/index.html`;
res.json({ success: true, message: 'Site aberto com sucesso', url: siteUrl });
} catch (error) {
console.error('Erro ao abrir site:', error);
res.status(500).json({ error: 'Erro interno do servidor' });
}
});
// Abrir página de navegação via HTTP
app.post('/api/open-navigation', (req, res) => {
try {
const { path: sitePath } = req.body;
if (!sitePath) {
return res.status(400).json({ error: 'Caminho do site é obrigatório' });
}
const folderName = path.basename(sitePath);
const navUrl = `http://localhost:${PORT}/${folderName}/navegacao.html`;
console.log(`🌍 Opening navigation via HTTP: ${navUrl}`);
// Abrir navegacao.html no navegador padrão
exec(`start "" "${navUrl}"`, (error) => {
if (error) {
console.error('Erro ao abrir navegação:', error);
return res.status(500).json({ error: 'Não foi possível abrir a navegação via HTTP' });
}
res.json({ success: true, message: 'Navegação aberta via HTTP com sucesso', url: navUrl });
});
} catch (error) {
console.error('Erro ao abrir navegação:', error);
res.status(500).json({ error: 'Erro interno do servidor' });
}
});
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index-new.html'));
});
// Serve old interface
app.get('/old', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// Serve favicon requests
app.get('/favicon.ico', (req, res) => {
res.redirect('/favicon.svg');
});
// Handle common missing resources
app.get('/index', (req, res) => {
res.redirect('/');
});
// AI Agent Route (MiniMax)
app.post('/api/ai/agent', async (req, res) => {
const { systemPrompt, userMessage } = req.body;
const apiKey = process.env.MINIMAX_API_KEY;
if (!apiKey) {
return res.status(500).json({ error: 'MINIMAX_API_KEY is missing in .env' });
}
try {
const response = await axios.post('https://api.minimax.io/v1/chat/completions', {
model: "MiniMax-M2.7",
messages: [
{ role: "system", content: systemPrompt || "Você é um assistente de inteligência artificial útil." },
{ role: "user", content: userMessage }
],
max_tokens: 2048
}, {
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
timeout: 90000 // 90s
});
let reply = response.data.choices?.[0]?.message?.content || 'Processamento concluído, porém sem retorno.';
// Strip <think> reasoning tags if present
reply = reply.replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
res.json({ success: true, data: reply });
} catch (error) {
console.error('MiniMax AI Error:', error.response?.data || error.message);
res.status(500).json({ error: error.message, details: error.response?.data });
}
});
// Handle 404s
app.use((req, res) => {
res.status(404).json({
error: 'Not Found',
message: `Route ${req.path} not found`
});
});
// Error handler
app.use((err, req, res, next) => {
console.error('Server error:', err);
res.status(500).json({
error: 'Internal Server Error',
message: err.message
});
});
app.listen(PORT, () => {
console.log(`🌐 CloneWeb Interface running on http://localhost:${PORT}`);
console.log(`🔗 Make sure crawler service is running on http://localhost:3001`);
console.log(`📁 Serving static files from: ${path.join(__dirname, 'public')}`);
});