diff --git a/docs/port-discovery.md b/docs/port-discovery.md new file mode 100644 index 0000000..693c340 --- /dev/null +++ b/docs/port-discovery.md @@ -0,0 +1,88 @@ +# Proposta Técnica: Mecanismo de Descoberta de Portas e Port-Hunting + +> **Documento de Arquitetura e Especificação para o SDK Web `@cybercore/signer-sdk`** + +--- + +## 1. Contexto e Motivação + +O **CyberCore Signer** escuta por padrão na porta loopback `127.0.0.1:21325`. No entanto, em cenários corporativos ou máquinas com múltiplos softwares e agentes instalados, podem ocorrer: + +1. **Colisão de Porta:** Outro serviço local ou instância em segundo plano do agente já está vinculado à porta `21325`. +2. **Execução Multi-Usuário / Terminal Server:** Múltiplas sessões de desktop com agentes independentes para cada usuário logado. +3. **Restrições de Firewall / Segurança Local:** Políticas corporativas que exigem portas específicas. + +Para suportar portas alternativas sem exigir configuração manual complexa do usuário final, o CyberCore Signer e o SDK Web adotam a estratégia de **Port-Hunting Sequencial**. + +--- + +## 2. Configuração no Agente Desktop + +O agente pode ser iniciado com portas customizadas através de: + +- **Parâmetro de Linha de Comando:** `--port ` ou `-p ` (ex: `cybercore-signer --port 21326`) +- **Variável de Ambiente:** `CYBERCORE_PORT=21326` +- **Propriedade JVM:** `-Dcybercore.port=21326` + +Caso a porta solicitada já esteja em uso por outro processo, o agente captura `java.net.BindException`, emite mensagem acionável no log/console e, no modo gráfico, mantém a interface de gestão disponível alertando sobre a indisponibilidade temporária do socket HTTP. + +--- + +## 3. Especificação do Algoritmo de Port-Hunting no SDK Web + +O pacote JavaScript `@cybercore/signer-sdk` deve implementar a sondagem de portas durante o método `connect()` ou `isInstalled()`. + +### Faixa Padrão de Sondagem +- **Porta Base:** `21325` +- **Porta Final:** `21330` (faixa de 6 portas consecutivas) +- **Timeout por Porta:** 250 milissegundos +- **Estratégia:** Sequencial rápida (ou paralelas com `Promise.any`) + +### Algoritmo + +```typescript +export interface SignerOptions { + basePort?: number; + maxPort?: number; + timeoutMs?: number; +} + +export async function discoverActivePort(options: SignerOptions = {}): Promise { + const basePort = options.basePort ?? 21325; + const maxPort = options.maxPort ?? 21330; + const timeoutMs = options.timeoutMs ?? 250; + + for (let port = basePort; port <= maxPort; port++) { + try { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + + const resp = await fetch(`http://127.0.0.1:${port}/ccsp/v1/status`, { + method: 'GET', + signal: controller.signal, + headers: { 'Accept': 'application/json' } + }); + clearTimeout(timer); + + if (resp.ok) { + const body = await resp.json(); + if (body.status === 'ready' && body.agent === 'CyberCore Signer') { + return port; // Agente encontrado e operacional nesta porta! + } + } + } catch { + // Porta indisponível ou conexão recusada; continua para a próxima + } + } + + throw new Error('CyberCore Signer não encontrado na faixa de portas 21325-21330. O agente está em execução?'); +} +``` + +--- + +## 4. Segurança e Mitigações + +1. **Host Header Restrito:** Cada requisição deve sempre enviar `Host: 127.0.0.1:{port}` ou `Host: localhost:{port}`. O agente rejeita qualquer Host externo com HTTP 403. +2. **Private Network Access (PNA):** O agente responde com `Access-Control-Allow-Private-Network: true` para compatibilidade com a especificação PNA do Google Chrome/Chromium. +3. **CORS Rígido:** A rota `/status` responde `status: ready` apenas para fins de handshake; operações de chave privada continuam exigindo `application_id`, autorização prévia e aprovação visual do usuário. diff --git a/modules/cybercore-app/src/main/java/br/com/cybercore/signer/app/CyberCoreSignerApplication.java b/modules/cybercore-app/src/main/java/br/com/cybercore/signer/app/CyberCoreSignerApplication.java index 90546ae..9524324 100644 --- a/modules/cybercore-app/src/main/java/br/com/cybercore/signer/app/CyberCoreSignerApplication.java +++ b/modules/cybercore-app/src/main/java/br/com/cybercore/signer/app/CyberCoreSignerApplication.java @@ -22,12 +22,17 @@ public class CyberCoreSignerApplication { private static boolean developmentMode = false; private static boolean headlessMode = false; private static boolean noWaitMode = false; + private static int configuredPort = CcspServer.DEFAULT_PORT; private static volatile CcspServer runningServer = null; public static CcspServer getRunningServer() { return runningServer; } + public static int getConfiguredPort() { + return configuredPort; + } + public static void stopServer() { if (runningServer != null) { runningServer.stop(); @@ -98,7 +103,7 @@ public class CyberCoreSignerApplication { logger.info("Modo de desenvolvimento ativo (com certificados reais do sistema)."); } SigningService signingService = new SigningService(certManager); - runningServer = new CcspServer(certManager, signingService, null); + runningServer = new CcspServer(configuredPort, certManager, signingService, br.com.cybercore.signer.core.security.ApplicationRegistry.getInstance(), null); runningServer.start(); logger.info("Servidor local CCSP/1 pronto em http://127.0.0.1:{}", runningServer.getPort()); @@ -113,14 +118,34 @@ public class CyberCoreSignerApplication { } } catch (InterruptedException e) { Thread.currentThread().interrupt(); + } catch (java.net.BindException e) { + logger.error("ALERTA DE COLISÃO DE PORTA: A porta {} já está em uso por outra instância do CyberCore Signer ou outro serviço. Para usar uma porta alternativa, utilize o parâmetro --port ou defina a variável de ambiente CYBERCORE_PORT.", configuredPort); } catch (Exception e) { - logger.error("Falha ao inicializar servidor local CCSP/1 no modo console: {}", e.getMessage(), e); + if (e.getCause() instanceof java.net.BindException || (e.getMessage() != null && e.getMessage().contains("Address already in use"))) { + logger.error("ALERTA DE COLISÃO DE PORTA: A porta {} já está em uso por outra instância do CyberCore Signer ou outro serviço. Para usar uma porta alternativa, utilize o parâmetro --port ou defina a variável de ambiente CYBERCORE_PORT.", configuredPort); + } else { + logger.error("Falha ao inicializar servidor local CCSP/1 no modo console: {}", e.getMessage(), e); + } } } - private static void parseArguments(String[] args) { + public static void parseArguments(String[] args) { + String envPort = System.getenv("CYBERCORE_PORT"); + if (envPort != null && !envPort.isBlank()) { + try { + configuredPort = Integer.parseInt(envPort.trim()); + } catch (NumberFormatException ignored) {} + } + String propPort = System.getProperty("cybercore.port"); + if (propPort != null && !propPort.isBlank()) { + try { + configuredPort = Integer.parseInt(propPort.trim()); + } catch (NumberFormatException ignored) {} + } + if (args != null) { - for (String arg : args) { + for (int i = 0; i < args.length; i++) { + String arg = args[i]; if ("--dev".equalsIgnoreCase(arg) || "-dev".equalsIgnoreCase(arg)) { developmentMode = true; } else if ("--headless".equalsIgnoreCase(arg) || "-headless".equalsIgnoreCase(arg) @@ -129,6 +154,12 @@ public class CyberCoreSignerApplication { } else if ("--no-wait".equalsIgnoreCase(arg) || "-no-wait".equalsIgnoreCase(arg) || "--test".equalsIgnoreCase(arg)) { noWaitMode = true; + } else if (("--port".equalsIgnoreCase(arg) || "-p".equalsIgnoreCase(arg)) && i + 1 < args.length) { + try { + configuredPort = Integer.parseInt(args[++i].trim()); + } catch (NumberFormatException e) { + logger.warn("Porta inválida especificada em --port: '{}'. Usando porta padrão {}.", args[i], CcspServer.DEFAULT_PORT); + } } } } diff --git a/modules/cybercore-app/src/test/java/br/com/cybercore/signer/app/CyberCoreSignerApplicationTest.java b/modules/cybercore-app/src/test/java/br/com/cybercore/signer/app/CyberCoreSignerApplicationTest.java index 0398c33..f601ed6 100644 --- a/modules/cybercore-app/src/test/java/br/com/cybercore/signer/app/CyberCoreSignerApplicationTest.java +++ b/modules/cybercore-app/src/test/java/br/com/cybercore/signer/app/CyberCoreSignerApplicationTest.java @@ -11,10 +11,20 @@ class CyberCoreSignerApplicationTest { @DisplayName("Deve inicializar a aplicação com argumentos sem erros") void shouldBootstrapWithoutErrors() { try { - assertThatCode(() -> CyberCoreSignerApplication.main(new String[]{"--dev", "--headless", "--no-wait"})) + assertThatCode(() -> CyberCoreSignerApplication.main(new String[]{"--dev", "--headless", "--no-wait", "--port", "29999"})) .doesNotThrowAnyException(); } finally { CyberCoreSignerApplication.stopServer(); } } + + @Test + @DisplayName("Deve realizar o parse correto do argumento --port") + void shouldParsePortArgument() { + CyberCoreSignerApplication.parseArguments(new String[]{"--port", "25555"}); + org.assertj.core.api.Assertions.assertThat(CyberCoreSignerApplication.getConfiguredPort()).isEqualTo(25555); + + CyberCoreSignerApplication.parseArguments(new String[]{"-p", "26666"}); + org.assertj.core.api.Assertions.assertThat(CyberCoreSignerApplication.getConfiguredPort()).isEqualTo(26666); + } } diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/CyberCoreDesktopApplication.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/CyberCoreDesktopApplication.java index 1ff0243..fd694d6 100644 --- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/CyberCoreDesktopApplication.java +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/CyberCoreDesktopApplication.java @@ -510,21 +510,34 @@ public class CyberCoreDesktopApplication extends Application { } }; + int targetPort = resolveConfiguredPort(); try { - this.ccspServer = new CcspServer(this.certificateManager, this.signingService, promptListener); + this.ccspServer = new CcspServer(targetPort, this.certificateManager, this.signingService, ApplicationRegistry.getInstance(), promptListener); this.ccspServer.start(); logger.info("Servidor local CCSP/1 inicializado na porta {}", this.ccspServer.getPort()); } catch (java.net.BindException be) { - logger.warn("Porta 21325 já está ocupada por outra instância do CyberCore Signer em execução. O agente continuará ativo na interface gráfica."); + logger.warn("ALERTA DE COLISÃO DE PORTA: A porta {} já está ocupada por outra instância do CyberCore Signer ou outro serviço. Para usar outra porta, configure CYBERCORE_PORT ou use --port . O agente continuará ativo na interface gráfica.", targetPort); } catch (Exception e) { if (e.getCause() instanceof java.net.BindException || (e.getMessage() != null && e.getMessage().contains("Address already in use"))) { - logger.warn("Porta 21325 já está ocupada por outra instância do CyberCore Signer em execução. O agente continuará ativo na interface gráfica."); + logger.warn("ALERTA DE COLISÃO DE PORTA: A porta {} já está ocupada por outra instância do CyberCore Signer ou outro serviço. Para usar outra porta, configure CYBERCORE_PORT ou use --port . O agente continuará ativo na interface gráfica.", targetPort); } else { logger.error("Falha ao inicializar servidor local CCSP/1: {}", e.getMessage(), e); } } } + private int resolveConfiguredPort() { + String env = System.getenv("CYBERCORE_PORT"); + if (env != null && !env.isBlank()) { + try { return Integer.parseInt(env.trim()); } catch (Exception ignored) {} + } + String prop = System.getProperty("cybercore.port"); + if (prop != null && !prop.isBlank()) { + try { return Integer.parseInt(prop.trim()); } catch (Exception ignored) {} + } + return CcspServer.DEFAULT_PORT; + } + private void initSmartCardWatcher() { if (Boolean.getBoolean("cybercore.diag.no-smartcard-watcher")) { logger.warn("Diagnóstico: Monitor dinâmico de smart cards DESATIVADO via -Dcybercore.diag.no-smartcard-watcher=true");