Skip to content

Commit

Permalink
Update Apache HTTP Client to version 5.4
Browse files Browse the repository at this point in the history
WIP
  • Loading branch information
nfalco79 committed Jan 6, 2025
1 parent c8722d2 commit f9e250f
Show file tree
Hide file tree
Showing 14 changed files with 321 additions and 253 deletions.
20 changes: 8 additions & 12 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>apache-httpcomponents-client-4-api</artifactId>
<groupId>io.jenkins.plugins</groupId>
<artifactId>apache-httpcomponents-client-5-api</artifactId>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
Expand All @@ -92,6 +92,12 @@
<artifactId>git</artifactId>
<!-- TODO: remove when in bom -->
<version>5.7.0</version>
<exclusions>
<exclusion>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>apache-httpcomponents-client-4-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
Expand Down Expand Up @@ -233,16 +239,6 @@
</execution>
</executions>
</plugin>
<plugin> <!-- TODO scm-api gratuitously restricts deprecated APIs, preventing us from compiling even deprecated classes in *this* plugin -->
<groupId>org.kohsuke</groupId>
<artifactId>access-modifier-checker</artifactId>
<executions>
<execution>
<id>default-enforce</id>
<phase /> <!-- https://stackoverflow.com/a/19540026/12916 -->
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.infradna.tool</groupId>
<artifactId>bridge-method-injector</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@
import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;
import hudson.plugins.git.GitSCM;
import jenkins.authentication.tokens.api.AuthenticationTokenContext;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpRequest;

/**
* Support for various different methods of authenticating with Bitbucket
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import com.cloudbees.jenkins.plugins.bitbucket.filesystem.BitbucketSCMFile;
import com.cloudbees.jenkins.plugins.bitbucket.impl.client.AbstractBitbucketApi;
import com.cloudbees.jenkins.plugins.bitbucket.impl.credentials.BitbucketUsernamePasswordAuthenticator;
import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketApiUtils;
import com.cloudbees.jenkins.plugins.bitbucket.impl.util.JsonParser;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
import com.damnhandy.uri.template.UriTemplate;
Expand All @@ -69,24 +70,26 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import javax.imageio.ImageIO;
import jenkins.scm.api.SCMFile;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpHost;
import org.apache.http.HttpStatus;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.io.SocketConfig;
import org.apache.hc.core5.http.message.BasicNameValuePair;

import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MINUTES;

public class BitbucketCloudApiClient extends AbstractBitbucketApi implements BitbucketApi {

private static final HttpHost API_HOST = HttpHost.create("https://api.bitbucket.org");
private static final HttpHost API_HOST = BitbucketApiUtils.toHttpHost("https://api.bitbucket.org");
private static final String V2_API_BASE_URL = "https://api.bitbucket.org/2.0/repositories";
private static final String V2_WORKSPACES_API_BASE_URL = "https://api.bitbucket.org/2.0/workspaces";
private static final String REPO_URL_TEMPLATE = V2_API_BASE_URL + "{/owner,repo}";
Expand All @@ -109,11 +112,24 @@ public class BitbucketCloudApiClient extends AbstractBitbucketApi implements Bit

private static HttpClientConnectionManager connectionManager() {
try {
PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(); // NOSONAR
connManager.setDefaultMaxPerRoute(20);
connManager.setMaxTotal(22);
connManager.setSocketConfig(API_HOST, SocketConfig.custom().setSoTimeout(60 * 1000).build());
return connManager;
int connectTimeout = Integer.getInteger("http.connect.timeout", 10);
int socketTimeout = Integer.getInteger("http.socket.timeout", 60);

ConnectionConfig connCfg = ConnectionConfig.custom()
.setConnectTimeout(connectTimeout, TimeUnit.SECONDS)
.setSocketTimeout(socketTimeout, TimeUnit.SECONDS)
.build();

SocketConfig socketConfig = SocketConfig.custom()
.setSoTimeout(60, TimeUnit.SECONDS)
.build();

return PoolingHttpClientConnectionManagerBuilder.create()
.setMaxConnPerRoute(20)
.setMaxConnTotal(22)
.setDefaultConnectionConfig(connCfg)
.setSocketConfigResolver(host -> host.getTargetHost().equals(API_HOST) ? socketConfig : SocketConfig.DEFAULT)
.build();
} catch (Exception e) {
// in case of exception this avoids ClassNotFoundError which prevents the classloader from loading this class again
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@

import java.io.IOException;
import java.io.InputStream;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
import org.apache.hc.core5.http.io.entity.EntityUtils;

public class ClosingConnectionInputStream extends InputStream {

private final CloseableHttpResponse response;

private final HttpRequestBase method;
private final HttpUriRequest method;

private final HttpClientConnectionManager connectionManager;

private final InputStream delegate;

public ClosingConnectionInputStream(final CloseableHttpResponse response, final HttpRequestBase method,
public ClosingConnectionInputStream(final CloseableHttpResponse response, final HttpUriRequest method,
final HttpClientConnectionManager connectionmanager)
throws UnsupportedOperationException, IOException {
this.response = response;
Expand All @@ -35,8 +35,8 @@ public int available() throws IOException {
public void close() throws IOException {
EntityUtils.consume(response.getEntity());
delegate.close();
method.releaseConnection();
connectionManager.closeExpiredConnections();
//TODO method.releaseConnection();
//TODO connectionManager.closeExpiredConnections();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,35 +45,35 @@
import jenkins.model.Jenkins;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.ServiceUnavailableRetryStrategy;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.StandardHttpRequestRetryHandler;
import org.apache.http.util.EntityUtils;
import org.apache.hc.client5.http.auth.AuthCache;
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.CredentialsProvider;
import org.apache.hc.client5.http.auth.CredentialsStore;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.classic.methods.HttpDelete;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpHead;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.classic.methods.HttpPut;
import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.client5.http.impl.auth.BasicScheme;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.util.TimeValue;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.ProtectedExternally;

Expand All @@ -98,9 +98,9 @@ protected String truncateMiddle(@CheckForNull String value, int maxLength) {
}

protected BitbucketRequestException buildResponseException(CloseableHttpResponse response, String errorMessage) {
String headers = StringUtils.join(response.getAllHeaders(), "\n");
String message = String.format("HTTP request error.%nStatus: %s%nResponse: %s%n%s", response.getStatusLine(), errorMessage, headers);
return new BitbucketRequestException(response.getStatusLine().getStatusCode(), message);
String headers = StringUtils.join(response.getHeaders(), "\n");
String message = String.format("HTTP request error.%nStatus: %s%nResponse: %s%n%s", response.getReasonPhrase(), errorMessage, headers);
return new BitbucketRequestException(response.getCode(), message);
}

protected String getResponseContent(CloseableHttpResponse response) throws IOException {
Expand Down Expand Up @@ -139,25 +139,21 @@ private long getLenghtFromHeader(CloseableHttpResponse response) {
}

protected HttpClientBuilder setupClientBuilder(@Nullable String host) {
int connectTimeout = Integer.getInteger("http.connect.timeout", 10);
int connectionRequestTimeout = Integer.getInteger("http.connect.request.timeout", 60);
int socketTimeout = Integer.getInteger("http.socket.timeout", 60);

RequestConfig config = RequestConfig.custom()
.setConnectTimeout(connectTimeout * 1000)
.setConnectionRequestTimeout(connectionRequestTimeout * 1000)
.setSocketTimeout(socketTimeout * 1000)
.setConnectionRequestTimeout(connectionRequestTimeout, TimeUnit.SECONDS)
.build();

HttpClientConnectionManager connectionManager = getConnectionManager();
ServiceUnavailableRetryStrategy serviceUnavailableStrategy = new ExponentialBackOffServiceUnavailableRetryStrategy(2, TimeUnit.SECONDS.toMillis(5), TimeUnit.HOURS.toMillis(1));
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create()
.useSystemProperties()
.setConnectionManager(connectionManager)
.setConnectionManagerShared(connectionManager != null)
.setServiceUnavailableRetryStrategy(serviceUnavailableStrategy)
.setRetryHandler(new StandardHttpRequestRetryHandler())
.setRetryStrategy(new ExponentialBackOffRetryStrategy(2, TimeUnit.SECONDS.toMillis(5), TimeUnit.HOURS.toMillis(1)))
.setDefaultRequestConfig(config)
.evictExpiredConnections()
.evictIdleConnections(TimeValue.ofSeconds(2))
.disableCookieManagement();

if (authenticator != null) {
Expand Down Expand Up @@ -200,7 +196,9 @@ private void setClientProxyParams(String host, HttpClientBuilder builder) {
// may have been already set in com.cloudbees.jenkins.plugins.bitbucket.api.credentials.BitbucketUsernamePasswordAuthenticator.configureContext(HttpClientContext, HttpHost)
context.setCredentialsProvider(credentialsProvider);
}
credentialsProvider.setCredentials(new AuthScope(proxyHttpHost), new UsernamePasswordCredentials(username, password));
if (credentialsProvider instanceof CredentialsStore credentialsStore) {
credentialsStore.setCredentials(new AuthScope(proxyHttpHost), new UsernamePasswordCredentials(username, password.toCharArray()));
}
AuthCache authCache = context.getAuthCache();
if (authCache == null) {
authCache = new BasicAuthCache();
Expand All @@ -221,23 +219,23 @@ private void setClientProxyParams(String host, HttpClientBuilder builder) {
protected abstract CloseableHttpClient getClient();

protected CloseableHttpResponse executeMethod(HttpHost host,
HttpRequestBase httpMethod,
HttpUriRequest httpMethod,
boolean requireAuthentication) throws IOException {
if (requireAuthentication && authenticator != null) {
authenticator.configureRequest(httpMethod);
}
return getClient().execute(host, httpMethod, context);
}

protected CloseableHttpResponse executeMethod(HttpHost host, HttpRequestBase httpMethod) throws IOException {
protected CloseableHttpResponse executeMethod(HttpHost host, HttpUriRequest httpMethod) throws IOException {
return executeMethod(host, httpMethod, true);
}

protected String doRequest(HttpRequestBase request, boolean requireAuthentication) throws IOException {
protected String doRequest(HttpUriRequest request, boolean requireAuthentication) throws IOException {
try (CloseableHttpResponse response = executeMethod(getHost(), request, requireAuthentication)) {
int statusCode = response.getStatusLine().getStatusCode();
int statusCode = response.getCode();
if (statusCode == HttpStatus.SC_NOT_FOUND) {
throw new FileNotFoundException("URL: " + request.getURI());
throw new FileNotFoundException("URL: " + request.getRequestUri());
}
if (statusCode == HttpStatus.SC_NO_CONTENT) {
EntityUtils.consume(response.getEntity());
Expand All @@ -254,23 +252,13 @@ protected String doRequest(HttpRequestBase request, boolean requireAuthenticatio
throw e;
} catch (IOException e) {
throw new IOException("Communication error for url: " + request, e);
} finally {
release(request);
}
}

protected String doRequest(HttpRequestBase request) throws IOException {
protected String doRequest(HttpUriRequest request) throws IOException {
return doRequest(request, true);
}

private void release(HttpRequestBase method) {
method.releaseConnection();
HttpClientConnectionManager connectionManager = getConnectionManager();
if (connectionManager != null) {
connectionManager.closeExpiredConnections();
}
}

/*
* Caller's responsible to close the InputStream.
*/
Expand All @@ -289,7 +277,7 @@ protected InputStream getRequestAsInputStream(String path) throws IOException {
}

CloseableHttpResponse response = executeMethod(host, httpget);
int statusCode = response.getStatusLine().getStatusCode();
int statusCode = response.getCode();
if (statusCode == HttpStatus.SC_NOT_FOUND) {
EntityUtils.consume(response.getEntity());
throw new FileNotFoundException("URL: " + path);
Expand All @@ -305,7 +293,7 @@ protected int headRequestStatus(String path) throws IOException {
HttpHead httpHead = new HttpHead(path);
try (CloseableHttpResponse response = executeMethod(getHost(), httpHead)) {
EntityUtils.consume(response.getEntity());
return response.getStatusLine().getStatusCode();
return response.getCode();
} catch (IOException e) {
throw new IOException("Communication error for url: " + path, e);
} finally {
Expand Down
Loading

0 comments on commit f9e250f

Please sign in to comment.