From 5b3f72d5277697608385f74e755eb7640636661e Mon Sep 17 00:00:00 2001 From: Nikolas Falco Date: Fri, 27 Jun 2025 23:50:02 +0200 Subject: [PATCH] Improve manage of the connection pool when use multiple ClientCertificate credentials using a library that allow to add identity material for an existing SSLContext. --- pom.xml | 11 +++++ ...tbucketClientCertificateAuthenticator.java | 47 +++++++++---------- .../client/BitbucketServerAPIClient.java | 35 ++++++++++++-- 3 files changed, 64 insertions(+), 29 deletions(-) diff --git a/pom.xml b/pom.xml index 16d9bdc0e..f6d726285 100644 --- a/pom.xml +++ b/pom.xml @@ -79,6 +79,17 @@ io.jenkins.plugins apache-httpcomponents-client-5-api + + io.github.hakky54 + sslcontext-kickstart-for-apache5 + 9.1.1-SNAPSHOT + + + org.slf4j + slf4j-api + + + io.jenkins.plugins commons-lang3-api diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/credentials/BitbucketClientCertificateAuthenticator.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/credentials/BitbucketClientCertificateAuthenticator.java index c02c90adb..ce2d710fa 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/credentials/BitbucketClientCertificateAuthenticator.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/credentials/BitbucketClientCertificateAuthenticator.java @@ -25,25 +25,23 @@ package com.cloudbees.jenkins.plugins.bitbucket.impl.credentials; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketException; -import com.cloudbees.jenkins.plugins.bitbucket.impl.client.BitbucketTlsSocketStrategy; import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials; import hudson.util.Secret; -import java.security.KeyManagementException; import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.UnrecoverableKeyException; -import javax.net.ssl.SSLContext; -import org.apache.hc.client5.http.protocol.HttpClientContext; +import java.util.logging.Logger; +import nl.altindag.ssl.SSLFactory; +import nl.altindag.ssl.util.KeyManagerUtils; +import nl.altindag.ssl.util.KeyStoreUtils; import org.apache.hc.core5.http.HttpHost; -import org.apache.hc.core5.ssl.SSLContextBuilder; -import org.apache.hc.core5.ssl.SSLContexts; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; /** * Authenticates against Bitbucket using a TLS client certificate */ public class BitbucketClientCertificateAuthenticator implements BitbucketAuthenticator { + private static final Logger logger = Logger.getLogger(BitbucketClientCertificateAuthenticator.class.getName()); + private final String credentialsId; private final KeyStore keyStore; private final Secret password; @@ -55,23 +53,22 @@ public BitbucketClientCertificateAuthenticator(StandardCertificateCredentials cr } /** - * Sets the SSLContext for the builder to one that will connect with the selected certificate. - * @param context The client builder context + * Sets the SSLContext for the builder to one that will connect with the + * selected certificate. + * + * @param sslFactory The client SSL context configured in the connection + * pool * @param host the target host name */ - @Override - public void configureContext(HttpClientContext context, HttpHost host) { - try { - context.setAttribute(BitbucketTlsSocketStrategy.SOCKET_FACTORY_REGISTRY, buildSSLContext()); // override SSL registry for this context - } catch (NoSuchAlgorithmException | UnrecoverableKeyException | KeyStoreException | KeyManagementException e) { - throw new BitbucketException("Failed to set up SSL context from provided client certificate", e); - } - } - - private SSLContext buildSSLContext() throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException, KeyManagementException { - SSLContextBuilder contextBuilder = SSLContexts.custom(); - contextBuilder.loadKeyMaterial(keyStore, Secret.toString(password).toCharArray()); - return contextBuilder.build(); + @Restricted(NoExternalUse.class) + public void configureContext(SSLFactory sslFactory, HttpHost host) { + sslFactory.getKeyManager().ifPresent(baseKeyManager -> { + for (String alias : KeyStoreUtils.getAliases(keyStore)) { + KeyManagerUtils.addIdentityMaterial(baseKeyManager, keyStore, Secret.toString(password).toCharArray()); + KeyManagerUtils.addIdentityRoute(baseKeyManager, alias, host.toString()); + logger.info(() -> "Add new identity material (keyStore) " + alias + " to the SSLContext."); + } + }); } @Override diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java index ff0e93971..7b702e21a 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java @@ -39,7 +39,6 @@ import com.cloudbees.jenkins.plugins.bitbucket.client.repository.UserRoleInRepository; import com.cloudbees.jenkins.plugins.bitbucket.filesystem.BitbucketSCMFile; import com.cloudbees.jenkins.plugins.bitbucket.impl.client.AbstractBitbucketApi; -import com.cloudbees.jenkins.plugins.bitbucket.impl.client.BitbucketTlsSocketStrategy; import com.cloudbees.jenkins.plugins.bitbucket.impl.credentials.BitbucketAccessTokenAuthenticator; import com.cloudbees.jenkins.plugins.bitbucket.impl.credentials.BitbucketClientCertificateAuthenticator; import com.cloudbees.jenkins.plugins.bitbucket.impl.credentials.BitbucketUsernamePasswordAuthenticator; @@ -75,6 +74,7 @@ import java.io.InputStream; import java.lang.reflect.ParameterizedType; import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -88,11 +88,15 @@ import jenkins.scm.api.SCMFile; import jenkins.scm.api.SCMFile.Type; import jenkins.scm.impl.avatars.AvatarImage; +import nl.altindag.ssl.SSLFactory; +import nl.altindag.ssl.SSLFactory.Builder; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.message.BasicNameValuePair; @@ -140,10 +144,24 @@ public class BitbucketServerAPIClient extends AbstractBitbucketApi implements Bi private static final String API_MIRRORS_PATH = "/rest/mirroring/1.0/mirrorServers"; private static final Integer DEFAULT_PAGE_LIMIT = 200; + private static Builder sslFactoryBuilder() { + Builder builder = SSLFactory.builder() + .withSystemTrustMaterial() + .withDefaultTrustMaterial() + .withTrustMaterial(Paths.get("D:\\Download\\JENKINS-75676\\jenkins\\trustStore.jks"), "JENKINS-75676".toCharArray()) + .withDummyIdentityMaterial() + .withInflatableIdentityMaterial(); + if (System.getProperty("javax.net.ssl.trustStore") != null) { + builder.withSystemPropertyDerivedTrustMaterial(); + } + return builder; + } + + private static final SSLFactory sslFactory = sslFactoryBuilder().build(); private static final HttpClientConnectionManager connectionManager = connectionManagerBuilder() - .setMaxConnPerRoute(20) - .setMaxConnTotal(40 /* should be 20 * number of server instances */) - .setTlsSocketStrategy(new BitbucketTlsSocketStrategy()) + .setMaxConnPerRoute(1) // FIXME restore to 20 + .setMaxConnTotal(1 /* should be 20 * number of server instances */) // FIXME restore to 40 + .setTlsSocketStrategy(new DefaultClientTlsStrategy(sslFactory.getSslContext())) .build(); /** @@ -162,6 +180,7 @@ public class BitbucketServerAPIClient extends AbstractBitbucketApi implements Bi private final BitbucketServerWebhookImplementation webhookImplementation; private final CloseableHttpClient client; + public BitbucketServerAPIClient(@NonNull String baseURL, @NonNull String owner, @CheckForNull String repositoryName, @CheckForNull BitbucketAuthenticator authenticator, boolean userCentric) { this(baseURL, owner, repositoryName, authenticator, userCentric, BitbucketServerEndpoint.findWebhookImplementation(baseURL)); @@ -182,6 +201,14 @@ public BitbucketServerAPIClient(@NonNull String baseURL, @NonNull String owner, this.client = setupClientBuilder().build(); } + @Override + protected HttpClientBuilder setupClientBuilder() { + if (getAuthenticator() instanceof BitbucketClientCertificateAuthenticator certificateAuthenticator) { + certificateAuthenticator.configureContext(sslFactory, getHost()); + } + return super.setupClientBuilder(); + } + @Override protected boolean isSupportedAuthenticator(@CheckForNull BitbucketAuthenticator authenticator) { return authenticator == null