headers) {
- this.publicKey = publicKey;
- this.headers = headers;
- }
-
- @Override
- public void beforeProperties(@NonNull JnlpConnectionState event) {
- if (event instanceof Jnlp4ConnectionState) {
- X509Certificate certificate = ((Jnlp4ConnectionState) event).getCertificate();
- if (certificate != null) {
- String fingerprint = KeyUtils.fingerprint(certificate.getPublicKey());
- if (!KeyUtils.equals(publicKey, certificate.getPublicKey())) {
- event.reject(new ConnectionRefusalException("Expecting identity " + fingerprint));
- }
- events.status("Remote identity confirmed: " + fingerprint);
- }
- }
- }
-
- @Override
- public void afterProperties(@NonNull JnlpConnectionState event) {
- event.approve();
- }
-
- @Override
- public void beforeChannel(@NonNull JnlpConnectionState event) {
- ChannelBuilder bldr = event.getChannelBuilder().withMode(Channel.Mode.BINARY);
- if (jarCache != null) {
- bldr.withJarCache(jarCache);
- }
- }
-
- @Override
- public void afterChannel(@NonNull JnlpConnectionState event) {
- // store the new cookie for next connection attempt
- String cookie = event.getProperty(JnlpConnectionState.COOKIE_KEY);
- if (cookie == null) {
- headers.remove(JnlpConnectionState.COOKIE_KEY);
- } else {
- headers.put(JnlpConnectionState.COOKIE_KEY, cookie);
- }
- }
- }
}
diff --git a/src/main/java/hudson/remoting/Launcher.java b/src/main/java/hudson/remoting/Launcher.java
index 78161dbd7..7a0d1819a 100644
--- a/src/main/java/hudson/remoting/Launcher.java
+++ b/src/main/java/hudson/remoting/Launcher.java
@@ -78,6 +78,7 @@
import org.jenkinsci.remoting.engine.WorkDirManager;
import org.jenkinsci.remoting.util.DurationFormatter;
import org.jenkinsci.remoting.util.PathUtils;
+import org.jenkinsci.remoting.util.SSLUtils;
import org.jenkinsci.remoting.util.https.NoCheckHostnameVerifier;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
@@ -517,7 +518,7 @@ private synchronized void initialize() throws IOException {
// Initialize certificates
createX509Certificates();
try {
- sslSocketFactory = Engine.getSSLSocketFactory(x509Certificates, noCertificateCheck);
+ sslSocketFactory = SSLUtils.getSSLSocketFactory(x509Certificates, noCertificateCheck);
} catch (GeneralSecurityException | PrivilegedActionException e) {
throw new RuntimeException(e);
}
@@ -1121,7 +1122,9 @@ private Engine createEngine() throws IOException {
engine.setJarCache(new FileSystemJarCache(jarCache, true));
}
engine.setNoReconnect(noReconnect);
- engine.setNoReconnectAfter(noReconnectAfter);
+ if (noReconnectAfter != null) {
+ engine.setNoReconnectAfter(noReconnectAfter);
+ }
engine.setKeepAlive(!noKeepAlive);
if (noCertificateCheck) {
diff --git a/src/main/java/org/jenkinsci/remoting/engine/EndpointConnector.java b/src/main/java/org/jenkinsci/remoting/engine/EndpointConnector.java
new file mode 100644
index 000000000..d438fffb7
--- /dev/null
+++ b/src/main/java/org/jenkinsci/remoting/engine/EndpointConnector.java
@@ -0,0 +1,48 @@
+package org.jenkinsci.remoting.engine;
+
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import edu.umd.cs.findbugs.annotations.Nullable;
+import hudson.remoting.Channel;
+import java.io.Closeable;
+import java.net.URL;
+import java.util.concurrent.Future;
+
+/**
+ * Represents a connection to a remote endpoint to open a {@link Channel}.
+ *
+ * First, {@link hudson.remoting.Engine} creates an instance of {@link EndpointConnector} and calls {@link #waitUntilReady()}. Once it has returned {@code true}, it calls {@link #connect()} to establish the connection.
+ *
+ * Then {@link #getProtocol()} and {@link #getUrl()} are called to get the protocol and URL of the endpoint.
+ *
+ * Upon termination, @{link #close()} is called.
+ *
+ * @since TODO
+ */
+public interface EndpointConnector extends Closeable {
+
+ /**
+ * @return a future to the channel to be established. Returns null if the connection cannot be established at all.
+ * @throws Exception
+ */
+ @CheckForNull
+ Future connect() throws Exception;
+
+ /**
+ * Waits until the connection can be established.
+ * @return true if the connection is ready, null if the connection never got ready
+ * @throws InterruptedException if the thread is interrupted
+ */
+ @CheckForNull
+ Boolean waitUntilReady() throws InterruptedException;
+
+ /**
+ * @return The name of the protocol used by this connection.
+ */
+ String getProtocol();
+
+ /**
+ * @return the URL of the endpoint, if {@link #waitUntilReady()} returned {@code true}.
+ */
+ @Nullable
+ URL getUrl();
+}
diff --git a/src/main/java/org/jenkinsci/remoting/engine/EndpointConnectorData.java b/src/main/java/org/jenkinsci/remoting/engine/EndpointConnectorData.java
new file mode 100644
index 000000000..ffd5cfae8
--- /dev/null
+++ b/src/main/java/org/jenkinsci/remoting/engine/EndpointConnectorData.java
@@ -0,0 +1,32 @@
+package org.jenkinsci.remoting.engine;
+
+import hudson.remoting.EngineListenerSplitter;
+import hudson.remoting.JarCache;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Captures the data needed to connect to any endpoint.
+ * @param agentName the agent name
+ * @param secretKey the secret key
+ * @param executor the thread pool to use for handling TCP connections
+ * @param events the listener to log to
+ * @param noReconnectAfter Specifies the duration after which the connection should not be re-established.
+ * @param candidateCertificates the list of certificates to be used for the connection
+ * @param disableHttpsCertValidation whether to disable HTTPS certificate validation
+ * @param jarCache Where to store the jar cache
+ * @param proxyCredentials Credentials to use for proxy authentication, if any.
+ * @since TODO
+ */
+public record EndpointConnectorData(
+ String agentName,
+ String secretKey,
+ ExecutorService executor,
+ EngineListenerSplitter events,
+ Duration noReconnectAfter,
+ List candidateCertificates,
+ boolean disableHttpsCertValidation,
+ JarCache jarCache,
+ String proxyCredentials) {}
diff --git a/src/main/java/org/jenkinsci/remoting/engine/InboundTCPConnector.java b/src/main/java/org/jenkinsci/remoting/engine/InboundTCPConnector.java
new file mode 100644
index 000000000..20f1a960d
--- /dev/null
+++ b/src/main/java/org/jenkinsci/remoting/engine/InboundTCPConnector.java
@@ -0,0 +1,254 @@
+package org.jenkinsci.remoting.engine;
+
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.remoting.Channel;
+import hudson.remoting.ChannelBuilder;
+import hudson.remoting.Engine;
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.Socket;
+import java.net.URL;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPublicKey;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Future;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import org.jenkinsci.remoting.protocol.IOHub;
+import org.jenkinsci.remoting.protocol.cert.DelegatingX509ExtendedTrustManager;
+import org.jenkinsci.remoting.protocol.cert.PublicKeyMatchingX509ExtendedTrustManager;
+import org.jenkinsci.remoting.protocol.impl.ConnectionRefusalException;
+import org.jenkinsci.remoting.util.KeyUtils;
+import org.jenkinsci.remoting.util.SSLUtils;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
+/**
+ * Connects to a controller using inbound TCP.
+ */
+@Restricted(NoExternalUse.class)
+public class InboundTCPConnector implements EndpointConnector {
+ private static final Logger LOGGER = Logger.getLogger(InboundTCPConnector.class.getName());
+
+ private final JnlpEndpointResolver jnlpEndpointResolver;
+ private final List candidateUrls;
+ private final DelegatingX509ExtendedTrustManager agentTrustManager;
+ private final boolean keepAlive;
+ private final EndpointConnectorData data;
+
+ private URL url;
+ /**
+ * Name of the protocol that was used to successfully connect to the controller.
+ */
+ private String protocolName;
+
+ /**
+ * Tracks {@link Closeable} resources that need to be closed when this connector is closed.
+ */
+ @NonNull
+ private final List closeables = new ArrayList<>();
+
+ @Override
+ public URL getUrl() {
+ return url;
+ }
+
+ public InboundTCPConnector(
+ EndpointConnectorData data,
+ @NonNull List candidateUrls,
+ @CheckForNull DelegatingX509ExtendedTrustManager agentTrustManager,
+ boolean keepAlive,
+ @NonNull JnlpEndpointResolver jnlpEndpointResolver) {
+ this.data = data;
+ this.candidateUrls = new ArrayList<>(candidateUrls);
+ this.agentTrustManager = agentTrustManager;
+ this.keepAlive = keepAlive;
+ this.jnlpEndpointResolver = jnlpEndpointResolver;
+ }
+
+ private class EngineJnlpConnectionStateListener extends JnlpConnectionStateListener {
+
+ private final RSAPublicKey publicKey;
+ private final Map headers;
+
+ public EngineJnlpConnectionStateListener(RSAPublicKey publicKey, Map headers) {
+ this.publicKey = publicKey;
+ this.headers = headers;
+ }
+
+ @Override
+ public void beforeProperties(@NonNull JnlpConnectionState event) {
+ if (event instanceof Jnlp4ConnectionState) {
+ X509Certificate certificate = ((Jnlp4ConnectionState) event).getCertificate();
+ if (certificate != null) {
+ String fingerprint = KeyUtils.fingerprint(certificate.getPublicKey());
+ if (!KeyUtils.equals(publicKey, certificate.getPublicKey())) {
+ event.reject(new ConnectionRefusalException("Expecting identity " + fingerprint));
+ }
+ data.events().status("Remote identity confirmed: " + fingerprint);
+ }
+ }
+ }
+
+ @Override
+ public void afterProperties(@NonNull JnlpConnectionState event) {
+ event.approve();
+ }
+
+ @Override
+ public void beforeChannel(@NonNull JnlpConnectionState event) {
+ ChannelBuilder bldr = event.getChannelBuilder().withMode(Channel.Mode.BINARY);
+ if (data.jarCache() != null) {
+ bldr.withJarCache(data.jarCache());
+ }
+ }
+
+ @Override
+ public void afterChannel(@NonNull JnlpConnectionState event) {
+ // store the new cookie for next connection attempt
+ String cookie = event.getProperty(JnlpConnectionState.COOKIE_KEY);
+ if (cookie == null) {
+ headers.remove(JnlpConnectionState.COOKIE_KEY);
+ } else {
+ headers.put(JnlpConnectionState.COOKIE_KEY, cookie);
+ }
+ }
+ }
+
+ @Override
+ public Future connect() throws Exception {
+ var hub = IOHub.create(data.executor());
+ closeables.add(hub);
+ var context = SSLUtils.createSSLContext(agentTrustManager);
+
+ final JnlpAgentEndpoint endpoint = RetryUtils.succeedsWithRetries(
+ jnlpEndpointResolver::resolve,
+ data.noReconnectAfter(),
+ data.events(),
+ x -> "Could not locate server among " + candidateUrls + ": " + x.getMessage());
+ if (endpoint == null) {
+ data.events().status("Could not resolve server among " + this.candidateUrls);
+ return null;
+ }
+ url = endpoint.getServiceUrl();
+
+ data.events()
+ .status(String.format(
+ "Agent discovery successful%n"
+ + " Agent address: %s%n"
+ + " Agent port: %d%n"
+ + " Identity: %s",
+ endpoint.getHost(), endpoint.getPort(), KeyUtils.fingerprint(endpoint.getPublicKey())));
+ PublicKeyMatchingX509ExtendedTrustManager delegate = new PublicKeyMatchingX509ExtendedTrustManager();
+ RSAPublicKey publicKey = endpoint.getPublicKey();
+ if (publicKey != null) {
+ // This is so that JNLP4-connect will only connect if the public key matches
+ // if the public key is not published then JNLP4-connect will refuse to connect
+ delegate.add(publicKey);
+ }
+ this.agentTrustManager.setDelegate(delegate);
+
+ data.events().status("Handshaking");
+ // must be read-write
+ final Map headers = new HashMap<>();
+ headers.put(JnlpConnectionState.CLIENT_NAME_KEY, data.agentName());
+ headers.put(JnlpConnectionState.SECRET_KEY, data.secretKey());
+ // Create the protocols that will be attempted to connect to the controller.
+ var clientProtocols = new JnlpProtocolHandlerFactory(data.executor())
+ .withIOHub(hub)
+ .withSSLContext(context)
+ .withPreferNonBlockingIO(false) // we only have one connection, prefer blocking I/O
+ .handlers();
+ var negotiatedProtocols = clientProtocols.stream()
+ .filter(JnlpProtocolHandler::isEnabled)
+ .filter(p -> endpoint.isProtocolSupported(p.getName()))
+ .collect(Collectors.toSet());
+ var serverProtocols = endpoint.getProtocols() == null ? "?" : String.join(",", endpoint.getProtocols());
+ LOGGER.info(buildDebugProtocolsMessage(serverProtocols, clientProtocols, negotiatedProtocols));
+ for (var protocol : negotiatedProtocols) {
+ var jnlpSocket = RetryUtils.succeedsWithRetries(
+ () -> {
+ data.events().status("Connecting to " + endpoint.describe() + " using " + protocol.getName());
+ // default is 30 mins. See PingThread for the ping interval
+ final Socket s = endpoint.open(Engine.SOCKET_TIMEOUT);
+ s.setKeepAlive(keepAlive);
+ return s;
+ },
+ data.noReconnectAfter(),
+ data.events());
+ if (jnlpSocket == null) {
+ return null;
+ }
+ closeables.add(jnlpSocket);
+ try {
+ protocolName = protocol.getName();
+ return protocol.connect(
+ jnlpSocket, headers, new EngineJnlpConnectionStateListener(endpoint.getPublicKey(), headers));
+ } catch (IOException ioe) {
+ data.events().status("Protocol " + protocol.getName() + " failed to establish channel", ioe);
+ protocolName = null;
+ } catch (RuntimeException e) {
+ data.events().status("Protocol " + protocol.getName() + " encountered a runtime error", e);
+ protocolName = null;
+ }
+ // On failure form a new connection.
+ jnlpSocket.close();
+ closeables.remove(jnlpSocket);
+ }
+ if (negotiatedProtocols.isEmpty()) {
+ data.events()
+ .status(
+ "reconnect rejected",
+ new Exception("The server rejected the connection: None of the protocols were accepted"));
+ } else {
+ data.events()
+ .status(
+ "reconnect rejected",
+ new Exception("The server rejected the connection: None of the protocols are enabled"));
+ }
+ return null;
+ }
+
+ @NonNull
+ private static String buildDebugProtocolsMessage(
+ String serverProtocols,
+ List> clientProtocols,
+ Set> negotiatedProtocols) {
+ return "Protocols support: Server " + "[" + serverProtocols + "]"
+ + ", Client " + "["
+ + clientProtocols.stream()
+ .map(p -> p.getName() + (!p.isEnabled() ? " (disabled)" : ""))
+ .collect(Collectors.joining(","))
+ + "]"
+ + ", Negociated: " + "["
+ + negotiatedProtocols.stream().map(JnlpProtocolHandler::getName).collect(Collectors.joining(","))
+ + "]";
+ }
+
+ @Override
+ public Boolean waitUntilReady() throws InterruptedException {
+ jnlpEndpointResolver.waitForReady();
+ return true;
+ }
+
+ @Override
+ public String getProtocol() {
+ return protocolName;
+ }
+
+ @Override
+ public void close() {
+ closeables.forEach(c -> {
+ try {
+ c.close();
+ } catch (IOException e) {
+ data.events().status("Failed to close resource " + c, e);
+ }
+ });
+ }
+}
diff --git a/src/main/java/org/jenkinsci/remoting/engine/JnlpAgentEndpoint.java b/src/main/java/org/jenkinsci/remoting/engine/JnlpAgentEndpoint.java
index 58001338c..cf3a13392 100644
--- a/src/main/java/org/jenkinsci/remoting/engine/JnlpAgentEndpoint.java
+++ b/src/main/java/org/jenkinsci/remoting/engine/JnlpAgentEndpoint.java
@@ -133,6 +133,10 @@ public JnlpAgentEndpoint(
this.proxyCredentials = proxyCredentials;
}
+ String describe() {
+ return getHost() + ':' + getPort();
+ }
+
/**
* Gets the socket address.
*
diff --git a/src/main/java/org/jenkinsci/remoting/engine/JnlpAgentEndpointConfigurator.java b/src/main/java/org/jenkinsci/remoting/engine/JnlpAgentEndpointConfigurator.java
index 9055a843b..a331f37dc 100644
--- a/src/main/java/org/jenkinsci/remoting/engine/JnlpAgentEndpointConfigurator.java
+++ b/src/main/java/org/jenkinsci/remoting/engine/JnlpAgentEndpointConfigurator.java
@@ -23,11 +23,11 @@
*/
package org.jenkinsci.remoting.engine;
+import hudson.remoting.EngineListenerSplitter;
import java.io.IOException;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.Set;
-import java.util.logging.Level;
import java.util.logging.Logger;
public class JnlpAgentEndpointConfigurator extends JnlpEndpointResolver {
@@ -38,17 +38,24 @@ public class JnlpAgentEndpointConfigurator extends JnlpEndpointResolver {
private final Set protocols;
private final String directionConnection;
private final String proxyCredentials;
+ private final EngineListenerSplitter events;
public JnlpAgentEndpointConfigurator(
- String directConnection, String instanceIdentity, Set protocols, String proxyCredentials) {
+ String directConnection,
+ String instanceIdentity,
+ Set protocols,
+ String proxyCredentials,
+ EngineListenerSplitter events) {
this.directionConnection = directConnection;
this.instanceIdentity = instanceIdentity;
this.protocols = protocols;
this.proxyCredentials = proxyCredentials;
+ this.events = events;
}
@Override
public JnlpAgentEndpoint resolve() throws IOException {
+ events.status("Using direct connection to " + directionConnection);
RSAPublicKey identity;
try {
identity = getIdentity(instanceIdentity);
@@ -65,7 +72,5 @@ public JnlpAgentEndpoint resolve() throws IOException {
}
@Override
- public void waitForReady() {
- LOGGER.log(Level.INFO, "Sleeping 10s before reconnect.");
- }
+ public void waitForReady() {}
}
diff --git a/src/main/java/org/jenkinsci/remoting/engine/JnlpAgentEndpointResolver.java b/src/main/java/org/jenkinsci/remoting/engine/JnlpAgentEndpointResolver.java
index 6d6b7e936..9edded544 100644
--- a/src/main/java/org/jenkinsci/remoting/engine/JnlpAgentEndpointResolver.java
+++ b/src/main/java/org/jenkinsci/remoting/engine/JnlpAgentEndpointResolver.java
@@ -27,22 +27,19 @@
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.remoting.Engine;
+import hudson.remoting.EngineListenerSplitter;
import hudson.remoting.Launcher;
import hudson.remoting.NoProxyEvaluator;
-import hudson.remoting.Util;
import java.io.IOException;
import java.net.Authenticator;
-import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
-import java.net.NoRouteToHostException;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.Socket;
import java.net.SocketAddress;
-import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
@@ -51,7 +48,6 @@
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.time.Duration;
-import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Iterator;
@@ -69,10 +65,8 @@
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
-import org.jenkinsci.remoting.util.DurationFormatter;
import org.jenkinsci.remoting.util.ThrowableUtils;
import org.jenkinsci.remoting.util.VersionNumber;
-import org.jenkinsci.remoting.util.https.NoCheckHostnameVerifier;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
@@ -97,12 +91,15 @@ public class JnlpAgentEndpointResolver extends JnlpEndpointResolver {
private SSLSocketFactory sslSocketFactory;
- private boolean disableHttpsCertValidation;
-
- private HostnameVerifier hostnameVerifier;
+ private boolean noReconnect;
+ @NonNull
private Duration noReconnectAfter;
+ private EngineListenerSplitter events;
+
+ private boolean first = true;
+
/**
* If specified, only the protocols from the list will be tried during the connection.
* The option provides protocol names, but the order of the check is defined internally and cannot be changed.
@@ -119,85 +116,24 @@ public JnlpAgentEndpointResolver(
String proxyCredentials,
String tunnel,
SSLSocketFactory sslSocketFactory,
- boolean disableHttpsCertValidation,
- Duration noReconnectAfter) {
+ boolean noReconnect,
+ @NonNull Duration noReconnectAfter,
+ EngineListenerSplitter events) {
this.jenkinsUrls = new ArrayList<>(jenkinsUrls);
this.agentName = agentName;
this.credentials = credentials;
this.proxyCredentials = proxyCredentials;
this.tunnel = tunnel;
this.sslSocketFactory = sslSocketFactory;
- setDisableHttpsCertValidation(disableHttpsCertValidation);
+ this.noReconnect = noReconnect;
this.noReconnectAfter = noReconnectAfter;
- }
-
- public SSLSocketFactory getSslSocketFactory() {
- return sslSocketFactory;
- }
-
- public void setSslSocketFactory(SSLSocketFactory sslSocketFactory) {
- this.sslSocketFactory = sslSocketFactory;
- }
-
- public String getCredentials() {
- return credentials;
- }
-
- public void setCredentials(String credentials) {
- this.credentials = credentials;
- }
-
- public void setCredentials(String user, String pass) {
- this.credentials = user + ":" + pass;
- }
-
- public String getProxyCredentials() {
- return proxyCredentials;
- }
-
- public void setProxyCredentials(String proxyCredentials) {
- this.proxyCredentials = proxyCredentials;
- }
-
- public void setProxyCredentials(String user, String pass) {
- this.proxyCredentials = user + ":" + pass;
- }
-
- @CheckForNull
- public String getTunnel() {
- return tunnel;
- }
-
- public void setTunnel(@CheckForNull String tunnel) {
- this.tunnel = tunnel;
- }
-
- /**
- * Determine if certificate checking should be ignored for JNLP endpoint
- *
- * @return {@code true} if the HTTPs certificate is disabled, endpoint check is ignored
- */
- public boolean isDisableHttpsCertValidation() {
- return disableHttpsCertValidation;
- }
-
- /**
- * Sets if the HTTPs certificate check should be disabled.
- *
- * This behavior is not recommended.
- */
- public void setDisableHttpsCertValidation(boolean disableHttpsCertValidation) {
- this.disableHttpsCertValidation = disableHttpsCertValidation;
- if (disableHttpsCertValidation) {
- this.hostnameVerifier = new NoCheckHostnameVerifier();
- } else {
- this.hostnameVerifier = null;
- }
+ this.events = events;
}
@CheckForNull
@Override
public JnlpAgentEndpoint resolve() throws IOException {
+ events.status("Locating server among " + this.jenkinsUrls);
IOException firstError = null;
for (String jenkinsUrl : jenkinsUrls) {
if (jenkinsUrl == null) {
@@ -218,8 +154,8 @@ public JnlpAgentEndpoint resolve() throws IOException {
}
// find out the TCP port
- HttpURLConnection con = (HttpURLConnection) openURLConnection(
- salURL, agentName, credentials, proxyCredentials, sslSocketFactory, hostnameVerifier);
+ HttpURLConnection con = (HttpURLConnection)
+ openURLConnection(salURL, agentName, credentials, proxyCredentials, sslSocketFactory, null);
try {
try {
con.setConnectTimeout(30000);
@@ -275,7 +211,7 @@ public JnlpAgentEndpoint resolve() throws IOException {
+ "to define the supported protocols.");
} else {
LOGGER.log(
- Level.INFO, "Remoting server accepts the following protocols: {0}", agentProtocolNames);
+ Level.FINE, "Remoting server accepts the following protocols: " + agentProtocolNames);
}
}
@@ -293,6 +229,11 @@ public JnlpAgentEndpoint resolve() throws IOException {
}
String idHeader = con.getHeaderField("X-Instance-Identity");
+ if (idHeader == null) {
+ firstError = ThrowableUtils.chain(
+ firstError, new IOException(jenkinsUrl + " is missing instance-identity plugin"));
+ continue;
+ }
RSAPublicKey identity;
try {
identity = getIdentity(idHeader);
@@ -433,55 +374,15 @@ private URL toAgentListenerURL(@NonNull String jenkinsUrl) throws MalformedURLEx
@Override
public void waitForReady() throws InterruptedException {
- Thread t = Thread.currentThread();
- String oldName = t.getName();
- try {
- int retries = 0;
- Instant firstAttempt = Instant.now();
- while (true) {
- // TODO refactor various sleep statements into a common method
- if (Util.shouldBailOut(firstAttempt, noReconnectAfter)) {
- LOGGER.info("Bailing out after " + DurationFormatter.format(noReconnectAfter));
- return;
- }
- Thread.sleep(1000 * 10);
- // Jenkins top page might be read-protected. see http://www.nabble
- // .com/more-lenient-retry-logic-in-Engine.waitForServerToBack-td24703172.html
- if (jenkinsUrls.isEmpty()) {
- // returning here will cause the whole loop to be broken and all the urls to be tried again
- return;
- }
- String firstUrl = jenkinsUrls.get(0);
- try {
- URL url = toAgentListenerURL(firstUrl);
-
- retries++;
- t.setName(oldName + ": trying " + url + " for " + retries + " times");
-
- HttpURLConnection con = (HttpURLConnection) openURLConnection(
- url, agentName, credentials, proxyCredentials, sslSocketFactory, hostnameVerifier);
- con.setConnectTimeout(5000);
- con.setReadTimeout(5000);
- con.connect();
- if (con.getResponseCode() == 200) {
- return;
- }
- LOGGER.log(
- Level.INFO,
- "Controller isn''t ready to talk to us on {0}. Will try again: response code={1}",
- new Object[] {url, con.getResponseCode()});
- } catch (SocketTimeoutException | ConnectException | NoRouteToHostException e) {
- LOGGER.log(Level.INFO, "Failed to connect to {0}. Will try again: {1} {2}", new String[] {
- firstUrl, e.getClass().getName(), e.getMessage()
- });
- } catch (IOException e) {
- // report the failure
- LOGGER.log(Level.INFO, e, () -> "Failed to connect to " + firstUrl + ". Will try again");
- }
- }
- } finally {
- t.setName(oldName);
+ if (RetryUtils.succeedsWithRetries(
+ this::ping,
+ first && noReconnect ? Duration.ZERO : noReconnectAfter,
+ events,
+ x -> "Could not locate server among " + jenkinsUrls + ": " + x.getMessage())
+ == null) {
+ throw new RuntimeException("Could not locate server among " + jenkinsUrls);
}
+ first = false;
}
@CheckForNull
@@ -637,4 +538,26 @@ public static URLConnection openURLConnection(
}
return con;
}
+
+ @SuppressFBWarnings(value = "NP_BOOLEAN_RETURN_NULL", justification = "null is used to indicate no connection")
+ private Boolean ping() throws IOException {
+ for (String jenkinsUrl : jenkinsUrls) {
+ URL url = toAgentListenerURL(jenkinsUrl);
+ HttpURLConnection con = (HttpURLConnection)
+ openURLConnection(url, agentName, credentials, proxyCredentials, sslSocketFactory, null);
+ con.setConnectTimeout(5000);
+ con.setReadTimeout(5000);
+ con.connect();
+ if (con.getResponseCode() == 200) {
+ return true;
+ } else if (con.getResponseCode() == 404) {
+ events.status("Controller isn't ready to talk to us on " + url
+ + ". Maybe TCP port for inbound agents is disabled?");
+ } else {
+ events.status("Controller isn't ready to talk to us on " + url + ". Will try again: response code="
+ + con.getResponseCode());
+ }
+ }
+ return null;
+ }
}
diff --git a/src/main/java/org/jenkinsci/remoting/engine/RetryUtils.java b/src/main/java/org/jenkinsci/remoting/engine/RetryUtils.java
new file mode 100644
index 000000000..01854f55c
--- /dev/null
+++ b/src/main/java/org/jenkinsci/remoting/engine/RetryUtils.java
@@ -0,0 +1,90 @@
+package org.jenkinsci.remoting.engine;
+
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.remoting.EngineListenerSplitter;
+import hudson.remoting.Util;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.concurrent.Callable;
+import java.util.function.Function;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.jenkinsci.remoting.util.DurationFormatter;
+
+class RetryUtils {
+ private static final Logger LOGGER = Logger.getLogger(RetryUtils.class.getName());
+
+ @CheckForNull
+ static T succeedsWithRetries(
+ @NonNull Callable supplier, @NonNull Duration noReconnectAfter, @NonNull EngineListenerSplitter events)
+ throws InterruptedException {
+ return succeedsWithRetries(supplier, noReconnectAfter, events, x -> "Failed to connect: " + x.getMessage());
+ }
+ /**
+ * Evaluates a supplier with exponential backoff until it provides a non-null value or the timeout is reached.
+ * @param supplier supplies an object. If null, retries with exponential backoff will be attempted.
+ * @return true if the condition succeeded, false if the condition failed and the timeout was reached
+ * @throws InterruptedException if the thread was interrupted while waiting.
+ */
+ @CheckForNull
+ static T succeedsWithRetries(
+ @NonNull Callable supplier,
+ @NonNull Duration noReconnectAfter,
+ @NonNull EngineListenerSplitter events,
+ @NonNull Function exceptionConsumer)
+ throws InterruptedException {
+ for (var exponentialRetry = new ExponentialRetry(noReconnectAfter);
+ exponentialRetry != null;
+ exponentialRetry = exponentialRetry.next(events)) {
+ try {
+ var result = supplier.call();
+ if (result != null) {
+ return result;
+ }
+ } catch (Exception x) {
+ var msg = exceptionConsumer.apply(x);
+ events.status(msg);
+ LOGGER.log(Level.FINE, msg, x);
+ }
+ }
+ return null;
+ }
+
+ private record ExponentialRetry(
+ int factor,
+ Instant beginning,
+ Duration delay,
+ Duration timeout,
+ Duration incrementDelay,
+ Duration maxDelay) {
+
+ ExponentialRetry(Duration timeout) {
+ this(2, Instant.now(), Duration.ofSeconds(0), timeout, Duration.ofSeconds(1), Duration.ofSeconds(10));
+ }
+
+ private static Duration min(Duration a, Duration b) {
+ return a.compareTo(b) < 0 ? a : b;
+ }
+
+ boolean timeoutExceeded() {
+ return Util.shouldBailOut(beginning, timeout);
+ }
+
+ ExponentialRetry next(EngineListenerSplitter events) throws InterruptedException {
+ var next = new ExponentialRetry(factor, beginning, nextDelay(), timeout, incrementDelay, maxDelay);
+ if (next.timeoutExceeded()) {
+ events.status("Bailing out after " + DurationFormatter.format(next.timeout));
+ return null;
+ } else {
+ events.status("Waiting " + DurationFormatter.format(next.delay) + " before retry");
+ Thread.sleep(next.delay.toMillis());
+ }
+ return next;
+ }
+
+ private Duration nextDelay() {
+ return min(maxDelay, delay.multipliedBy(factor).plus(incrementDelay));
+ }
+ }
+}
diff --git a/src/main/java/org/jenkinsci/remoting/engine/WebSocketConnector.java b/src/main/java/org/jenkinsci/remoting/engine/WebSocketConnector.java
new file mode 100644
index 000000000..c5dd0e32b
--- /dev/null
+++ b/src/main/java/org/jenkinsci/remoting/engine/WebSocketConnector.java
@@ -0,0 +1,368 @@
+package org.jenkinsci.remoting.engine;
+
+import static org.jenkinsci.remoting.util.SSLUtils.getSSLContext;
+
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import hudson.remoting.AbstractByteBufferCommandTransport;
+import hudson.remoting.Capability;
+import hudson.remoting.Channel;
+import hudson.remoting.ChannelBuilder;
+import hudson.remoting.ChannelClosedException;
+import hudson.remoting.ChunkHeader;
+import hudson.remoting.Engine;
+import hudson.remoting.EngineListenerSplitter;
+import hudson.remoting.JarCache;
+import hudson.remoting.Launcher;
+import hudson.remoting.NoProxyEvaluator;
+import jakarta.websocket.ClientEndpointConfig;
+import jakarta.websocket.CloseReason;
+import jakarta.websocket.ContainerProvider;
+import jakarta.websocket.Endpoint;
+import jakarta.websocket.EndpointConfig;
+import jakarta.websocket.HandshakeResponse;
+import jakarta.websocket.Session;
+import jakarta.websocket.WebSocketContainer;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import org.glassfish.tyrus.client.ClientManager;
+import org.glassfish.tyrus.client.ClientProperties;
+import org.glassfish.tyrus.client.SslEngineConfigurator;
+import org.jenkinsci.remoting.util.VersionNumber;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
+/**
+ * Connects to a controller using WebSockets.
+ */
+@Restricted(NoExternalUse.class)
+public class WebSocketConnector implements EndpointConnector {
+ private static final Logger LOGGER = Logger.getLogger(WebSocketConnector.class.getName());
+ private final EndpointConnectorData data;
+
+ @Override
+ @NonNull
+ public URL getUrl() {
+ return url;
+ }
+
+ /**
+ * The URL to connect to.
+ */
+ @NonNull
+ private final URL url;
+
+ /**
+ * Headers to be added to the initial request for connection.
+ */
+ @NonNull
+ private final Map headers;
+
+ /**
+ * A custom hostname verifier to use for HTTPS connections.
+ */
+ @CheckForNull
+ private final HostnameVerifier hostnameVerifier;
+
+ public WebSocketConnector(
+ EndpointConnectorData data,
+ @NonNull URL url,
+ @CheckForNull Map headers,
+ @CheckForNull HostnameVerifier hostnameVerifier) {
+ this.data = data;
+ this.url = url;
+ this.headers = headers == null ? Map.of() : new HashMap<>(headers);
+ this.hostnameVerifier = hostnameVerifier;
+ }
+
+ @SuppressFBWarnings(
+ value = {"URLCONNECTION_SSRF_FD", "NP_BOOLEAN_RETURN_NULL"},
+ justification = "url is provided by the user, and we are trying to connect to it")
+ private Boolean pingSuccessful() throws MalformedURLException {
+ // Unlike JnlpAgentEndpointResolver, we do not use $jenkins/tcpSlaveAgentListener/, as that will be
+ // a 404 if the TCP port is disabled.
+ URL ping = new URL(url, "login");
+ try {
+ HttpURLConnection conn = (HttpURLConnection) ping.openConnection();
+ int status = conn.getResponseCode();
+ conn.disconnect();
+ if (status == 200) {
+ return Boolean.TRUE;
+ } else {
+ data.events().status(ping + " is not ready: " + status);
+ }
+ } catch (IOException x) {
+ data.events().status(ping + " is not ready: " + x.getMessage());
+ }
+ return null;
+ }
+
+ @Override
+ public void close() throws IOException {
+ // no-op
+ }
+
+ private static class HeaderHandler extends ClientEndpointConfig.Configurator {
+ private final Map> addedHeaders;
+ private final EngineListenerSplitter events;
+ private Capability remoteCapability;
+
+ HeaderHandler(Map> addedHeaders, EngineListenerSplitter events) {
+ this.addedHeaders = new HashMap<>(addedHeaders);
+ this.events = events;
+ this.remoteCapability = new Capability();
+ }
+
+ @Override
+ public void beforeRequest(Map> headers) {
+ headers.putAll(addedHeaders);
+ LOGGER.fine(() -> "Sending: " + headers);
+ }
+
+ @Override
+ public void afterResponse(HandshakeResponse hr) {
+ LOGGER.fine(() -> "Receiving: " + hr.getHeaders());
+ List remotingMinimumVersion = hr.getHeaders().get(Engine.REMOTING_MINIMUM_VERSION_HEADER);
+ if (remotingMinimumVersion != null && !remotingMinimumVersion.isEmpty()) {
+ VersionNumber minimumSupportedVersion = new VersionNumber(remotingMinimumVersion.get(0));
+ VersionNumber currentVersion = new VersionNumber(Launcher.VERSION);
+ if (currentVersion.isOlderThan(minimumSupportedVersion)) {
+ events.error(
+ new IOException("Agent version " + minimumSupportedVersion + " or newer is required."));
+ }
+ }
+ try {
+ List cookies = hr.getHeaders().get(Engine.WEBSOCKET_COOKIE_HEADER);
+ if (cookies != null && !cookies.isEmpty()) {
+ addedHeaders.put(Engine.WEBSOCKET_COOKIE_HEADER, List.of(cookies.get(0)));
+ } else {
+ addedHeaders.remove(Engine.WEBSOCKET_COOKIE_HEADER);
+ }
+ List advertisedCapability = hr.getHeaders().get(Capability.KEY);
+ if (advertisedCapability == null) {
+ LOGGER.warning("Did not receive " + Capability.KEY + " header");
+ } else {
+ remoteCapability = Capability.fromASCII(advertisedCapability.get(0));
+ LOGGER.fine(() -> "received " + remoteCapability);
+ }
+ } catch (IOException x) {
+ events.error(x);
+ }
+ }
+ }
+
+ private static class AgentEndpoint extends Endpoint {
+ private final CompletableFuture futureChannel;
+ private final EngineListenerSplitter events;
+ private final String agentName;
+ private final ExecutorService executor;
+ private final JarCache jarCache;
+ private final Supplier capabilitySupplier;
+
+ AgentEndpoint(
+ String agentName,
+ ExecutorService executor,
+ JarCache jarCache,
+ Supplier capabilitySupplier,
+ EngineListenerSplitter events) {
+ this.futureChannel = new CompletableFuture<>();
+ this.agentName = agentName;
+ this.executor = executor;
+ this.jarCache = jarCache;
+ this.capabilitySupplier = capabilitySupplier;
+ this.events = events;
+ }
+
+ public Future getChannel() {
+ return futureChannel;
+ }
+
+ @SuppressFBWarnings(value = "UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR", justification = "just trust me here")
+ AgentEndpoint.Transport transport;
+
+ @Override
+ public void onOpen(Session session, EndpointConfig config) {
+ events.status("WebSocket connection open");
+ session.addMessageHandler(ByteBuffer.class, this::onMessage);
+ try {
+ transport = new Transport(session);
+ futureChannel.complete(new ChannelBuilder(agentName, executor)
+ .withJarCacheOrDefault(jarCache)
+ . // unless EngineJnlpConnectionStateListener can be used for this purpose
+ build(transport));
+ } catch (IOException x) {
+ events.error(x);
+ }
+ }
+
+ private void onMessage(ByteBuffer message) {
+ try {
+ transport.receive(message);
+ } catch (IOException x) {
+ events.error(x);
+ } catch (InterruptedException x) {
+ events.error(x);
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ @Override
+ @SuppressFBWarnings(
+ value = "RV_RETURN_VALUE_IGNORED_BAD_PRACTICE",
+ justification =
+ "We want the transport.terminate method to run asynchronously and don't want to wait for its status.")
+ public void onClose(Session session, CloseReason closeReason) {
+ LOGGER.fine(() -> "onClose: " + closeReason);
+ // making this call async to avoid potential deadlocks when some thread is holding a lock on the
+ // channel object while this thread is trying to acquire it to call Transport#terminate
+ var channel = futureChannel.join();
+ channel.executor.submit(() -> transport.terminate(new ChannelClosedException(channel, null)));
+ }
+
+ @Override
+ @SuppressFBWarnings(
+ value = "RV_RETURN_VALUE_IGNORED_BAD_PRACTICE",
+ justification =
+ "We want the transport.terminate method to run asynchronously and don't want to wait for its status.")
+ public void onError(Session session, Throwable x) {
+ // TODO or would events.error(x) be better?
+ LOGGER.log(Level.FINE, null, x);
+ // as above
+ var channel = futureChannel.join();
+ channel.executor.submit(() -> transport.terminate(new ChannelClosedException(channel, x)));
+ }
+
+ class Transport extends AbstractByteBufferCommandTransport {
+ final Session session;
+
+ Transport(Session session) {
+ super(true);
+ this.session = session;
+ }
+
+ @Override
+ protected void write(ByteBuffer headerAndData) throws IOException {
+ LOGGER.finest(() -> "sending message of length " + (headerAndData.remaining() - ChunkHeader.SIZE));
+ try {
+ session.getAsyncRemote().sendBinary(headerAndData).get(5, TimeUnit.MINUTES);
+ } catch (Exception x) {
+ throw new IOException(x);
+ }
+ }
+
+ @Override
+ public Capability getRemoteCapability() {
+ return capabilitySupplier.get();
+ }
+
+ @Override
+ public void closeWrite() throws IOException {
+ events.status("Write side closed");
+ session.close();
+ }
+
+ @Override
+ public void closeRead() throws IOException {
+ events.status("Read side closed");
+ session.close();
+ }
+ }
+ }
+
+ @Override
+ public Future connect() throws Exception {
+ String localCap = new Capability().toASCII();
+ final Map> addedHeaders = new HashMap<>();
+ addedHeaders.put(JnlpConnectionState.CLIENT_NAME_KEY, List.of(data.agentName()));
+ addedHeaders.put(JnlpConnectionState.SECRET_KEY, List.of(data.secretKey()));
+ addedHeaders.put(Capability.KEY, List.of(localCap));
+ for (Map.Entry entry : headers.entrySet()) {
+ addedHeaders.put(entry.getKey(), List.of(entry.getValue()));
+ }
+ String wsUrl = url.toString().replaceFirst("^http", "ws");
+ WebSocketContainer container = ContainerProvider.getWebSocketContainer();
+ if (container instanceof ClientManager) {
+ ClientManager client = (ClientManager) container;
+
+ String proxyHost = System.getProperty("http.proxyHost", System.getenv("proxy_host"));
+ String proxyPort = System.getProperty("http.proxyPort");
+ if (proxyHost != null && "http".equals(url.getProtocol()) && NoProxyEvaluator.shouldProxy(url.getHost())) {
+ URI proxyUri;
+ if (proxyPort != null) {
+ proxyUri = URI.create(String.format("http://%s:%s", proxyHost, proxyPort));
+ } else {
+ proxyUri = URI.create(String.format("http://%s", proxyHost));
+ }
+ client.getProperties().put(ClientProperties.PROXY_URI, proxyUri);
+ if (data.proxyCredentials() != null) {
+ client.getProperties()
+ .put(
+ ClientProperties.PROXY_HEADERS,
+ Map.of(
+ "Proxy-Authorization",
+ "Basic "
+ + Base64.getEncoder()
+ .encodeToString(data.proxyCredentials()
+ .getBytes(StandardCharsets.UTF_8))));
+ }
+ }
+
+ SSLContext sslContext = getSSLContext(data.candidateCertificates(), data.disableHttpsCertValidation());
+ if (sslContext != null) {
+ SslEngineConfigurator sslEngineConfigurator = new SslEngineConfigurator(sslContext);
+ if (hostnameVerifier != null) {
+ sslEngineConfigurator.setHostnameVerifier(hostnameVerifier);
+ }
+ client.getProperties().put(ClientProperties.SSL_ENGINE_CONFIGURATOR, sslEngineConfigurator);
+ }
+ }
+ return RetryUtils.succeedsWithRetries(
+ () -> {
+ var clientEndpointConfigurator = new HeaderHandler(addedHeaders, data.events());
+ var endpointInstance = new AgentEndpoint(
+ data.agentName(),
+ data.executor(),
+ data.jarCache(),
+ () -> clientEndpointConfigurator.remoteCapability,
+ data.events());
+ container.connectToServer(
+ endpointInstance,
+ ClientEndpointConfig.Builder.create()
+ .configurator(clientEndpointConfigurator)
+ .build(),
+ URI.create(wsUrl + "wsagents/"));
+ return endpointInstance.getChannel();
+ },
+ data.noReconnectAfter(),
+ data.events());
+ }
+
+ @Override
+ public Boolean waitUntilReady() throws InterruptedException {
+ return RetryUtils.succeedsWithRetries(this::pingSuccessful, data.noReconnectAfter(), data.events());
+ }
+
+ @Override
+ public String getProtocol() {
+ return "WebSocket";
+ }
+}
diff --git a/src/main/java/org/jenkinsci/remoting/util/SSLUtils.java b/src/main/java/org/jenkinsci/remoting/util/SSLUtils.java
new file mode 100644
index 000000000..83eb4b0e3
--- /dev/null
+++ b/src/main/java/org/jenkinsci/remoting/util/SSLUtils.java
@@ -0,0 +1,214 @@
+package org.jenkinsci.remoting.util;
+
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.security.AccessController;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import org.jenkinsci.remoting.protocol.cert.DelegatingX509ExtendedTrustManager;
+import org.jenkinsci.remoting.util.https.NoCheckTrustManager;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
+@Restricted(NoExternalUse.class)
+public final class SSLUtils {
+ private SSLUtils() {}
+
+ private static final Logger LOGGER = Logger.getLogger(SSLUtils.class.getName());
+
+ @SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "File path is loaded from system properties.")
+ static KeyStore getCacertsKeyStore()
+ throws PrivilegedActionException, KeyStoreException, NoSuchProviderException, CertificateException,
+ NoSuchAlgorithmException, IOException {
+ Map properties =
+ AccessController.doPrivileged((PrivilegedExceptionAction