Skip to content

Commit

Permalink
Introduce HttpNotifier
Browse files Browse the repository at this point in the history
StashNotifier gets a HttpNotifier that encapsulates all of the
client-specific code. Old methods are left intact, but deprecated, in
case other consumers are referencing these methods.
  • Loading branch information
sghill committed Oct 23, 2021
1 parent 8d9dafa commit 40750a1
Show file tree
Hide file tree
Showing 7 changed files with 444 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
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 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;

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

@Override
public NotificationResult send(URI uri, JSONObject payload, NotificationSettings settings, 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,9 @@
package org.jenkinsci.plugins.stashNotifier;

import net.sf.json.JSONObject;

import java.net.URI;

public interface HttpNotifier {
NotificationResult send(URI uri, JSONObject payload, NotificationSettings settings, NotificationContext context);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.jenkinsci.plugins.stashNotifier;

import java.io.PrintStream;

public class NotificationContext {
private final PrintStream logger;
private final String runId;

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

public PrintStream getLogger() {
return logger;
}

public String getRunId() {
return runId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.jenkinsci.plugins.stashNotifier;

import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials;

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;
}

public UsernamePasswordCredentials getCredentials() {
return credentials;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
import org.apache.http.HttpEntity;
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;
Expand All @@ -64,7 +63,6 @@
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.jenkinsci.Symbol;
import org.jenkinsci.plugins.displayurlapi.DisplayURLProvider;
import org.jenkinsci.plugins.tokenmacro.MacroEvaluationException;
Expand Down Expand Up @@ -160,6 +158,7 @@ public class StashNotifier extends Notifier implements SimpleBuildStep {
private final boolean considerUnstableAsSuccess;

private final JenkinsLocationConfiguration globalConfig;
private final HttpNotifier httpNotifier;

// public members ----------------------------------------------------------

Expand All @@ -180,7 +179,8 @@ public BuildStepMonitor getRequiredMonitorService() {
boolean prependParentProjectKey,
boolean disableInprogressNotification,
boolean considerUnstableAsSuccess,
JenkinsLocationConfiguration globalConfig
JenkinsLocationConfiguration globalConfig,
HttpNotifier httpNotifier
) {

this.stashServerBaseUrl = stashServerBaseUrl != null && stashServerBaseUrl.endsWith("/")
Expand All @@ -205,6 +205,7 @@ public BuildStepMonitor getRequiredMonitorService() {
this.disableInprogressNotification = disableInprogressNotification;
this.considerUnstableAsSuccess = considerUnstableAsSuccess;
this.globalConfig = globalConfig;
this.httpNotifier = httpNotifier;
}

@DataBoundConstructor
Expand Down Expand Up @@ -233,7 +234,8 @@ public StashNotifier(
prependParentProjectKey,
disableInprogressNotification,
considerUnstableAsSuccess,
JenkinsLocationConfiguration.get()
JenkinsLocationConfiguration.get(),
new DefaultApacheHttpNotifier()
);
}

Expand Down Expand Up @@ -455,7 +457,9 @@ protected Collection<String> lookupCommitSha1s(
* Returns the HttpClient through which the REST call is made. Uses an
* unsafe TrustStrategy in case the user specified a HTTPS URL and
* set the ignoreUnverifiedSSLPeer flag.
* @see DefaultApacheHttpNotifier#getHttpClient(PrintStream, URI, boolean)
*/
@Deprecated
protected CloseableHttpClient getHttpClient(PrintStream logger, Run<?, ?> run, String stashServer) throws Exception {
DescriptorImpl globalSettings = getDescriptor();

Expand Down Expand Up @@ -509,7 +513,9 @@ protected CloseableHttpClient getHttpClient(PrintStream logger, Run<?, ?> run, S

/**
* Helper in place to allow us to define out HttpClient SSL context
* @see DefaultApacheHttpNotifier#buildSslContext(boolean, Credentials)
*/
@Deprecated
private SSLContext buildSslContext(boolean ignoreUnverifiedSSL, Credentials credentials) throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
SSLContextBuilder contextBuilder = SSLContexts.custom();
contextBuilder.setProtocol("TLS");
Expand All @@ -524,6 +530,10 @@ private SSLContext buildSslContext(boolean ignoreUnverifiedSSL, Credentials cred
return contextBuilder.build();
}

/**
* @see DefaultApacheHttpNotifier#configureProxy(HttpClientBuilder, URL)
*/
@Deprecated
private void configureProxy(HttpClientBuilder builder, URL url) {
Jenkins jenkins = Jenkins.getInstance();
ProxyConfiguration proxyConfig = jenkins.proxy;
Expand Down Expand Up @@ -782,16 +792,15 @@ protected NotificationResult notifyStash(
= getCredentials(UsernamePasswordCredentials.class, run.getParent());

URI uri = BuildStatusUriFactory.create(stashURL, commitSha1);
HttpPost req = createRequest(uri, payload, usernamePasswordCredentials);
try (CloseableHttpClient client = getHttpClient(logger, run, stashURL)) {
HttpResponse res = client.execute(req);
if (res.getStatusLine().getStatusCode() != 204) {
return NotificationResult.newFailure(
EntityUtils.toString(res.getEntity()));
} else {
return NotificationResult.newSuccess();
}
}
NotificationSettings settings = new NotificationSettings(
ignoreUnverifiedSSLPeer || getDescriptor().isIgnoreUnverifiedSsl(),
usernamePasswordCredentials
);
NotificationContext context = new NotificationContext(
logger,
run.getExternalizableId()
);
return httpNotifier.send(uri, payload, settings, context);
}

/**
Expand Down Expand Up @@ -892,11 +901,13 @@ protected HttpPost createRequest(
* Returns the HTTP POST request ready to be sent to the Bitbucket build API for
* the given run and change set.
*
* @see DefaultApacheHttpNotifier#createRequest(URI, JSONObject, UsernamePasswordCredentials)
* @param uri uri for the POST request
* @param payload a entity containing the parameters for Bitbucket
* @param credentials the SHA1 of the commit that was built
* @return the HTTP POST request to the Bitbucket build API
*/
@Deprecated
protected HttpPost createRequest(
final URI uri,
final JSONObject payload,
Expand Down

0 comments on commit 40750a1

Please sign in to comment.