aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/org/connectbot/transport
diff options
context:
space:
mode:
authorKenny Root <kenny@the-b.org>2014-10-01 23:04:51 +0100
committerKenny Root <kenny@the-b.org>2014-10-01 12:48:19 +0100
commit49b779dcaf03e3598d2709b321e20ea029b25163 (patch)
tree05af547b1f1433d7dd6f7373d0b25a455e053a03 /app/src/main/java/org/connectbot/transport
parentd64786d9197090c74072b648e487e3d34817bb57 (diff)
downloadconnectbot-49b779dcaf03e3598d2709b321e20ea029b25163.tar.gz
connectbot-49b779dcaf03e3598d2709b321e20ea029b25163.tar.bz2
connectbot-49b779dcaf03e3598d2709b321e20ea029b25163.zip
Convert to gradle build system
Diffstat (limited to 'app/src/main/java/org/connectbot/transport')
-rw-r--r--app/src/main/java/org/connectbot/transport/AbsTransport.java254
-rw-r--r--app/src/main/java/org/connectbot/transport/Local.java219
-rw-r--r--app/src/main/java/org/connectbot/transport/SSH.java958
-rw-r--r--app/src/main/java/org/connectbot/transport/Telnet.java330
-rw-r--r--app/src/main/java/org/connectbot/transport/TransportFactory.java132
5 files changed, 1893 insertions, 0 deletions
diff --git a/app/src/main/java/org/connectbot/transport/AbsTransport.java b/app/src/main/java/org/connectbot/transport/AbsTransport.java
new file mode 100644
index 0000000..18397ea
--- /dev/null
+++ b/app/src/main/java/org/connectbot/transport/AbsTransport.java
@@ -0,0 +1,254 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * 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 org.connectbot.transport;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import org.connectbot.bean.HostBean;
+import org.connectbot.bean.PortForwardBean;
+import org.connectbot.service.TerminalBridge;
+import org.connectbot.service.TerminalManager;
+
+import android.content.Context;
+import android.net.Uri;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public abstract class AbsTransport {
+ HostBean host;
+ TerminalBridge bridge;
+ TerminalManager manager;
+
+ String emulation;
+
+ public AbsTransport() {}
+
+ public AbsTransport(HostBean host, TerminalBridge bridge, TerminalManager manager) {
+ this.host = host;
+ this.bridge = bridge;
+ this.manager = manager;
+ }
+
+ /**
+ * @return protocol part of the URI
+ */
+ public static String getProtocolName() {
+ return "unknown";
+ }
+
+ /**
+ * Encode the current transport into a URI that can be passed via intent calls.
+ * @return URI to host
+ */
+ public static Uri getUri(String input) {
+ return null;
+ }
+
+ /**
+ * Causes transport to connect to the target host. After connecting but before a
+ * session is started, must call back to {@link TerminalBridge#onConnected()}.
+ * After that call a session may be opened.
+ */
+ public abstract void connect();
+
+ /**
+ * Reads from the transport. Transport must support reading into a the byte array
+ * <code>buffer</code> at the start of <code>offset</code> and a maximum of
+ * <code>length</code> bytes. If the remote host disconnects, throw an
+ * {@link IOException}.
+ * @param buffer byte buffer to store read bytes into
+ * @param offset where to start writing in the buffer
+ * @param length maximum number of bytes to read
+ * @return number of bytes read
+ * @throws IOException when remote host disconnects
+ */
+ public abstract int read(byte[] buffer, int offset, int length) throws IOException;
+
+ /**
+ * Writes to the transport. If the host is not yet connected, simply return without
+ * doing anything. An {@link IOException} should be thrown if there is an error after
+ * connection.
+ * @param buffer bytes to write to transport
+ * @throws IOException when there is a problem writing after connection
+ */
+ public abstract void write(byte[] buffer) throws IOException;
+
+ /**
+ * Writes to the transport. See {@link #write(byte[])} for behavior details.
+ * @param c character to write to the transport
+ * @throws IOException when there is a problem writing after connection
+ */
+ public abstract void write(int c) throws IOException;
+
+ /**
+ * Flushes the write commands to the transport.
+ * @throws IOException when there is a problem writing after connection
+ */
+ public abstract void flush() throws IOException;
+
+ /**
+ * Closes the connection to the terminal. Note that the resulting failure to read
+ * should call {@link TerminalBridge#dispatchDisconnect(boolean)}.
+ */
+ public abstract void close();
+
+ /**
+ * Tells the transport what dimensions the display is currently
+ * @param columns columns of text
+ * @param rows rows of text
+ * @param width width in pixels
+ * @param height height in pixels
+ */
+ public abstract void setDimensions(int columns, int rows, int width, int height);
+
+ public void setOptions(Map<String,String> options) {
+ // do nothing
+ }
+
+ public Map<String,String> getOptions() {
+ return null;
+ }
+
+ public void setCompression(boolean compression) {
+ // do nothing
+ }
+
+ public void setUseAuthAgent(String useAuthAgent) {
+ // do nothing
+ }
+
+ public void setEmulation(String emulation) {
+ this.emulation = emulation;
+ }
+
+ public String getEmulation() {
+ return emulation;
+ }
+
+ public void setHost(HostBean host) {
+ this.host = host;
+ }
+
+ public void setBridge(TerminalBridge bridge) {
+ this.bridge = bridge;
+ }
+
+ public void setManager(TerminalManager manager) {
+ this.manager = manager;
+ }
+
+ /**
+ * Whether or not this transport type can forward ports.
+ * @return true on ability to forward ports
+ */
+ public boolean canForwardPorts() {
+ return false;
+ }
+
+ /**
+ * Adds the {@link PortForwardBean} to the list.
+ * @param portForward the port forward bean to add
+ * @return true on successful addition
+ */
+ public boolean addPortForward(PortForwardBean portForward) {
+ return false;
+ }
+
+ /**
+ * Enables a port forward member. After calling this method, the port forward should
+ * be operational iff it could be enabled by the transport.
+ * @param portForward member of our current port forwards list to enable
+ * @return true on successful port forward setup
+ */
+ public boolean enablePortForward(PortForwardBean portForward) {
+ return false;
+ }
+
+ /**
+ * Disables a port forward member. After calling this method, the port forward should
+ * be non-functioning iff it could be disabled by the transport.
+ * @param portForward member of our current port forwards list to enable
+ * @return true on successful port forward tear-down
+ */
+ public boolean disablePortForward(PortForwardBean portForward) {
+ return false;
+ }
+
+ /**
+ * Removes the {@link PortForwardBean} from the available port forwards.
+ * @param portForward the port forward bean to remove
+ * @return true on successful removal
+ */
+ public boolean removePortForward(PortForwardBean portForward) {
+ return false;
+ }
+
+ /**
+ * Gets a list of the {@link PortForwardBean} currently used by this transport.
+ * @return the list of port forwards
+ */
+ public List<PortForwardBean> getPortForwards() {
+ return null;
+ }
+
+ public abstract boolean isConnected();
+ public abstract boolean isSessionOpen();
+
+ /**
+ * @return int default port for protocol
+ */
+ public abstract int getDefaultPort();
+
+ /**
+ * @param username
+ * @param hostname
+ * @param port
+ * @return
+ */
+ public abstract String getDefaultNickname(String username, String hostname, int port);
+
+ /**
+ * @param uri
+ * @param selectionKeys
+ * @param selectionValues
+ */
+ public abstract void getSelectionArgs(Uri uri, Map<String, String> selection);
+
+ /**
+ * @param uri
+ * @return
+ */
+ public abstract HostBean createHost(Uri uri);
+
+ /**
+ * @param context context containing the correct resources
+ * @return string that hints at the format for connection
+ */
+ public static String getFormatHint(Context context) {
+ return "???";
+ }
+
+ /**
+ * @return
+ */
+ public abstract boolean usesNetwork();
+}
diff --git a/app/src/main/java/org/connectbot/transport/Local.java b/app/src/main/java/org/connectbot/transport/Local.java
new file mode 100644
index 0000000..5ace1b0
--- /dev/null
+++ b/app/src/main/java/org/connectbot/transport/Local.java
@@ -0,0 +1,219 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * 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 org.connectbot.transport;
+
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Map;
+
+import org.connectbot.R;
+import org.connectbot.bean.HostBean;
+import org.connectbot.service.TerminalBridge;
+import org.connectbot.service.TerminalManager;
+import org.connectbot.util.HostDatabase;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+
+import com.google.ase.Exec;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class Local extends AbsTransport {
+ private static final String TAG = "ConnectBot.Local";
+ private static final String PROTOCOL = "local";
+
+ private static final String DEFAULT_URI = "local:#Local";
+
+ private FileDescriptor shellFd;
+
+ private FileInputStream is;
+ private FileOutputStream os;
+
+ /**
+ *
+ */
+ public Local() {
+ }
+
+ /**
+ * @param host
+ * @param bridge
+ * @param manager
+ */
+ public Local(HostBean host, TerminalBridge bridge, TerminalManager manager) {
+ super(host, bridge, manager);
+ }
+
+ public static String getProtocolName() {
+ return PROTOCOL;
+ }
+
+ @Override
+ public void close() {
+ try {
+ if (os != null) {
+ os.close();
+ os = null;
+ }
+ if (is != null) {
+ is.close();
+ is = null;
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Couldn't close shell", e);
+ }
+ }
+
+ @Override
+ public void connect() {
+ int[] pids = new int[1];
+
+ try {
+ shellFd = Exec.createSubprocess("/system/bin/sh", "-", null, pids);
+ } catch (Exception e) {
+ bridge.outputLine(manager.res.getString(R.string.local_shell_unavailable));
+ Log.e(TAG, "Cannot start local shell", e);
+ return;
+ }
+
+ final int shellPid = pids[0];
+ Runnable exitWatcher = new Runnable() {
+ public void run() {
+ Exec.waitFor(shellPid);
+
+ bridge.dispatchDisconnect(false);
+ }
+ };
+
+ Thread exitWatcherThread = new Thread(exitWatcher);
+ exitWatcherThread.setName("LocalExitWatcher");
+ exitWatcherThread.setDaemon(true);
+ exitWatcherThread.start();
+
+ is = new FileInputStream(shellFd);
+ os = new FileOutputStream(shellFd);
+
+ bridge.onConnected();
+ }
+
+ @Override
+ public void flush() throws IOException {
+ os.flush();
+ }
+
+ @Override
+ public String getDefaultNickname(String username, String hostname, int port) {
+ return DEFAULT_URI;
+ }
+
+ @Override
+ public int getDefaultPort() {
+ return 0;
+ }
+
+ @Override
+ public boolean isConnected() {
+ return is != null && os != null;
+ }
+
+ @Override
+ public boolean isSessionOpen() {
+ return is != null && os != null;
+ }
+
+ @Override
+ public int read(byte[] buffer, int start, int len) throws IOException {
+ if (is == null) {
+ bridge.dispatchDisconnect(false);
+ throw new IOException("session closed");
+ }
+ return is.read(buffer, start, len);
+ }
+
+ @Override
+ public void setDimensions(int columns, int rows, int width, int height) {
+ try {
+ Exec.setPtyWindowSize(shellFd, rows, columns, width, height);
+ } catch (Exception e) {
+ Log.e(TAG, "Couldn't resize pty", e);
+ }
+ }
+
+ @Override
+ public void write(byte[] buffer) throws IOException {
+ if (os != null)
+ os.write(buffer);
+ }
+
+ @Override
+ public void write(int c) throws IOException {
+ if (os != null)
+ os.write(c);
+ }
+
+ public static Uri getUri(String input) {
+ Uri uri = Uri.parse(DEFAULT_URI);
+
+ if (input != null && input.length() > 0) {
+ uri = uri.buildUpon().fragment(input).build();
+ }
+
+ return uri;
+ }
+
+ @Override
+ public HostBean createHost(Uri uri) {
+ HostBean host = new HostBean();
+
+ host.setProtocol(PROTOCOL);
+
+ String nickname = uri.getFragment();
+ if (nickname == null || nickname.length() == 0) {
+ host.setNickname(getDefaultNickname(host.getUsername(),
+ host.getHostname(), host.getPort()));
+ } else {
+ host.setNickname(uri.getFragment());
+ }
+
+ return host;
+ }
+
+ @Override
+ public void getSelectionArgs(Uri uri, Map<String, String> selection) {
+ selection.put(HostDatabase.FIELD_HOST_PROTOCOL, PROTOCOL);
+ selection.put(HostDatabase.FIELD_HOST_NICKNAME, uri.getFragment());
+ }
+
+ public static String getFormatHint(Context context) {
+ return context.getString(R.string.hostpref_nickname_title);
+ }
+
+ /* (non-Javadoc)
+ * @see org.connectbot.transport.AbsTransport#usesNetwork()
+ */
+ @Override
+ public boolean usesNetwork() {
+ return false;
+ }
+}
diff --git a/app/src/main/java/org/connectbot/transport/SSH.java b/app/src/main/java/org/connectbot/transport/SSH.java
new file mode 100644
index 0000000..6ef9745
--- /dev/null
+++ b/app/src/main/java/org/connectbot/transport/SSH.java
@@ -0,0 +1,958 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * 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 org.connectbot.transport;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.security.KeyPair;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.interfaces.DSAPrivateKey;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.connectbot.R;
+import org.connectbot.bean.HostBean;
+import org.connectbot.bean.PortForwardBean;
+import org.connectbot.bean.PubkeyBean;
+import org.connectbot.service.TerminalBridge;
+import org.connectbot.service.TerminalManager;
+import org.connectbot.service.TerminalManager.KeyHolder;
+import org.connectbot.util.HostDatabase;
+import org.connectbot.util.PubkeyDatabase;
+import org.connectbot.util.PubkeyUtils;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+
+import com.trilead.ssh2.AuthAgentCallback;
+import com.trilead.ssh2.ChannelCondition;
+import com.trilead.ssh2.Connection;
+import com.trilead.ssh2.ConnectionInfo;
+import com.trilead.ssh2.ConnectionMonitor;
+import com.trilead.ssh2.DynamicPortForwarder;
+import com.trilead.ssh2.InteractiveCallback;
+import com.trilead.ssh2.KnownHosts;
+import com.trilead.ssh2.LocalPortForwarder;
+import com.trilead.ssh2.ServerHostKeyVerifier;
+import com.trilead.ssh2.Session;
+import com.trilead.ssh2.crypto.PEMDecoder;
+import com.trilead.ssh2.signature.DSASHA1Verify;
+import com.trilead.ssh2.signature.RSASHA1Verify;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class SSH extends AbsTransport implements ConnectionMonitor, InteractiveCallback, AuthAgentCallback {
+ public SSH() {
+ super();
+ }
+
+ /**
+ * @param bridge
+ * @param db
+ */
+ public SSH(HostBean host, TerminalBridge bridge, TerminalManager manager) {
+ super(host, bridge, manager);
+ }
+
+ private static final String PROTOCOL = "ssh";
+ private static final String TAG = "ConnectBot.SSH";
+ private static final int DEFAULT_PORT = 22;
+
+ private static final String AUTH_PUBLICKEY = "publickey",
+ AUTH_PASSWORD = "password",
+ AUTH_KEYBOARDINTERACTIVE = "keyboard-interactive";
+
+ private final static int AUTH_TRIES = 20;
+
+ static final Pattern hostmask;
+ static {
+ hostmask = Pattern.compile("^(.+)@([0-9a-z.-]+)(:(\\d+))?$", Pattern.CASE_INSENSITIVE);
+ }
+
+ private boolean compression = false;
+ private volatile boolean authenticated = false;
+ private volatile boolean connected = false;
+ private volatile boolean sessionOpen = false;
+
+ private boolean pubkeysExhausted = false;
+ private boolean interactiveCanContinue = true;
+
+ private Connection connection;
+ private Session session;
+ private ConnectionInfo connectionInfo;
+
+ private OutputStream stdin;
+ private InputStream stdout;
+ private InputStream stderr;
+
+ private static final int conditions = ChannelCondition.STDOUT_DATA
+ | ChannelCondition.STDERR_DATA
+ | ChannelCondition.CLOSED
+ | ChannelCondition.EOF;
+
+ private List<PortForwardBean> portForwards = new LinkedList<PortForwardBean>();
+
+ private int columns;
+ private int rows;
+
+ private int width;
+ private int height;
+
+ private String useAuthAgent = HostDatabase.AUTHAGENT_NO;
+ private String agentLockPassphrase;
+
+ public class HostKeyVerifier implements ServerHostKeyVerifier {
+ public boolean verifyServerHostKey(String hostname, int port,
+ String serverHostKeyAlgorithm, byte[] serverHostKey) throws IOException {
+
+ // read in all known hosts from hostdb
+ KnownHosts hosts = manager.hostdb.getKnownHosts();
+ Boolean result;
+
+ String matchName = String.format(Locale.US, "%s:%d", hostname, port);
+
+ String fingerprint = KnownHosts.createHexFingerprint(serverHostKeyAlgorithm, serverHostKey);
+
+ String algorithmName;
+ if ("ssh-rsa".equals(serverHostKeyAlgorithm))
+ algorithmName = "RSA";
+ else if ("ssh-dss".equals(serverHostKeyAlgorithm))
+ algorithmName = "DSA";
+ else if (serverHostKeyAlgorithm.startsWith("ecdsa-"))
+ algorithmName = "EC";
+ else
+ algorithmName = serverHostKeyAlgorithm;
+
+ switch(hosts.verifyHostkey(matchName, serverHostKeyAlgorithm, serverHostKey)) {
+ case KnownHosts.HOSTKEY_IS_OK:
+ bridge.outputLine(manager.res.getString(R.string.terminal_sucess, algorithmName, fingerprint));
+ return true;
+
+ case KnownHosts.HOSTKEY_IS_NEW:
+ // prompt user
+ bridge.outputLine(manager.res.getString(R.string.host_authenticity_warning, hostname));
+ bridge.outputLine(manager.res.getString(R.string.host_fingerprint, algorithmName, fingerprint));
+
+ result = bridge.promptHelper.requestBooleanPrompt(null, manager.res.getString(R.string.prompt_continue_connecting));
+ if(result == null) return false;
+ if(result.booleanValue()) {
+ // save this key in known database
+ manager.hostdb.saveKnownHost(hostname, port, serverHostKeyAlgorithm, serverHostKey);
+ }
+ return result.booleanValue();
+
+ case KnownHosts.HOSTKEY_HAS_CHANGED:
+ String header = String.format("@ %s @",
+ manager.res.getString(R.string.host_verification_failure_warning_header));
+
+ char[] atsigns = new char[header.length()];
+ Arrays.fill(atsigns, '@');
+ String border = new String(atsigns);
+
+ bridge.outputLine(border);
+ bridge.outputLine(manager.res.getString(R.string.host_verification_failure_warning));
+ bridge.outputLine(border);
+
+ bridge.outputLine(String.format(manager.res.getString(R.string.host_fingerprint),
+ algorithmName, fingerprint));
+
+ // Users have no way to delete keys, so we'll prompt them for now.
+ result = bridge.promptHelper.requestBooleanPrompt(null, manager.res.getString(R.string.prompt_continue_connecting));
+ if(result == null) return false;
+ if(result.booleanValue()) {
+ // save this key in known database
+ manager.hostdb.saveKnownHost(hostname, port, serverHostKeyAlgorithm, serverHostKey);
+ }
+ return result.booleanValue();
+
+ default:
+ return false;
+ }
+ }
+
+ }
+
+ private void authenticate() {
+ try {
+ if (connection.authenticateWithNone(host.getUsername())) {
+ finishConnection();
+ return;
+ }
+ } catch(Exception e) {
+ Log.d(TAG, "Host does not support 'none' authentication.");
+ }
+
+ bridge.outputLine(manager.res.getString(R.string.terminal_auth));
+
+ try {
+ long pubkeyId = host.getPubkeyId();
+
+ if (!pubkeysExhausted &&
+ pubkeyId != HostDatabase.PUBKEYID_NEVER &&
+ connection.isAuthMethodAvailable(host.getUsername(), AUTH_PUBLICKEY)) {
+
+ // if explicit pubkey defined for this host, then prompt for password as needed
+ // otherwise just try all in-memory keys held in terminalmanager
+
+ if (pubkeyId == HostDatabase.PUBKEYID_ANY) {
+ // try each of the in-memory keys
+ bridge.outputLine(manager.res
+ .getString(R.string.terminal_auth_pubkey_any));
+ for (Entry<String, KeyHolder> entry : manager.loadedKeypairs.entrySet()) {
+ if (entry.getValue().bean.isConfirmUse()
+ && !promptForPubkeyUse(entry.getKey()))
+ continue;
+
+ if (this.tryPublicKey(host.getUsername(), entry.getKey(),
+ entry.getValue().pair)) {
+ finishConnection();
+ break;
+ }
+ }
+ } else {
+ bridge.outputLine(manager.res.getString(R.string.terminal_auth_pubkey_specific));
+ // use a specific key for this host, as requested
+ PubkeyBean pubkey = manager.pubkeydb.findPubkeyById(pubkeyId);
+
+ if (pubkey == null)
+ bridge.outputLine(manager.res.getString(R.string.terminal_auth_pubkey_invalid));
+ else
+ if (tryPublicKey(pubkey))
+ finishConnection();
+ }
+
+ pubkeysExhausted = true;
+ } else if (interactiveCanContinue &&
+ connection.isAuthMethodAvailable(host.getUsername(), AUTH_KEYBOARDINTERACTIVE)) {
+ // this auth method will talk with us using InteractiveCallback interface
+ // it blocks until authentication finishes
+ bridge.outputLine(manager.res.getString(R.string.terminal_auth_ki));
+ interactiveCanContinue = false;
+ if(connection.authenticateWithKeyboardInteractive(host.getUsername(), this)) {
+ finishConnection();
+ } else {
+ bridge.outputLine(manager.res.getString(R.string.terminal_auth_ki_fail));
+ }
+ } else if (connection.isAuthMethodAvailable(host.getUsername(), AUTH_PASSWORD)) {
+ bridge.outputLine(manager.res.getString(R.string.terminal_auth_pass));
+ String password = bridge.getPromptHelper().requestStringPrompt(null,
+ manager.res.getString(R.string.prompt_password));
+ if (password != null
+ && connection.authenticateWithPassword(host.getUsername(), password)) {
+ finishConnection();
+ } else {
+ bridge.outputLine(manager.res.getString(R.string.terminal_auth_pass_fail));
+ }
+ } else {
+ bridge.outputLine(manager.res.getString(R.string.terminal_auth_fail));
+ }
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Connection went away while we were trying to authenticate", e);
+ return;
+ } catch(Exception e) {
+ Log.e(TAG, "Problem during handleAuthentication()", e);
+ }
+ }
+
+ /**
+ * Attempt connection with database row pointed to by cursor.
+ * @param cursor
+ * @return true for successful authentication
+ * @throws NoSuchAlgorithmException
+ * @throws InvalidKeySpecException
+ * @throws IOException
+ */
+ private boolean tryPublicKey(PubkeyBean pubkey) throws NoSuchAlgorithmException, InvalidKeySpecException, IOException {
+ KeyPair pair = null;
+
+ if(manager.isKeyLoaded(pubkey.getNickname())) {
+ // load this key from memory if its already there
+ Log.d(TAG, String.format("Found unlocked key '%s' already in-memory", pubkey.getNickname()));
+
+ if (pubkey.isConfirmUse()) {
+ if (!promptForPubkeyUse(pubkey.getNickname()))
+ return false;
+ }
+
+ pair = manager.getKey(pubkey.getNickname());
+ } else {
+ // otherwise load key from database and prompt for password as needed
+ String password = null;
+ if (pubkey.isEncrypted()) {
+ password = bridge.getPromptHelper().requestStringPrompt(null,
+ manager.res.getString(R.string.prompt_pubkey_password, pubkey.getNickname()));
+
+ // Something must have interrupted the prompt.
+ if (password == null)
+ return false;
+ }
+
+ if(PubkeyDatabase.KEY_TYPE_IMPORTED.equals(pubkey.getType())) {
+ // load specific key using pem format
+ pair = PEMDecoder.decode(new String(pubkey.getPrivateKey()).toCharArray(), password);
+ } else {
+ // load using internal generated format
+ PrivateKey privKey;
+ try {
+ privKey = PubkeyUtils.decodePrivate(pubkey.getPrivateKey(),
+ pubkey.getType(), password);
+ } catch (Exception e) {
+ String message = String.format("Bad password for key '%s'. Authentication failed.", pubkey.getNickname());
+ Log.e(TAG, message, e);
+ bridge.outputLine(message);
+ return false;
+ }
+
+ PublicKey pubKey = PubkeyUtils.decodePublic(pubkey.getPublicKey(), pubkey.getType());
+
+ // convert key to trilead format
+ pair = new KeyPair(pubKey, privKey);
+ Log.d(TAG, "Unlocked key " + PubkeyUtils.formatKey(pubKey));
+ }
+
+ Log.d(TAG, String.format("Unlocked key '%s'", pubkey.getNickname()));
+
+ // save this key in memory
+ manager.addKey(pubkey, pair);
+ }
+
+ return tryPublicKey(host.getUsername(), pubkey.getNickname(), pair);
+ }
+
+ private boolean tryPublicKey(String username, String keyNickname, KeyPair pair) throws IOException {
+ //bridge.outputLine(String.format("Attempting 'publickey' with key '%s' [%s]...", keyNickname, trileadKey.toString()));
+ boolean success = connection.authenticateWithPublicKey(username, pair);
+ if(!success)
+ bridge.outputLine(manager.res.getString(R.string.terminal_auth_pubkey_fail, keyNickname));
+ return success;
+ }
+
+ /**
+ * Internal method to request actual PTY terminal once we've finished
+ * authentication. If called before authenticated, it will just fail.
+ */
+ private void finishConnection() {
+ authenticated = true;
+
+ for (PortForwardBean portForward : portForwards) {
+ try {
+ enablePortForward(portForward);
+ bridge.outputLine(manager.res.getString(R.string.terminal_enable_portfoward, portForward.getDescription()));
+ } catch (Exception e) {
+ Log.e(TAG, "Error setting up port forward during connect", e);
+ }
+ }
+
+ if (!host.getWantSession()) {
+ bridge.outputLine(manager.res.getString(R.string.terminal_no_session));
+ bridge.onConnected();
+ return;
+ }
+
+ try {
+ session = connection.openSession();
+
+ if (!useAuthAgent.equals(HostDatabase.AUTHAGENT_NO))
+ session.requestAuthAgentForwarding(this);
+
+ session.requestPTY(getEmulation(), columns, rows, width, height, null);
+ session.startShell();
+
+ stdin = session.getStdin();
+ stdout = session.getStdout();
+ stderr = session.getStderr();
+
+ sessionOpen = true;
+
+ bridge.onConnected();
+ } catch (IOException e1) {
+ Log.e(TAG, "Problem while trying to create PTY in finishConnection()", e1);
+ }
+
+ }
+
+ @Override
+ public void connect() {
+ connection = new Connection(host.getHostname(), host.getPort());
+ connection.addConnectionMonitor(this);
+
+ try {
+ connection.setCompression(compression);
+ } catch (IOException e) {
+ Log.e(TAG, "Could not enable compression!", e);
+ }
+
+ try {
+ /* Uncomment when debugging SSH protocol:
+ DebugLogger logger = new DebugLogger() {
+
+ public void log(int level, String className, String message) {
+ Log.d("SSH", message);
+ }
+
+ };
+ Logger.enabled = true;
+ Logger.logger = logger;
+ */
+ connectionInfo = connection.connect(new HostKeyVerifier());
+ connected = true;
+
+ if (connectionInfo.clientToServerCryptoAlgorithm
+ .equals(connectionInfo.serverToClientCryptoAlgorithm)
+ && connectionInfo.clientToServerMACAlgorithm
+ .equals(connectionInfo.serverToClientMACAlgorithm)) {
+ bridge.outputLine(manager.res.getString(R.string.terminal_using_algorithm,
+ connectionInfo.clientToServerCryptoAlgorithm,
+ connectionInfo.clientToServerMACAlgorithm));
+ } else {
+ bridge.outputLine(manager.res.getString(
+ R.string.terminal_using_c2s_algorithm,
+ connectionInfo.clientToServerCryptoAlgorithm,
+ connectionInfo.clientToServerMACAlgorithm));
+
+ bridge.outputLine(manager.res.getString(
+ R.string.terminal_using_s2c_algorithm,
+ connectionInfo.serverToClientCryptoAlgorithm,
+ connectionInfo.serverToClientMACAlgorithm));
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Problem in SSH connection thread during authentication", e);
+
+ // Display the reason in the text.
+ bridge.outputLine(e.getCause().getMessage());
+
+ onDisconnect();
+ return;
+ }
+
+ try {
+ // enter a loop to keep trying until authentication
+ int tries = 0;
+ while (connected && !connection.isAuthenticationComplete() && tries++ < AUTH_TRIES) {
+ authenticate();
+
+ // sleep to make sure we dont kill system
+ Thread.sleep(1000);
+ }
+ } catch(Exception e) {
+ Log.e(TAG, "Problem in SSH connection thread during authentication", e);
+ }
+ }
+
+ @Override
+ public void close() {
+ connected = false;
+
+ if (session != null) {
+ session.close();
+ session = null;
+ }
+
+ if (connection != null) {
+ connection.close();
+ connection = null;
+ }
+ }
+
+ private void onDisconnect() {
+ close();
+
+ bridge.dispatchDisconnect(false);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ if (stdin != null)
+ stdin.flush();
+ }
+
+ @Override
+ public int read(byte[] buffer, int start, int len) throws IOException {
+ int bytesRead = 0;
+
+ if (session == null)
+ return 0;
+
+ int newConditions = session.waitForCondition(conditions, 0);
+
+ if ((newConditions & ChannelCondition.STDOUT_DATA) != 0) {
+ bytesRead = stdout.read(buffer, start, len);
+ }
+
+ if ((newConditions & ChannelCondition.STDERR_DATA) != 0) {
+ byte discard[] = new byte[256];
+ while (stderr.available() > 0) {
+ stderr.read(discard);
+ }
+ }
+
+ if ((newConditions & ChannelCondition.EOF) != 0) {
+ onDisconnect();
+ throw new IOException("Remote end closed connection");
+ }
+
+ return bytesRead;
+ }
+
+ @Override
+ public void write(byte[] buffer) throws IOException {
+ if (stdin != null)
+ stdin.write(buffer);
+ }
+
+ @Override
+ public void write(int c) throws IOException {
+ if (stdin != null)
+ stdin.write(c);
+ }
+
+ @Override
+ public Map<String, String> getOptions() {
+ Map<String, String> options = new HashMap<String, String>();
+
+ options.put("compression", Boolean.toString(compression));
+
+ return options;
+ }
+
+ @Override
+ public void setOptions(Map<String, String> options) {
+ if (options.containsKey("compression"))
+ compression = Boolean.parseBoolean(options.get("compression"));
+ }
+
+ public static String getProtocolName() {
+ return PROTOCOL;
+ }
+
+ @Override
+ public boolean isSessionOpen() {
+ return sessionOpen;
+ }
+
+ @Override
+ public boolean isConnected() {
+ return connected;
+ }
+
+ public void connectionLost(Throwable reason) {
+ onDisconnect();
+ }
+
+ @Override
+ public boolean canForwardPorts() {
+ return true;
+ }
+
+ @Override
+ public List<PortForwardBean> getPortForwards() {
+ return portForwards;
+ }
+
+ @Override
+ public boolean addPortForward(PortForwardBean portForward) {
+ return portForwards.add(portForward);
+ }
+
+ @Override
+ public boolean removePortForward(PortForwardBean portForward) {
+ // Make sure we don't have a phantom forwarder.
+ disablePortForward(portForward);
+
+ return portForwards.remove(portForward);
+ }
+
+ @Override
+ public boolean enablePortForward(PortForwardBean portForward) {
+ if (!portForwards.contains(portForward)) {
+ Log.e(TAG, "Attempt to enable port forward not in list");
+ return false;
+ }
+
+ if (!authenticated)
+ return false;
+
+ if (HostDatabase.PORTFORWARD_LOCAL.equals(portForward.getType())) {
+ LocalPortForwarder lpf = null;
+ try {
+ lpf = connection.createLocalPortForwarder(
+ new InetSocketAddress(InetAddress.getLocalHost(), portForward.getSourcePort()),
+ portForward.getDestAddr(), portForward.getDestPort());
+ } catch (Exception e) {
+ Log.e(TAG, "Could not create local port forward", e);
+ return false;
+ }
+
+ if (lpf == null) {
+ Log.e(TAG, "returned LocalPortForwarder object is null");
+ return false;
+ }
+
+ portForward.setIdentifier(lpf);
+ portForward.setEnabled(true);
+ return true;
+ } else if (HostDatabase.PORTFORWARD_REMOTE.equals(portForward.getType())) {
+ try {
+ connection.requestRemotePortForwarding("", portForward.getSourcePort(), portForward.getDestAddr(), portForward.getDestPort());
+ } catch (Exception e) {
+ Log.e(TAG, "Could not create remote port forward", e);
+ return false;
+ }
+
+ portForward.setEnabled(true);
+ return true;
+ } else if (HostDatabase.PORTFORWARD_DYNAMIC5.equals(portForward.getType())) {
+ DynamicPortForwarder dpf = null;
+
+ try {
+ dpf = connection.createDynamicPortForwarder(
+ new InetSocketAddress(InetAddress.getLocalHost(), portForward.getSourcePort()));
+ } catch (Exception e) {
+ Log.e(TAG, "Could not create dynamic port forward", e);
+ return false;
+ }
+
+ portForward.setIdentifier(dpf);
+ portForward.setEnabled(true);
+ return true;
+ } else {
+ // Unsupported type
+ Log.e(TAG, String.format("attempt to forward unknown type %s", portForward.getType()));
+ return false;
+ }
+ }
+
+ @Override
+ public boolean disablePortForward(PortForwardBean portForward) {
+ if (!portForwards.contains(portForward)) {
+ Log.e(TAG, "Attempt to disable port forward not in list");
+ return false;
+ }
+
+ if (!authenticated)
+ return false;
+
+ if (HostDatabase.PORTFORWARD_LOCAL.equals(portForward.getType())) {
+ LocalPortForwarder lpf = null;
+ lpf = (LocalPortForwarder)portForward.getIdentifier();
+
+ if (!portForward.isEnabled() || lpf == null) {
+ Log.d(TAG, String.format("Could not disable %s; it appears to be not enabled or have no handler", portForward.getNickname()));
+ return false;
+ }
+
+ portForward.setEnabled(false);
+
+ try {
+ lpf.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Could not stop local port forwarder, setting enabled to false", e);
+ return false;
+ }
+
+ return true;
+ } else if (HostDatabase.PORTFORWARD_REMOTE.equals(portForward.getType())) {
+ portForward.setEnabled(false);
+
+ try {
+ connection.cancelRemotePortForwarding(portForward.getSourcePort());
+ } catch (IOException e) {
+ Log.e(TAG, "Could not stop remote port forwarding, setting enabled to false", e);
+ return false;
+ }
+
+ return true;
+ } else if (HostDatabase.PORTFORWARD_DYNAMIC5.equals(portForward.getType())) {
+ DynamicPortForwarder dpf = null;
+ dpf = (DynamicPortForwarder)portForward.getIdentifier();
+
+ if (!portForward.isEnabled() || dpf == null) {
+ Log.d(TAG, String.format("Could not disable %s; it appears to be not enabled or have no handler", portForward.getNickname()));
+ return false;
+ }
+
+ portForward.setEnabled(false);
+
+ try {
+ dpf.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Could not stop dynamic port forwarder, setting enabled to false", e);
+ return false;
+ }
+
+ return true;
+ } else {
+ // Unsupported type
+ Log.e(TAG, String.format("attempt to forward unknown type %s", portForward.getType()));
+ return false;
+ }
+ }
+
+ @Override
+ public void setDimensions(int columns, int rows, int width, int height) {
+ this.columns = columns;
+ this.rows = rows;
+
+ if (sessionOpen) {
+ try {
+ session.resizePTY(columns, rows, width, height);
+ } catch (IOException e) {
+ Log.e(TAG, "Couldn't send resize PTY packet", e);
+ }
+ }
+ }
+
+ @Override
+ public int getDefaultPort() {
+ return DEFAULT_PORT;
+ }
+
+ @Override
+ public String getDefaultNickname(String username, String hostname, int port) {
+ if (port == DEFAULT_PORT) {
+ return String.format(Locale.US, "%s@%s", username, hostname);
+ } else {
+ return String.format(Locale.US, "%s@%s:%d", username, hostname, port);
+ }
+ }
+
+ public static Uri getUri(String input) {
+ Matcher matcher = hostmask.matcher(input);
+
+ if (!matcher.matches())
+ return null;
+
+ StringBuilder sb = new StringBuilder();
+
+ sb.append(PROTOCOL)
+ .append("://")
+ .append(Uri.encode(matcher.group(1)))
+ .append('@')
+ .append(matcher.group(2));
+
+ String portString = matcher.group(4);
+ int port = DEFAULT_PORT;
+ if (portString != null) {
+ try {
+ port = Integer.parseInt(portString);
+ if (port < 1 || port > 65535) {
+ port = DEFAULT_PORT;
+ }
+ } catch (NumberFormatException nfe) {
+ // Keep the default port
+ }
+ }
+
+ if (port != DEFAULT_PORT) {
+ sb.append(':')
+ .append(port);
+ }
+
+ sb.append("/#")
+ .append(Uri.encode(input));
+
+ Uri uri = Uri.parse(sb.toString());
+
+ return uri;
+ }
+
+ /**
+ * Handle challenges from keyboard-interactive authentication mode.
+ */
+ public String[] replyToChallenge(String name, String instruction, int numPrompts, String[] prompt, boolean[] echo) {
+ interactiveCanContinue = true;
+ String[] responses = new String[numPrompts];
+ for(int i = 0; i < numPrompts; i++) {
+ // request response from user for each prompt
+ responses[i] = bridge.promptHelper.requestStringPrompt(instruction, prompt[i]);
+ }
+ return responses;
+ }
+
+ @Override
+ public HostBean createHost(Uri uri) {
+ HostBean host = new HostBean();
+
+ host.setProtocol(PROTOCOL);
+
+ host.setHostname(uri.getHost());
+
+ int port = uri.getPort();
+ if (port < 0)
+ port = DEFAULT_PORT;
+ host.setPort(port);
+
+ host.setUsername(uri.getUserInfo());
+
+ String nickname = uri.getFragment();
+ if (nickname == null || nickname.length() == 0) {
+ host.setNickname(getDefaultNickname(host.getUsername(),
+ host.getHostname(), host.getPort()));
+ } else {
+ host.setNickname(uri.getFragment());
+ }
+
+ return host;
+ }
+
+ @Override
+ public void getSelectionArgs(Uri uri, Map<String, String> selection) {
+ selection.put(HostDatabase.FIELD_HOST_PROTOCOL, PROTOCOL);
+ selection.put(HostDatabase.FIELD_HOST_NICKNAME, uri.getFragment());
+ selection.put(HostDatabase.FIELD_HOST_HOSTNAME, uri.getHost());
+
+ int port = uri.getPort();
+ if (port < 0)
+ port = DEFAULT_PORT;
+ selection.put(HostDatabase.FIELD_HOST_PORT, Integer.toString(port));
+ selection.put(HostDatabase.FIELD_HOST_USERNAME, uri.getUserInfo());
+ }
+
+ @Override
+ public void setCompression(boolean compression) {
+ this.compression = compression;
+ }
+
+ public static String getFormatHint(Context context) {
+ return String.format("%s@%s:%s",
+ context.getString(R.string.format_username),
+ context.getString(R.string.format_hostname),
+ context.getString(R.string.format_port));
+ }
+
+ @Override
+ public void setUseAuthAgent(String useAuthAgent) {
+ this.useAuthAgent = useAuthAgent;
+ }
+
+ public Map<String,byte[]> retrieveIdentities() {
+ Map<String,byte[]> pubKeys = new HashMap<String,byte[]>(manager.loadedKeypairs.size());
+
+ for (Entry<String,KeyHolder> entry : manager.loadedKeypairs.entrySet()) {
+ KeyPair pair = entry.getValue().pair;
+
+ try {
+ PrivateKey privKey = pair.getPrivate();
+ if (privKey instanceof RSAPrivateKey) {
+ RSAPublicKey pubkey = (RSAPublicKey) pair.getPublic();
+ pubKeys.put(entry.getKey(), RSASHA1Verify.encodeSSHRSAPublicKey(pubkey));
+ } else if (privKey instanceof DSAPrivateKey) {
+ DSAPublicKey pubkey = (DSAPublicKey) pair.getPublic();
+ pubKeys.put(entry.getKey(), DSASHA1Verify.encodeSSHDSAPublicKey(pubkey));
+ } else
+ continue;
+ } catch (IOException e) {
+ continue;
+ }
+ }
+
+ return pubKeys;
+ }
+
+ public KeyPair getKeyPair(byte[] publicKey) {
+ String nickname = manager.getKeyNickname(publicKey);
+
+ if (nickname == null)
+ return null;
+
+ if (useAuthAgent.equals(HostDatabase.AUTHAGENT_NO)) {
+ Log.e(TAG, "");
+ return null;
+ } else if (useAuthAgent.equals(HostDatabase.AUTHAGENT_CONFIRM) ||
+ manager.loadedKeypairs.get(nickname).bean.isConfirmUse()) {
+ if (!promptForPubkeyUse(nickname))
+ return null;
+ }
+ return manager.getKey(nickname);
+ }
+
+ private boolean promptForPubkeyUse(String nickname) {
+ Boolean result = bridge.promptHelper.requestBooleanPrompt(null,
+ manager.res.getString(R.string.prompt_allow_agent_to_use_key,
+ nickname));
+ return result;
+ }
+
+ public boolean addIdentity(KeyPair pair, String comment, boolean confirmUse, int lifetime) {
+ PubkeyBean pubkey = new PubkeyBean();
+// pubkey.setType(PubkeyDatabase.KEY_TYPE_IMPORTED);
+ pubkey.setNickname(comment);
+ pubkey.setConfirmUse(confirmUse);
+ pubkey.setLifetime(lifetime);
+ manager.addKey(pubkey, pair);
+ return true;
+ }
+
+ public boolean removeAllIdentities() {
+ manager.loadedKeypairs.clear();
+ return true;
+ }
+
+ public boolean removeIdentity(byte[] publicKey) {
+ return manager.removeKey(publicKey);
+ }
+
+ public boolean isAgentLocked() {
+ return agentLockPassphrase != null;
+ }
+
+ public boolean requestAgentUnlock(String unlockPassphrase) {
+ if (agentLockPassphrase == null)
+ return false;
+
+ if (agentLockPassphrase.equals(unlockPassphrase))
+ agentLockPassphrase = null;
+
+ return agentLockPassphrase == null;
+ }
+
+ public boolean setAgentLock(String lockPassphrase) {
+ if (agentLockPassphrase != null)
+ return false;
+
+ agentLockPassphrase = lockPassphrase;
+ return true;
+ }
+
+ /* (non-Javadoc)
+ * @see org.connectbot.transport.AbsTransport#usesNetwork()
+ */
+ @Override
+ public boolean usesNetwork() {
+ return true;
+ }
+}
diff --git a/app/src/main/java/org/connectbot/transport/Telnet.java b/app/src/main/java/org/connectbot/transport/Telnet.java
new file mode 100644
index 0000000..5fde2f6
--- /dev/null
+++ b/app/src/main/java/org/connectbot/transport/Telnet.java
@@ -0,0 +1,330 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * 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 org.connectbot.transport;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.nio.charset.Charset;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.connectbot.R;
+import org.connectbot.bean.HostBean;
+import org.connectbot.service.TerminalBridge;
+import org.connectbot.service.TerminalManager;
+import org.connectbot.util.HostDatabase;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+import de.mud.telnet.TelnetProtocolHandler;
+
+/**
+ * Telnet transport implementation.<br/>
+ * Original idea from the JTA telnet package (de.mud.telnet)
+ *
+ * @author Kenny Root
+ *
+ */
+public class Telnet extends AbsTransport {
+ private static final String TAG = "ConnectBot.Telnet";
+ private static final String PROTOCOL = "telnet";
+
+ private static final int DEFAULT_PORT = 23;
+
+ private TelnetProtocolHandler handler;
+ private Socket socket;
+
+ private InputStream is;
+ private OutputStream os;
+ private int width;
+ private int height;
+
+ private boolean connected = false;
+
+ static final Pattern hostmask;
+ static {
+ hostmask = Pattern.compile("^([0-9a-z.-]+)(:(\\d+))?$", Pattern.CASE_INSENSITIVE);
+ }
+
+ public Telnet() {
+ handler = new TelnetProtocolHandler() {
+ /** get the current terminal type */
+ @Override
+ public String getTerminalType() {
+ return getEmulation();
+ }
+
+ /** get the current window size */
+ @Override
+ public int[] getWindowSize() {
+ return new int[] { width, height };
+ }
+
+ /** notify about local echo */
+ @Override
+ public void setLocalEcho(boolean echo) {
+ /* EMPTY */
+ }
+
+ /** write data to our back end */
+ @Override
+ public void write(byte[] b) throws IOException {
+ if (os != null)
+ os.write(b);
+ }
+
+ /** sent on IAC EOR (prompt terminator for remote access systems). */
+ @Override
+ public void notifyEndOfRecord() {
+ }
+
+ @Override
+ protected String getCharsetName() {
+ Charset charset = bridge.getCharset();
+ if (charset != null)
+ return charset.name();
+ else
+ return "";
+ }
+ };
+ }
+
+ /**
+ * @param host
+ * @param bridge
+ * @param manager
+ */
+ public Telnet(HostBean host, TerminalBridge bridge, TerminalManager manager) {
+ super(host, bridge, manager);
+ }
+
+ public static String getProtocolName() {
+ return PROTOCOL;
+ }
+
+ @Override
+ public void connect() {
+ try {
+ socket = new Socket(host.getHostname(), host.getPort());
+
+ connected = true;
+
+ is = socket.getInputStream();
+ os = socket.getOutputStream();
+
+ bridge.onConnected();
+ } catch (UnknownHostException e) {
+ Log.d(TAG, "IO Exception connecting to host", e);
+ } catch (IOException e) {
+ Log.d(TAG, "IO Exception connecting to host", e);
+ }
+ }
+
+ @Override
+ public void close() {
+ connected = false;
+ if (socket != null)
+ try {
+ socket.close();
+ socket = null;
+ } catch (IOException e) {
+ Log.d(TAG, "Error closing telnet socket.", e);
+ }
+ }
+
+ @Override
+ public void flush() throws IOException {
+ os.flush();
+ }
+
+ @Override
+ public int getDefaultPort() {
+ return DEFAULT_PORT;
+ }
+
+ @Override
+ public boolean isConnected() {
+ return connected;
+ }
+
+ @Override
+ public boolean isSessionOpen() {
+ return connected;
+ }
+
+ @Override
+ public int read(byte[] buffer, int start, int len) throws IOException {
+ /* process all already read bytes */
+ int n = 0;
+
+ do {
+ n = handler.negotiate(buffer, start);
+ if (n > 0)
+ return n;
+ } while (n == 0);
+
+ while (n <= 0) {
+ do {
+ n = handler.negotiate(buffer, start);
+ if (n > 0)
+ return n;
+ } while (n == 0);
+ n = is.read(buffer, start, len);
+ if (n < 0) {
+ bridge.dispatchDisconnect(false);
+ throw new IOException("Remote end closed connection.");
+ }
+
+ handler.inputfeed(buffer, start, n);
+ n = handler.negotiate(buffer, start);
+ }
+ return n;
+ }
+
+ @Override
+ public void write(byte[] buffer) throws IOException {
+ try {
+ if (os != null)
+ os.write(buffer);
+ } catch (SocketException e) {
+ bridge.dispatchDisconnect(false);
+ }
+ }
+
+ @Override
+ public void write(int c) throws IOException {
+ try {
+ if (os != null)
+ os.write(c);
+ } catch (SocketException e) {
+ bridge.dispatchDisconnect(false);
+ }
+ }
+
+ @Override
+ public void setDimensions(int columns, int rows, int width, int height) {
+ try {
+ handler.setWindowSize(columns, rows);
+ } catch (IOException e) {
+ Log.e(TAG, "Couldn't resize remote terminal", e);
+ }
+ }
+
+ @Override
+ public String getDefaultNickname(String username, String hostname, int port) {
+ if (port == DEFAULT_PORT) {
+ return String.format("%s", hostname);
+ } else {
+ return String.format("%s:%d", hostname, port);
+ }
+ }
+
+ public static Uri getUri(String input) {
+ Matcher matcher = hostmask.matcher(input);
+
+ if (!matcher.matches())
+ return null;
+
+ StringBuilder sb = new StringBuilder();
+
+ sb.append(PROTOCOL)
+ .append("://")
+ .append(matcher.group(1));
+
+ String portString = matcher.group(3);
+ int port = DEFAULT_PORT;
+ if (portString != null) {
+ try {
+ port = Integer.parseInt(portString);
+ if (port < 1 || port > 65535) {
+ port = DEFAULT_PORT;
+ }
+ } catch (NumberFormatException nfe) {
+ // Keep the default port
+ }
+ }
+
+ if (port != DEFAULT_PORT) {
+ sb.append(':');
+ sb.append(port);
+ }
+
+ sb.append("/#")
+ .append(Uri.encode(input));
+
+ Uri uri = Uri.parse(sb.toString());
+
+ return uri;
+ }
+
+ @Override
+ public HostBean createHost(Uri uri) {
+ HostBean host = new HostBean();
+
+ host.setProtocol(PROTOCOL);
+
+ host.setHostname(uri.getHost());
+
+ int port = uri.getPort();
+ if (port < 0)
+ port = DEFAULT_PORT;
+ host.setPort(port);
+
+ String nickname = uri.getFragment();
+ if (nickname == null || nickname.length() == 0) {
+ host.setNickname(getDefaultNickname(host.getUsername(),
+ host.getHostname(), host.getPort()));
+ } else {
+ host.setNickname(uri.getFragment());
+ }
+
+ return host;
+ }
+
+ @Override
+ public void getSelectionArgs(Uri uri, Map<String, String> selection) {
+ selection.put(HostDatabase.FIELD_HOST_PROTOCOL, PROTOCOL);
+ selection.put(HostDatabase.FIELD_HOST_NICKNAME, uri.getFragment());
+ selection.put(HostDatabase.FIELD_HOST_HOSTNAME, uri.getHost());
+
+ int port = uri.getPort();
+ if (port < 0)
+ port = DEFAULT_PORT;
+ selection.put(HostDatabase.FIELD_HOST_PORT, Integer.toString(port));
+ }
+
+ public static String getFormatHint(Context context) {
+ return String.format("%s:%s",
+ context.getString(R.string.format_hostname),
+ context.getString(R.string.format_port));
+ }
+
+ /* (non-Javadoc)
+ * @see org.connectbot.transport.AbsTransport#usesNetwork()
+ */
+ @Override
+ public boolean usesNetwork() {
+ return true;
+ }
+}
diff --git a/app/src/main/java/org/connectbot/transport/TransportFactory.java b/app/src/main/java/org/connectbot/transport/TransportFactory.java
new file mode 100644
index 0000000..72e5e08
--- /dev/null
+++ b/app/src/main/java/org/connectbot/transport/TransportFactory.java
@@ -0,0 +1,132 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2007 Kenny Root, Jeffrey Sharkey
+ *
+ * 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 org.connectbot.transport;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.connectbot.bean.HostBean;
+import org.connectbot.util.HostDatabase;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class TransportFactory {
+ private static final String TAG = "ConnectBot.TransportFactory";
+
+ private static String[] transportNames = {
+ SSH.getProtocolName(),
+ Telnet.getProtocolName(),
+ Local.getProtocolName(),
+ };
+
+ /**
+ * @param protocol
+ * @return
+ */
+ public static AbsTransport getTransport(String protocol) {
+ if (SSH.getProtocolName().equals(protocol)) {
+ return new SSH();
+ } else if (Telnet.getProtocolName().equals(protocol)) {
+ return new Telnet();
+ } else if (Local.getProtocolName().equals(protocol)) {
+ return new Local();
+ } else {
+ return null;
+ }
+ }
+
+ public static Uri getUri(String scheme, String input) {
+ Log.d("TransportFactory", String.format(
+ "Attempting to discover URI for scheme=%s on input=%s", scheme,
+ input));
+ if (SSH.getProtocolName().equals(scheme))
+ return SSH.getUri(input);
+ else if (Telnet.getProtocolName().equals(scheme))
+ return Telnet.getUri(input);
+ else if (Local.getProtocolName().equals(scheme)) {
+ Log.d("TransportFactory", "Got to the local parsing area");
+ return Local.getUri(input);
+ } else
+ return null;
+ }
+
+ public static String[] getTransportNames() {
+ return transportNames;
+ }
+
+ public static boolean isSameTransportType(AbsTransport a, AbsTransport b) {
+ if (a == null || b == null)
+ return false;
+
+ return a.getClass().equals(b.getClass());
+ }
+
+ public static boolean canForwardPorts(String protocol) {
+ // TODO uh, make this have less knowledge about its children
+ if (SSH.getProtocolName().equals(protocol)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @param protocol text name of protocol
+ * @param context
+ * @return expanded format hint
+ */
+ public static String getFormatHint(String protocol, Context context) {
+ if (SSH.getProtocolName().equals(protocol)) {
+ return SSH.getFormatHint(context);
+ } else if (Telnet.getProtocolName().equals(protocol)) {
+ return Telnet.getFormatHint(context);
+ } else if (Local.getProtocolName().equals(protocol)) {
+ return Local.getFormatHint(context);
+ } else {
+ return AbsTransport.getFormatHint(context);
+ }
+ }
+
+ /**
+ * @param hostdb Handle to HostDatabase
+ * @param uri URI to target server
+ * @param host HostBean in which to put the results
+ * @return true when host was found
+ */
+ public static HostBean findHost(HostDatabase hostdb, Uri uri) {
+ AbsTransport transport = getTransport(uri.getScheme());
+
+ Map<String, String> selection = new HashMap<String, String>();
+
+ transport.getSelectionArgs(uri, selection);
+ if (selection.size() == 0) {
+ Log.e(TAG, String.format("Transport %s failed to do something useful with URI=%s",
+ uri.getScheme(), uri.toString()));
+ throw new IllegalStateException("Failed to get needed selection arguments");
+ }
+
+ return hostdb.findHost(selection);
+ }
+}