Merge remote-tracking branch

'origin/GP-1481_ghidra1_CancelServerConnect--SQUASHED' (Closes #2661)
This commit is contained in:
ghidra1
2021-12-29 10:23:16 -05:00
4 changed files with 107 additions and 27 deletions
@@ -32,9 +32,9 @@ import ghidra.framework.remote.*;
import ghidra.framework.remote.security.SSHKeyManager; import ghidra.framework.remote.security.SSHKeyManager;
import ghidra.net.*; import ghidra.net.*;
import ghidra.util.*; import ghidra.util.*;
import ghidra.util.exception.AssertException; import ghidra.util.exception.*;
import ghidra.util.exception.UserAccessException;
import ghidra.util.task.TaskLauncher; import ghidra.util.task.TaskLauncher;
import ghidra.util.task.TaskMonitor;
/** /**
* <code>ClientUtil</code> allows a user to connect to a Repository Server and obtain its handle. * <code>ClientUtil</code> 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 * 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. * NOTE: Use of this method when PKI authentication is enabled is not supported.
* @param host server hostname * @param host server hostname
* @param port first Ghidra Server port (0=use default) * @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 IOException thrown if an IO Error occurs (e.g., server not found).
* @throws RemoteException if server interface is incompatible or another server-side * @throws RemoteException if server interface is incompatible or another server-side
* error occurs. * error occurs.
* @throws CancelledException if connection attempt was cancelled
*/ */
public static void checkGhidraServer(String host, int port) throws IOException { public static void checkGhidraServer(String host, int port, TaskMonitor monitor)
ServerConnectTask.getGhidraServerHandle(new ServerInfo(host, port)); 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 * @throws GeneralSecurityException if server authentication fails due to
* credential access error (e.g., PKI cert failure) * credential access error (e.g., PKI cert failure)
* @throws IOException thrown if an IO Error occurs. * @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) static RemoteRepositoryServerHandle connect(ServerInfo server)
throws LoginException, GeneralSecurityException, IOException { throws LoginException, GeneralSecurityException, IOException, CancelledException {
getClientAuthenticator(); getClientAuthenticator();
boolean allowLoginRetry = (clientAuthenticator instanceof DefaultClientAuthenticator); boolean allowLoginRetry = (clientAuthenticator instanceof DefaultClientAuthenticator);
RemoteRepositoryServerHandle hdl = null; RemoteRepositoryServerHandle hdl = null;
ServerConnectTask connectTask = new ServerConnectTask(server, allowLoginRetry); ServerConnectTask connectTask = new ServerConnectTask(server, allowLoginRetry);
if (!SystemUtilities.isInHeadlessMode() && SystemUtilities.isEventDispatchThread()) { if (SystemUtilities.isInHeadlessMode()) {
// Must be done in modal dialog to allow possible authentication prompts connectTask.run(TaskMonitor.DUMMY); // headless - can't cancel
// from another thread.
TaskLauncher.launch(connectTask);
} }
else { 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(); hdl = connectTask.getRepositoryServerHandle();
if (hdl == null) { if (hdl == null) {
Exception e = connectTask.getException(); Exception e = connectTask.getException();
@@ -137,7 +137,13 @@ public class RepositoryServerAdapter {
Throwable cause = null; Throwable cause = null;
try { try {
serverHandle = ClientUtil.connect(server); try {
serverHandle = ClientUtil.connect(server);
}
catch (CancelledException e) {
// ignore
Msg.debug(this, "Server connect cancelled by user");
}
unexpectedDisconnect = false; unexpectedDisconnect = false;
if (serverHandle != null) { if (serverHandle != null) {
Msg.info(this, "Connected to Ghidra Server at " + serverInfoStr); Msg.info(this, "Connected to Ghidra Server at " + serverInfoStr);
@@ -15,7 +15,9 @@
*/ */
package ghidra.framework.client; package ghidra.framework.client;
import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.rmi.*; import java.rmi.*;
import java.rmi.registry.LocateRegistry; import java.rmi.registry.LocateRegistry;
@@ -36,8 +38,8 @@ import ghidra.framework.model.ServerInfo;
import ghidra.framework.remote.*; import ghidra.framework.remote.*;
import ghidra.net.ApplicationKeyManagerFactory; import ghidra.net.ApplicationKeyManagerFactory;
import ghidra.util.Msg; import ghidra.util.Msg;
import ghidra.util.task.Task; import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor; import ghidra.util.task.*;
/** /**
* Task for connecting to server with Swing thread. * 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 * @param allowLoginRetry true if login retry allowed during authentication
*/ */
ServerConnectTask(ServerInfo server, boolean allowLoginRetry) { 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.server = server;
this.allowLoginRetry = allowLoginRetry; this.allowLoginRetry = allowLoginRetry;
} }
@@ -64,12 +66,14 @@ class ServerConnectTask extends Task {
/** /**
* Completes and necessary authentication and obtains a repository handle. * Completes and necessary authentication and obtains a repository handle.
* If a connection error occurs, an exception will be stored ({@link #getException()}. * 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) * @see ghidra.util.task.Task#run(ghidra.util.task.TaskMonitor)
*/ */
@Override @Override
public void run(TaskMonitor monitor) { public void run(TaskMonitor monitor) throws CancelledException {
monitor = TaskMonitor.dummyIfNull(monitor);
try { try {
hdl = getRepositoryServerHandle(ClientUtil.getUserName()); hdl = getRepositoryServerHandle(ClientUtil.getUserName(), monitor);
} }
catch (RemoteException e) { catch (RemoteException e) {
exc = e; exc = e;
@@ -81,6 +85,12 @@ class ServerConnectTask extends Task {
catch (Exception e) { catch (Exception e) {
exc = 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 * Obtain a remote instance of the Ghidra Server Handle object
* @param server server information * @param server server information
* @param monitor cancellable monitor
* @return Ghidra Server Handle object * @return Ghidra Server Handle object
* @throws IOException * @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; GhidraServerHandle gsh = null;
boolean canCancel = monitor.isCancelEnabled(); // original state
try { try {
// Test SSL Handshake to ensure that user is able to decrypt keystore. // Test SSL Handshake to ensure that user is able to decrypt keystore.
// This is intended to work around an RMI issue where a continuous // This is intended to work around an RMI issue where a continuous
// retry condition can occur when a user cancels the password entry // retry condition can occur when a user cancels the password entry
// for their keystore which should cancel any connection attempt // for their keystore which should cancel any connection attempt
testServerSSLConnection(server); testServerSSLConnection(server, monitor);
monitor.setCancelEnabled(false);
monitor.setMessage("Connecting...");
Registry reg; Registry reg;
try { try {
@@ -191,20 +208,50 @@ class ServerConnectTask extends Task {
} }
throw e; throw e;
} }
finally {
monitor.setCancelEnabled(canCancel);
monitor.setMessage("");
}
return gsh; 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. * Attempts server connection and completes any necessary authentication.
* @param defaultUserID * @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 IOException
* @throws LoginException * @throws LoginException
*/ */
private RemoteRepositoryServerHandle getRepositoryServerHandle(String defaultUserID) private RemoteRepositoryServerHandle getRepositoryServerHandle(String defaultUserID,
throws IOException, LoginException { TaskMonitor monitor)
throws IOException, LoginException, CancelledException {
GhidraServerHandle gsh = getGhidraServerHandle(server); GhidraServerHandle gsh = getGhidraServerHandle(server, monitor);
if (gsh == null) { if (gsh == null) {
return 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()); RMIServerPortFactory portFactory = new RMIServerPortFactory(server.getPortNumber());
SslRMIClientSocketFactory factory = new SslRMIClientSocketFactory(); SslRMIClientSocketFactory factory = new SslRMIClientSocketFactory();
String serverName = server.getServerName(); String serverName = server.getServerName();
int sslRmiPort = portFactory.getRMISSLPort(); 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 // Complete SSL handshake to trigger client keystore access if required
// which will give user ability to cancel without involving RMI which // which will give user ability to cancel without involving RMI which
// will avoid RMI reconnect attempts // will avoid RMI reconnect attempts
socket.startHandshake(); socket.startHandshake();
} }
finally {
monitor.checkCanceled(); // circumvent any IOException which may have occured
}
} }
private static void checkServerBindNames(Registry reg) throws RemoteException { private static void checkServerBindNames(Registry reg) throws RemoteException {
@@ -36,6 +36,7 @@ import ghidra.framework.remote.GhidraServerHandle;
import ghidra.net.ApplicationKeyManagerFactory; import ghidra.net.ApplicationKeyManagerFactory;
import ghidra.server.remote.ServerTestUtil; import ghidra.server.remote.ServerTestUtil;
import ghidra.test.AbstractGhidraHeadlessIntegrationTest; import ghidra.test.AbstractGhidraHeadlessIntegrationTest;
import ghidra.util.task.TaskMonitor;
import utilities.util.FileUtilities; import utilities.util.FileUtilities;
@Category(PortSensitiveCategory.class) @Category(PortSensitiveCategory.class)
@@ -110,7 +111,8 @@ public class GhidraServerSerialFilterFailureTest extends AbstractGhidraHeadlessI
ServerInfo server = new ServerInfo("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT); ServerInfo server = new ServerInfo("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT);
GhidraServerHandle serverHandle = ServerConnectTask.getGhidraServerHandle(server); GhidraServerHandle serverHandle =
ServerConnectTask.getGhidraServerHandle(server, TaskMonitor.DUMMY);
try { try {
serverHandle.getRepositoryServer(getBogusUserSubject(), new Callback[0]); serverHandle.getRepositoryServer(getBogusUserSubject(), new Callback[0]);