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(':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')

View File

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

View File

@ -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<String> gdbCommandOption =
Property.fromAccessors(String.class, this::getGdbCommand, this::setGdbCommand);
@ -40,29 +40,29 @@ public class GdbOverSshDebuggerModelFactory implements DebuggerModelFactory {
public final Property<Boolean> 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<String> 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<Integer> 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<String> 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<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.
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 =
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() {

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;
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 "<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() {
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");
configRepo = OpenSSHConfig.parseFile(configFile);
}
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
}
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) {
Msg.error(this, "SSH connection/authentication cancelled by user");
throw new IOException("SSH connection/authentication cancelled by user", e);
}
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

View File

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

View File

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

View File

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

View File

@ -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);
// 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 session.getExitStatus();
}
catch (InterruptedIOException e) {
throw new InterruptedException();
}
catch (IOException e) {
throw new RuntimeException(e);
}
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();
}
}

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.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());
}
}

View File

@ -158,7 +158,10 @@ public interface ConfigurableFactory<T> {
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);
}
}
}
}

View File

@ -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.
* <p>
* 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.
* <p>
* 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,8 +128,9 @@ 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.
@ -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,6 +193,7 @@ 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/
* @return a count of how many times the 'occur' char appears in the strings
@ -225,11 +227,11 @@ public class StringUtilities {
* Generate a quoted string from US-ASCII character bytes assuming 1-byte chars.
* <p>
* 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.
* <p>
* 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.).
* <p>
* 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,6 +319,7 @@ public class StringUtilities {
* Returns true if the given string starts with <code>prefix</code> ignoring case.
* <p>
* Note: This method is equivalent to calling:
*
* <pre>
* string.regionMatches(true, 0, prefix, 0, prefix.length());
* </pre>
@ -336,6 +339,7 @@ public class StringUtilities {
* Returns true if the given string ends with <code>postfix</code>, ignoring case.
* <p>
* Note: This method is equivalent to calling:
*
* <pre>
* int startIndex = string.length() - postfix.length();
* string.regionMatches(true, startOffset, postfix, 0, postfix.length());
@ -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.
* <p>
* This methods creates an empty string entry in the result array for initial and trailing
* separator chars, as well as for consecutive separators.
@ -532,9 +533,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.
*
* @param s the string to parse
* @param preserveTokens true signals to treat consecutive newlines as multiple lines; false
@ -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 <code>\n</code> 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 <code>\n</code> and then pads each string with the
* given pad string. Finally, the updated lines are formed into a single string.
* <p>
* 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
* 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*".
* <p>
* 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) {
@ -758,7 +753,8 @@ public class StringUtilities {
* <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 MyFile.java</li>
* <li>StringUtilities.getLastWord("/This/is/my/last/word/MyFile.java", "/") returns
* MyFile.java</li>
* </ul>
*
* @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}.
*
* <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
* @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
@ -863,8 +860,9 @@ public class StringUtilities {
* larger than the given length, then it will be trimmed to fit that length <b>after adding
* ellipses</b>
*
* <p>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.
* <p>
* 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 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
* added to signal that content was removed. Thus, the actual number of removed characters
* will be <code>(s.length() - max) + {@value StringUtilities#ELLIPSES}</code> length.
* Trims the given string the <code>max</code> number of characters. Ellipses will be added to
* signal that content was removed. Thus, the actual number of removed characters will be
* <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
* minimum, we can display the {@value #ELLIPSES} plus one character from the front and
* back of the string.
* <p>
* The given <code>max</code> 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) {
@ -1069,12 +1065,12 @@ 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.
* 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
*
* <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|
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|