Skip to content

Commit

Permalink
Merge pull request #273 from sghill/iso-client
Browse files Browse the repository at this point in the history
Swappable HTTP Client
  • Loading branch information
scaytrase committed Nov 15, 2021
2 parents 122a477 + ce009ba commit 6683f37
Show file tree
Hide file tree
Showing 15 changed files with 827 additions and 54 deletions.
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;
}
}

0 comments on commit 6683f37

Please sign in to comment.