GP-1481 add support for cancelling hung Ghidra Server connect attempt

This commit is contained in:
ghidra1 2021-12-28 12:03:16 -05:00
parent 948e4edeb1
commit 0ef2367ed4
4 changed files with 107 additions and 27 deletions

View File

@ -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;
/**
* <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
* 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();

View File

@ -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);

View File

@ -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 {

View File

@ -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]);