Merge remote-tracking branch

'origin/GP-3050-2935-ghidra1_ServerAddressAndConnectTimeout--SQUASHED'
(Closes #4924, Closes #4928)
This commit is contained in:
ghidra1
2023-02-03 14:27:05 -05:00
11 changed files with 139 additions and 91 deletions

View File

@@ -18,7 +18,6 @@ package ghidra.framework.client;
import java.awt.Component;
import java.io.IOException;
import java.net.Authenticator;
import java.net.UnknownHostException;
import java.rmi.*;
import java.security.GeneralSecurityException;
import java.util.Arrays;
@@ -50,7 +49,7 @@ public class ClientUtil {
/**
* Set client authenticator
* @param authenticator
* @param authenticator client authenticator instance
*/
public static synchronized void setClientAuthenticator(ClientAuthenticator authenticator) {
clientAuthenticator = authenticator;
@@ -106,14 +105,6 @@ public class ClientUtil {
// ensure that default callback is setup if possible
getClientAuthenticator();
host = host.trim().toLowerCase();
try {
host = InetNameLookup.getCanonicalHostName(host);
}
catch (UnknownHostException e) {
Msg.warn(ClientUtil.class, "Failed to resolve hostname for " + host);
}
if (port <= 0) {
port = GhidraServerHandle.DEFAULT_PORT;
}
@@ -145,22 +136,14 @@ public class ClientUtil {
* Eliminate the specified repository server from the connection cache
* @param host host name or IP address
* @param port port (0: use default port)
* @throws IOException
*/
public static void clearRepositoryAdapter(String host, int port) throws IOException {
host = host.trim().toLowerCase();
String hostAddr = host;
try {
hostAddr = InetNameLookup.getCanonicalHostName(host);
}
catch (UnknownHostException e) {
throw new IOException("Repository server lookup failed: " + host);
}
public static void clearRepositoryAdapter(String host, int port) {
if (port == 0) {
port = GhidraServerHandle.DEFAULT_PORT;
}
ServerInfo server = new ServerInfo(hostAddr, port);
ServerInfo server = new ServerInfo(host, port);
RepositoryServerAdapter serverAdapter = serverHandles.remove(server);
if (serverAdapter != null) {
serverAdapter.disconnect();
@@ -170,6 +153,7 @@ public class ClientUtil {
/**
* Returns default user login name. Actual user name used by repository
* should be obtained from RepositoryServerAdapter.getUser
* @return default user name
*/
public static String getUserName() {
String name = SystemUtilities.getUserName();
@@ -372,7 +356,7 @@ public class ClientUtil {
* @param parent dialog parent
* @param handle server handle
* @param serverInfo server information
* @throws IOException
* @throws IOException if error occurs while updating password
*/
public static void changePassword(Component parent, RepositoryServerHandle handle,
String serverInfo) throws IOException {
@@ -451,7 +435,7 @@ public class ClientUtil {
sigCb.getRecognizedAuthorities(), sigCb.getToken());
sigCb.sign(signedToken.certChain, signedToken.signature);
Msg.info(ClientUtil.class, "PKI Authenticating to " + serverName + " as user '" +
signedToken.certChain[0].getSubjectDN() + "'");
signedToken.certChain[0].getSubjectX500Principal() + "'");
}
catch (Exception e) {
String msg = e.getMessage();

View File

@@ -18,10 +18,12 @@ package ghidra.framework.client;
import java.io.Closeable;
import java.io.IOException;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.rmi.*;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.security.cert.Certificate;
import java.util.HashSet;
import javax.net.ssl.SSLHandshakeException;
@@ -46,6 +48,8 @@ import ghidra.util.task.*;
*/
class ServerConnectTask extends Task {
private static final int LIVENESS_CHECK_TIMEOUT_MS = 3000;
private ServerInfo server;
//private String defaultUserID;
private boolean allowLoginRetry;
@@ -98,6 +102,7 @@ class ServerConnectTask extends Task {
* if handle is null after running task. If both the exception
* and handle are null, it implies the connection attempt was cancelled
* by the user.
* @return exception which occured during a failed connection attempt, or null
*/
Exception getException() {
return exc;
@@ -123,16 +128,6 @@ class ServerConnectTask extends Task {
return subj;
}
private static String getPreferredHostname(String name) {
try {
return InetNameLookup.getCanonicalHostName(name);
}
catch (UnknownHostException e) {
Msg.warn(ServerConnectTask.class, "Failed to resolve hostname for " + name);
}
return name;
}
private static boolean isSSLHandshakeCancelled(SSLHandshakeException e) throws IOException {
if (e.getMessage().indexOf("bad_certificate") > 0) {
if (ApplicationKeyManagerFactory.getPreferredKeyStore() == null) {
@@ -154,8 +149,8 @@ class ServerConnectTask extends Task {
* @param server server information
* @param monitor cancellable monitor
* @return Ghidra Server Handle object
* @throws IOException
* @throws CancelledException
* @throws IOException if a connection error occurs
* @throws CancelledException if connection attempt was cancelled
*/
public static GhidraServerHandle getGhidraServerHandle(ServerInfo server, TaskMonitor monitor)
throws IOException, CancelledException {
@@ -163,6 +158,7 @@ class ServerConnectTask extends Task {
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
@@ -172,17 +168,10 @@ class ServerConnectTask extends Task {
monitor.setCancelEnabled(false);
monitor.setMessage("Connecting...");
Registry reg;
try {
// attempt to connect with older Ghidra Server registry without using SSL/TLS
reg = LocateRegistry.getRegistry(server.getServerName(), server.getPortNumber());
checkServerBindNames(reg);
}
catch (IOException e) {
reg = LocateRegistry.getRegistry(server.getServerName(), server.getPortNumber(),
Registry reg =
LocateRegistry.getRegistry(server.getServerName(), server.getPortNumber(),
new SslRMIClientSocketFactory());
checkServerBindNames(reg);
}
checkServerBindNames(reg);
gsh = (GhidraServerHandle) reg.lookup(GhidraServerHandle.BIND_NAME);
gsh.checkCompatibility(GhidraServerHandle.INTERFACE_VERSION);
@@ -241,20 +230,17 @@ class ServerConnectTask extends Task {
/**
* Attempts server connection and completes any necessary authentication.
* @param defaultUserID
* @param defaultUserID default user ID (actual ID used established during authentication)
* @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
* @throws IOException if server connection fails
* @throws LoginException login failure
*/
private RemoteRepositoryServerHandle getRepositoryServerHandle(String defaultUserID,
TaskMonitor monitor)
throws IOException, LoginException, CancelledException {
GhidraServerHandle gsh = getGhidraServerHandle(server, monitor);
if (gsh == null) {
return null;
}
Callback[] callbacks = null;
try {
@@ -275,7 +261,6 @@ class ServerConnectTask extends Task {
}
}
String serverName = getPreferredHostname(server.getServerName());
AnonymousCallback onlyAnonymousCb = null;
while (true) {
try {
@@ -298,8 +283,8 @@ class ServerConnectTask extends Task {
// SSH option only available in conjunction with password
// based authentication which will be used if SSH attempt fails
hasSSHSignatureCallback = false; // only try SSH once
ClientUtil.processSSHSignatureCallback(callbacks, serverName,
defaultUserID);
ClientUtil.processSSHSignatureCallback(callbacks,
server.getServerName(), defaultUserID);
}
else if (pkiSignatureCb != null) {
// when using PKI - no other authentication callback will be used
@@ -317,14 +302,15 @@ class ServerConnectTask extends Task {
}
loopOK = false; // only try once
ClientUtil.processSignatureCallback(serverName, pkiSignatureCb);
ClientUtil.processSignatureCallback(server.getServerName(),
pkiSignatureCb);
}
else {
// assume all other callback scenarios are password based
// anonymous option must be explicitly chosen over username/password
// when processing password callback
if (!ClientUtil.processPasswordCallbacks(callbacks, serverName,
defaultUserID, loginError)) {
if (!ClientUtil.processPasswordCallbacks(callbacks,
server.getServerName(), defaultUserID, loginError)) {
return null; // Cancelled by user
}
}
@@ -336,7 +322,7 @@ class ServerConnectTask extends Task {
gsh.getRepositoryServer(getLocalUserSubject(), callbacks);
if (rsh.isReadOnly()) {
Msg.showInfo(this, null, "Anonymous Server Login",
"You have been logged-in anonymously to " + serverName +
"You have been logged-in anonymously to " + server.getServerName() +
"\nRead-only permission is granted to repositories which allow anonymous access");
}
return rsh;
@@ -374,7 +360,30 @@ class ServerConnectTask extends Task {
}
}
private static void testServerSSLConnection(ServerInfo server, TaskMonitor monitor)
/**
* Socket implementation with very short connect timeout
*/
private static class FastConnectionFailSocket extends Socket {
FastConnectionFailSocket(String host, int port) throws UnknownHostException, IOException {
super(host, port);
}
public void connect(SocketAddress endpoint) throws IOException {
connect(endpoint, LIVENESS_CHECK_TIMEOUT_MS);
}
}
/**
* Initiate an SSLSocket connection in order to ensure that any neccesary client/server
* certificate validation is performed.
* @param server server to which connection should be verified. For the Ghidra Server
* this should correspond to the RMI Registry port {@link GhidraServerHandle#DEFAULT_PORT}.
* @param monitor connection task monitor
* @return certificate chain of server
* @throws IOException if connection failure occurs
* @throws CancelledException if connection attempt is cancelled
*/
private static Certificate[] testServerSSLConnection(ServerInfo server, TaskMonitor monitor)
throws IOException, CancelledException {
RMIServerPortFactory portFactory = new RMIServerPortFactory(server.getPortNumber());
@@ -384,7 +393,18 @@ class ServerConnectTask extends Task {
monitor.setCancelEnabled(true);
monitor.setMessage("Checking Server Liveness...");
// Perform simple socket test connection with short timeout to verify connectivity.
try (Socket socket = new FastConnectionFailSocket(serverName, sslRmiPort);
ConnectCancelledListener cancelListener =
new ConnectCancelledListener(monitor, () -> forceClose(socket))) {
// do nothing - connect occurs during instantiation
}
finally {
monitor.checkCanceled(); // circumvent any IOException which may have occured
}
// Perform secure socket test connection to prime keystore use without RMI involvement
try (SSLSocket socket = (SSLSocket) factory.createSocket(serverName, sslRmiPort);
ConnectCancelledListener cancelListener =
new ConnectCancelledListener(monitor, () -> forceClose(socket))) {
@@ -392,6 +412,7 @@ class ServerConnectTask extends Task {
// which will give user ability to cancel without involving RMI which
// will avoid RMI reconnect attempts
socket.startHandshake();
return socket.getSession().getPeerCertificates();
}
finally {
monitor.checkCanceled(); // circumvent any IOException which may have occured

View File

@@ -1,6 +1,5 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -38,7 +37,8 @@ public class ServerInfo implements Serializable {
}
/**
* Get the server name.
* Get the server hostname or IP address as originally specified.
* @return hostname or IP address as originally specified
*/
public String getServerName() {
return host;
@@ -46,6 +46,7 @@ public class ServerInfo implements Serializable {
/**
* Get the port number.
* @return port number
*/
public int getPortNumber() {
return portNumber;

View File

@@ -1,97 +0,0 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.remote;
import java.net.InetAddress;
import java.net.UnknownHostException;
import ghidra.util.Msg;
public class InetNameLookup {
private static final long MAX_TIME_MS = 10000;
private static volatile boolean lookupEnabled = true;
private static volatile boolean disableOnFailure = false;
private InetNameLookup() {
// static use only
}
public static void setDisableOnFailure(boolean state) {
disableOnFailure = state;
}
public static void setLookupEnabled(boolean enable) {
lookupEnabled = enable;
}
public static boolean isEnabled() {
return lookupEnabled;
}
/**
* Gets the fully qualified domain name for this IP address or hostname.
* Best effort method, meaning we may not be able to return
* the FQDN depending on the underlying system configuration.
*
* @param host IP address or hostname
*
* @return the fully qualified domain name for this IP address,
* or if the operation is not allowed/fails
* the original host name specified.
*
* @throws UnknownHostException the forward lookup of the specified address
* failed
*/
public static String getCanonicalHostName(String host) throws UnknownHostException {
String bestGuess = host;
if (lookupEnabled) {
// host may have multiple IP addresses
boolean found = false;
long fastest = Long.MAX_VALUE;
for (InetAddress addr : InetAddress.getAllByName(host)) {
long startTime = System.currentTimeMillis();
String name = addr.getCanonicalHostName();
long elapsedTime = System.currentTimeMillis() - startTime;
if (!name.equals(addr.getHostAddress())) {
if (host.equalsIgnoreCase(name)) {
return name; // name found matches original - use it
}
bestGuess = name; // name found - update best guess
found = true;
}
else {
// keep fastest reverse lookup time
fastest = Math.min(fastest, elapsedTime);
}
}
if (!found) {
// if lookup failed to produce a name - log warning
Msg.warn(InetNameLookup.class, "Failed to resolve IP Address: " + host +
" (Reverse DNS may not be properly configured or you may have a network problem)");
if (disableOnFailure && fastest > MAX_TIME_MS) {
// if lookup failed and was slow - disable future lookups if disableOnFailure is true
Msg.warn(InetNameLookup.class,
"Reverse network name lookup has been disabled automatically due to lookup failure.");
lookupEnabled = false;
}
}
}
return bestGuess;
}
}