247 lines
7.9 KiB
TypeScript
247 lines
7.9 KiB
TypeScript
import axios from 'axios';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import { createHash } from 'crypto';
|
|
|
|
// Tentativa de importar o UAParser com fallback para evitar quebra caso não esteja instalado
|
|
let UAParser;
|
|
try {
|
|
UAParser = require('ua-parser-js');
|
|
} catch (error) {
|
|
console.warn('Aviso: UAParser não está disponível. Usando método alternativo para analisar User-Agent.');
|
|
// Implementação simples de fallback para quando a biblioteca não está disponível
|
|
UAParser = class SimpleBrowserDetect {
|
|
constructor(userAgent) {
|
|
this.userAgent = userAgent;
|
|
}
|
|
|
|
getResult() {
|
|
const isMobile = /mobile|android|iphone|ipod|ipad/i.test(this.userAgent);
|
|
const isWindows = /windows/i.test(this.userAgent);
|
|
const isMac = /macintosh|mac os/i.test(this.userAgent);
|
|
const isLinux = /linux/i.test(this.userAgent);
|
|
|
|
const chromeMatch = this.userAgent.match(/chrome\/(\d+)/i);
|
|
const firefoxMatch = this.userAgent.match(/firefox\/(\d+)/i);
|
|
const safariMatch = this.userAgent.match(/safari\/(\d+)/i);
|
|
const edgeMatch = this.userAgent.match(/edge\/(\d+)|edg\/(\d+)/i);
|
|
|
|
return {
|
|
browser: {
|
|
name: edgeMatch ? 'Edge' :
|
|
chromeMatch ? 'Chrome' :
|
|
firefoxMatch ? 'Firefox' :
|
|
safariMatch ? 'Safari' : 'Desconhecido',
|
|
version: (edgeMatch || chromeMatch || firefoxMatch || safariMatch || ['', '0'])[1],
|
|
major: (edgeMatch || chromeMatch || firefoxMatch || safariMatch || ['', '0'])[1],
|
|
},
|
|
engine: {
|
|
name: 'Desconhecido',
|
|
version: '',
|
|
},
|
|
os: {
|
|
name: isWindows ? 'Windows' :
|
|
isMac ? 'Mac OS' :
|
|
isLinux ? 'Linux' : 'Desconhecido',
|
|
version: '',
|
|
},
|
|
device: {
|
|
vendor: '',
|
|
model: '',
|
|
type: isMobile ? 'mobile' : 'desktop',
|
|
},
|
|
cpu: {
|
|
architecture: '',
|
|
}
|
|
};
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Classe para coletar e validar metadados de arquivos conforme RFC 3227
|
|
* (Diretrizes para Coleta e Arquivamento de Evidências)
|
|
*/
|
|
export class MetadataCollector {
|
|
/**
|
|
* Coleta metadados avançados a partir dos dados fornecidos
|
|
*/
|
|
async collectMetadata(data: {
|
|
userAgent: string;
|
|
ipAddress: string;
|
|
timestamp: Date;
|
|
fileName: string;
|
|
fileModified?: Date;
|
|
fileSize: number;
|
|
viewportWidth?: string;
|
|
viewportHeight?: string;
|
|
language?: string;
|
|
referrer?: string;
|
|
latitude?: number;
|
|
longitude?: number;
|
|
}) {
|
|
try {
|
|
// Gerar identificador único para a coleta
|
|
const collectionId = uuidv4();
|
|
const collectionTimestamp = new Date();
|
|
|
|
// Parser para informações do navegador
|
|
const parser = new UAParser(data.userAgent);
|
|
const browserInfo = parser.getResult();
|
|
|
|
// Obter geolocalização a partir do IP se não fornecida diretamente
|
|
let geoInfo = null;
|
|
if (!data.latitude && !data.longitude) {
|
|
try {
|
|
geoInfo = await this.getGeolocationFromIP(data.ipAddress);
|
|
} catch (error) {
|
|
console.warn('Falha ao obter geolocalização:', error);
|
|
}
|
|
}
|
|
|
|
// Construir o objeto de metadados
|
|
return {
|
|
// Identificação da coleta
|
|
collectionId,
|
|
collectionTimestamp: collectionTimestamp.toISOString(),
|
|
|
|
// Dados do usuário
|
|
userInfo: {
|
|
ipAddress: data.ipAddress,
|
|
userAgent: data.userAgent,
|
|
language: data.language || 'não informado',
|
|
browser: {
|
|
name: browserInfo.browser.name || 'desconhecido',
|
|
version: browserInfo.browser.version || 'desconhecido',
|
|
major: browserInfo.browser.major || 'desconhecido',
|
|
},
|
|
engine: {
|
|
name: browserInfo.engine.name || 'desconhecido',
|
|
version: browserInfo.engine.version || 'desconhecido',
|
|
},
|
|
os: {
|
|
name: browserInfo.os.name || 'desconhecido',
|
|
version: browserInfo.os.version || 'desconhecido',
|
|
},
|
|
device: {
|
|
vendor: browserInfo.device.vendor || 'desconhecido',
|
|
model: browserInfo.device.model || 'desconhecido',
|
|
type: browserInfo.device.type || 'desconhecido',
|
|
},
|
|
cpu: {
|
|
architecture: browserInfo.cpu.architecture || 'desconhecido',
|
|
},
|
|
viewportSize: data.viewportWidth && data.viewportHeight
|
|
? `${data.viewportWidth}x${data.viewportHeight}`
|
|
: 'não informado',
|
|
referrer: data.referrer || 'não informado',
|
|
},
|
|
|
|
// Dados do arquivo
|
|
fileInfo: {
|
|
name: data.fileName,
|
|
size: data.fileSize,
|
|
sizeFormatted: this.formatFileSize(data.fileSize),
|
|
modifiedDate: data.fileModified ? data.fileModified.toISOString() : 'não informado',
|
|
},
|
|
|
|
// Dados de localização
|
|
locationInfo: geoInfo || {
|
|
latitude: data.latitude || null,
|
|
longitude: data.longitude || null,
|
|
city: 'não disponível',
|
|
region: 'não disponível',
|
|
country: 'não disponível',
|
|
timezone: 'não disponível'
|
|
},
|
|
|
|
// Timestamp oficial do sistema
|
|
timestamp: data.timestamp.toISOString(),
|
|
|
|
// Hash da coleta para garantir integridade
|
|
integrityHash: this.generateIntegrityHash(
|
|
data.ipAddress,
|
|
data.userAgent,
|
|
data.fileName,
|
|
data.fileSize.toString(),
|
|
collectionTimestamp.toISOString()
|
|
)
|
|
};
|
|
} catch (error) {
|
|
console.error('Erro ao coletar metadados:', error);
|
|
|
|
// Retornar metadados simplificados se algo der errado
|
|
return {
|
|
timestamp: new Date().toISOString(),
|
|
userAgent: data.userAgent,
|
|
ipAddress: data.ipAddress,
|
|
fileName: data.fileName,
|
|
fileSize: data.fileSize,
|
|
error: 'Falha na coleta completa de metadados'
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Formata o tamanho do arquivo para exibição
|
|
*/
|
|
private formatFileSize(bytes: number): string {
|
|
if (bytes < 1024) return bytes + ' bytes';
|
|
else if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
|
|
else if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
|
|
else return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
|
}
|
|
|
|
/**
|
|
* Gera um hash de integridade para os metadados coletados
|
|
*/
|
|
private generateIntegrityHash(...inputs: string[]): string {
|
|
try {
|
|
return createHash('sha256')
|
|
.update(inputs.join('|'))
|
|
.digest('hex');
|
|
} catch (error) {
|
|
console.error('Erro ao gerar hash de integridade:', error);
|
|
return 'hash-error-' + Date.now();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obtém informações de geolocalização a partir do endereço IP
|
|
*/
|
|
private async getGeolocationFromIP(ipAddress: string) {
|
|
try {
|
|
// Apenas para endereços IP públicos, ignorar IPs locais
|
|
if (ipAddress === '127.0.0.1' || ipAddress === '::1' || ipAddress.startsWith('192.168.') || ipAddress.startsWith('10.')) {
|
|
return null;
|
|
}
|
|
|
|
// Usar API pública de geolocalização
|
|
const response = await axios.get(`https://ipinfo.io/${ipAddress}/json`);
|
|
const data = response.data;
|
|
|
|
// Extrair coordenadas (formato: "lat,long")
|
|
let latitude = null;
|
|
let longitude = null;
|
|
|
|
if (data.loc) {
|
|
const [lat, lng] = data.loc.split(',');
|
|
latitude = parseFloat(lat);
|
|
longitude = parseFloat(lng);
|
|
}
|
|
|
|
return {
|
|
latitude,
|
|
longitude,
|
|
city: data.city || 'não disponível',
|
|
region: data.region || 'não disponível',
|
|
country: data.country || 'não disponível',
|
|
timezone: data.timezone || 'não disponível',
|
|
provider: 'ipinfo.io'
|
|
};
|
|
} catch (error) {
|
|
console.error('Erro ao obter geolocalização:', error);
|
|
return null;
|
|
}
|
|
}
|
|
}
|