feat(core): support configurable port, clear collision error handling and discovery proposal
This commit is contained in:
@@ -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 <numero>` ou `-p <numero>` (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<number> {
|
||||||
|
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.
|
||||||
+35
-4
@@ -22,12 +22,17 @@ public class CyberCoreSignerApplication {
|
|||||||
private static boolean developmentMode = false;
|
private static boolean developmentMode = false;
|
||||||
private static boolean headlessMode = false;
|
private static boolean headlessMode = false;
|
||||||
private static boolean noWaitMode = false;
|
private static boolean noWaitMode = false;
|
||||||
|
private static int configuredPort = CcspServer.DEFAULT_PORT;
|
||||||
private static volatile CcspServer runningServer = null;
|
private static volatile CcspServer runningServer = null;
|
||||||
|
|
||||||
public static CcspServer getRunningServer() {
|
public static CcspServer getRunningServer() {
|
||||||
return runningServer;
|
return runningServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int getConfiguredPort() {
|
||||||
|
return configuredPort;
|
||||||
|
}
|
||||||
|
|
||||||
public static void stopServer() {
|
public static void stopServer() {
|
||||||
if (runningServer != null) {
|
if (runningServer != null) {
|
||||||
runningServer.stop();
|
runningServer.stop();
|
||||||
@@ -98,7 +103,7 @@ public class CyberCoreSignerApplication {
|
|||||||
logger.info("Modo de desenvolvimento ativo (com certificados reais do sistema).");
|
logger.info("Modo de desenvolvimento ativo (com certificados reais do sistema).");
|
||||||
}
|
}
|
||||||
SigningService signingService = new SigningService(certManager);
|
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();
|
runningServer.start();
|
||||||
logger.info("Servidor local CCSP/1 pronto em http://127.0.0.1:{}", runningServer.getPort());
|
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) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
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 <porta> ou defina a variável de ambiente CYBERCORE_PORT.", configuredPort);
|
||||||
} catch (Exception e) {
|
} 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 <porta> 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) {
|
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)) {
|
if ("--dev".equalsIgnoreCase(arg) || "-dev".equalsIgnoreCase(arg)) {
|
||||||
developmentMode = true;
|
developmentMode = true;
|
||||||
} else if ("--headless".equalsIgnoreCase(arg) || "-headless".equalsIgnoreCase(arg)
|
} 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)
|
} else if ("--no-wait".equalsIgnoreCase(arg) || "-no-wait".equalsIgnoreCase(arg)
|
||||||
|| "--test".equalsIgnoreCase(arg)) {
|
|| "--test".equalsIgnoreCase(arg)) {
|
||||||
noWaitMode = true;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-1
@@ -11,10 +11,20 @@ class CyberCoreSignerApplicationTest {
|
|||||||
@DisplayName("Deve inicializar a aplicação com argumentos sem erros")
|
@DisplayName("Deve inicializar a aplicação com argumentos sem erros")
|
||||||
void shouldBootstrapWithoutErrors() {
|
void shouldBootstrapWithoutErrors() {
|
||||||
try {
|
try {
|
||||||
assertThatCode(() -> CyberCoreSignerApplication.main(new String[]{"--dev", "--headless", "--no-wait"}))
|
assertThatCode(() -> CyberCoreSignerApplication.main(new String[]{"--dev", "--headless", "--no-wait", "--port", "29999"}))
|
||||||
.doesNotThrowAnyException();
|
.doesNotThrowAnyException();
|
||||||
} finally {
|
} finally {
|
||||||
CyberCoreSignerApplication.stopServer();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-3
@@ -510,21 +510,34 @@ public class CyberCoreDesktopApplication extends Application {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
int targetPort = resolveConfiguredPort();
|
||||||
try {
|
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();
|
this.ccspServer.start();
|
||||||
logger.info("Servidor local CCSP/1 inicializado na porta {}", this.ccspServer.getPort());
|
logger.info("Servidor local CCSP/1 inicializado na porta {}", this.ccspServer.getPort());
|
||||||
} catch (java.net.BindException be) {
|
} 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 <porta>. O agente continuará ativo na interface gráfica.", targetPort);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (e.getCause() instanceof java.net.BindException || (e.getMessage() != null && e.getMessage().contains("Address already in use"))) {
|
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 <porta>. O agente continuará ativo na interface gráfica.", targetPort);
|
||||||
} else {
|
} else {
|
||||||
logger.error("Falha ao inicializar servidor local CCSP/1: {}", e.getMessage(), e);
|
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() {
|
private void initSmartCardWatcher() {
|
||||||
if (Boolean.getBoolean("cybercore.diag.no-smartcard-watcher")) {
|
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");
|
logger.warn("Diagnóstico: Monitor dinâmico de smart cards DESATIVADO via -Dcybercore.diag.no-smartcard-watcher=true");
|
||||||
|
|||||||
Reference in New Issue
Block a user