506 lines
17 KiB
TypeScript
506 lines
17 KiB
TypeScript
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 });
|
|
}
|
|
};
|