diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ClientUtil.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ClientUtil.java index 11ac208cba..af1056cb44 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ClientUtil.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ClientUtil.java @@ -32,9 +32,9 @@ import ghidra.framework.remote.*; import ghidra.framework.remote.security.SSHKeyManager; import ghidra.net.*; import ghidra.util.*; -import ghidra.util.exception.AssertException; -import ghidra.util.exception.UserAccessException; +import ghidra.util.exception.*; import ghidra.util.task.TaskLauncher; +import ghidra.util.task.TaskMonitor; /** * ClientUtil allows a user to connect to a Repository Server and obtain its handle. @@ -294,16 +294,19 @@ public class ClientUtil { /** * Connect to a Ghidra Server and verify compatibility. This method can be used - * to affectively "ping" the Ghidra Server to verify the ability to connect. + * to effectively "ping" the Ghidra Server to verify the ability to connect. * NOTE: Use of this method when PKI authentication is enabled is not supported. * @param host server hostname * @param port first Ghidra Server port (0=use default) + * @param monitor cancellable monitor * @throws IOException thrown if an IO Error occurs (e.g., server not found). * @throws RemoteException if server interface is incompatible or another server-side * error occurs. + * @throws CancelledException if connection attempt was cancelled */ - public static void checkGhidraServer(String host, int port) throws IOException { - ServerConnectTask.getGhidraServerHandle(new ServerInfo(host, port)); + public static void checkGhidraServer(String host, int port, TaskMonitor monitor) + throws IOException, CancelledException { + ServerConnectTask.getGhidraServerHandle(new ServerInfo(host, port), monitor); } /** @@ -319,24 +322,28 @@ public class ClientUtil { * @throws GeneralSecurityException if server authentication fails due to * credential access error (e.g., PKI cert failure) * @throws IOException thrown if an IO Error occurs. + * @throws CancelledException if connection cancelled by user (does not apply to Headless use) */ static RemoteRepositoryServerHandle connect(ServerInfo server) - throws LoginException, GeneralSecurityException, IOException { + throws LoginException, GeneralSecurityException, IOException, CancelledException { getClientAuthenticator(); boolean allowLoginRetry = (clientAuthenticator instanceof DefaultClientAuthenticator); RemoteRepositoryServerHandle hdl = null; ServerConnectTask connectTask = new ServerConnectTask(server, allowLoginRetry); - if (!SystemUtilities.isInHeadlessMode() && SystemUtilities.isEventDispatchThread()) { - // Must be done in modal dialog to allow possible authentication prompts - // from another thread. - - TaskLauncher.launch(connectTask); + if (SystemUtilities.isInHeadlessMode()) { + connectTask.run(TaskMonitor.DUMMY); // headless - can't cancel } else { - connectTask.run(null); + // Must be done in modal dialog to allow cancellation and possible authentication prompts + // from another thread. + TaskLauncher.launch(connectTask); + if (connectTask.isCancelled()) { + throw new CancelledException(); + } } + hdl = connectTask.getRepositoryServerHandle(); if (hdl == null) { Exception e = connectTask.getException(); diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryServerAdapter.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryServerAdapter.java index 028f6f6311..a2532eceae 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryServerAdapter.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryServerAdapter.java @@ -137,7 +137,13 @@ public class RepositoryServerAdapter { Throwable cause = null; try { - serverHandle = ClientUtil.connect(server); + try { + serverHandle = ClientUtil.connect(server); + } + catch (CancelledException e) { + // ignore + Msg.debug(this, "Server connect cancelled by user"); + } unexpectedDisconnect = false; if (serverHandle != null) { Msg.info(this, "Connected to Ghidra Server at " + serverInfoStr); diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ServerConnectTask.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ServerConnectTask.java index 461595376d..de4ae466ae 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ServerConnectTask.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ServerConnectTask.java @@ -15,7 +15,9 @@ */ package ghidra.framework.client; +import java.io.Closeable; import java.io.IOException; +import java.net.Socket; import java.net.UnknownHostException; import java.rmi.*; import java.rmi.registry.LocateRegistry; @@ -36,8 +38,8 @@ import ghidra.framework.model.ServerInfo; import ghidra.framework.remote.*; import ghidra.net.ApplicationKeyManagerFactory; import ghidra.util.Msg; -import ghidra.util.task.Task; -import ghidra.util.task.TaskMonitor; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.*; /** * Task for connecting to server with Swing thread. @@ -56,7 +58,7 @@ class ServerConnectTask extends Task { * @param allowLoginRetry true if login retry allowed during authentication */ ServerConnectTask(ServerInfo server, boolean allowLoginRetry) { - super("Connecting to " + server.getServerName(), false, false, true); + super("Connecting to " + server.getServerName(), true, false, true); this.server = server; this.allowLoginRetry = allowLoginRetry; } @@ -64,12 +66,14 @@ class ServerConnectTask extends Task { /** * Completes and necessary authentication and obtains a repository handle. * If a connection error occurs, an exception will be stored ({@link #getException()}. + * @throws CancelledException if task cancelled * @see ghidra.util.task.Task#run(ghidra.util.task.TaskMonitor) */ @Override - public void run(TaskMonitor monitor) { + public void run(TaskMonitor monitor) throws CancelledException { + monitor = TaskMonitor.dummyIfNull(monitor); try { - hdl = getRepositoryServerHandle(ClientUtil.getUserName()); + hdl = getRepositoryServerHandle(ClientUtil.getUserName(), monitor); } catch (RemoteException e) { exc = e; @@ -81,6 +85,12 @@ class ServerConnectTask extends Task { catch (Exception e) { exc = e; } + finally { + if (monitor.isCancelled()) { + exc = null; + throw new CancelledException(); + } + } } /** @@ -142,18 +152,25 @@ class ServerConnectTask extends Task { /** * Obtain a remote instance of the Ghidra Server Handle object * @param server server information + * @param monitor cancellable monitor * @return Ghidra Server Handle object * @throws IOException + * @throws CancelledException */ - public static GhidraServerHandle getGhidraServerHandle(ServerInfo server) throws IOException { + public static GhidraServerHandle getGhidraServerHandle(ServerInfo server, TaskMonitor monitor) + throws IOException, CancelledException { GhidraServerHandle gsh = null; + boolean canCancel = monitor.isCancelEnabled(); // original state try { // Test SSL Handshake to ensure that user is able to decrypt keystore. // This is intended to work around an RMI issue where a continuous // retry condition can occur when a user cancels the password entry // for their keystore which should cancel any connection attempt - testServerSSLConnection(server); + testServerSSLConnection(server, monitor); + + monitor.setCancelEnabled(false); + monitor.setMessage("Connecting..."); Registry reg; try { @@ -191,20 +208,50 @@ class ServerConnectTask extends Task { } throw e; } + finally { + monitor.setCancelEnabled(canCancel); + monitor.setMessage(""); + } return gsh; } + private static class ConnectCancelledListener implements CancelledListener, Closeable { + + private TaskMonitor monitor; + private CancelledListener callback; + + ConnectCancelledListener(TaskMonitor monitor, CancelledListener callback) { + this.monitor = monitor; + this.callback = callback; + monitor.addCancelledListener(this); + } + + @Override + public void cancelled() { + if (callback != null) { + callback.cancelled(); + } + } + + @Override + public void close() throws IOException { + monitor.removeCancelledListener(this); + } + } + /** * Attempts server connection and completes any necessary authentication. * @param defaultUserID - * @return server handle or null if authentication was cancelled by user + * @param monitor task monitor for connection cancellation + * @return server handle or null if authentication or connection attempt was cancelled by user * @throws IOException * @throws LoginException */ - private RemoteRepositoryServerHandle getRepositoryServerHandle(String defaultUserID) - throws IOException, LoginException { + private RemoteRepositoryServerHandle getRepositoryServerHandle(String defaultUserID, + TaskMonitor monitor) + throws IOException, LoginException, CancelledException { - GhidraServerHandle gsh = getGhidraServerHandle(server); + GhidraServerHandle gsh = getGhidraServerHandle(server, monitor); if (gsh == null) { return null; } @@ -318,19 +365,37 @@ class ServerConnectTask extends Task { } } - private static void testServerSSLConnection(ServerInfo server) throws IOException { + private static void forceClose(Socket s) { + try { + s.close(); + } + catch (IOException e) { + // ignore + } + } + + private static void testServerSSLConnection(ServerInfo server, TaskMonitor monitor) + throws IOException, CancelledException { RMIServerPortFactory portFactory = new RMIServerPortFactory(server.getPortNumber()); SslRMIClientSocketFactory factory = new SslRMIClientSocketFactory(); String serverName = server.getServerName(); int sslRmiPort = portFactory.getRMISSLPort(); - try (SSLSocket socket = (SSLSocket) factory.createSocket(serverName, sslRmiPort)) { + monitor.setCancelEnabled(true); + monitor.setMessage("Checking Server Liveness..."); + + try (SSLSocket socket = (SSLSocket) factory.createSocket(serverName, sslRmiPort); + ConnectCancelledListener cancelListener = + new ConnectCancelledListener(monitor, () -> forceClose(socket))) { // Complete SSL handshake to trigger client keystore access if required // which will give user ability to cancel without involving RMI which // will avoid RMI reconnect attempts socket.startHandshake(); } + finally { + monitor.checkCanceled(); // circumvent any IOException which may have occured + } } private static void checkServerBindNames(Registry reg) throws RemoteException { diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/client/GhidraServerSerialFilterFailureTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/client/GhidraServerSerialFilterFailureTest.java index ac11e85b63..82125297eb 100644 --- a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/client/GhidraServerSerialFilterFailureTest.java +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/client/GhidraServerSerialFilterFailureTest.java @@ -36,6 +36,7 @@ import ghidra.framework.remote.GhidraServerHandle; import ghidra.net.ApplicationKeyManagerFactory; import ghidra.server.remote.ServerTestUtil; import ghidra.test.AbstractGhidraHeadlessIntegrationTest; +import ghidra.util.task.TaskMonitor; import utilities.util.FileUtilities; @Category(PortSensitiveCategory.class) @@ -110,7 +111,8 @@ public class GhidraServerSerialFilterFailureTest extends AbstractGhidraHeadlessI ServerInfo server = new ServerInfo("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT); - GhidraServerHandle serverHandle = ServerConnectTask.getGhidraServerHandle(server); + GhidraServerHandle serverHandle = + ServerConnectTask.getGhidraServerHandle(server, TaskMonitor.DUMMY); try { serverHandle.getRepositoryServer(getBogusUserSubject(), new Callback[0]);