Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swappable HTTP Client #273

Merged
merged 13 commits into from
Nov 15, 2021
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.jenkinsci.plugins.stashNotifier;

import org.apache.commons.lang.StringUtils;

import java.net.URI;

public class BuildStatusUriFactory {
private BuildStatusUriFactory() {
}

public static URI create(String baseUri, String commit) {
String tidyBase = StringUtils.removeEnd(baseUri.toString(), "/");
String uri = String.join("/", tidyBase, "rest/build-status/1.0/commits", commit);
return URI.create(uri);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package org.jenkinsci.plugins.stashNotifier;

import com.cloudbees.plugins.credentials.Credentials;
import com.cloudbees.plugins.credentials.common.CertificateCredentials;
import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.ProxyConfiguration;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.AuthenticationException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustAllStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.BasicScheme;
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.HttpClients;
import org.apache.http.impl.client.ProxyAuthenticationStrategy;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.SSLContext;
import java.io.PrintStream;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;

class DefaultApacheHttpNotifier implements HttpNotifier {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultApacheHttpNotifier.class);

@Override
public @NonNull NotificationResult send(@NonNull URI uri, @NonNull JSONObject payload, @NonNull NotificationSettings settings, @NonNull NotificationContext context) {
PrintStream logger = context.getLogger();
try (CloseableHttpClient client = getHttpClient(logger, uri, settings.isIgnoreUnverifiedSSL())) {
HttpPost req = createRequest(uri, payload, settings.getCredentials());
HttpResponse res = client.execute(req);
if (res.getStatusLine().getStatusCode() != 204) {
return NotificationResult.newFailure(EntityUtils.toString(res.getEntity()));
} else {
return NotificationResult.newSuccess();
}
} catch (Exception e) {
LOGGER.warn("{} failed to send {} to Bitbucket Server at {}", context.getRunId(), payload, uri, e);
logger.println("Failed to notify Bitbucket Server");
return NotificationResult.newFailure(e.getMessage());
}
}

HttpPost createRequest(
final URI uri,
final JSONObject payload,
final UsernamePasswordCredentials credentials) throws AuthenticationException {

HttpPost req = new HttpPost(uri.toString());

if (credentials != null) {
req.addHeader(new BasicScheme().authenticate(
new org.apache.http.auth.UsernamePasswordCredentials(
credentials.getUsername(),
credentials.getPassword().getPlainText()),
req,
null));
}

req.addHeader(HttpHeaders.CONTENT_TYPE, "application/json");
req.setEntity(new StringEntity(payload.toString(), "UTF-8"));

return req;
}

CloseableHttpClient getHttpClient(PrintStream logger, URI stashServer, boolean ignoreUnverifiedSSL) throws Exception {
final int timeoutInMilliseconds = 60_000;

RequestConfig.Builder requestBuilder = RequestConfig.custom()
.setSocketTimeout(timeoutInMilliseconds)
.setConnectTimeout(timeoutInMilliseconds)
.setConnectionRequestTimeout(timeoutInMilliseconds);

HttpClientBuilder clientBuilder = HttpClients.custom();
clientBuilder.setDefaultRequestConfig(requestBuilder.build());

URL url = stashServer.toURL();

if (url.getProtocol().equals("https") && ignoreUnverifiedSSL) {
// add unsafe trust manager to avoid thrown SSLPeerUnverifiedException
try {
SSLContext sslContext = buildSslContext(ignoreUnverifiedSSL, null);
SSLConnectionSocketFactory sslConnSocketFactory = new SSLConnectionSocketFactory(
sslContext,
new String[]{"TLSv1", "TLSv1.1", "TLSv1.2"},
null,
NoopHostnameVerifier.INSTANCE
);
clientBuilder.setSSLSocketFactory(sslConnSocketFactory);

Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("https", sslConnSocketFactory)
.register("http", PlainConnectionSocketFactory.INSTANCE)
.build();

HttpClientConnectionManager connectionManager = new BasicHttpClientConnectionManager(registry);
clientBuilder.setConnectionManager(connectionManager);
} catch (NoSuchAlgorithmException nsae) {
logger.println("Couldn't establish SSL context:");
nsae.printStackTrace(logger);
} catch (KeyManagementException | KeyStoreException e) {
logger.println("Couldn't initialize SSL context:");
e.printStackTrace(logger);
}
}

// Configure the proxy, if needed
// Using the Jenkins methods handles the noProxyHost settings
configureProxy(clientBuilder, url);

return clientBuilder.build();
}

SSLContext buildSslContext(boolean ignoreUnverifiedSSL, Credentials credentials) throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
SSLContextBuilder contextBuilder = SSLContexts.custom();
contextBuilder.setProtocol("TLS");
if (credentials instanceof CertificateCredentials) {
contextBuilder.loadKeyMaterial(
((CertificateCredentials) credentials).getKeyStore(),
((CertificateCredentials) credentials).getPassword().getPlainText().toCharArray());
}
if (ignoreUnverifiedSSL) {
contextBuilder.loadTrustMaterial(null, TrustAllStrategy.INSTANCE);
}
return contextBuilder.build();
}

void configureProxy(HttpClientBuilder builder, URL url) {
Jenkins jenkins = Jenkins.getInstance();
ProxyConfiguration proxyConfig = jenkins.proxy;
if (proxyConfig == null) {
return;
}

Proxy proxy = proxyConfig.createProxy(url.getHost());
if (proxy == null || proxy.type() != Proxy.Type.HTTP) {
return;
}

SocketAddress addr = proxy.address();
if (addr == null || !(addr instanceof InetSocketAddress)) {
return;
}

InetSocketAddress proxyAddr = (InetSocketAddress) addr;
HttpHost proxyHost = new HttpHost(proxyAddr.getAddress().getHostAddress(), proxyAddr.getPort());
builder.setProxy(proxyHost);

String proxyUser = proxyConfig.getUserName();
if (proxyUser != null) {
String proxyPass = proxyConfig.getPassword();
BasicCredentialsProvider cred = new BasicCredentialsProvider();
cred.setCredentials(new AuthScope(proxyHost),
new org.apache.http.auth.UsernamePasswordCredentials(proxyUser, proxyPass));
builder.setDefaultCredentialsProvider(cred)
.setProxyAuthenticationStrategy(new ProxyAuthenticationStrategy());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.jenkinsci.plugins.stashNotifier;

import edu.umd.cs.findbugs.annotations.NonNull;

/**
* This is the default way of selecting a {@link HttpNotifier}.
*
* Always returns {@link DefaultApacheHttpNotifier} for backwards compatibility with v1.20 and earlier.
*/
class DefaultHttpNotifierSelector implements HttpNotifierSelector {
private final HttpNotifier httpNotifier;

DefaultHttpNotifierSelector(HttpNotifier httpNotifier) {
this.httpNotifier = httpNotifier;
}

/**
* @param context unused
* @return singleton {@link DefaultApacheHttpNotifier}
*/
@Override
public @NonNull HttpNotifier select(@NonNull SelectionContext context) {
return httpNotifier;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.jenkinsci.plugins.stashNotifier;

import edu.umd.cs.findbugs.annotations.NonNull;
import net.sf.json.JSONObject;

import java.net.URI;

/**
* Implement this interface to change the way requests are made to Bitbucket.
*/
public interface HttpNotifier {
/**
* Basic contract for sending Bitbucket build status notifications.
*
* @param uri fully-formed URI (stash-base-uri/rest/build-status/1.0/commits/commit-id)
* @param payload body of status to post
* @param settings user or administrator defined settings for the request
* @param context build info
* @return result of posting status
*/
@NonNull
NotificationResult send(@NonNull URI uri, @NonNull JSONObject payload, @NonNull NotificationSettings settings, @NonNull NotificationContext context);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.jenkinsci.plugins.stashNotifier;

import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.Run;
import hudson.model.TaskListener;

/**
* Implement this interface to have more control over which {@link HttpNotifier}
* will be used at runtime.
*
* @see DefaultHttpNotifierSelector
*/
public interface HttpNotifierSelector {

/**
* Invoked once per Bitbucket notification. {@link SelectionContext} makes
* this method useful for performing migrations on a running system without
* restarts.
*
* @see StashNotifier#prebuild(AbstractBuild, BuildListener)
* @see StashNotifier#perform(Run, FilePath, Launcher, TaskListener)
* @param context parameters useful for selecting a notifier
* @return selected notifier
*/
@NonNull HttpNotifier select(@NonNull SelectionContext context);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.jenkinsci.plugins.stashNotifier;

import hudson.model.Run;

import java.io.PrintStream;

/**
* Properties from the build where this is running.
*/
public class NotificationContext {
private final PrintStream logger;
private final String runId;

public NotificationContext(PrintStream logger, String runId) {
this.logger = logger;
this.runId = runId;
}

/**
* Anything logged here will show up in the running build's console log.
*
* @return handle to build's log
*/
public PrintStream getLogger() {
return logger;
}

/**
* This is the {@link Run#getExternalizableId()} from the running build,
* useful for detailed server-side logging (such as through slf4j).
*
* @return build's id
*/
public String getRunId() {
return runId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.jenkinsci.plugins.stashNotifier;

import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials;
import edu.umd.cs.findbugs.annotations.CheckForNull;

/**
* Properties defined by a user or administrator about how they want the
* notification to be sent.
*/
public class NotificationSettings {
private final boolean ignoreUnverifiedSSL;
private final UsernamePasswordCredentials credentials;

public NotificationSettings(boolean ignoreUnverifiedSSL, UsernamePasswordCredentials credentials) {
this.ignoreUnverifiedSSL = ignoreUnverifiedSSL;
this.credentials = credentials;
}

public boolean isIgnoreUnverifiedSSL() {
return ignoreUnverifiedSSL;
}

@CheckForNull
public UsernamePasswordCredentials getCredentials() {
return credentials;
}
}
Loading