GP-1387: Porting GDB/SSH to JSch

This commit is contained in:
Dan 2021-10-19 13:43:56 -04:00 committed by Ryan Kurtz
parent b2a553073f
commit 072ab7435a
19 changed files with 637 additions and 533 deletions

View file

@ -0,0 +1 @@
MODULE FILE LICENSE: lib/jsch-0.1.55.jar JSch License

View file

@ -27,6 +27,7 @@ dependencies {
api project(':Framework-Debugging') api project(':Framework-Debugging')
api project(':Debugger-gadp') api project(':Debugger-gadp')
api project(':Python') api project(':Python')
api 'com.jcraft:jsch:0.1.55'
testImplementation project(path: ':Framework-AsyncComm', configuration: 'testArtifacts') testImplementation project(path: ':Framework-AsyncComm', configuration: 'testArtifacts')
testImplementation project(path: ':Framework-Debugging', configuration: 'testArtifacts') testImplementation project(path: ':Framework-Debugging', configuration: 'testArtifacts')

View file

@ -1,4 +1,4 @@
##VERSION: 2.0 ##VERSION: 2.0
##MODULE IP: Jython License ##MODULE IP: JSch License
Module.manifest||GHIDRA||||END| Module.manifest||GHIDRA||||END|
data/scripts/define_info_proc_mappings||GHIDRA||||END| data/scripts/define_info_proc_mappings||GHIDRA||||END|

View file

@ -30,7 +30,7 @@ import ghidra.dbg.util.ConfigurableFactory.FactoryDescription;
htmlDetails = "Launch a GDB session over an SSH connection") htmlDetails = "Launch a GDB session over an SSH connection")
public class GdbOverSshDebuggerModelFactory implements DebuggerModelFactory { public class GdbOverSshDebuggerModelFactory implements DebuggerModelFactory {
private String gdbCmd = "gdb"; private String gdbCmd = "/usr/bin/gdb";
@FactoryOption("GDB launch command") @FactoryOption("GDB launch command")
public final Property<String> gdbCommandOption = public final Property<String> gdbCommandOption =
Property.fromAccessors(String.class, this::getGdbCommand, this::setGdbCommand); Property.fromAccessors(String.class, this::getGdbCommand, this::setGdbCommand);
@ -40,29 +40,29 @@ public class GdbOverSshDebuggerModelFactory implements DebuggerModelFactory {
public final Property<Boolean> useExistingOption = public final Property<Boolean> useExistingOption =
Property.fromAccessors(boolean.class, this::isUseExisting, this::setUseExisting); Property.fromAccessors(boolean.class, this::isUseExisting, this::setUseExisting);
private String hostname = "localhost"; private String hostname = GhidraSshPtyFactory.DEFAULT_HOSTNAME;
@FactoryOption("SSH hostname") @FactoryOption("SSH hostname")
public final Property<String> hostnameOption = public final Property<String> hostnameOption =
Property.fromAccessors(String.class, this::getHostname, this::setHostname); Property.fromAccessors(String.class, this::getHostname, this::setHostname);
private int port = 22; private int port = GhidraSshPtyFactory.DEFAULT_PORT;
@FactoryOption("SSH TCP port") @FactoryOption("SSH TCP port")
public final Property<Integer> portOption = public final Property<Integer> portOption =
Property.fromAccessors(Integer.class, this::getPort, this::setPort); Property.fromAccessors(Integer.class, this::getPort, this::setPort);
private String username = "user"; private String username = GhidraSshPtyFactory.DEFAULT_USERNAME;
@FactoryOption("SSH username") @FactoryOption("SSH username")
public final Property<String> usernameOption = public final Property<String> usernameOption =
Property.fromAccessors(String.class, this::getUsername, this::setUsername); Property.fromAccessors(String.class, this::getUsername, this::setUsername);
private String keyFile = ""; private String configFile = GhidraSshPtyFactory.DEFAULT_CONFIG_FILE;
@FactoryOption("SSH identity (blank for password auth)") @FactoryOption("Open SSH config file")
public final Property<String> keyFileOption = public final Property<String> keyFileOption =
Property.fromAccessors(String.class, this::getKeyFile, this::setKeyFile); Property.fromAccessors(String.class, this::getConfigFile, this::setConfigFile);
// Always default to false, despite local system, because remote is likely Linux. // Always default to false, despite local system, because remote is likely Linux.
private boolean useCrlf = false; private boolean useCrlf = false;
@FactoryOption("Use DOS line endings (unchecked for UNIX)") @FactoryOption("Use DOS line endings (unchecked for UNIX remote)")
public final Property<Boolean> crlfNewLineOption = public final Property<Boolean> crlfNewLineOption =
Property.fromAccessors(Boolean.class, this::isUseCrlf, this::setUseCrlf); Property.fromAccessors(Boolean.class, this::isUseCrlf, this::setUseCrlf);
@ -73,7 +73,7 @@ public class GdbOverSshDebuggerModelFactory implements DebuggerModelFactory {
GhidraSshPtyFactory factory = new GhidraSshPtyFactory(); GhidraSshPtyFactory factory = new GhidraSshPtyFactory();
factory.setHostname(hostname); factory.setHostname(hostname);
factory.setPort(port); factory.setPort(port);
factory.setKeyFile(keyFile); factory.setConfigFile(configFile);
factory.setUsername(username); factory.setUsername(username);
return new GdbModelImpl(factory); return new GdbModelImpl(factory);
}).thenCompose(model -> { }).thenCompose(model -> {
@ -136,12 +136,12 @@ public class GdbOverSshDebuggerModelFactory implements DebuggerModelFactory {
this.username = username; this.username = username;
} }
public String getKeyFile() { public String getConfigFile() {
return keyFile; return configFile;
} }
public void setKeyFile(String keyFile) { public void setConfigFile(String configFile) {
this.keyFile = keyFile; this.configFile = configFile;
} }
public boolean isUseCrlf() { public boolean isUseCrlf() {

View file

@ -1,54 +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 agent.gdb.pty.ssh;
import ch.ethz.ssh2.KnownHosts;
import ch.ethz.ssh2.ServerHostKeyVerifier;
import docking.widgets.OptionDialog;
import ghidra.util.Msg;
public class GhidraSshHostKeyVerifier implements ServerHostKeyVerifier {
private final KnownHosts database;
public GhidraSshHostKeyVerifier(KnownHosts database) {
this.database = database;
}
@Override
public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm,
byte[] serverHostKey) throws Exception {
switch (database.verifyHostkey(hostname, serverHostKeyAlgorithm, serverHostKey)) {
case KnownHosts.HOSTKEY_IS_OK:
return true;
case KnownHosts.HOSTKEY_IS_NEW:
int response = OptionDialog.showYesNoDialogWithNoAsDefaultButton(null,
"Unknown SSH Server Host Key",
"<html><b>The server " + hostname + " is not known.</b> " +
"It is highly recommended you log in to the server using a standard " +
"SSH client to confirm the host key first.<br><br>" +
"Do you want to continue?</html>");
return response == OptionDialog.YES_OPTION;
case KnownHosts.HOSTKEY_HAS_CHANGED:
Msg.showError(this, null, "SSH Server Host Key Changed",
"<html><b>The server " + hostname + " has a different key than before!</b>" +
"Use a standard SSH client to resolve the issue.</html>");
return false;
default:
throw new IllegalStateException();
}
}
}

View file

@ -15,25 +15,147 @@
*/ */
package agent.gdb.pty.ssh; package agent.gdb.pty.ssh;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Objects; import java.util.Objects;
import javax.swing.JOptionPane;
import org.apache.commons.text.StringEscapeUtils;
import com.jcraft.jsch.*;
import com.jcraft.jsch.ConfigRepository.Config;
import agent.gdb.pty.PtyFactory; import agent.gdb.pty.PtyFactory;
import ch.ethz.ssh2.Connection;
import ch.ethz.ssh2.KnownHosts;
import docking.DockingWindowManager; import docking.DockingWindowManager;
import docking.widgets.PasswordDialog; import docking.widgets.PasswordDialog;
import ghidra.util.Msg; import ghidra.util.*;
import ghidra.util.exception.CancelledException;
public class GhidraSshPtyFactory implements PtyFactory { public class GhidraSshPtyFactory implements PtyFactory {
private String hostname = "localhost"; private static final String TITLE = "GDB via SSH";
private int port = 22; private static final int WRAP_LEN = 80;
private String username = "user";
private String keyFile = "~/.ssh/id_rsa";
private Connection sshConn; public static final String DEFAULT_HOSTNAME = "localhost";
public static final int DEFAULT_PORT = 22;
public static final String DEFAULT_USERNAME = "user";
public static final String DEFAULT_CONFIG_FILE = "~/.ssh/config";
private class RequireTTYAlwaysConfig implements Config {
private final Config delegate;
public RequireTTYAlwaysConfig(Config delegate) {
this.delegate = delegate;
}
@Override
public String getHostname() {
return delegate.getHostname();
}
@Override
public String getUser() {
return delegate.getUser();
}
@Override
public int getPort() {
return delegate.getPort();
}
@Override
public String getValue(String key) {
if ("RequestTTY".equals(key)) {
return "yes";
}
return delegate.getValue(key);
}
@Override
public String[] getValues(String key) {
if ("RequestTTY".equals(key)) {
return new String[] { "yes" };
}
return delegate.getValues(key);
}
}
private class RequireTTYAlwaysConfigRepo implements ConfigRepository {
private final ConfigRepository delegate;
public RequireTTYAlwaysConfigRepo(ConfigRepository delegate) {
this.delegate = delegate;
}
@Override
public Config getConfig(String host) {
if (delegate == null) {
return new RequireTTYAlwaysConfig(ConfigRepository.defaultConfig);
}
return new RequireTTYAlwaysConfig(delegate.getConfig(host));
}
}
private class GhidraUserInfo implements UserInfo {
private String password;
private String passphrase;
public String doPromptSecret(String prompt) {
PasswordDialog dialog =
new PasswordDialog(TITLE, "SSH", hostname, prompt, null, null);
DockingWindowManager.showDialog(dialog);
if (dialog.okWasPressed()) {
return new String(dialog.getPassword());
}
return null;
}
public String html(String message) {
// TODO: I shouldn't have to do this. Why won't swing wrap?
String wrapped = StringUtilities.wrapToWidth(message, WRAP_LEN);
return "<html><pre>" + StringEscapeUtils.escapeHtml4(wrapped).replace("\n", "<br>");
}
@Override
public String getPassphrase() {
return passphrase;
}
@Override
public String getPassword() {
return password;
}
@Override
public boolean promptPassword(String message) {
password = doPromptSecret(message);
return password != null;
}
@Override
public boolean promptPassphrase(String message) {
passphrase = doPromptSecret(message);
return passphrase != null;
}
@Override
public boolean promptYesNo(String message) {
return JOptionPane.showConfirmDialog(null, html(message), TITLE,
JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE) == JOptionPane.YES_OPTION;
}
@Override
public void showMessage(String message) {
JOptionPane.showMessageDialog(null, html(message), TITLE,
JOptionPane.INFORMATION_MESSAGE);
}
}
private String hostname = DEFAULT_HOSTNAME;
private int port = DEFAULT_PORT;
private String username = DEFAULT_USERNAME;
private String configFile = DEFAULT_CONFIG_FILE;
private Session session;
public String getHostname() { public String getHostname() {
return hostname; return hostname;
@ -59,81 +181,50 @@ public class GhidraSshPtyFactory implements PtyFactory {
this.username = Objects.requireNonNull(username); this.username = Objects.requireNonNull(username);
} }
public String getKeyFile() { public String getConfigFile() {
return keyFile; return configFile;
} }
/** public void setConfigFile(String configFile) {
* Set the keyfile path, or empty for password authentication only this.configFile = configFile;
*
* @param keyFile the path
*/
public void setKeyFile(String keyFile) {
this.keyFile = Objects.requireNonNull(keyFile);
} }
public static char[] promptPassword(String hostname, String prompt) throws CancelledException { protected Session connectAndAuthenticate() throws IOException {
PasswordDialog dialog = JSch jsch = new JSch();
new PasswordDialog("GDB via SSH", "SSH", hostname, prompt, null, ConfigRepository configRepo = null;
"");
DockingWindowManager.showDialog(dialog);
if (dialog.okWasPressed()) {
return dialog.getPassword();
}
throw new CancelledException();
}
protected Connection connectAndAuthenticate() throws IOException {
boolean success = false;
File knownHostsFile = new File(System.getProperty("user.home") + "/.ssh/known_hosts");
KnownHosts knownHosts = new KnownHosts();
if (knownHostsFile.exists()) {
knownHosts.addHostkeys(knownHostsFile);
}
Connection sshConn = new Connection(hostname, port);
try { try {
sshConn.connect(new GhidraSshHostKeyVerifier(knownHosts)); configRepo = OpenSSHConfig.parseFile(configFile);
if ("".equals(keyFile.trim())) {
// TODO: Find an API that uses char[] so I can clear it!
String password = new String(promptPassword(hostname, "Password for " + username));
if (!sshConn.authenticateWithPassword(username, password)) {
Msg.error(this, "SSH password authentication failed");
throw new IOException("SSH password authentication failed");
}
}
else {
File pemFile = new File(keyFile);
if (!pemFile.canRead()) {
throw new IOException("Key file " + keyFile +
" cannot be read. Does it exist? Do you have permission?");
}
String password = new String(promptPassword(hostname, "Password for " + pemFile));
if (!sshConn.authenticateWithPublicKey(username, pemFile, password)) {
Msg.error(this, "SSH pukey authentication failed");
throw new IOException("SSH pukey authentication failed");
}
}
success = true;
return sshConn;
} }
catch (CancelledException e) { catch (IOException e) {
Msg.error(this, "SSH connection/authentication cancelled by user"); Msg.warn(this, "ssh config file " + configFile + " could not be parsed.");
throw new IOException("SSH connection/authentication cancelled by user", e); // I guess the config file doesn't exist. Just go on
} }
finally { jsch.setConfigRepository(new RequireTTYAlwaysConfigRepo(configRepo));
if (!success) {
sshConn.close(); try {
} Session session =
jsch.getSession(username.length() == 0 ? null : username, hostname, port);
session.setUserInfo(new GhidraUserInfo());
session.connect();
return session;
}
catch (JSchException e) {
Msg.error(this, "SSH connection error");
throw new IOException("SSH connection error", e);
} }
} }
@Override @Override
public SshPty openpty() throws IOException { public SshPty openpty() throws IOException {
if (sshConn == null || !sshConn.isAuthenticationComplete()) { if (session == null) {
sshConn = connectAndAuthenticate(); session = connectAndAuthenticate();
}
try {
return new SshPty((ChannelExec) session.openChannel("exec"));
}
catch (JSchException e) {
throw new IOException("SSH connection error", e);
} }
return new SshPty(sshConn.openSession());
} }
@Override @Override

View file

@ -15,32 +15,36 @@
*/ */
package agent.gdb.pty.ssh; package agent.gdb.pty.ssh;
import java.io.IOException; import java.io.*;
import com.jcraft.jsch.*;
import agent.gdb.pty.*; import agent.gdb.pty.*;
import ch.ethz.ssh2.Session;
public class SshPty implements Pty { public class SshPty implements Pty {
private final Session session; private final ChannelExec channel;
private final OutputStream out;
private final InputStream in;
public SshPty(Session session) throws IOException { public SshPty(ChannelExec channel) throws JSchException, IOException {
this.session = session; this.channel = channel;
session.requestDumbPTY();
out = channel.getOutputStream();
in = channel.getInputStream();
} }
@Override @Override
public PtyParent getParent() { public PtyParent getParent() {
// TODO: Need I worry about stderr? I thought both pointed to the same tty.... return new SshPtyParent(out, in);
return new SshPtyParent(session.getStdin(), session.getStdout());
} }
@Override @Override
public PtyChild getChild() { public PtyChild getChild() {
return new SshPtyChild(session); return new SshPtyChild(channel, out, in);
} }
@Override @Override
public void close() throws IOException { public void close() throws IOException {
session.close(); channel.disconnect();
} }
} }

View file

@ -16,23 +16,26 @@
package agent.gdb.pty.ssh; package agent.gdb.pty.ssh;
import java.io.*; import java.io.*;
import java.util.Arrays;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.help.UnsupportedOperationException; import javax.help.UnsupportedOperationException;
import com.jcraft.jsch.*;
import agent.gdb.pty.PtyChild; import agent.gdb.pty.PtyChild;
import ch.ethz.ssh2.Session; import ghidra.dbg.util.ShellUtils;
import ghidra.util.Msg; import ghidra.util.Msg;
public class SshPtyChild extends SshPtyEndpoint implements PtyChild { public class SshPtyChild extends SshPtyEndpoint implements PtyChild {
private String name; private final ChannelExec channel;
private final Session session;
public SshPtyChild(Session session) { private String name;
super(null, null);
this.session = session; public SshPtyChild(ChannelExec channel, OutputStream outputStream, InputStream inputStream) {
super(outputStream, inputStream);
this.channel = channel;
} }
@Override @Override
@ -48,34 +51,37 @@ public class SshPtyChild extends SshPtyEndpoint implements PtyChild {
.map(e -> e.getKey() + "=" + e.getValue()) .map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining(" ")) + .collect(Collectors.joining(" ")) +
" "; " ";
String cmdStr = Stream.of(args).collect(Collectors.joining(" ")); String cmdStr = ShellUtils.generateLine(Arrays.asList(args));
channel.setCommand(envStr + cmdStr);
try { try {
session.execCommand(envStr + cmdStr); channel.connect();
} }
catch (Throwable t) { catch (JSchException e) {
Msg.error(this, "Could not execute remote command: " + envStr + cmdStr, t); throw new IOException("SSH error", e);
throw t;
} }
return new SshPtySession(session); return new SshPtySession(channel);
} }
private String getTtyNameAndStartNullSession() throws IOException { private String getTtyNameAndStartNullSession() throws IOException {
// NB. Using [InputStream/Buffered]Reader will close my stream. Cannot do that.
InputStream stdout = session.getStdout();
// NB. UNIX sleep is only required to support integer durations // NB. UNIX sleep is only required to support integer durations
session.execCommand( channel.setCommand(
"sh -c 'tty && cltrc() { echo; } && trap ctrlc INT && while true; do sleep " + ("sh -c 'tty && ctrlc() { echo; } && trap ctrlc INT && while true; do sleep " +
Integer.MAX_VALUE + "; done'", Integer.MAX_VALUE + "; done'"));
"UTF-8"); try {
channel.connect();
}
catch (JSchException e) {
throw new IOException("SSH error", e);
}
byte[] buf = new byte[1024]; // Should be plenty byte[] buf = new byte[1024]; // Should be plenty
for (int i = 0; i < 1024; i++) { for (int i = 0; i < 1024; i++) {
int chr = stdout.read(); int chr = inputStream.read();
if (chr == '\n' || chr == -1) { if (chr == '\n' || chr == -1) {
return new String(buf, 0, i + 1).trim(); return new String(buf, 0, i + 1, "UTF-8").trim();
} }
buf[i] = (byte) chr; buf[i] = (byte) chr;
} }
throw new IOException("Remote tty name exceeds 1024 bytes?"); throw new IOException("Expected pty name. Got " + new String(buf, 0, 1024, "UTF-8"));
} }
@Override @Override

View file

@ -21,13 +21,12 @@ import java.io.OutputStream;
import agent.gdb.pty.PtyEndpoint; import agent.gdb.pty.PtyEndpoint;
public class SshPtyEndpoint implements PtyEndpoint { public class SshPtyEndpoint implements PtyEndpoint {
private final OutputStream outputStream; protected final OutputStream outputStream;
private final InputStream inputStream; protected final InputStream inputStream;
public SshPtyEndpoint(OutputStream outputStream, InputStream inputStream) { public SshPtyEndpoint(OutputStream outputStream, InputStream inputStream) {
this.outputStream = outputStream; this.outputStream = outputStream;
this.inputStream = inputStream; this.inputStream = inputStream;
} }
@Override @Override

View file

@ -15,43 +15,30 @@
*/ */
package agent.gdb.pty.ssh; package agent.gdb.pty.ssh;
import java.io.IOException; import com.jcraft.jsch.Channel;
import java.io.InterruptedIOException;
import agent.gdb.pty.PtySession; import agent.gdb.pty.PtySession;
import ch.ethz.ssh2.ChannelCondition;
import ch.ethz.ssh2.Session;
public class SshPtySession implements PtySession { public class SshPtySession implements PtySession {
private final Session session; private final Channel channel;
public SshPtySession(Session session) { public SshPtySession(Channel channel) {
this.session = session; this.channel = channel;
} }
@Override @Override
public Integer waitExited() throws InterruptedException { public Integer waitExited() throws InterruptedException {
try { // Doesn't look like there's a clever way to wait. So do the spin sleep :(
session.waitForCondition(ChannelCondition.EOF, 0); while (!channel.isEOF()) {
// NB. May not be available Thread.sleep(1000);
return session.getExitStatus();
}
catch (InterruptedIOException e) {
throw new InterruptedException();
}
catch (IOException e) {
throw new RuntimeException(e);
} }
// NB. May not be available
return channel.getExitStatus();
} }
@Override @Override
public void destroyForcibly() { public void destroyForcibly() {
/** channel.disconnect();
* TODO: This is imperfect, since it terminates the whole SSH session, not just the pty
* session. I don't think that's terribly critical for our use case, but we should adjust
* the spec to account for this, or devise a better implementation.
*/
session.close();
} }
} }

View file

@ -0,0 +1,44 @@
/* ###
* 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 agent.gdb.model.ssh;
import java.util.Map;
import agent.gdb.GdbOverSshDebuggerModelFactory;
import agent.gdb.pty.ssh.SshPtyTest;
import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.test.AbstractModelHost;
import ghidra.util.exception.CancelledException;
public class SshJoinGdbModelHost extends AbstractModelHost {
@Override
public DebuggerModelFactory getModelFactory() {
return new GdbOverSshDebuggerModelFactory();
}
@Override
public Map<String, Object> getFactoryOptions() {
try {
return Map.ofEntries(
Map.entry("SSH username", SshPtyTest.promptUser()),
Map.entry("Use existing session via new-ui", true));
}
catch (CancelledException e) {
throw new AssertionError("Cancelled", e);
}
}
}

View file

@ -0,0 +1,36 @@
/* ###
* 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 agent.gdb.model.ssh;
import static org.junit.Assume.assumeFalse;
import org.junit.Before;
import agent.gdb.model.AbstractModelForGdbFactoryTest;
import ghidra.util.SystemUtilities;
public class SshJoinModelForGdbFactoryTest extends AbstractModelForGdbFactoryTest {
@Before
public void checkInteractive() {
assumeFalse(SystemUtilities.isInTestingBatchMode());
}
@Override
public ModelHost modelHost() throws Throwable {
return new SshJoinGdbModelHost();
}
}

View file

@ -0,0 +1,29 @@
/* ###
* 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 agent.gdb.model.ssh;
import org.junit.experimental.categories.Category;
import agent.gdb.model.AbstractModelForGdbSessionLauncherTest;
import generic.test.category.NightlyCategory;
@Category(NightlyCategory.class) // this may actually be an @PortSensitive test
public class SshJoinModelForGdbSessionLauncherTest extends AbstractModelForGdbSessionLauncherTest {
@Override
public ModelHost modelHost() throws Throwable {
return new SshJoinGdbModelHost();
}
}

View file

@ -1,195 +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 agent.gdb.pty.ssh;
import static org.junit.Assume.assumeFalse;
import java.io.IOException;
import java.io.InputStream;
import org.junit.Before;
import org.junit.Test;
import ch.ethz.ssh2.*;
import ghidra.app.script.AskDialog;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.util.SystemUtilities;
import ghidra.util.exception.CancelledException;
public class SshExperimentsTest extends AbstractGhidraHeadedIntegrationTest {
@Before
public void checkInteractive() {
assumeFalse(SystemUtilities.isInTestingBatchMode());
}
@Test
public void testExpExecCommandIsAsync()
throws IOException, CancelledException, InterruptedException {
Connection conn = new Connection("localhost");
conn.addConnectionMonitor(new ConnectionMonitor() {
@Override
public void connectionLost(Throwable reason) {
System.err.println("Lost connection: " + reason);
}
});
conn.connect();
String user = SshPtyTest.promptUser();
while (true) {
char[] password =
GhidraSshPtyFactory.promptPassword("localhost", "Password for " + user);
boolean auth = conn.authenticateWithPassword(user, new String(password));
if (auth) {
break;
}
System.err.println("Authentication Failed");
}
Session session = conn.openSession();
System.err.println("PRE: signal=" + session.getExitSignal());
Thread thread = new Thread("reader") {
@Override
public void run() {
InputStream stdout = session.getStdout();
try {
stdout.transferTo(System.out);
}
catch (IOException e) {
e.printStackTrace();
}
}
};
thread.setDaemon(true);
thread.start();
// Demonstrates that execCommand returns before the remote command exits
System.err.println("Invoking sleep remotely");
session.execCommand("sleep 10");
System.err.println("Returned from execCommand");
}
@Test
public void testExpEOFImpliesCommandExited()
throws IOException, CancelledException, InterruptedException {
Connection conn = new Connection("localhost");
conn.addConnectionMonitor(new ConnectionMonitor() {
@Override
public void connectionLost(Throwable reason) {
System.err.println("Lost connection: " + reason);
}
});
conn.connect();
AskDialog<String> dialog = new AskDialog<>("SSH", "Username:", AskDialog.STRING, "");
if (dialog.isCanceled()) {
throw new CancelledException();
}
String user = dialog.getValueAsString();
while (true) {
char[] password =
GhidraSshPtyFactory.promptPassword("localhost", "Password for " + user);
boolean auth = conn.authenticateWithPassword(user, new String(password));
if (auth) {
break;
}
System.err.println("Authentication Failed");
}
Session session = conn.openSession();
System.err.println("PRE: signal=" + session.getExitSignal());
Thread thread = new Thread("reader") {
@Override
public void run() {
InputStream stdout = session.getStdout();
try {
stdout.transferTo(System.out);
}
catch (IOException e) {
e.printStackTrace();
}
}
};
thread.setDaemon(true);
thread.start();
// Demonstrates the ability to wait for the specific command
System.err.println("Invoking sleep remotely");
session.execCommand("sleep 3");
session.waitForCondition(ChannelCondition.EOF, 0);
System.err.println("Returned from waitForCondition");
}
@Test
public void testExpEnvWorks()
throws IOException, CancelledException, InterruptedException {
Connection conn = new Connection("localhost");
conn.addConnectionMonitor(new ConnectionMonitor() {
@Override
public void connectionLost(Throwable reason) {
System.err.println("Lost connection: " + reason);
}
});
conn.connect();
AskDialog<String> dialog = new AskDialog<>("SSH", "Username:", AskDialog.STRING, "");
if (dialog.isCanceled()) {
throw new CancelledException();
}
String user = dialog.getValueAsString();
while (true) {
char[] password =
GhidraSshPtyFactory.promptPassword("localhost", "Password for " + user);
boolean auth = conn.authenticateWithPassword(user, new String(password));
if (auth) {
break;
}
System.err.println("Authentication Failed");
}
Session session = conn.openSession();
System.err.println("PRE: signal=" + session.getExitSignal());
Thread thread = new Thread("reader") {
@Override
public void run() {
InputStream stdout = session.getStdout();
try {
stdout.transferTo(System.out);
}
catch (IOException e) {
e.printStackTrace();
}
}
};
thread.setDaemon(true);
thread.start();
// Demonstrates a syntax for specifying env.
// I suspect this depends on the remote shell.
System.err.println("Echoing...");
session.execCommand("MY_DATA=test bash -c 'echo data:$MY_DATA:end'");
session.waitForCondition(ChannelCondition.EOF, 0);
System.err.println("Done");
}
}

View file

@ -18,7 +18,7 @@ package agent.gdb.pty.ssh;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeFalse;
import java.io.IOException; import java.io.*;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -36,9 +36,7 @@ public class SshPtyTest extends AbstractGhidraHeadedIntegrationTest {
public void setupSshPtyTest() throws CancelledException { public void setupSshPtyTest() throws CancelledException {
assumeFalse(SystemUtilities.isInTestingBatchMode()); assumeFalse(SystemUtilities.isInTestingBatchMode());
factory = new GhidraSshPtyFactory(); factory = new GhidraSshPtyFactory();
factory.setHostname("localhost");
factory.setUsername(promptUser()); factory.setUsername(promptUser());
factory.setKeyFile("");
} }
public static String promptUser() throws CancelledException { public static String promptUser() throws CancelledException {
@ -49,11 +47,41 @@ public class SshPtyTest extends AbstractGhidraHeadedIntegrationTest {
return dialog.getValueAsString(); return dialog.getValueAsString();
} }
public static class StreamPumper extends Thread {
private final InputStream in;
private final OutputStream out;
public StreamPumper(InputStream in, OutputStream out) {
setDaemon(true);
this.in = in;
this.out = out;
}
@Override
public void run() {
byte[] buf = new byte[1024];
try {
while (true) {
int len = in.read(buf);
if (len <= 0) {
break;
}
out.write(buf, 0, len);
}
}
catch (IOException e) {
}
}
}
@Test @Test
public void testSessionBash() throws IOException, InterruptedException { public void testSessionBash() throws IOException, InterruptedException {
try (SshPty pty = factory.openpty()) { try (SshPty pty = factory.openpty()) {
PtySession bash = pty.getChild().session(new String[] { "bash" }, null); PtySession bash = pty.getChild().session(new String[] { "bash" }, null);
pty.getParent().getOutputStream().write("exit\n".getBytes()); OutputStream out = pty.getParent().getOutputStream();
out.write("exit\n".getBytes("UTF-8"));
out.flush();
new StreamPumper(pty.getParent().getInputStream(), System.out).start();
assertEquals(0, bash.waitExited().intValue()); assertEquals(0, bash.waitExited().intValue());
} }
} }

View file

@ -158,7 +158,10 @@ public interface ConfigurableFactory<T> {
if (codec == null) { if (codec == null) {
continue; continue;
} }
property.setValue(codec.read(saveState, opt.getKey(), null)); Object read = codec.read(saveState, opt.getKey(), null);
if (read != null) {
property.setValue(read);
}
} }
} }
} }

View file

@ -72,14 +72,13 @@ public class StringUtilities {
public static final int UNICODE_REPLACEMENT = 0xFFFD; public static final int UNICODE_REPLACEMENT = 0xFFFD;
/** /**
* Unicode Byte Order Marks (BOM) characters are special characters in the Unicode * Unicode Byte Order Marks (BOM) characters are special characters in the Unicode character
* character space that signal endian-ness of the text. * space that signal endian-ness of the text.
* <p> * <p>
* The value for the BigEndian version (0xFEFF) works for both 16 and 32 bit * The value for the BigEndian version (0xFEFF) works for both 16 and 32 bit character values.
* character values.
* <p> * <p>
* There are separate values for Little Endian Byte Order Marks for 16 and 32 bit * There are separate values for Little Endian Byte Order Marks for 16 and 32 bit characters
* characters because the 32 bit value is shifted left by 16 bits. * because the 32 bit value is shifted left by 16 bits.
*/ */
public static final int UNICODE_BE_BYTE_ORDER_MARK = 0xFEFF; public static final int UNICODE_BE_BYTE_ORDER_MARK = 0xFEFF;
public static final int UNICODE_LE16_BYTE_ORDER_MARK = 0x0____FFFE; public static final int UNICODE_LE16_BYTE_ORDER_MARK = 0x0____FFFE;
@ -93,9 +92,9 @@ public class StringUtilities {
} }
/** /**
* Returns true if the given character is a special character. * Returns true if the given character is a special character. For example a '\n' or '\\'. A
* For example a '\n' or '\\'. A value of 0 is not considered special for this purpose * value of 0 is not considered special for this purpose as it is handled separately because it
* as it is handled separately because it has more varied use cases. * has more varied use cases.
* *
* @param c the character * @param c the character
* @return true if the given character is a special character * @return true if the given character is a special character
@ -105,9 +104,9 @@ public class StringUtilities {
} }
/** /**
* Returns true if the given codePoint (ie. full unicode 32bit character) is a special character. * Returns true if the given codePoint (ie. full unicode 32bit character) is a special
* For example a '\n' or '\\'. A value of 0 is not considered special for this purpose * character. For example a '\n' or '\\'. A value of 0 is not considered special for this
* as it is handled separately because it has more varied use cases. * purpose as it is handled separately because it has more varied use cases.
* *
* @param codePoint the codePoint (ie. character), see {@link String#codePointAt(int)} * @param codePoint the codePoint (ie. character), see {@link String#codePointAt(int)}
* @return true if the given character is a special character * @return true if the given character is a special character
@ -119,9 +118,9 @@ public class StringUtilities {
/** /**
* Determines if a string is enclosed in double quotes (ASCII 34 (0x22)) * Determines if a string is enclosed in double quotes (ASCII 34 (0x22))
*
* @param str String to test for double-quote enclosure * @param str String to test for double-quote enclosure
* @return True if the first and last characters are the double-quote character, * @return True if the first and last characters are the double-quote character, false otherwise
* false otherwise
*/ */
public static boolean isDoubleQuoted(String str) { public static boolean isDoubleQuoted(String str) {
Matcher m = DOUBLE_QUOTED_STRING_PATTERN.matcher(str); Matcher m = DOUBLE_QUOTED_STRING_PATTERN.matcher(str);
@ -129,11 +128,12 @@ public class StringUtilities {
} }
/** /**
* If the given string is enclosed in double quotes, extract the inner text. * If the given string is enclosed in double quotes, extract the inner text. Otherwise, return
* Otherwise, return the given string unmodified. * the given string unmodified.
*
* @param str String to match and extract from * @param str String to match and extract from
* @return The inner text of a doubly-quoted string, or the original string if not * @return The inner text of a doubly-quoted string, or the original string if not
* double-quoted. * double-quoted.
*/ */
public static String extractFromDoubleQuotes(String str) { public static String extractFromDoubleQuotes(String str) {
Matcher m = DOUBLE_QUOTED_STRING_PATTERN.matcher(str); Matcher m = DOUBLE_QUOTED_STRING_PATTERN.matcher(str);
@ -145,6 +145,7 @@ public class StringUtilities {
/** /**
* Returns true if the character is in displayable character range * Returns true if the character is in displayable character range
*
* @param c the character * @param c the character
* @return true if the character is in displayable character range * @return true if the character is in displayable character range
*/ */
@ -176,9 +177,9 @@ public class StringUtilities {
} }
/** /**
* Converts the character into a string. * Converts the character into a string. If the character is special, it will actually render
* If the character is special, it will actually render the character. * the character. For example, given '\n' the output would be "\\n".
* For example, given '\n' the output would be "\\n". *
* @param c the character to convert into a string * @param c the character to convert into a string
* @return the converted character * @return the converted character
*/ */
@ -192,8 +193,9 @@ public class StringUtilities {
/** /**
* Returns a count of how many times the 'occur' char appears in the strings. * Returns a count of how many times the 'occur' char appears in the strings.
*
* @param string the string to look inside * @param string the string to look inside
* @param occur the character to look for/ * @param occur the character to look for/
* @return a count of how many times the 'occur' char appears in the strings * @return a count of how many times the 'occur' char appears in the strings
*/ */
public static int countOccurrences(String string, char occur) { public static int countOccurrences(String string, char occur) {
@ -225,11 +227,11 @@ public class StringUtilities {
* Generate a quoted string from US-ASCII character bytes assuming 1-byte chars. * Generate a quoted string from US-ASCII character bytes assuming 1-byte chars.
* <p> * <p>
* Special characters and non-printable characters will be escaped using C character escape * Special characters and non-printable characters will be escaped using C character escape
* conventions (e.g., \t, \n, \\uHHHH, etc.). If a character size other than 1-byte is * conventions (e.g., \t, \n, \\uHHHH, etc.). If a character size other than 1-byte is required
* required the alternate form of this method should be used. * the alternate form of this method should be used.
* <p> * <p>
* The result string will be single quoted (ie. "'") if the input byte array is * The result string will be single quoted (ie. "'") if the input byte array is 1 byte long,
* 1 byte long, otherwise the result will be double-quoted ('"'). * otherwise the result will be double-quoted ('"').
* *
* @param bytes character string bytes * @param bytes character string bytes
* @return escaped string for display use * @return escaped string for display use
@ -254,8 +256,8 @@ public class StringUtilities {
* Special characters and non-printable characters will be escaped using C character escape * Special characters and non-printable characters will be escaped using C character escape
* conventions (e.g., \t, \n, \\uHHHH, etc.). * conventions (e.g., \t, \n, \\uHHHH, etc.).
* <p> * <p>
* The result string will be single quoted (ie. "'") if the input byte array is * The result string will be single quoted (ie. "'") if the input byte array is 1 character long
* 1 character long (ie. charSize), otherwise the result will be double-quoted ('"'). * (ie. charSize), otherwise the result will be double-quoted ('"').
* *
* @param bytes array of bytes * @param bytes array of bytes
* @param charSize number of bytes per character (1, 2, 4). * @param charSize number of bytes per character (1, 2, 4).
@ -317,8 +319,9 @@ public class StringUtilities {
* Returns true if the given string starts with <code>prefix</code> ignoring case. * Returns true if the given string starts with <code>prefix</code> ignoring case.
* <p> * <p>
* Note: This method is equivalent to calling: * Note: This method is equivalent to calling:
*
* <pre> * <pre>
* string.regionMatches( true, 0, prefix, 0, prefix.length() ); * string.regionMatches(true, 0, prefix, 0, prefix.length());
* </pre> * </pre>
* *
* @param string the string which may contain the prefix * @param string the string which may contain the prefix
@ -336,9 +339,10 @@ public class StringUtilities {
* Returns true if the given string ends with <code>postfix</code>, ignoring case. * Returns true if the given string ends with <code>postfix</code>, ignoring case.
* <p> * <p>
* Note: This method is equivalent to calling: * Note: This method is equivalent to calling:
*
* <pre> * <pre>
* int startIndex = string.length() - postfix.length(); * int startIndex = string.length() - postfix.length();
* string.regionMatches( true, startOffset, postfix, 0, postfix.length() ); * string.regionMatches(true, startOffset, postfix, 0, postfix.length());
* </pre> * </pre>
* *
* @param string the string which may end with <code>postfix</code> * @param string the string which may end with <code>postfix</code>
@ -416,13 +420,14 @@ public class StringUtilities {
} }
/** /**
* Returns the index of the first whole word occurrence of the search word within * Returns the index of the first whole word occurrence of the search word within the given
* the given text. A whole word is defined as the character before and after the occurrence * text. A whole word is defined as the character before and after the occurrence must not be a
* must not be a JavaIdentifierPart. * JavaIdentifierPart.
*
* @param text the text to be searched. * @param text the text to be searched.
* @param searchWord the word to search for. * @param searchWord the word to search for.
* @return the index of the first whole word occurrence of the search word within * @return the index of the first whole word occurrence of the search word within the given
* the given text, or -1 if not found. * text, or -1 if not found.
*/ */
public static int indexOfWord(String text, String searchWord) { public static int indexOfWord(String text, String searchWord) {
int index = 0; int index = 0;
@ -440,14 +445,15 @@ public class StringUtilities {
} }
/** /**
* Returns true if the substring within the text string starting at startIndex and having * Returns true if the substring within the text string starting at startIndex and having the
* the given length is a whole word. A whole word is defined as the character before and after * given length is a whole word. A whole word is defined as the character before and after the
* the occurrence must not be a JavaIdentifierPart. * occurrence must not be a JavaIdentifierPart.
*
* @param text the text containing the potential word. * @param text the text containing the potential word.
* @param startIndex the start index of the potential word within the text. * @param startIndex the start index of the potential word within the text.
* @param length the length of the potential word * @param length the length of the potential word
* @return true if the substring within the text string starting at startIndex and having * @return true if the substring within the text string starting at startIndex and having the
* the given length is a whole word. * given length is a whole word.
*/ */
public static boolean isWholeWord(String text, int startIndex, int length) { public static boolean isWholeWord(String text, int startIndex, int length) {
if (startIndex > 0) { if (startIndex > 0) {
@ -466,11 +472,9 @@ public class StringUtilities {
} }
/** /**
* Convert tabs in the given string to spaces using * Convert tabs in the given string to spaces using a default tab width of 8 spaces.
* a default tab width of 8 spaces.
* *
* @param str * @param str string containing tabs
* string containing tabs
* @return string that has spaces for tabs * @return string that has spaces for tabs
*/ */
public static String convertTabsToSpaces(String str) { public static String convertTabsToSpaces(String str) {
@ -480,10 +484,8 @@ public class StringUtilities {
/** /**
* Convert tabs in the given string to spaces. * Convert tabs in the given string to spaces.
* *
* @param str * @param str string containing tabs
* string containing tabs * @param tabSize length of the tab
* @param tabSize
* length of the tab
* @return string that has spaces for tabs * @return string that has spaces for tabs
*/ */
public static String convertTabsToSpaces(String str, int tabSize) { public static String convertTabsToSpaces(String str, int tabSize) {
@ -516,9 +518,8 @@ public class StringUtilities {
} }
/** /**
* Parses a string containing multiple lines into an array where each * Parses a string containing multiple lines into an array where each element in the array
* element in the array contains only a single line. The "\n" character is * contains only a single line. The "\n" character is used as the delimiter for lines.
* used as the delimiter for lines.
* <p> * <p>
* This methods creates an empty string entry in the result array for initial and trailing * This methods creates an empty string entry in the result array for initial and trailing
* separator chars, as well as for consecutive separators. * separator chars, as well as for consecutive separators.
@ -532,13 +533,12 @@ public class StringUtilities {
} }
/** /**
* Parses a string containing multiple lines into an array where each * Parses a string containing multiple lines into an array where each element in the array
* element in the array contains only a single line. The "\n" character is * contains only a single line. The "\n" character is used as the delimiter for lines.
* used as the delimiter for lines.
* *
* @param s the string to parse * @param s the string to parse
* @param preserveTokens true signals to treat consecutive newlines as multiple lines; false * @param preserveTokens true signals to treat consecutive newlines as multiple lines; false
* signals to treat consecutive newlines as a single line break * signals to treat consecutive newlines as a single line break
* @return an array of lines; an empty array if the given value is null or empty * @return an array of lines; an empty array if the given value is null or empty
*/ */
public static String[] toLines(String s, boolean preserveTokens) { public static String[] toLines(String s, boolean preserveTokens) {
@ -557,8 +557,7 @@ public class StringUtilities {
} }
/** /**
* Enforces the given length upon the given string by trimming and then padding as * Enforces the given length upon the given string by trimming and then padding as necessary.
* necessary.
* *
* @param s the String to fix * @param s the String to fix
* @param pad the pad character to use if padding is required * @param pad the pad character to use if padding is required
@ -572,9 +571,9 @@ public class StringUtilities {
} }
/** /**
* Pads the source string to the specified length, using the filler string * Pads the source string to the specified length, using the filler string as the pad. If length
* as the pad. If length is negative, left justifies the string, appending * is negative, left justifies the string, appending the filler; if length is positive, right
* the filler; if length is positive, right justifies the source string. * justifies the source string.
* *
* @param source the original string to pad. * @param source the original string to pad.
* @param filler the type of characters with which to pad * @param filler the type of characters with which to pad
@ -610,8 +609,8 @@ public class StringUtilities {
} }
/** /**
* Splits the given string into lines using <code>\n</code> and then pads each string * Splits the given string into lines using <code>\n</code> and then pads each string with the
* with the given pad string. Finally, the updated lines are formed into a single string. * given pad string. Finally, the updated lines are formed into a single string.
* <p> * <p>
* This is useful for constructing complicated <code>toString()</code> representations. * This is useful for constructing complicated <code>toString()</code> representations.
* *
@ -636,13 +635,11 @@ public class StringUtilities {
} }
/** /**
* Finds the word at the given index in the given string. For example, the * Finds the word at the given index in the given string. For example, the string "The tree is
* string "The tree is green" and the index of 5, the result would be * green" and the index of 5, the result would be "tree".
* "tree".
* *
* @param s the string to search * @param s the string to search
* @param index * @param index the index into the string to "seed" the word.
* the index into the string to "seed" the word.
* @return String the word contained at the given index. * @return String the word contained at the given index.
*/ */
public static String findWord(String s, int index) { public static String findWord(String s, int index) {
@ -650,17 +647,16 @@ public class StringUtilities {
} }
/** /**
* Finds the word at the given index in the given string; if the word * Finds the word at the given index in the given string; if the word contains the given
* contains the given charToAllow, then allow it in the string. For example, * charToAllow, then allow it in the string. For example, the string "The tree* is green" and
* the string "The tree* is green" and the index of 5, charToAllow is '*', * the index of 5, charToAllow is '*', then the result would be "tree*".
* then the result would be "tree*".
* <p> * <p>
* If the search yields only whitespace, then the empty string will be returned. * If the search yields only whitespace, then the empty string will be returned.
* *
* @param s the string to search * @param s the string to search
* @param index the index into the string to "seed" the word. * @param index the index into the string to "seed" the word.
* @param charsToAllow chars that normally would be considered invalid, e.g., '*' so * @param charsToAllow chars that normally would be considered invalid, e.g., '*' so that the
* that the word can be returned with the charToAllow * word can be returned with the charToAllow
* @return String the word contained at the given index. * @return String the word contained at the given index.
*/ */
public static String findWord(String s, int index, char[] charsToAllow) { public static String findWord(String s, int index, char[] charsToAllow) {
@ -706,8 +702,8 @@ public class StringUtilities {
} }
/** /**
* Loosely defined as a character that we would expected to be an normal ascii content meant * Loosely defined as a character that we would expected to be an normal ascii content meant for
* for consumption by a human. Also, provided allows chars will pass the test. * consumption by a human. Also, provided allows chars will pass the test.
* *
* @param c the char to check * @param c the char to check
* @param charsToAllow characters that will cause this method to return true * @param charsToAllow characters that will cause this method to return true
@ -730,8 +726,7 @@ public class StringUtilities {
/** /**
* Finds the starting position of the last word in the given string. * Finds the starting position of the last word in the given string.
* *
* @param s * @param s the string to search
* the string to search
* @return int the starting position of the last word, -1 if not found * @return int the starting position of the last word, -1 if not found
*/ */
public static int findLastWordPosition(String s) { public static int findLastWordPosition(String s) {
@ -752,13 +747,14 @@ public class StringUtilities {
} }
/** /**
* Takes a path-like string and retrieves the last non-empty item. Examples: * Takes a path-like string and retrieves the last non-empty item. Examples:
* <ul> * <ul>
* <li>StringUtilities.getLastWord("/This/is/my/last/word/", "/") returns word</li> * <li>StringUtilities.getLastWord("/This/is/my/last/word/", "/") returns word</li>
* <li>StringUtilities.getLastWord("/This/is/my/last/word/", "/") returns word</li> * <li>StringUtilities.getLastWord("/This/is/my/last/word/", "/") returns word</li>
* <li>StringUtilities.getLastWord("This.is.my.last.word", ".") returns word</li> * <li>StringUtilities.getLastWord("This.is.my.last.word", ".") returns word</li>
* <li>StringUtilities.getLastWord("/This/is/my/last/word/MyFile.java", ".") returns java</li> * <li>StringUtilities.getLastWord("/This/is/my/last/word/MyFile.java", ".") returns java</li>
* <li>StringUtilities.getLastWord("/This/is/my/last/word/MyFile.java", "/") returns MyFile.java</li> * <li>StringUtilities.getLastWord("/This/is/my/last/word/MyFile.java", "/") returns
* MyFile.java</li>
* </ul> * </ul>
* *
* @param s the string from which to get the last word * @param s the string from which to get the last word
@ -778,9 +774,9 @@ public class StringUtilities {
} }
/** /**
* Converts an integer into a string. * Converts an integer into a string. For example, given an integer 0x41424344, the returned
* For example, given an integer 0x41424344, * string would be "ABCD".
* the returned string would be "ABCD". *
* @param value the integer value * @param value the integer value
* @return the converted string * @return the converted string
*/ */
@ -796,10 +792,12 @@ public class StringUtilities {
} }
/** /**
* Creates a JSON string for the given object using all of its fields. To control the * Creates a JSON string for the given object using all of its fields. To control the fields
* fields that are in the result string, see {@link Json}. * that are in the result string, see {@link Json}.
*
* <P>
* This is here as a marker to point users to the real {@link Json} String utility.
* *
* <P>This is here as a marker to point users to the real {@link Json} String utility.
* @param o the object for which to create a string * @param o the object for which to create a string
* @return the string * @return the string
*/ */
@ -818,12 +816,11 @@ public class StringUtilities {
} }
/** /**
* Merge two strings into one. * Merge two strings into one. If one string contains the other, then the largest is returned.
* If one string contains the other, then the largest is returned. * If both strings are null then null is returned. If both strings are empty, the empty string
* If both strings are null then null is returned. * is returned. If the original two strings differ, this adds the second string to the first
* If both strings are empty, the empty string is returned. * separated by a newline.
* If the original two strings differ, this adds the second string *
* to the first separated by a newline.
* @param string1 the first string * @param string1 the first string
* @param string2 the second string * @param string2 the second string
* @return the merged string * @return the merged string
@ -859,12 +856,13 @@ public class StringUtilities {
} }
/** /**
* Limits the given string to the given <code>max</code> number of characters. If the string is * Limits the given string to the given <code>max</code> number of characters. If the string is
* larger than the given length, then it will be trimmed to fit that length <b>after adding * larger than the given length, then it will be trimmed to fit that length <b>after adding
* ellipses</b> * ellipses</b>
* *
* <p>The given <code>max</code> value must be at least 4. This is to ensure that, at a * <p>
* minimum, we can display the {@value #ELLIPSES} plus one character. * The given <code>max</code> value must be at least 4. This is to ensure that, at a minimum, we
* can display the {@value #ELLIPSES} plus one character.
* *
* @param original The string to be limited * @param original The string to be limited
* @param max The maximum number of characters to display (including ellipses, if trimmed). * @param max The maximum number of characters to display (including ellipses, if trimmed).
@ -892,15 +890,16 @@ public class StringUtilities {
} }
/** /**
* Trims the given string the <code>max</code> number of characters. Ellipses will be * Trims the given string the <code>max</code> number of characters. Ellipses will be added to
* added to signal that content was removed. Thus, the actual number of removed characters * signal that content was removed. Thus, the actual number of removed characters will be
* will be <code>(s.length() - max) + {@value StringUtilities#ELLIPSES}</code> length. * <code>(s.length() - max) + {@value StringUtilities#ELLIPSES}</code> length.
* *
* <p>If the string fits within the max, then the string will be returned. * <p>
* If the string fits within the max, then the string will be returned.
* *
* <p>The given <code>max</code> value must be at least 5. This is to ensure that, at a * <p>
* minimum, we can display the {@value #ELLIPSES} plus one character from the front and * The given <code>max</code> value must be at least 5. This is to ensure that, at a minimum, we
* back of the string. * can display the {@value #ELLIPSES} plus one character from the front and back of the string.
* *
* @param s the string to trim * @param s the string to trim
* @param max the max number of characters to allow. * @param max the max number of characters to allow.
@ -936,15 +935,13 @@ public class StringUtilities {
} }
/** /**
* This method looks for all occurrences of successive asterisks (i.e., * This method looks for all occurrences of successive asterisks (i.e., "**") and replace with a
* "**") and replace with a single asterisk, which is an equivalent usage in * single asterisk, which is an equivalent usage in Ghidra. This is necessary due to some symbol
* Ghidra. This is necessary due to some symbol names which cause the * names which cause the pattern matching process to become unusable. An example string that
* pattern matching process to become unusable. An example string that
* causes this problem is * causes this problem is
* "s_CLSID\{ADB880A6-D8FF-11CF-9377-00AA003B7A11}\InprocServer3_01001400". * "s_CLSID\{ADB880A6-D8FF-11CF-9377-00AA003B7A11}\InprocServer3_01001400".
* *
* @param value * @param value The string to be checked.
* The string to be checked.
* @return The updated string. * @return The updated string.
*/ */
public static String fixMultipleAsterisks(String value) { public static String fixMultipleAsterisks(String value) {
@ -959,8 +956,8 @@ public class StringUtilities {
} }
/** /**
* Returns true if the character is OK to be contained inside C language string. That * Returns true if the character is OK to be contained inside C language string. That is, the
* is, the string should not be tokenized on this char. * string should not be tokenized on this char.
* *
* @param c the char * @param c the char
* @return boolean true if it is allows in a C string * @return boolean true if it is allows in a C string
@ -990,13 +987,13 @@ public class StringUtilities {
} }
/** /**
* Replaces escaped characters in a string to corresponding control characters. For example * Replaces escaped characters in a string to corresponding control characters. For example a
* a string containing a backslash character followed by a 'n' character would be replaced * string containing a backslash character followed by a 'n' character would be replaced with a
* with a single line feed (0x0a) character. One use for this is to to allow users to * single line feed (0x0a) character. One use for this is to to allow users to type strings in a
* type strings in a text field and include control characters such as line feeds and tabs. * text field and include control characters such as line feeds and tabs.
* *
* The string that contains 'a','b','c', '\', 'n', 'd', '\', 'u', '0', '0', '0', '1', 'e' would become * The string that contains 'a','b','c', '\', 'n', 'd', '\', 'u', '0', '0', '0', '1', 'e' would
* 'a','b','c',0x0a,'d', 0x01, e" * become 'a','b','c',0x0a,'d', 0x01, e"
* *
* @param str The string to convert escape sequences to control characters. * @param str The string to convert escape sequences to control characters.
* @return a new string with escape sequences converted to control characters. * @return a new string with escape sequences converted to control characters.
@ -1033,8 +1030,8 @@ public class StringUtilities {
} }
/** /**
* Attempt to handle character escape sequence. Note that only a single Java character * Attempt to handle character escape sequence. Note that only a single Java character will be
* will be produced which limits the range of valid character value. * produced which limits the range of valid character value.
* *
* @param string string containing escape sequences * @param string string containing escape sequences
* @param escapeSequence escape sequence (e.g., "\\u") * @param escapeSequence escape sequence (e.g., "\\u")
@ -1042,8 +1039,7 @@ public class StringUtilities {
* @param index current position within string * @param index current position within string
* @param builder the builder into which the results will be added * @param builder the builder into which the results will be added
* *
* @return true if escape sequence processed and added a single character * @return true if escape sequence processed and added a single character to the builder.
* to the builder.
*/ */
private static boolean handleEscapeSequence(String string, String escapeSequence, int hexLength, private static boolean handleEscapeSequence(String string, String escapeSequence, int hexLength,
int index, StringBuilder builder) { int index, StringBuilder builder) {
@ -1068,13 +1064,13 @@ public class StringUtilities {
} }
/** /**
* Replaces known control characters in a string to corresponding escape sequences. For example * Replaces known control characters in a string to corresponding escape sequences. For example
* a string containing a line feed character would be converted to backslash character * a string containing a line feed character would be converted to backslash character followed
* followed by an 'n' character. One use for this is to display strings in a manner to * by an 'n' character. One use for this is to display strings in a manner to easily see the
* easily see the embedded control characters. * embedded control characters.
* *
* The string that contains 'a','b','c',0x0a,'d', 0x01, 'e' would become * The string that contains 'a','b','c',0x0a,'d', 0x01, 'e' would become 'a','b','c', '\', 'n',
* 'a','b','c', '\', 'n', 'd', 0x01, 'e' * 'd', 0x01, 'e'
* *
* @param str The string to convert control characters to escape sequences * @param str The string to convert control characters to escape sequences
* @return a new string with all the control characters converted to escape sequences. * @return a new string with all the control characters converted to escape sequences.
@ -1097,14 +1093,13 @@ public class StringUtilities {
} }
/** /**
* Maps known control characters to corresponding escape sequences. For example * Maps known control characters to corresponding escape sequences. For example a line feed
* a line feed character would be converted to backslash '\\' character * character would be converted to backslash '\\' character followed by an 'n' character. One
* followed by an 'n' character. One use for this is to display strings in a manner to * use for this is to display strings in a manner to easily see the embedded control characters.
* easily see the embedded control characters.
* *
* @param codePoint The character to convert to escape sequence string * @param codePoint The character to convert to escape sequence string
* @return a new string with equivalent to escape sequence, or original character (as * @return a new string with equivalent to escape sequence, or original character (as a string)
* a string) if not in the control character mapping. * if not in the control character mapping.
*/ */
public static String convertCodePointToEscapeSequence(int codePoint) { public static String convertCodePointToEscapeSequence(int codePoint) {
int charCount = Character.charCount(codePoint); int charCount = Character.charCount(codePoint);
@ -1114,4 +1109,102 @@ public class StringUtilities {
} }
return new String(new int[] { codePoint }, 0, 1); return new String(new int[] { codePoint }, 0, 1);
} }
/**
* About the worst way to wrap lines ever
*/
public static class LineWrapper {
enum Mode {
INIT, WORD, SPACE;
}
private final int width;
private StringBuffer result = new StringBuffer();
private int len = 0;
public LineWrapper(int width) {
this.width = width;
}
public LineWrapper append(CharSequence cs) {
Mode mode = Mode.INIT;
int b = 0;
for (int f = 0; f < cs.length(); f++) {
char c = cs.charAt(f);
if (c == '\n') {
if (mode == Mode.SPACE) {
appendSpace(cs.subSequence(b, f));
}
else if (mode == Mode.WORD) {
appendWord(cs.subSequence(b, f));
}
mode = Mode.INIT;
appendLinesep();
b = f + 1;
}
else if (Character.isWhitespace(c)) {
if (mode == Mode.WORD) {
appendWord(cs.subSequence(b, f));
b = f;
}
mode = Mode.SPACE;
}
else {
if (mode == Mode.SPACE) {
appendSpace(cs.subSequence(b, f));
b = f;
}
mode = Mode.WORD;
}
}
if (mode == Mode.WORD) {
appendWord(cs.subSequence(b, cs.length()));
}
else if (mode == Mode.SPACE) {
appendSpace(cs.subSequence(b, cs.length()));
}
return this;
}
private void appendWord(CharSequence word) {
len += word.length();
result.append(word);
}
private void appendSpace(CharSequence space) {
if (len > width) {
appendLinesep();
len += space.length() - 1;
result.append(space.subSequence(1, space.length()));
}
else {
len += space.length();
result.append(space);
}
}
private void appendLinesep() {
result.append("\n");
len = 0;
}
public String finish() {
return result.toString();
}
}
/**
* Wrap the given string at whitespace to best fit within the given line width
*
* <p>
* If it is not possible to fit a word in the given width, it will be put on a line by itself,
* and that line will be allowed to exceed the given width.
*
* @param str the string to wrap
* @param width the max width of each line, unless a single word exceeds it
* @return
*/
public static String wrapToWidth(String str, int width) {
return new LineWrapper(width).append(str).finish();
}
} }

30
licenses/JSch_License.txt Normal file
View file

@ -0,0 +1,30 @@
JSch 0.0.* was released under the GNU LGPL license. Later, we have switched
over to a BSD-style license.
------------------------------------------------------------------------------
Copyright (c) 2002-2015 Atsuhiko Yamanaka, JCraft,Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the distribution.
3. The names of the authors may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,
INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -11,6 +11,7 @@ FAMFAMFAM_Mini_Icons_-_Public_Domain.txt||LICENSE||||END|
GPL_2_With_Classpath_Exception.txt||LICENSE||||END| GPL_2_With_Classpath_Exception.txt||LICENSE||||END|
INRIA_License.txt||LICENSE||||END| INRIA_License.txt||LICENSE||||END|
JDOM_License.txt||LICENSE||||END| JDOM_License.txt||LICENSE||||END|
JSch_License.txt||LICENSE||||END|
Jython_License.txt||LICENSE||||END| Jython_License.txt||LICENSE||||END|
LGPL_2.1.txt||LICENSE||||END| LGPL_2.1.txt||LICENSE||||END|
LGPL_3.0.html||LICENSE||||END| LGPL_3.0.html||LICENSE||||END|