diff --git a/Ghidra/Debug/Debugger-agent-gdb/Module.manifest b/Ghidra/Debug/Debugger-agent-gdb/Module.manifest index e69de29bb2..e4bddd6d84 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/Module.manifest +++ b/Ghidra/Debug/Debugger-agent-gdb/Module.manifest @@ -0,0 +1 @@ +MODULE FILE LICENSE: lib/jsch-0.1.55.jar JSch License diff --git a/Ghidra/Debug/Debugger-agent-gdb/build.gradle b/Ghidra/Debug/Debugger-agent-gdb/build.gradle index 46f7d710d9..4f6104601e 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/build.gradle +++ b/Ghidra/Debug/Debugger-agent-gdb/build.gradle @@ -27,6 +27,7 @@ dependencies { api project(':Framework-Debugging') api project(':Debugger-gadp') api project(':Python') + api 'com.jcraft:jsch:0.1.55' testImplementation project(path: ':Framework-AsyncComm', configuration: 'testArtifacts') testImplementation project(path: ':Framework-Debugging', configuration: 'testArtifacts') diff --git a/Ghidra/Debug/Debugger-agent-gdb/certification.manifest b/Ghidra/Debug/Debugger-agent-gdb/certification.manifest index 1d2d84eed7..a3c4a5d5fa 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/certification.manifest +++ b/Ghidra/Debug/Debugger-agent-gdb/certification.manifest @@ -1,4 +1,4 @@ ##VERSION: 2.0 -##MODULE IP: Jython License +##MODULE IP: JSch License Module.manifest||GHIDRA||||END| data/scripts/define_info_proc_mappings||GHIDRA||||END| diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbOverSshDebuggerModelFactory.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbOverSshDebuggerModelFactory.java index ac5c73e666..7c1140290c 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbOverSshDebuggerModelFactory.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/GdbOverSshDebuggerModelFactory.java @@ -30,7 +30,7 @@ import ghidra.dbg.util.ConfigurableFactory.FactoryDescription; htmlDetails = "Launch a GDB session over an SSH connection") public class GdbOverSshDebuggerModelFactory implements DebuggerModelFactory { - private String gdbCmd = "gdb"; + private String gdbCmd = "/usr/bin/gdb"; @FactoryOption("GDB launch command") public final Property gdbCommandOption = Property.fromAccessors(String.class, this::getGdbCommand, this::setGdbCommand); @@ -40,29 +40,29 @@ public class GdbOverSshDebuggerModelFactory implements DebuggerModelFactory { public final Property useExistingOption = Property.fromAccessors(boolean.class, this::isUseExisting, this::setUseExisting); - private String hostname = "localhost"; + private String hostname = GhidraSshPtyFactory.DEFAULT_HOSTNAME; @FactoryOption("SSH hostname") public final Property hostnameOption = Property.fromAccessors(String.class, this::getHostname, this::setHostname); - private int port = 22; + private int port = GhidraSshPtyFactory.DEFAULT_PORT; @FactoryOption("SSH TCP port") public final Property portOption = Property.fromAccessors(Integer.class, this::getPort, this::setPort); - private String username = "user"; + private String username = GhidraSshPtyFactory.DEFAULT_USERNAME; @FactoryOption("SSH username") public final Property usernameOption = Property.fromAccessors(String.class, this::getUsername, this::setUsername); - private String keyFile = ""; - @FactoryOption("SSH identity (blank for password auth)") + private String configFile = GhidraSshPtyFactory.DEFAULT_CONFIG_FILE; + @FactoryOption("Open SSH config file") public final Property 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. private boolean useCrlf = false; - @FactoryOption("Use DOS line endings (unchecked for UNIX)") + @FactoryOption("Use DOS line endings (unchecked for UNIX remote)") public final Property crlfNewLineOption = Property.fromAccessors(Boolean.class, this::isUseCrlf, this::setUseCrlf); @@ -73,7 +73,7 @@ public class GdbOverSshDebuggerModelFactory implements DebuggerModelFactory { GhidraSshPtyFactory factory = new GhidraSshPtyFactory(); factory.setHostname(hostname); factory.setPort(port); - factory.setKeyFile(keyFile); + factory.setConfigFile(configFile); factory.setUsername(username); return new GdbModelImpl(factory); }).thenCompose(model -> { @@ -136,12 +136,12 @@ public class GdbOverSshDebuggerModelFactory implements DebuggerModelFactory { this.username = username; } - public String getKeyFile() { - return keyFile; + public String getConfigFile() { + return configFile; } - public void setKeyFile(String keyFile) { - this.keyFile = keyFile; + public void setConfigFile(String configFile) { + this.configFile = configFile; } public boolean isUseCrlf() { diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/GhidraSshHostKeyVerifier.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/GhidraSshHostKeyVerifier.java deleted file mode 100644 index c49b67c1a6..0000000000 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/GhidraSshHostKeyVerifier.java +++ /dev/null @@ -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", - "The server " + hostname + " is not known. " + - "It is highly recommended you log in to the server using a standard " + - "SSH client to confirm the host key first.

" + - "Do you want to continue?"); - return response == OptionDialog.YES_OPTION; - case KnownHosts.HOSTKEY_HAS_CHANGED: - Msg.showError(this, null, "SSH Server Host Key Changed", - "The server " + hostname + " has a different key than before!" + - "Use a standard SSH client to resolve the issue."); - return false; - default: - throw new IllegalStateException(); - } - } -} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/GhidraSshPtyFactory.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/GhidraSshPtyFactory.java index 727ebece88..68fdb1954e 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/GhidraSshPtyFactory.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/GhidraSshPtyFactory.java @@ -15,25 +15,147 @@ */ package agent.gdb.pty.ssh; -import java.io.File; import java.io.IOException; 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 ch.ethz.ssh2.Connection; -import ch.ethz.ssh2.KnownHosts; import docking.DockingWindowManager; import docking.widgets.PasswordDialog; -import ghidra.util.Msg; -import ghidra.util.exception.CancelledException; +import ghidra.util.*; public class GhidraSshPtyFactory implements PtyFactory { - private String hostname = "localhost"; - private int port = 22; - private String username = "user"; - private String keyFile = "~/.ssh/id_rsa"; + private static final String TITLE = "GDB via SSH"; + private static final int WRAP_LEN = 80; - 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 "
" + StringEscapeUtils.escapeHtml4(wrapped).replace("\n", "
"); + } + + @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() { return hostname; @@ -59,81 +181,50 @@ public class GhidraSshPtyFactory implements PtyFactory { this.username = Objects.requireNonNull(username); } - public String getKeyFile() { - return keyFile; + public String getConfigFile() { + return configFile; } - /** - * Set the keyfile path, or empty for password authentication only - * - * @param keyFile the path - */ - public void setKeyFile(String keyFile) { - this.keyFile = Objects.requireNonNull(keyFile); + public void setConfigFile(String configFile) { + this.configFile = configFile; } - public static char[] promptPassword(String hostname, String prompt) throws CancelledException { - PasswordDialog dialog = - new PasswordDialog("GDB via SSH", "SSH", hostname, prompt, 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); + protected Session connectAndAuthenticate() throws IOException { + JSch jsch = new JSch(); + ConfigRepository configRepo = null; try { - sshConn.connect(new GhidraSshHostKeyVerifier(knownHosts)); - 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; + configRepo = OpenSSHConfig.parseFile(configFile); } - catch (CancelledException e) { - Msg.error(this, "SSH connection/authentication cancelled by user"); - throw new IOException("SSH connection/authentication cancelled by user", e); + catch (IOException e) { + Msg.warn(this, "ssh config file " + configFile + " could not be parsed."); + // I guess the config file doesn't exist. Just go on } - finally { - if (!success) { - sshConn.close(); - } + jsch.setConfigRepository(new RequireTTYAlwaysConfigRepo(configRepo)); + + 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 public SshPty openpty() throws IOException { - if (sshConn == null || !sshConn.isAuthenticationComplete()) { - sshConn = connectAndAuthenticate(); + if (session == null) { + 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 diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPty.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPty.java index acf05aa5dc..53c5f0e5a4 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPty.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPty.java @@ -15,32 +15,36 @@ */ package agent.gdb.pty.ssh; -import java.io.IOException; +import java.io.*; + +import com.jcraft.jsch.*; import agent.gdb.pty.*; -import ch.ethz.ssh2.Session; 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 { - this.session = session; - session.requestDumbPTY(); + public SshPty(ChannelExec channel) throws JSchException, IOException { + this.channel = channel; + + out = channel.getOutputStream(); + in = channel.getInputStream(); } @Override public PtyParent getParent() { - // TODO: Need I worry about stderr? I thought both pointed to the same tty.... - return new SshPtyParent(session.getStdin(), session.getStdout()); + return new SshPtyParent(out, in); } @Override public PtyChild getChild() { - return new SshPtyChild(session); + return new SshPtyChild(channel, out, in); } @Override public void close() throws IOException { - session.close(); + channel.disconnect(); } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyChild.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyChild.java index 6fe81db06f..d4c9f06d87 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyChild.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyChild.java @@ -16,23 +16,26 @@ package agent.gdb.pty.ssh; import java.io.*; +import java.util.Arrays; import java.util.Map; import java.util.stream.Collectors; -import java.util.stream.Stream; import javax.help.UnsupportedOperationException; +import com.jcraft.jsch.*; + import agent.gdb.pty.PtyChild; -import ch.ethz.ssh2.Session; +import ghidra.dbg.util.ShellUtils; import ghidra.util.Msg; public class SshPtyChild extends SshPtyEndpoint implements PtyChild { - private String name; - private final Session session; + private final ChannelExec channel; - public SshPtyChild(Session session) { - super(null, null); - this.session = session; + private String name; + + public SshPtyChild(ChannelExec channel, OutputStream outputStream, InputStream inputStream) { + super(outputStream, inputStream); + this.channel = channel; } @Override @@ -48,34 +51,37 @@ public class SshPtyChild extends SshPtyEndpoint implements PtyChild { .map(e -> e.getKey() + "=" + e.getValue()) .collect(Collectors.joining(" ")) + " "; - String cmdStr = Stream.of(args).collect(Collectors.joining(" ")); + String cmdStr = ShellUtils.generateLine(Arrays.asList(args)); + channel.setCommand(envStr + cmdStr); try { - session.execCommand(envStr + cmdStr); + channel.connect(); } - catch (Throwable t) { - Msg.error(this, "Could not execute remote command: " + envStr + cmdStr, t); - throw t; + catch (JSchException e) { + throw new IOException("SSH error", e); } - return new SshPtySession(session); + return new SshPtySession(channel); } 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 - session.execCommand( - "sh -c 'tty && cltrc() { echo; } && trap ctrlc INT && while true; do sleep " + - Integer.MAX_VALUE + "; done'", - "UTF-8"); + channel.setCommand( + ("sh -c 'tty && ctrlc() { echo; } && trap ctrlc INT && while true; do sleep " + + Integer.MAX_VALUE + "; done'")); + try { + channel.connect(); + } + catch (JSchException e) { + throw new IOException("SSH error", e); + } byte[] buf = new byte[1024]; // Should be plenty for (int i = 0; i < 1024; i++) { - int chr = stdout.read(); + int chr = inputStream.read(); 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; } - throw new IOException("Remote tty name exceeds 1024 bytes?"); + throw new IOException("Expected pty name. Got " + new String(buf, 0, 1024, "UTF-8")); } @Override diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyEndpoint.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyEndpoint.java index b308bf2af8..6614c4fd2d 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyEndpoint.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtyEndpoint.java @@ -21,13 +21,12 @@ import java.io.OutputStream; import agent.gdb.pty.PtyEndpoint; public class SshPtyEndpoint implements PtyEndpoint { - private final OutputStream outputStream; - private final InputStream inputStream; + protected final OutputStream outputStream; + protected final InputStream inputStream; public SshPtyEndpoint(OutputStream outputStream, InputStream inputStream) { this.outputStream = outputStream; this.inputStream = inputStream; - } @Override diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtySession.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtySession.java index 050cc29903..e1d04c693e 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtySession.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/pty/ssh/SshPtySession.java @@ -15,43 +15,30 @@ */ package agent.gdb.pty.ssh; -import java.io.IOException; -import java.io.InterruptedIOException; +import com.jcraft.jsch.Channel; import agent.gdb.pty.PtySession; -import ch.ethz.ssh2.ChannelCondition; -import ch.ethz.ssh2.Session; public class SshPtySession implements PtySession { - private final Session session; + private final Channel channel; - public SshPtySession(Session session) { - this.session = session; + public SshPtySession(Channel channel) { + this.channel = channel; } @Override public Integer waitExited() throws InterruptedException { - try { - session.waitForCondition(ChannelCondition.EOF, 0); - // NB. May not be available - return session.getExitStatus(); - } - catch (InterruptedIOException e) { - throw new InterruptedException(); - } - catch (IOException e) { - throw new RuntimeException(e); + // Doesn't look like there's a clever way to wait. So do the spin sleep :( + while (!channel.isEOF()) { + Thread.sleep(1000); } + // NB. May not be available + return channel.getExitStatus(); } @Override public void destroyForcibly() { - /** - * 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(); + channel.disconnect(); } } diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshJoinGdbModelHost.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshJoinGdbModelHost.java new file mode 100644 index 0000000000..4b6080ab97 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshJoinGdbModelHost.java @@ -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 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); + } + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshJoinModelForGdbFactoryTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshJoinModelForGdbFactoryTest.java new file mode 100644 index 0000000000..76eb0bb2a8 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshJoinModelForGdbFactoryTest.java @@ -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(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshJoinModelForGdbSessionLauncherTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshJoinModelForGdbSessionLauncherTest.java new file mode 100644 index 0000000000..4c09c72b2d --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/model/ssh/SshJoinModelForGdbSessionLauncherTest.java @@ -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(); + } +} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/ssh/SshExperimentsTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/ssh/SshExperimentsTest.java deleted file mode 100644 index bdfba163a9..0000000000 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/ssh/SshExperimentsTest.java +++ /dev/null @@ -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 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 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"); - } -} diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/ssh/SshPtyTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/ssh/SshPtyTest.java index 192cc13221..39d61096bb 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/ssh/SshPtyTest.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/pty/ssh/SshPtyTest.java @@ -18,7 +18,7 @@ package agent.gdb.pty.ssh; import static org.junit.Assert.assertEquals; import static org.junit.Assume.assumeFalse; -import java.io.IOException; +import java.io.*; import org.junit.Before; import org.junit.Test; @@ -36,9 +36,7 @@ public class SshPtyTest extends AbstractGhidraHeadedIntegrationTest { public void setupSshPtyTest() throws CancelledException { assumeFalse(SystemUtilities.isInTestingBatchMode()); factory = new GhidraSshPtyFactory(); - factory.setHostname("localhost"); factory.setUsername(promptUser()); - factory.setKeyFile(""); } public static String promptUser() throws CancelledException { @@ -49,11 +47,41 @@ public class SshPtyTest extends AbstractGhidraHeadedIntegrationTest { 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 public void testSessionBash() throws IOException, InterruptedException { try (SshPty pty = factory.openpty()) { 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()); } } diff --git a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/ConfigurableFactory.java b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/ConfigurableFactory.java index 671d566c94..aa98844877 100644 --- a/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/ConfigurableFactory.java +++ b/Ghidra/Debug/Framework-Debugging/src/main/java/ghidra/dbg/util/ConfigurableFactory.java @@ -158,7 +158,10 @@ public interface ConfigurableFactory { if (codec == null) { continue; } - property.setValue(codec.read(saveState, opt.getKey(), null)); + Object read = codec.read(saveState, opt.getKey(), null); + if (read != null) { + property.setValue(read); + } } } } diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/StringUtilities.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/StringUtilities.java index c122aa20fa..53d4601290 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/StringUtilities.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/StringUtilities.java @@ -72,14 +72,13 @@ public class StringUtilities { public static final int UNICODE_REPLACEMENT = 0xFFFD; /** - * Unicode Byte Order Marks (BOM) characters are special characters in the Unicode - * character space that signal endian-ness of the text. + * Unicode Byte Order Marks (BOM) characters are special characters in the Unicode character + * space that signal endian-ness of the text. *

- * The value for the BigEndian version (0xFEFF) works for both 16 and 32 bit - * character values. + * The value for the BigEndian version (0xFEFF) works for both 16 and 32 bit character values. *

- * There are separate values for Little Endian Byte Order Marks for 16 and 32 bit - * characters because the 32 bit value is shifted left by 16 bits. + * There are separate values for Little Endian Byte Order Marks for 16 and 32 bit characters + * 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_LE16_BYTE_ORDER_MARK = 0x0____FFFE; @@ -93,9 +92,9 @@ public class StringUtilities { } /** - * Returns true if the given character is a special character. - * For example a '\n' or '\\'. A value of 0 is not considered special for this purpose - * as it is handled separately because it has more varied use cases. + * Returns true if the given character is a special character. For example a '\n' or '\\'. A + * value of 0 is not considered special for this purpose as it is handled separately because it + * has more varied use cases. * * @param c the 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. - * For example a '\n' or '\\'. A value of 0 is not considered special for this purpose - * as it is handled separately because it has more varied use cases. + * Returns true if the given codePoint (ie. full unicode 32bit character) is a special + * character. For example a '\n' or '\\'. A value of 0 is not considered special for this + * purpose as it is handled separately because it has more varied use cases. * * @param codePoint the codePoint (ie. character), see {@link String#codePointAt(int)} * @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)) + * * @param str String to test for double-quote enclosure - * @return True if the first and last characters are the double-quote character, - * false otherwise + * @return True if the first and last characters are the double-quote character, false otherwise */ public static boolean isDoubleQuoted(String 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. - * Otherwise, return the given string unmodified. + * If the given string is enclosed in double quotes, extract the inner text. Otherwise, return + * the given string unmodified. + * * @param str String to match and extract from * @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) { 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 + * * @param c the character * @return true if the character is in displayable character range */ @@ -176,9 +177,9 @@ public class StringUtilities { } /** - * Converts the character into a string. - * If the character is special, it will actually render the character. - * For example, given '\n' the output would be "\\n". + * Converts the character into a string. If the character is special, it will actually render + * the character. For example, given '\n' the output would be "\\n". + * * @param c the character to convert into a string * @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. + * * @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 */ 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. *

* 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 - * required the alternate form of this method should be used. + * conventions (e.g., \t, \n, \\uHHHH, etc.). If a character size other than 1-byte is required + * the alternate form of this method should be used. *

- * The result string will be single quoted (ie. "'") if the input byte array is - * 1 byte long, otherwise the result will be double-quoted ('"'). + * The result string will be single quoted (ie. "'") if the input byte array is 1 byte long, + * otherwise the result will be double-quoted ('"'). * * @param bytes character string bytes * @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 * conventions (e.g., \t, \n, \\uHHHH, etc.). *

- * The result string will be single quoted (ie. "'") if the input byte array is - * 1 character long (ie. charSize), otherwise the result will be double-quoted ('"'). + * The result string will be single quoted (ie. "'") if the input byte array is 1 character long + * (ie. charSize), otherwise the result will be double-quoted ('"'). * * @param bytes array of bytes * @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 prefix ignoring case. *

* Note: This method is equivalent to calling: + * *

-	 * 	string.regionMatches( true, 0, prefix, 0, prefix.length() );
+	 * string.regionMatches(true, 0, prefix, 0, prefix.length());
 	 * 
* * @param string the string which may contain the prefix @@ -336,9 +339,10 @@ public class StringUtilities { * Returns true if the given string ends with postfix, ignoring case. *

* Note: This method is equivalent to calling: + * *

-	 *  int startIndex = string.length() - postfix.length();
-	 * 	string.regionMatches( true, startOffset, postfix, 0, postfix.length() );
+	 * int startIndex = string.length() - postfix.length();
+	 * string.regionMatches(true, startOffset, postfix, 0, postfix.length());
 	 * 
* * @param string the string which may end with postfix @@ -416,13 +420,14 @@ public class StringUtilities { } /** - * Returns the index of the first whole word occurrence of the search word within - * the given text. A whole word is defined as the character before and after the occurrence - * must not be a JavaIdentifierPart. + * Returns the index of the first whole word occurrence of the search word within the given + * text. A whole word is defined as the character before and after the occurrence must not be a + * JavaIdentifierPart. + * * @param text the text to be searched. * @param searchWord the word to search for. - * @return the index of the first whole word occurrence of the search word within - * the given text, or -1 if not found. + * @return the index of the first whole word occurrence of the search word within the given + * text, or -1 if not found. */ public static int indexOfWord(String text, String searchWord) { int index = 0; @@ -440,14 +445,15 @@ public class StringUtilities { } /** - * Returns true if the substring within the text string starting at startIndex and having - * the given length is a whole word. A whole word is defined as the character before and after - * the occurrence must not be a JavaIdentifierPart. + * Returns true if the substring within the text string starting at startIndex and having the + * given length is a whole word. A whole word is defined as the character before and after the + * occurrence must not be a JavaIdentifierPart. + * * @param text the text containing the potential word. * @param startIndex the start index of the potential word within the text. * @param length the length of the potential word - * @return true if the substring within the text string starting at startIndex and having - * the given length is a whole word. + * @return true if the substring within the text string starting at startIndex and having the + * given length is a whole word. */ public static boolean isWholeWord(String text, int startIndex, int length) { if (startIndex > 0) { @@ -466,11 +472,9 @@ public class StringUtilities { } /** - * Convert tabs in the given string to spaces using - * a default tab width of 8 spaces. + * Convert tabs in the given string to spaces using a default tab width of 8 spaces. * - * @param str - * string containing tabs + * @param str string containing tabs * @return string that has spaces for tabs */ public static String convertTabsToSpaces(String str) { @@ -480,10 +484,8 @@ public class StringUtilities { /** * Convert tabs in the given string to spaces. * - * @param str - * string containing tabs - * @param tabSize - * length of the tab + * @param str string containing tabs + * @param tabSize length of the tab * @return string that has spaces for tabs */ 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 - * element in the array contains only a single line. The "\n" character is - * used as the delimiter for lines. + * Parses a string containing multiple lines into an array where each element in the array + * contains only a single line. The "\n" character is used as the delimiter for lines. *

* This methods creates an empty string entry in the result array for initial and trailing * 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 - * element in the array contains only a single line. The "\n" character is - * used as the delimiter for lines. + * Parses a string containing multiple lines into an array where each element in the array + * contains only a single line. The "\n" character is used as the delimiter for lines. * * @param s the string to parse * @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 */ 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 - * necessary. + * Enforces the given length upon the given string by trimming and then padding as necessary. * * @param s the String to fix * @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 - * as the pad. If length is negative, left justifies the string, appending - * the filler; if length is positive, right justifies the source string. + * Pads the source string to the specified length, using the filler string as the pad. If length + * is negative, left justifies the string, appending the filler; if length is positive, right + * justifies the source string. * * @param source the original string 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 \n and then pads each string - * with the given pad string. Finally, the updated lines are formed into a single string. + * Splits the given string into lines using \n and then pads each string with the + * given pad string. Finally, the updated lines are formed into a single string. *

* This is useful for constructing complicated toString() representations. * @@ -636,13 +635,11 @@ public class StringUtilities { } /** - * Finds the word at the given index in the given string. For example, the - * string "The tree is green" and the index of 5, the result would be - * "tree". + * Finds the word at the given index in the given string. For example, the string "The tree is + * green" and the index of 5, the result would be "tree". * * @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. * @return String the word contained at the given 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 - * contains the given charToAllow, then allow it in the string. For example, - * the string "The tree* is green" and the index of 5, charToAllow is '*', - * then the result would be "tree*". + * Finds the word at the given index in the given string; if the word contains the given + * charToAllow, then allow it in the string. For example, the string "The tree* is green" and + * the index of 5, charToAllow is '*', then the result would be "tree*". *

* If the search yields only whitespace, then the empty string will be returned. * * @param s the string to search * @param index the index into the string to "seed" the word. - * @param charsToAllow chars that normally would be considered invalid, e.g., '*' so - * that the word can be returned with the charToAllow + * @param charsToAllow chars that normally would be considered invalid, e.g., '*' so that the + * word can be returned with the charToAllow * @return String the word contained at the given index. */ 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 - * for consumption by a human. Also, provided allows chars will pass the test. + * Loosely defined as a character that we would expected to be an normal ascii content meant for + * consumption by a human. Also, provided allows chars will pass the test. * * @param c the char to check * @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. * - * @param s - * the string to search + * @param s the string to search * @return int the starting position of the last word, -1 if not found */ 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: *

    - *
  • StringUtilities.getLastWord("/This/is/my/last/word/", "/") returns word
  • - *
  • StringUtilities.getLastWord("/This/is/my/last/word/", "/") returns word
  • - *
  • StringUtilities.getLastWord("This.is.my.last.word", ".") returns word
  • - *
  • StringUtilities.getLastWord("/This/is/my/last/word/MyFile.java", ".") returns java
  • - *
  • StringUtilities.getLastWord("/This/is/my/last/word/MyFile.java", "/") returns MyFile.java
  • + *
  • StringUtilities.getLastWord("/This/is/my/last/word/", "/") returns word
  • + *
  • StringUtilities.getLastWord("/This/is/my/last/word/", "/") returns word
  • + *
  • StringUtilities.getLastWord("This.is.my.last.word", ".") returns word
  • + *
  • StringUtilities.getLastWord("/This/is/my/last/word/MyFile.java", ".") returns java
  • + *
  • StringUtilities.getLastWord("/This/is/my/last/word/MyFile.java", "/") returns + * MyFile.java
  • *
* * @param s the string from which to get the last word @@ -778,9 +774,9 @@ public class StringUtilities { } /** - * Converts an integer into a string. - * For example, given an integer 0x41424344, - * the returned string would be "ABCD". + * Converts an integer into a string. For example, given an integer 0x41424344, the returned + * string would be "ABCD". + * * @param value the integer value * @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 - * fields that are in the result string, see {@link Json}. + * Creates a JSON string for the given object using all of its fields. To control the fields + * that are in the result string, see {@link Json}. + * + *

+ * This is here as a marker to point users to the real {@link Json} String utility. * - *

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 * @return the string */ @@ -818,12 +816,11 @@ public class StringUtilities { } /** - * Merge two strings into one. - * 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 is returned. - * If the original two strings differ, this adds the second string - * to the first separated by a newline. + * Merge two strings into one. 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 + * is returned. If the original two strings differ, this adds the second string to the first + * separated by a newline. + * * @param string1 the first string * @param string2 the second string * @return the merged string @@ -859,12 +856,13 @@ public class StringUtilities { } /** - * Limits the given string to the given max number of characters. If the string is + * Limits the given string to the given max number of characters. If the string is * larger than the given length, then it will be trimmed to fit that length after adding * ellipses * - *

The given max value must be at least 4. This is to ensure that, at a - * minimum, we can display the {@value #ELLIPSES} plus one character. + *

+ * The given max 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 max The maximum number of characters to display (including ellipses, if trimmed). @@ -892,15 +890,16 @@ public class StringUtilities { } /** - * Trims the given string the max number of characters. Ellipses will be - * added to signal that content was removed. Thus, the actual number of removed characters - * will be (s.length() - max) + {@value StringUtilities#ELLIPSES} length. + * Trims the given string the max number of characters. Ellipses will be added to + * signal that content was removed. Thus, the actual number of removed characters will be + * (s.length() - max) + {@value StringUtilities#ELLIPSES} length. * - *

If the string fits within the max, then the string will be returned. + *

+ * If the string fits within the max, then the string will be returned. * - *

The given max value must be at least 5. This is to ensure that, at a - * minimum, we can display the {@value #ELLIPSES} plus one character from the front and - * back of the string. + *

+ * The given max value must be at least 5. This is to ensure that, at a minimum, we + * can display the {@value #ELLIPSES} plus one character from the front and back of the string. * * @param s the string to trim * @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., - * "**") and replace with a single asterisk, which is an equivalent usage in - * Ghidra. This is necessary due to some symbol names which cause the - * pattern matching process to become unusable. An example string that + * This method looks for all occurrences of successive asterisks (i.e., "**") and replace with a + * single asterisk, which is an equivalent usage in Ghidra. This is necessary due to some symbol + * names which cause the pattern matching process to become unusable. An example string that * causes this problem is * "s_CLSID\{ADB880A6-D8FF-11CF-9377-00AA003B7A11}\InprocServer3_01001400". * - * @param value - * The string to be checked. + * @param value The string to be checked. * @return The updated string. */ 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 - * is, the string should not be tokenized on this char. + * Returns true if the character is OK to be contained inside C language string. That is, the + * string should not be tokenized on this char. * * @param c the char * @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 - * a string containing a backslash character followed by a 'n' character would be replaced - * with a single line feed (0x0a) character. One use for this is to to allow users to - * type strings in a text field and include control characters such as line feeds and tabs. + * Replaces escaped characters in a string to corresponding control characters. For example a + * string containing a backslash character followed by a 'n' character would be replaced with a + * single line feed (0x0a) character. One use for this is to to allow users to type strings in a + * 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 - * 'a','b','c',0x0a,'d', 0x01, e" + * The string that contains 'a','b','c', '\', 'n', 'd', '\', 'u', '0', '0', '0', '1', 'e' would + * become 'a','b','c',0x0a,'d', 0x01, e" * * @param str The string to convert escape sequences 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 - * will be produced which limits the range of valid character value. + * Attempt to handle character escape sequence. Note that only a single Java character will be + * produced which limits the range of valid character value. * * @param string string containing escape sequences * @param escapeSequence escape sequence (e.g., "\\u") @@ -1042,8 +1039,7 @@ public class StringUtilities { * @param index current position within string * @param builder the builder into which the results will be added * - * @return true if escape sequence processed and added a single character - * to the builder. + * @return true if escape sequence processed and added a single character to the builder. */ private static boolean handleEscapeSequence(String string, String escapeSequence, int hexLength, int index, StringBuilder builder) { @@ -1068,13 +1064,13 @@ public class StringUtilities { } /** - * 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 - * followed by an 'n' character. One use for this is to display strings in a manner to - * easily see the embedded control characters. + * 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 followed + * by an 'n' character. One use for this is to display strings in a manner to easily see the + * embedded control characters. * - * The string that contains 'a','b','c',0x0a,'d', 0x01, 'e' would become - * 'a','b','c', '\', 'n', 'd', 0x01, 'e' + * The string that contains 'a','b','c',0x0a,'d', 0x01, 'e' would become 'a','b','c', '\', 'n', + * 'd', 0x01, 'e' * * @param str The string to convert control characters 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 - * a line feed character would be converted to backslash '\\' character - * followed by an 'n' character. One use for this is to display strings in a manner to - * easily see the embedded control characters. + * Maps known control characters to corresponding escape sequences. For example a line feed + * character would be converted to backslash '\\' character followed by an 'n' character. One + * use for this is to display strings in a manner to easily see the embedded control characters. * * @param codePoint The character to convert to escape sequence string - * @return a new string with equivalent to escape sequence, or original character (as - * a string) if not in the control character mapping. + * @return a new string with equivalent to escape sequence, or original character (as a string) + * if not in the control character mapping. */ public static String convertCodePointToEscapeSequence(int codePoint) { int charCount = Character.charCount(codePoint); @@ -1114,4 +1109,102 @@ public class StringUtilities { } 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 + * + *

+ * 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(); + } } diff --git a/licenses/JSch_License.txt b/licenses/JSch_License.txt new file mode 100644 index 0000000000..73a825dab0 --- /dev/null +++ b/licenses/JSch_License.txt @@ -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. \ No newline at end of file diff --git a/licenses/certification.manifest b/licenses/certification.manifest index 0a4eef320a..c9b0b47d72 100644 --- a/licenses/certification.manifest +++ b/licenses/certification.manifest @@ -11,6 +11,7 @@ FAMFAMFAM_Mini_Icons_-_Public_Domain.txt||LICENSE||||END| GPL_2_With_Classpath_Exception.txt||LICENSE||||END| INRIA_License.txt||LICENSE||||END| JDOM_License.txt||LICENSE||||END| +JSch_License.txt||LICENSE||||END| Jython_License.txt||LICENSE||||END| LGPL_2.1.txt||LICENSE||||END| LGPL_3.0.html||LICENSE||||END|