import { getLocalIP } from './browserInfo'; export interface InformacoesDispositivo { ipAddress?: string; ipPublico?: string; ipLocal?: string; userAgent?: string; browser?: string; browserVersion?: string; engine?: string; sistemaOperacional?: string; osVersion?: string; arquitetura?: string; plataforma?: string; latitude?: number; longitude?: number; precisao?: number; altitude?: number | null; altitudeAccuracy?: number | null; heading?: number | null; speed?: number | null; confiabilidadeGPS?: number; // 0-1 suspeitaSpoofing?: boolean; motivoSuspeita?: string; endereco?: string; cidade?: string; estado?: string; pais?: string; timezone?: string; deviceType?: string; deviceModel?: string; screenResolution?: string; coresTela?: number; idioma?: string; isMobile?: boolean; isTablet?: boolean; isDesktop?: boolean; connectionType?: string; memoryInfo?: string; } /** * Detecta informações do navegador */ function detectarNavegador(): { browser: string; browserVersion: string; engine: string } { if (typeof navigator === 'undefined') { return { browser: 'Desconhecido', browserVersion: '', engine: '' }; } const ua = navigator.userAgent; let browser = 'Desconhecido'; let browserVersion = ''; let engine = ''; // Detectar engine if (ua.includes('Edg/')) { engine = 'EdgeHTML'; } else if (ua.includes('Chrome/')) { engine = 'Blink'; } else if (ua.includes('Firefox/')) { engine = 'Gecko'; } else if (ua.includes('Safari/') && !ua.includes('Chrome/')) { engine = 'WebKit'; } // Detectar navegador if (ua.includes('Edg/')) { browser = 'Edge'; const match = ua.match(/Edg\/(\d+)/); browserVersion = match ? match[1]! : ''; } else if (ua.includes('Chrome/') && !ua.includes('Edg/')) { browser = 'Chrome'; const match = ua.match(/Chrome\/(\d+)/); browserVersion = match ? match[1]! : ''; } else if (ua.includes('Firefox/')) { browser = 'Firefox'; const match = ua.match(/Firefox\/(\d+)/); browserVersion = match ? match[1]! : ''; } else if (ua.includes('Safari/') && !ua.includes('Chrome/')) { browser = 'Safari'; const match = ua.match(/Version\/(\d+)/); browserVersion = match ? match[1]! : ''; } else if (ua.includes('Opera/') || ua.includes('OPR/')) { browser = 'Opera'; const match = ua.match(/(?:Opera|OPR)\/(\d+)/); browserVersion = match ? match[1]! : ''; } return { browser, browserVersion, engine }; } /** * Detecta informações do sistema operacional */ function detectarSistemaOperacional(): { sistemaOperacional: string; osVersion: string; arquitetura: string; plataforma: string; } { if (typeof navigator === 'undefined') { return { sistemaOperacional: 'Desconhecido', osVersion: '', arquitetura: '', plataforma: '', }; } const ua = navigator.userAgent; const platform = navigator.platform || ''; let sistemaOperacional = 'Desconhecido'; let osVersion = ''; let arquitetura = ''; const plataforma = platform; // Detectar OS if (ua.includes('Windows NT')) { sistemaOperacional = 'Windows'; const match = ua.match(/Windows NT (\d+\.\d+)/); if (match) { const version = match[1]!; const versions: Record = { '10.0': '10/11', '6.3': '8.1', '6.2': '8', '6.1': '7', }; osVersion = versions[version] || version; } } else if (ua.includes('Mac OS X') || ua.includes('Macintosh')) { sistemaOperacional = 'macOS'; const match = ua.match(/Mac OS X (\d+[._]\d+)/); if (match) { osVersion = match[1]!.replace('_', '.'); } } else if (ua.includes('Linux')) { sistemaOperacional = 'Linux'; osVersion = 'Linux'; } else if (ua.includes('Android')) { sistemaOperacional = 'Android'; const match = ua.match(/Android (\d+(?:\.\d+)?)/); osVersion = match ? match[1]! : ''; } else if (ua.includes('iPhone') || ua.includes('iPad')) { sistemaOperacional = 'iOS'; const match = ua.match(/OS (\d+[._]\d+)/); if (match) { osVersion = match[1]!.replace('_', '.'); } } // Detectar arquitetura (se disponível) if ('cpuClass' in navigator) { arquitetura = (navigator as unknown as { cpuClass: string }).cpuClass; } return { sistemaOperacional, osVersion, arquitetura, plataforma }; } /** * Detecta tipo de dispositivo */ function detectarTipoDispositivo(): { deviceType: string; isMobile: boolean; isTablet: boolean; isDesktop: boolean; } { if (typeof navigator === 'undefined') { return { deviceType: 'Desconhecido', isMobile: false, isTablet: false, isDesktop: true, }; } const ua = navigator.userAgent; const isMobile = /Mobile|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua); const isTablet = /iPad|Android(?!.*Mobile)|Tablet/i.test(ua); const isDesktop = !isMobile && !isTablet; let deviceType = 'Desktop'; if (isTablet) { deviceType = 'Tablet'; } else if (isMobile) { deviceType = 'Mobile'; } return { deviceType, isMobile, isTablet, isDesktop }; } /** * Obtém informações da tela */ function obterInformacoesTela(): { screenResolution: string; coresTela: number } { if (typeof screen === 'undefined') { return { screenResolution: 'Desconhecido', coresTela: 0 }; } const screenResolution = `${screen.width}x${screen.height}`; const coresTela = screen.colorDepth || 24; return { screenResolution, coresTela }; } /** * Obtém informações de conexão */ async function obterInformacoesConexao(): Promise { if (typeof navigator === 'undefined' || !('connection' in navigator)) { return 'Desconhecido'; } const connection = (navigator as unknown as { connection?: { effectiveType?: string } }).connection; if (connection?.effectiveType) { return connection.effectiveType; } return 'Desconhecido'; } /** * Obtém informações de memória (se disponível) */ function obterInformacoesMemoria(): string { if (typeof navigator === 'undefined' || !('deviceMemory' in navigator)) { return 'Desconhecido'; } const deviceMemory = (navigator as unknown as { deviceMemory?: number }).deviceMemory; if (deviceMemory) { return `${deviceMemory} GB`; } return 'Desconhecido'; } /** * Calcula distância entre duas coordenadas (fórmula de Haversine) * Retorna distância em metros */ function calcularDistancia( lat1: number, lon1: number, lat2: number, lon2: number ): number { const R = 6371000; // Raio da Terra em metros const dLat = ((lat2 - lat1) * Math.PI) / 180; const dLon = ((lon2 - lon1) * Math.PI) / 180; const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos((lat1 * Math.PI) / 180) * Math.cos((lat2 * Math.PI) / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } /** * Obtém timezone aproximado por coordenadas */ function obterTimezonePorCoordenadas(latitude: number, longitude: number): string { // Pernambuco está em UTC-3 (America/Recife) if (longitude >= -45 && longitude <= -30 && latitude >= -10 && latitude <= 5) { return 'America/Recife'; // UTC-3 } // Fallback: usar timezone do sistema try { return Intl.DateTimeFormat().resolvedOptions().timeZone; } catch { return 'America/Recife'; // Default } } /** * Captura uma única leitura de localização com todas as propriedades disponíveis */ async function capturarLocalizacaoUnica( enableHighAccuracy: boolean = true, timeout: number = 10000 ): Promise<{ latitude?: number; longitude?: number; precisao?: number; altitude?: number | null; altitudeAccuracy?: number | null; heading?: number | null; speed?: number | null; timestamp?: number; confiabilidade: number; // 0-1 }> { return new Promise((resolve) => { if (typeof navigator === 'undefined' || !navigator.geolocation) { resolve({ confiabilidade: 0 }); return; } const timeoutId = setTimeout(() => { resolve({ confiabilidade: 0 }); }, timeout + 1000); navigator.geolocation.getCurrentPosition( (position) => { clearTimeout(timeoutId); const coords = position.coords; const { latitude, longitude, accuracy } = coords; // Validar coordenadas básicas if ( isNaN(latitude) || isNaN(longitude) || latitude === 0 || longitude === 0 || latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180 ) { resolve({ confiabilidade: 0 }); return; } // Calcular score de confiabilidade baseado em propriedades do GPS real const sinaisGPSReal = { temAltitude: coords.altitude !== null && coords.altitude !== 0, temAltitudeAccuracy: coords.altitudeAccuracy !== null && coords.altitudeAccuracy > 0, temHeading: coords.heading !== null && !isNaN(coords.heading), temSpeed: coords.speed !== null && !isNaN(coords.speed), precisaoBoa: accuracy < 20, // GPS real geralmente < 20m precisaoMedia: accuracy >= 20 && accuracy < 100, timestampPreciso: position.timestamp > 0 }; // Calcular confiabilidade: cada sinal adiciona pontos let pontos = 0; const maxPontos = 7; if (sinaisGPSReal.temAltitude) pontos += 1; if (sinaisGPSReal.temAltitudeAccuracy) pontos += 1; if (sinaisGPSReal.temHeading) pontos += 0.5; if (sinaisGPSReal.temSpeed) pontos += 0.5; if (sinaisGPSReal.precisaoBoa) pontos += 2; if (sinaisGPSReal.precisaoMedia) pontos += 1; if (sinaisGPSReal.timestampPreciso) pontos += 1; const confiabilidade = Math.min(pontos / maxPontos, 1); resolve({ latitude, longitude, precisao: accuracy, altitude: coords.altitude ?? null, altitudeAccuracy: coords.altitudeAccuracy ?? null, heading: coords.heading ?? null, speed: coords.speed ?? null, timestamp: position.timestamp, confiabilidade }); }, (error) => { clearTimeout(timeoutId); console.warn('Erro ao obter localização:', error.code, error.message); resolve({ confiabilidade: 0 }); }, { enableHighAccuracy, timeout, maximumAge: 0 // Sempre obter nova leitura } ); }); } /** * Obtém localização via GPS com múltiplas leituras para detectar spoofing * Apps de spoofing geralmente retornam valores idênticos em todas as leituras */ async function obterLocalizacaoMultipla(): Promise<{ latitude?: number; longitude?: number; precisao?: number; altitude?: number | null; altitudeAccuracy?: number | null; heading?: number | null; speed?: number | null; confiabilidade: number; // 0-1 suspeitaSpoofing: boolean; motivoSuspeita?: string; }> { if (typeof navigator === 'undefined' || !navigator.geolocation) { return { confiabilidade: 0, suspeitaSpoofing: true, motivoSuspeita: 'Geolocalização não suportada' }; } // Capturar 3 leituras com intervalo de 2 segundos entre elas const leituras: Array<{ lat: number; lon: number; precisao: number; altitude: number | null; confiabilidade: number; }> = []; for (let i = 0; i < 3; i++) { const leitura = await capturarLocalizacaoUnica(true, 8000); if (leitura.latitude && leitura.longitude && leitura.confiabilidade > 0) { leituras.push({ lat: leitura.latitude, lon: leitura.longitude, precisao: leitura.precisao || 999, altitude: leitura.altitude ?? null, confiabilidade: leitura.confiabilidade }); } // Aguardar 2 segundos entre leituras (exceto na última) if (i < 2) { await new Promise((resolve) => setTimeout(resolve, 2000)); } } if (leituras.length === 0) { return { confiabilidade: 0, suspeitaSpoofing: true, motivoSuspeita: 'Não foi possível obter localização' }; } // Se tivermos menos de 2 leituras, usar única leitura com baixa confiança if (leituras.length < 2) { const unica = leituras[0]; return { latitude: unica.lat, longitude: unica.lon, precisao: unica.precisao, altitude: unica.altitude, altitudeAccuracy: null, heading: null, speed: null, confiabilidade: unica.confiabilidade * 0.5, // Reduzir confiança por ter apenas 1 leitura suspeitaSpoofing: true, motivoSuspeita: 'Apenas uma leitura obtida' }; } // Verificar se todas as leituras são idênticas (suspeito de spoofing) const primeiraLeitura = leituras[0]; const todasIguais = leituras.every( (l) => Math.abs(l.lat - primeiraLeitura.lat) < 0.00001 && // ~1 metro Math.abs(l.lon - primeiraLeitura.lon) < 0.00001 ); if (todasIguais && leituras.length === 3) { // GPS real varia alguns metros, se todas são idênticas pode ser spoofing return { latitude: primeiraLeitura.lat, longitude: primeiraLeitura.lon, precisao: primeiraLeitura.precisao, altitude: primeiraLeitura.altitude, altitudeAccuracy: null, heading: null, speed: null, confiabilidade: primeiraLeitura.confiabilidade * 0.4, // Reduzir drasticamente confiança suspeitaSpoofing: true, motivoSuspeita: 'Todas as leituras são idênticas (GPS real varia alguns metros)' }; } // Calcular média das leituras e variância const mediaLat = leituras.reduce((sum, l) => sum + l.lat, 0) / leituras.length; const mediaLon = leituras.reduce((sum, l) => sum + l.lon, 0) / leituras.length; const mediaConfianca = leituras.reduce((sum, l) => sum + l.confiabilidade, 0) / leituras.length; // Calcular distância máxima entre leituras let distanciaMaxima = 0; for (let i = 0; i < leituras.length; i++) { for (let j = i + 1; j < leituras.length; j++) { const dist = calcularDistancia( leituras[i].lat, leituras[i].lon, leituras[j].lat, leituras[j].lon ); distanciaMaxima = Math.max(distanciaMaxima, dist); } } // Se distância máxima for muito grande (> 100m), pode indicar problemas const suspeitoPorDistancia = distanciaMaxima > 100; return { latitude: mediaLat, longitude: mediaLon, precisao: primeiraLeitura.precisao, altitude: primeiraLeitura.altitude, altitudeAccuracy: null, heading: null, speed: null, confiabilidade: suspeitoPorDistancia ? mediaConfianca * 0.6 : mediaConfianca, suspeitaSpoofing: suspeitoPorDistancia, motivoSuspeita: suspeitoPorDistancia ? `Variação muito grande entre leituras (${Math.round(distanciaMaxima)}m)` : undefined }; } /** * Obtém localização via GPS com múltiplas tentativas e validações anti-spoofing */ async function obterLocalizacao(): Promise<{ latitude?: number; longitude?: number; precisao?: number; altitude?: number | null; altitudeAccuracy?: number | null; heading?: number | null; speed?: number | null; confiabilidadeGPS?: number; suspeitaSpoofing?: boolean; motivoSuspeita?: string; endereco?: string; cidade?: string; estado?: string; pais?: string; }> { if (typeof navigator === 'undefined' || !navigator.geolocation) { console.warn('Geolocalização não suportada'); return {}; } // Usar múltiplas leituras para detectar spoofing const localizacaoMultipla = await obterLocalizacaoMultipla(); if (!localizacaoMultipla.latitude || !localizacaoMultipla.longitude) { console.warn('Não foi possível obter localização'); return { confiabilidadeGPS: 0, suspeitaSpoofing: true, motivoSuspeita: 'Não foi possível obter localização' }; } const { latitude, longitude, precisao, altitude, altitudeAccuracy, heading, speed, confiabilidade, suspeitaSpoofing, motivoSuspeita } = localizacaoMultipla; // Tentar obter endereço via reverse geocoding let endereco = ''; let cidade = ''; let estado = ''; let pais = ''; try { const response = await fetch( `https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}&zoom=18&addressdetails=1`, { headers: { 'User-Agent': 'SGSE-App/1.0' } } ); if (response.ok) { const data = (await response.json()) as { address?: { road?: string; house_number?: string; city?: string; town?: string; state?: string; country?: string; }; }; if (data.address) { const addr = data.address; if (addr.road) { endereco = `${addr.road}${addr.house_number ? `, ${addr.house_number}` : ''}`; } cidade = addr.city || addr.town || ''; estado = addr.state || ''; pais = addr.country || ''; } } } catch (error) { console.warn('Erro ao obter endereço:', error); } // Validar timezone vs localização if (typeof navigator !== 'undefined') { const timezoneAtual = Intl.DateTimeFormat().resolvedOptions().timeZone; const timezoneEsperado = obterTimezonePorCoordenadas(latitude, longitude); // Se timezone é muito diferente, pode ser suspeito if (timezoneAtual !== timezoneEsperado && timezoneAtual !== 'America/Recife' && timezoneEsperado !== 'America/Recife') { console.warn(`Timezone inconsistente: esperado ${timezoneEsperado}, atual ${timezoneAtual}`); } } console.log('Localização obtida com validações:', { latitude, longitude, confiabilidade: confiabilidade.toFixed(2), suspeitaSpoofing, motivoSuspeita }); return { latitude, longitude, precisao, altitude, altitudeAccuracy, heading, speed, confiabilidadeGPS: confiabilidade, suspeitaSpoofing: suspeitaSpoofing || false, motivoSuspeita, endereco, cidade, estado, pais }; } /** * Obtém IP público */ async function obterIPPublico(): Promise { try { const response = await fetch('https://api.ipify.org?format=json'); if (response.ok) { const data = (await response.json()) as { ip: string }; return data.ip; } } catch (error) { console.warn('Erro ao obter IP público:', error); } return undefined; } /** * Obtém todas as informações do dispositivo */ export async function obterInformacoesDispositivo(): Promise { const informacoes: InformacoesDispositivo = {}; // Informações básicas if (typeof navigator !== 'undefined') { informacoes.userAgent = navigator.userAgent; informacoes.idioma = navigator.language || navigator.languages?.[0]; informacoes.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; } // Informações do navegador const navegador = detectarNavegador(); informacoes.browser = navegador.browser; informacoes.browserVersion = navegador.browserVersion; informacoes.engine = navegador.engine; // Informações do sistema const sistema = detectarSistemaOperacional(); informacoes.sistemaOperacional = sistema.sistemaOperacional; informacoes.osVersion = sistema.osVersion; informacoes.arquitetura = sistema.arquitetura; informacoes.plataforma = sistema.plataforma; // Tipo de dispositivo const dispositivo = detectarTipoDispositivo(); informacoes.deviceType = dispositivo.deviceType; informacoes.isMobile = dispositivo.isMobile; informacoes.isTablet = dispositivo.isTablet; informacoes.isDesktop = dispositivo.isDesktop; // Informações da tela const tela = obterInformacoesTela(); informacoes.screenResolution = tela.screenResolution; informacoes.coresTela = tela.coresTela; // Informações de conexão e memória (assíncronas) const [connectionType, memoryInfo, ipPublico, ipLocal, localizacao] = await Promise.all([ obterInformacoesConexao(), Promise.resolve(obterInformacoesMemoria()), obterIPPublico(), getLocalIP(), obterLocalizacao(), ]); informacoes.connectionType = connectionType; informacoes.memoryInfo = memoryInfo; informacoes.ipPublico = ipPublico; informacoes.ipLocal = ipLocal; informacoes.latitude = localizacao.latitude; informacoes.longitude = localizacao.longitude; informacoes.precisao = localizacao.precisao; informacoes.altitude = localizacao.altitude ?? null; informacoes.altitudeAccuracy = localizacao.altitudeAccuracy ?? null; informacoes.heading = localizacao.heading ?? null; informacoes.speed = localizacao.speed ?? null; informacoes.confiabilidadeGPS = localizacao.confiabilidadeGPS; informacoes.suspeitaSpoofing = localizacao.suspeitaSpoofing; informacoes.motivoSuspeita = localizacao.motivoSuspeita; informacoes.endereco = localizacao.endereco; informacoes.cidade = localizacao.cidade; informacoes.estado = localizacao.estado; informacoes.pais = localizacao.pais; // IP address (usar público se disponível, senão local) informacoes.ipAddress = ipPublico || ipLocal; return informacoes; }