import { prisma } from '../prismaClient'; import { createHash } from 'crypto'; import { PDFDocument, StandardFonts } from 'pdf-lib'; import sharp from 'sharp'; import * as fs from 'fs/promises'; import * as path from 'path'; import { execSync } from 'child_process'; import { BlockchainService } from './BlockchainService'; import { ipDetectionMiddleware } from '../middleware/ipDetection'; import { FileValidator } from './FileValidator'; import { ReportGenerator } from './ReportGenerator'; // Adicionar import import { Request, Response } from 'express'; // Importar Request e Response // Tentativa de importar o MetadataCollector com fallback let MetadataCollectorClass; try { MetadataCollectorClass = require('./MetadataCollector').MetadataCollector; } catch (error) { console.warn('Aviso: MetadataCollector não está disponível, usando versão simplificada'); MetadataCollectorClass = require('./FallbackMetadataCollector').FallbackMetadataCollector; } export class ProvaService { private fileValidator: FileValidator; private reportGenerator: ReportGenerator; private storageDir: string; private metadataCollector: any; constructor() { this.fileValidator = new FileValidator(); this.reportGenerator = new ReportGenerator(); this.storageDir = path.join(process.cwd(), 'storage', 'provas'); // Inicializar o coletor de metadados com tratamento de erro try { this.metadataCollector = new MetadataCollectorClass(); } catch (error) { console.error('Erro ao inicializar MetadataCollector, usando objeto vazio:', error); this.metadataCollector = { collectMetadata: async (data: any) => ({ ...data, error: 'Falha na coleta de metadados' }) }; } this.ensureStorageDir(); } private async ensureStorageDir() { try { await fs.mkdir(this.storageDir, { recursive: true }); } catch (error) { console.error('Erro ao criar diretório de armazenamento:', error); } } async processFile(file: Express.Multer.File, metadata: any) { try { // 1. Validar arquivo e metadados const validationResult = await this.fileValidator.validateFile(file.buffer, file.mimetype); if (!validationResult.valid) { throw new Error(validationResult.error); } const metadataValidation = await this.fileValidator.validateMetadata(metadata); if (!metadataValidation.valid) { throw new Error(metadataValidation.error); } // 2. Coletar metadados avançados (com tratamento de erro) let enhancedMetadata; try { // Garantir que estamos usando o IP real do cliente const clientIP = this.extractRealClientIP(metadata.ipAddress, metadata); enhancedMetadata = await this.metadataCollector.collectMetadata({ userAgent: metadata.userAgent, ipAddress: clientIP, timestamp: new Date(metadata.timestamp), fileName: file.originalname, fileModified: metadata.modification_times ? new Date(parseInt(metadata.modification_times)) : undefined, fileSize: file.size, viewportWidth: metadata.viewportWidth, viewportHeight: metadata.viewportHeight, language: metadata.language, referrer: metadata.referrer, latitude: metadata.latitude ? parseFloat(metadata.latitude) : undefined, longitude: metadata.longitude ? parseFloat(metadata.longitude) : undefined }); } catch (error) { console.error('Erro ao coletar metadados avançados:', error); enhancedMetadata = { ...metadata, error: 'Falha na coleta de metadados avançados', fallback: true }; } // 3. Gerar checksums const checksums = this.generateChecksums(file.buffer); // 4. Registrar timestamp const timestamp = await this.createLocalTimestamp(file.buffer, checksums.sha256); // 5. Processar arquivo const processedFile = await this.processFileByType(file); // 6. Gerar URL de verificação const verificationUrl = `${process.env.PUBLIC_URL || 'http://localhost:5000'}/verificar/${checksums.sha256}`; // 7. Gerar relatório const reportResult = await this.reportGenerator.generateReport({ file, checksums, timestamp, metadata: { ...enhancedMetadata, // Garantir que as informações de IP estejam disponíveis ipAddress: enhancedMetadata.ipAddress, headers: metadata.headers, connectionInfo: metadata.connectionInfo, clientPort: metadata.clientPort }, processedFile, verificationUrl }); // 8. Salvar relatório const reportPath = path.join(this.storageDir, `${checksums.sha256}_report.pdf`); await fs.writeFile(reportPath, reportResult.buffer); // 9. Salvar no banco de dados const prova = await prisma.prova.create({ data: { fileName: file.originalname, mimeType: file.mimetype, checksumSha256: checksums.sha256, checksumSha512: checksums.sha512, checksumMd5: checksums.md5, metadata: enhancedMetadata, reportPath: reportPath, status: 'CONCLUIDO', timestamp: JSON.stringify(timestamp) } }); // 10. Salvar o arquivo original const originalFilePath = path.join(this.storageDir, `${checksums.sha256}_original`); await fs.writeFile(originalFilePath, file.buffer); // 11. Registrar log forense await this.logForensicInfo(file, enhancedMetadata, checksums); return prova; } catch (error) { console.error('Erro ao processar arquivo:', error); throw error; } finally { // Limpar recursos await this.fileValidator.cleanup(); } } /** * Extrai o endereço IP real do cliente, ignorando IPs locais quando possível * @param ipAddress O endereço IP fornecido na requisição * @param metadata Metadados adicionais para análise * @returns O endereço IP real do cliente */ private extractRealClientIP(ipAddress: string, metadata?: any): string { // Verificar se temos dados válidos para trabalhar if (!metadata) { return ipAddress || '0.0.0.0'; } console.log('Tentando extrair IP real. IP original:', ipAddress); // Lista de IPs locais/internos que devem ser evitados const localIPs = ['::1', '127.0.0.1', 'localhost', '::ffff:127.0.0.1', '::ffff:10.15.0.1']; // Verificar PRIMEIRO os headers HTTP que são a fonte mais confiável em produção if (metadata.headers) { // Headers comuns que podem conter o IP real if (metadata.headers['x-forwarded-for']) { const forwardedIps = metadata.headers['x-forwarded-for'].split(',').map(ip => ip.trim()); if (forwardedIps.length > 0 && !localIPs.includes(forwardedIps[0])) { console.log('Usando IP do x-forwarded-for:', forwardedIps[0]); return forwardedIps[0]; } } // Outros headers comuns const otherHeaders = ['x-real-ip', 'cf-connecting-ip', 'true-client-ip']; for (const header of otherHeaders) { if (metadata.headers[header] && !localIPs.includes(metadata.headers[header])) { console.log(`Usando IP do header ${header}:`, metadata.headers[header]); return metadata.headers[header]; } } } // Verificar nas informações de conexão if (metadata.connectionInfo && metadata.connectionInfo.forwardedIp) { if (!localIPs.includes(metadata.connectionInfo.forwardedIp)) { console.log('Usando IP encaminhado da conexão:', metadata.connectionInfo.forwardedIp); return metadata.connectionInfo.forwardedIp; } } // Se o IP não é local, usá-lo if (ipAddress && !localIPs.includes(ipAddress)) { console.log('Usando IP original:', ipAddress); return ipAddress; } // Se chegamos aqui, temos apenas IPs locais. Usar o IP com a porta como identificador const port = metadata.clientPort || metadata.connectionInfo?.socketPort || metadata.porta; if (port) { const ipWithPort = `${ipAddress || '127.0.0.1'}:${port}`; console.log('Usando identificador IP:porta:', ipWithPort); return ipWithPort; } // NUNCA retornar 0.0.0.0 - se nada funcionar, usar 127.0.0.1 com timestamp const fallbackIp = `127.0.0.1:${Date.now() % 10000}`; console.log('Usando fallback:', fallbackIp); return fallbackIp; } private generateChecksums(buffer: Buffer) { return { sha256: createHash('sha256').update(buffer).digest('hex'), sha512: createHash('sha512').update(buffer).digest('hex'), md5: createHash('md5').update(buffer).digest('hex') }; } private async createLocalTimestamp(buffer: Buffer, hash: string) { try { console.log('Criando timestamp local...'); // 1. Tempo atual em formato RFC3339 const now = new Date(); const isoString = now.toISOString(); // 2. Obter tempo NTP do servidor oficial para maior confiabilidade let ntpTime = ''; try { // Detectar plataforma e usar o comando apropriado if (process.platform === 'win32') { // Windows ntpTime = execSync('w32tm /stripchart /computer:time.nist.gov /samples:1 /dataonly').toString(); } else { // Linux/Unix/MacOS - usar implementação alternativa // Tentar usar o comando 'date' para obter a hora do sistema ntpTime = execSync('date -u +"%Y-%m-%dT%H:%M:%S.%3NZ"').toString().trim(); } } catch (e) { console.warn('Não foi possível obter tempo NTP, usando tempo local:', e); } // 3. Criar arquivo de timestamp com metadados e assinatura const timestampData = { hash, timestamp: isoString, ntpReference: ntpTime || 'Indisponível', unixTimestamp: Math.floor(now.getTime() / 1000) }; // 4. Salvar o arquivo de timestamp const timestampPath = path.join(this.storageDir, `${hash}.timestamp.json`); await fs.writeFile(timestampPath, JSON.stringify(timestampData, null, 2)); return { ...timestampData, path: timestampPath }; } catch (error) { console.error('Erro ao criar timestamp local:', error); // Em caso de erro, retornar um timestamp básico sem interromper o processo const basicTimestamp = { hash, timestamp: new Date().toISOString(), ntpReference: 'Indisponível (erro)', unixTimestamp: Math.floor(Date.now() / 1000), error: error.message }; return basicTimestamp; } } private async processFileByType(file: Express.Multer.File) { if (file.mimetype.startsWith('image/')) { try { // Para JPEGs, converter sempre para PNG para evitar problemas if (file.mimetype === 'image/jpeg') { return await sharp(file.buffer) .png() // Converter para PNG é mais seguro .resize(1200, 900, { fit: 'inside', withoutEnlargement: true }) .toBuffer(); } // Para outras imagens, manter o formato original return await sharp(file.buffer) .resize(1200, 900, { fit: 'inside', withoutEnlargement: true }) .toBuffer(); } catch (error) { console.error('Erro ao processar imagem com sharp:', error); // Se sharp falhar, tentar abordagem mais simples try { console.log('Tentando método alternativo de processamento de imagem...'); // Converter diretamente para PNG sem redimensionamento return await sharp(file.buffer) .png() .toBuffer(); } catch (fallbackError) { console.error('Falha total no processamento da imagem:', fallbackError); // Em último caso, retornar o buffer original return file.buffer; } } } return file.buffer; } async verifyFile(hash: string, ipInfo?: any) { try { // 1. Buscar prova no banco de dados const prova = await prisma.prova.findFirst({ where: { checksumSha256: hash } }); if (!prova) { return { verified: false, message: 'Arquivo não encontrado na base de dados' }; } // 2. Verificar se o arquivo original existe const originalFilePath = path.join(this.storageDir, `${hash}_original`); try { await fs.access(originalFilePath); } catch (error) { return { verified: false, message: 'Arquivo original não encontrado no servidor', prova }; } // 3. Verificar se o timestamp existe const timestampPath = path.join(this.storageDir, `${hash}.timestamp.json`); try { await fs.access(timestampPath); } catch (error) { return { verified: false, message: 'Timestamp não encontrado', prova }; } // 4. Ler o timestamp const timestampData = JSON.parse(await fs.readFile(timestampPath, 'utf-8')); // 5. Se fornecido, incluir informações de IP no registro if (ipInfo) { // Registrar essa verificação try { await this.logVerification(hash, ipInfo); } catch (logError) { console.error('Erro ao registrar verificação:', logError); } } return { verified: true, message: 'Arquivo verificado com sucesso', prova, timestamp: timestampData }; } catch (error) { console.error('Erro ao verificar arquivo:', error); return { verified: false, message: `Erro ao verificar: ${error.message}` }; } } // Método para registrar verificações private async logVerification(hash: string, ipInfo: any) { try { const verificationLog = { timestamp: new Date().toISOString(), hash, ip: ipInfo.ip || '127.0.0.1', porta: ipInfo.porta || '0', userAgent: ipInfo.userAgent || 'Desconhecido' }; const logPath = path.join(this.storageDir, `${hash}_verifications.json`); // Verificar se o arquivo já existe let existingLogs = []; try { const existingData = await fs.readFile(logPath, 'utf-8'); existingLogs = JSON.parse(existingData); if (!Array.isArray(existingLogs)) { existingLogs = []; } } catch (e) { // Arquivo não existe, criar novo } // Adicionar nova verificação existingLogs.push(verificationLog); // Salvar arquivo await fs.writeFile(logPath, JSON.stringify(existingLogs, null, 2)); console.log(`Verificação registrada para ${hash} de IP ${ipInfo.ip}`); } catch (error) { console.error('Erro ao registrar verificação:', error); } } // Novo método para registrar informações forenses private async logForensicInfo(file: Express.Multer.File, metadata: any, checksums: any) { try { const logEntry = { timestamp: new Date().toISOString(), fileName: file.originalname, fileSize: file.size, mimeType: file.mimetype, checksums, metadata, processingInfo: { processingNode: require('os').hostname(), processingUser: process.env.USER || process.env.USERNAME || 'unknown', processingTimestamp: new Date().toISOString(), systemInfo: { platform: process.platform, release: require('os').release(), cpus: require('os').cpus().length, memory: Math.round(require('os').totalmem() / (1024 * 1024 * 1024)) + 'GB', uptime: Math.round(require('os').uptime() / 3600) + 'h', } } }; const logPath = path.join(this.storageDir, `${checksums.sha256}_forensic.json`); await fs.writeFile(logPath, JSON.stringify(logEntry, null, 2)); console.log('Log forense registrado em:', logPath); } catch (error) { console.error('Erro ao registrar log forense:', error); // Não interromper o processamento por falha no log } } } // No handler de upload de arquivo: export const uploadProva = async (req: Request, res: Response) => { try { const file = req.file; if (!file) { return res.status(400).json({ error: 'Nenhum arquivo enviado' }); } // Coletar todas as informações possíveis sobre o cliente const metadata = { ipAddress: req.ip, // Express geralmente fornece isso remoteAddress: req.socket?.remoteAddress, // Endereço IP direto do socket porta: req.socket?.remotePort?.toString(), // Porta de conexão do cliente userAgent: req.headers['user-agent'], headers: req.headers, // Todos os headers HTTP timestamp: new Date().toISOString(), // Outros metadados do formulário ...req.body }; console.log('Metadados de conexão:', { ip: req.ip, remoteAddress: req.socket?.remoteAddress, remotePort: req.socket?.remotePort, headers: { 'x-forwarded-for': req.headers['x-forwarded-for'], 'x-real-ip': req.headers['x-real-ip'] } }); const provaService = new ProvaService(); const result = await provaService.processFile(file, metadata); return res.status(200).json(result); } catch (error) { console.error('Erro ao processar upload de prova:', error); return res.status(500).json({ error: error.message }); } };