diff --git a/CHANGELOG.md b/CHANGELOG.md index 86084de..884b05a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,3 @@ # Notas de versão - Atualizado CACERT +- Adicionado modo Multithreading diff --git a/cacert b/cacert index 5c23517..ada2f80 100644 Binary files a/cacert and b/cacert differ diff --git a/src/main/java/br/com/swconsultoria/certificado/Certificado.java b/src/main/java/br/com/swconsultoria/certificado/Certificado.java index d7067ce..02ef09d 100644 --- a/src/main/java/br/com/swconsultoria/certificado/Certificado.java +++ b/src/main/java/br/com/swconsultoria/certificado/Certificado.java @@ -32,9 +32,11 @@ public class Certificado { private String sslProtocol; private BigInteger numeroSerie; private Provider provider; + private boolean isModoMultithreading; public Certificado() { this.setSslProtocol(TLSV_1_2); + this.setModoMultithreading(false); } @Override diff --git a/src/main/java/br/com/swconsultoria/certificado/CertificadoService.java b/src/main/java/br/com/swconsultoria/certificado/CertificadoService.java index 86b5e76..4de4936 100644 --- a/src/main/java/br/com/swconsultoria/certificado/CertificadoService.java +++ b/src/main/java/br/com/swconsultoria/certificado/CertificadoService.java @@ -3,6 +3,7 @@ import br.com.swconsultoria.certificado.exception.CertificadoException; import br.com.swconsultoria.certificado.util.DocumentoUtil; import lombok.extern.java.Log; +import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.protocol.Protocol; import java.io.FileNotFoundException; @@ -35,35 +36,42 @@ public static void inicializaCertificado(Certificado certificado) throws Certifi inicializaCertificado(certificado, CertificadoService.class.getResourceAsStream("/cacert")); } + /** + * + *

Inicializa o certificado para a conexão SSL/TLS.

+ *

Importante: Quando NÃO estiver com o modo multithreading ativado (certificado.isModoMultithreading()) + * será registrado e utilizado um único certificado em todas as conexões até a próxima chamada à + * {@link #inicializaCertificado(Certificado, InputStream)}. É o modo antigo e padrão da biblioteca.

+ *

Quando estiver com o modo multithreading ativo o consumidor deverá obter um + * {@link org.apache.commons.httpclient.HttpClient} com protocolo e certificado exclusivos para ele usando o + * método {@link #getHttpsClient(Certificado, String, InputStream)}

+ * + * @param certificado {@link Certificado} a ser utilizado na conexão. + * @param cacert {@link java.io.InputStream} contendo o cacert + * @throws CertificadoException + */ + public static void inicializaCertificado(Certificado certificado, InputStream cacert) throws CertificadoException { + if (certificado == null) { + throw new IllegalArgumentException(CERTIFICADO_NAO_PODE_SER_NULO); + } - try { - - KeyStore keyStore = getKeyStore( - Optional.ofNullable(certificado).orElseThrow(() -> new IllegalArgumentException(CERTIFICADO_NAO_PODE_SER_NULO))); - SocketFactoryDinamico socketFactory = new SocketFactoryDinamico(keyStore, certificado.getNome(), certificado.getSenha(), - Optional.ofNullable(cacert).orElseThrow(() -> new IllegalArgumentException("Cacert não pode ser nulo.")), - certificado.getSslProtocol()); - Protocol protocol = new Protocol("https", socketFactory, 443); - Protocol.registerProtocol("https", protocol); - - log.info(String.format("JAVA-CERTIFICADO | Samuel Oliveira | samuel@swconsultoria.com.br " + - "| VERSAO=%s | DATA_VERSAO=%s | CNPJ/CPF=%s | VENCIMENTO=%s | ALIAS=%s | TIPO=%s | CAMINHO=%s | CACERT=%s | SSL=%s", - "3.7", - "11/07/2024", - certificado.getCnpjCpf(), - certificado.getDataHoraVencimento(), - certificado.getNome().toUpperCase(), - certificado.getTipoCertificado().toString(), - certificado.getArquivo(), - cacertProprio ? "Default" : "Customizado", - certificado.getSslProtocol())); - - } catch (KeyStoreException | NoSuchAlgorithmException | KeyManagementException | CertificateException | - IOException e) { - throw new CertificadoException(e.getMessage(), e); + if (!certificado.isModoMultithreading()) { + Protocol.registerProtocol("https", getProtocoloCertificado(certificado, cacert)); } + log.info(String.format("JAVA-CERTIFICADO | Samuel Oliveira | samuel@swconsultoria.com.br " + + "| VERSAO=%s | DATA_VERSAO=%s | CNPJ/CPF=%s | VENCIMENTO=%s | ALIAS=%s | TIPO=%s | CAMINHO=%s | CACERT=%s | SSL=%s | Multithreading=%s", + "3.8", + "14/10/2024", + certificado.getCnpjCpf(), + certificado.getDataHoraVencimento(), + certificado.getNome().toUpperCase(), + certificado.getTipoCertificado().toString(), + certificado.getArquivo(), + cacertProprio ? "Default" : "Customizado", + certificado.getSslProtocol(), + certificado.isModoMultithreading())); } public static Certificado certificadoPfxBytes(byte[] certificadoBytes, String senha) throws CertificadoException { @@ -278,4 +286,51 @@ public static Certificado getCertificadoByCnpjCpf(String cnpjCpf) throws Certifi cnpjCpf)); } + private static Protocol getProtocoloCertificado(final Certificado certificado, InputStream cacert) throws CertificadoException { + try { + KeyStore keyStore = getKeyStore( + Optional.ofNullable(certificado).orElseThrow(() -> new IllegalArgumentException(CERTIFICADO_NAO_PODE_SER_NULO))); + SocketFactoryDinamico socketFactory = new SocketFactoryDinamico(keyStore, certificado.getNome(), certificado.getSenha(), + Optional.ofNullable(cacert).orElseThrow(() -> new IllegalArgumentException("Cacert não pode ser nulo.")), + certificado.getSslProtocol()); + + return new Protocol("https", socketFactory, 443); + + } catch (KeyStoreException | NoSuchAlgorithmException | KeyManagementException | CertificateException | + IOException e) { + throw new CertificadoException(e.getMessage(), e); + } + } + + /** + * Utiliza cacert default da biblioteca. + * @see #getHttpsClient(Certificado, String, InputStream) + */ + public static HttpClient getHttpsClient(Certificado certificado, String url) throws CertificadoException { + return getHttpsClient(certificado, url, CertificadoService.class.getResourceAsStream("/cacert")); + } + + /** + *

Utilizar o {@link org.apache.commons.httpclient.HttpClient} gerado nesse método para evitar conflitos de + * certificados nas conexões HTTPS/TLS/SSL, especialmente em ambientes multithreading.

+ * + *

O consumidor desse método deverá usar esse cliente no stub desejado

+ * exemplo: + *
+     *     HttpClient client = getHttpsClient(certificado, url);
+     *     NFeStatusServico4Stub stub = new NFeStatusServico4Stub(url);
+     *     stub._getServiceClient().getOptions().setProperty(HTTPConstants.CACHED_HTTP_CLIENT, httpclient);
+     * 
+ * @param certificado {@link Certificado} a ser utilizado na conexão. + * @param url {@link String} a ser executada na conexão https + * @param cacert {@link java.io.InputStream} contendo o cacert + * @return {@link org.apache.commons.httpclient.HttpClient} configurado para a URL e Certificados informados. + * @throws CertificadoException + */ + public static HttpClient getHttpsClient(Certificado certificado, String url, final InputStream cacert) throws CertificadoException { + Protocol protocol = getProtocoloCertificado(certificado, cacert); + HttpClient httpclient = new HttpClient(); + httpclient.getHostConfiguration().setHost(url, 443, protocol); + return httpclient; + } } diff --git a/src/main/resources/cacert b/src/main/resources/cacert index 5c23517..ada2f80 100644 Binary files a/src/main/resources/cacert and b/src/main/resources/cacert differ diff --git a/src/test/java/br/com/swconsultoria/certificado/CertificadoServiceTest.java b/src/test/java/br/com/swconsultoria/certificado/CertificadoServiceTest.java index d3d2700..899c27f 100644 --- a/src/test/java/br/com/swconsultoria/certificado/CertificadoServiceTest.java +++ b/src/test/java/br/com/swconsultoria/certificado/CertificadoServiceTest.java @@ -4,12 +4,15 @@ import br.com.swconsultoria.certificado.util.DocumentoUtil; import mockit.Mock; import mockit.MockUp; +import org.apache.commons.httpclient.protocol.Protocol; +import org.apache.commons.httpclient.protocol.ProtocolSocketFactory; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Field; import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Paths; @@ -27,11 +30,11 @@ class CertificadoServiceTest { - private final String CERTIFICADO_CPF = "NaoUsar_CPF.pfx"; - private final String CERTIFICADO_CNPJ = "NaoUsar_CNPJ.pfx"; - private final String CPF = "99999999999"; - private final String CNPJ = "99999999999999"; - private final String SENHA = "123456"; + private static final String CERTIFICADO_CPF = "NaoUsar_CPF.pfx"; + private static final String CERTIFICADO_CNPJ = "NaoUsar_CNPJ.pfx"; + private static final String CPF = "99999999999"; + private static final String CNPJ = "99999999999999"; + private static final String SENHA = "123456"; @Test void certificadoPfxParametroNull() { @@ -172,10 +175,7 @@ void inicaConfiguracoesCorretamente() { } @Test - void inicaConfiguracoesParametrosNull() throws IOException, CertificadoException { - - InputStream cacert = CertificadoServiceTest.class.getResourceAsStream("cacert"); - Certificado certificado = CertificadoService.certificadoPfx(CERTIFICADO_CNPJ, SENHA); + void inicaConfiguracoesParametrosNull() { //Certificado Null Assertions.assertThrows(IllegalArgumentException.class, () -> @@ -207,4 +207,34 @@ void extraiCpfCnpjCorretamente() { } + /** + *

Testa a compatibilidade com consumidores "antigos", que ainda não estão no "novo modelo" de controle do + * certificado nas conexões TLS/SSL. Isso permitirá uma "migração gradual" dos consumidores.

+ *

Por padrão será utilizado o modo antigo, cada consumidor irá precisar explicitamente escolher o + * "modo multithreading", caso deseje.

+ */ + @Test + void compatibilidadeModoMultithreadingDesativado() throws FileNotFoundException, CertificadoException { + Certificado certificado = CertificadoService.certificadoPfx(CERTIFICADO_CPF, SENHA); + certificado.setModoMultithreading(false); + CertificadoService.inicializaCertificado(certificado); + + String alias = getHttpsProtocoloAlias("https"); + + assertEquals("certificado cpf teste", alias); + } + + private String getHttpsProtocoloAlias(String protocolId) { + try { + Protocol registeredProtocol = Protocol.getProtocol(protocolId); + ProtocolSocketFactory factory = registeredProtocol.getSocketFactory(); + + Class clazz = factory.getClass(); + Field getAliasField = clazz.getDeclaredField("alias"); + getAliasField.setAccessible(true); + return (String) getAliasField.get(factory); + } catch (Exception e) { + return null; + } + } } \ No newline at end of file