diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index f02d1564..e07959ec 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -3,17 +3,19 @@ import static org.apache.commons.lang.StringUtils.trimToEmpty; import java.io.Serializable; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; +import javax.net.ssl.*; import java.net.URL; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.util.List; import javax.annotation.CheckForNull; -import javax.annotation.Nonnull; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2.Auth2Descriptor; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NoneAuth; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.NaiveTrustManager; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.DataBoundConstructor; @@ -50,6 +52,7 @@ public class RemoteJenkinsServer extends AbstractDescribableImpl { @@ -153,6 +162,28 @@ public String getDisplayName() { return ""; } + /** + * Sets the TrustManager to be a "NaiveTrustManager", allowing us to ignore untrusted certificates + * Will set the connection to null, if a key management error occurred. + * + * ATTENTION: THIS IS VERY DANGEROUS AND SHOULD ONLY BE USED IF YOU KNOW WHAT YOU DO! + * @param conn The HttpsURLConnection you want to modify. + * @param trustAllCertificates A boolean, gotten from the Remote Hosts description + */ + public void makeConnectionTrustAllCertificates(HttpsURLConnection conn, boolean trustAllCertificates) + throws NoSuchAlgorithmException, KeyManagementException { + if (trustAllCertificates) { + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(new KeyManager[0], new TrustManager[]{new NaiveTrustManager()}, new SecureRandom()); + // SSLContext.setDefault(ctx); + conn.setSSLSocketFactory(ctx.getSocketFactory()); + + // Trust every hostname + HostnameVerifier allHostsValid = (hostname, session) -> true; + conn.setHostnameVerifier(allHostsValid); + } + } + /** * Validates the given address to see that it's well-formed, and is reachable. * @@ -161,7 +192,7 @@ public String getDisplayName() { * @return FormValidation object */ @Restricted(NoExternalUse.class) - public FormValidation doCheckAddress(@QueryParameter String address) { + public FormValidation doCheckAddress(@QueryParameter String address, @QueryParameter boolean trustAllCertificates) { URL host = null; @@ -180,11 +211,22 @@ public FormValidation doCheckAddress(@QueryParameter String address) { // check that the host is reachable try { - HttpURLConnection connection = (HttpURLConnection) host.openConnection(); - connection.setConnectTimeout(5000); - connection.connect(); + HttpsURLConnection conn = (HttpsURLConnection) host.openConnection(); + try { + makeConnectionTrustAllCertificates(conn, trustAllCertificates); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + return FormValidation.error(e, "A key management error occurred."); + } + conn.setConnectTimeout(5000); + conn.connect(); + + if (trustAllCertificates) { + return FormValidation.warning( + "Connection established! Accepting all certificates is potentially unsafe." + ); + } } catch (Exception e) { - return FormValidation.warning("Address looks good, but a connection could not be stablished."); + return FormValidation.warning("Address looks good, but a connection could not be established."); } return FormValidation.ok(); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java index 5773c60e..8ce67685 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java @@ -12,12 +12,15 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; +import javax.net.ssl.*; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; import java.net.URLEncoder; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.text.SimpleDateFormat; import java.time.Duration; import java.time.Instant; @@ -186,7 +189,7 @@ public static String encodeValue(String dirtyValue) { return cleanValue; } - private static String readInputStream(HttpURLConnection connection) throws IOException { + private static String readInputStream(HttpsURLConnection connection) throws IOException { BufferedReader rd = null; try { @@ -243,7 +246,7 @@ private static JenkinsCrumb getCrumb(BuildContext context, Auth2 overrideAuth, b context.logger.println("reuse cached crumb: " + globalHost); return jenkinsCrumb; } - HttpURLConnection connection = getAuthorizedConnection(context, crumbProviderUrl, overrideAuth); + HttpsURLConnection connection = getAuthorizedConnection(context, crumbProviderUrl, overrideAuth); int responseCode = connection.getResponseCode(); if (responseCode == 401) { throw new UnauthorizedException(crumbProviderUrl); @@ -277,7 +280,7 @@ private static JenkinsCrumb getCrumb(BuildContext context, Auth2 overrideAuth, b * @param context * @throws IOException */ - private static void addCrumbToConnection(HttpURLConnection connection, BuildContext context, Auth2 overrideAuth, + private static void addCrumbToConnection(HttpsURLConnection connection, BuildContext context, Auth2 overrideAuth, boolean isCacheEnabled) throws IOException { String method = connection.getRequestMethod(); if (method != null && method.equalsIgnoreCase("POST")) { @@ -288,7 +291,19 @@ private static void addCrumbToConnection(HttpURLConnection connection, BuildCont } } - private static HttpURLConnection getAuthorizedConnection(BuildContext context, URL url, Auth2 overrideAuth) + /** + * Returns an authorized HttpsURLConnection + * If the user wanted to trust all certificates, the TrustManager and HostVerifier of the connection + * will be set properly. + * + * ATTENTION: THIS IS VERY DANGEROUS AND SHOULD ONLY BE USED IF YOU KNOW WHAT YOU DO! + * @param context The build context + * @param url The url to the remote build + * @param overrideAuth + * @return An authorized connection with or without a NaiveTrustManager + * @throws IOException + */ + private static HttpsURLConnection getAuthorizedConnection(BuildContext context, URL url, Auth2 overrideAuth) throws IOException { URLConnection connection = context.effectiveRemoteServer.isUseProxy() ? ProxyConfiguration.open(url) : url.openConnection(); @@ -302,8 +317,24 @@ private static HttpURLConnection getAuthorizedConnection(BuildContext context, U // Set Authorization Header configured globally for remoteServer serverAuth.setAuthorizationHeader(connection, context); } + HttpsURLConnection conn = (HttpsURLConnection) connection; - return (HttpURLConnection) connection; + if (context.effectiveRemoteServer.getTrustAllCertificates()){ + // Installing the naive manage + try { + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(new KeyManager[0], new TrustManager[]{new NaiveTrustManager()}, new SecureRandom()); + // SSLContext.setDefault(ctx); + conn.setSSLSocketFactory(ctx.getSocketFactory()); + + // Trust every hostname + HostnameVerifier allHostsValid = (hostname, session) -> true; + conn.setHostnameVerifier(allHostsValid); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + context.logger.println("Unable to trust all certificates."); + } + } + return conn; } private static String getUrlWithoutParameters(String url) { @@ -387,7 +418,6 @@ public static String buildTriggerUrl(String jobNameOrUrl, String securityToken, * of a failed connection, the method calls it self recursively and increments * the number of attempts. * - * @see sendHTTPCall * @param urlString * the URL that needs to be called. * @param requestType @@ -429,7 +459,7 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT } URL url = new URL(urlString); - HttpURLConnection conn = getAuthorizedConnection(context, url, overrideAuth); + HttpsURLConnection conn = getAuthorizedConnection(context, url, overrideAuth); try { conn.setDoInput(true); @@ -440,7 +470,7 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT // wait up to 5 seconds for the connection to be open conn.setConnectTimeout(5000); if (HTTP_POST.equalsIgnoreCase(requestType)) { - // use longer timeout during POST due to not performing retrys since POST is not idem-potent + // use longer timeout during POST due to not performing retries since POST is not idem-potent conn.setReadTimeout(30000); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); conn.setRequestProperty("Content-Length", String.valueOf(postDataBytes.length)); @@ -491,7 +521,7 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT // non-parameterized jobs), it returns a string indicating the status. // But in newer versions of Jenkins, it just returns an empty response. // So we need to compensate and check for both. - if (responseCode >= 400 || JSONUtils.mayBeJSON(response) == false) { + if (responseCode >= 400 || !JSONUtils.mayBeJSON(response)) { return new ConnectionResponse(responseHeader, response, responseCode); } else { responseObject = (JSONObject) JSONSerializer.toJSON(response); @@ -509,12 +539,12 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT // If we have connectionRetryLimit set to > 0 then retry that many times. if (numberOfAttempts <= retryLimit) { context.logger.println(String.format( - "Connection to remote server failed %s, waiting for to retry - %s seconds until next attempt. URL: %s, parameters: %s", + "Connection to remote server failed %s, waiting to retry - %s seconds until next attempt. URL: %s, parameters: %s", (responseCode == 0 ? "" : "[" + responseCode + "]"), pollInterval, getUrlWithoutParameters(urlString), parmsString)); // Sleep for 'pollInterval' seconds. - // Sleep takes miliseconds so need to convert this.pollInterval to milisecopnds + // Sleep takes milliseconds so need to convert this.pollInterval to milliseconds // (x 1000) try { // Could do with a better way of sleeping... diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/NaiveTrustManager.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/NaiveTrustManager.java new file mode 100644 index 00000000..84de9ab9 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/NaiveTrustManager.java @@ -0,0 +1,18 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils; + +import javax.net.ssl.*; +import java.security.cert.X509Certificate; + +// Trust every server +public class NaiveTrustManager implements X509TrustManager { + @Override + public void checkClientTrusted(X509Certificate[] arg0, String arg1) {} + + @Override + public void checkServerTrusted(X509Certificate[] arg0, String arg1) {} + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } +} diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-trustAllCertificates.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-trustAllCertificates.html new file mode 100644 index 00000000..8cdf8413 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-trustAllCertificates.html @@ -0,0 +1,21 @@ +
+
+ Trust all certificates +
+ +

+ It is possible to override/rewrite the 'Trust all certificate'-setting for each Job separately. + Setting this checkbox to 'true' will result in accepting all certificates for the given Job. +

+ +
+ If your remote Jenkins host has a + + self-signed certificate + + or its certificate is not trusted, you may want to enable this option. + It will accept untrusted certificates for the given host. +
+ +

This is unsafe and should only be used for testing or if you trust the host.

+
\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/config.jelly index f5300952..e7313a4c 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/config.jelly @@ -8,6 +8,9 @@ + + + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/help-trustAllCertificates.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/help-trustAllCertificates.html new file mode 100644 index 00000000..caf73c61 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/help-trustAllCertificates.html @@ -0,0 +1,16 @@ +
+
+ Trust all certificates +
+ +
+ If your remote Jenkins host has a + + self-signed certificate + + or its certificate is not trusted, you may want to enable this option. + It will accept untrusted certificates for the given host. +
+ +

This is unsafe and should only be used for testing or if you trust the host.

+
\ No newline at end of file