diff src/com/five_ten_sg/connectbot/transport/SSH.java @ 0:0ce5cc452d02

initial version
author Carl Byington <carl@five-ten-sg.com>
date Thu, 22 May 2014 10:41:19 -0700
parents
children 3b760b39962a
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/com/five_ten_sg/connectbot/transport/SSH.java	Thu May 22 10:41:19 2014 -0700
@@ -0,0 +1,1113 @@
+/*
+ * 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 com.five_ten_sg.connectbot.transport;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.URL;
+import java.net.MalformedURLException;
+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 com.five_ten_sg.connectbot.R;
+import com.five_ten_sg.connectbot.bean.HostBean;
+import com.five_ten_sg.connectbot.bean.PortForwardBean;
+import com.five_ten_sg.connectbot.bean.PubkeyBean;
+import com.five_ten_sg.connectbot.service.TerminalBridge;
+import com.five_ten_sg.connectbot.service.TerminalManager;
+import com.five_ten_sg.connectbot.service.TerminalManager.KeyHolder;
+import com.five_ten_sg.connectbot.util.HostDatabase;
+import com.five_ten_sg.connectbot.util.PubkeyDatabase;
+import com.five_ten_sg.connectbot.util.PubkeyUtils;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Environment;
+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.SCPClient;
+import com.trilead.ssh2.ServerHostKeyVerifier;
+import com.trilead.ssh2.Session;
+import com.trilead.ssh2.HTTPProxyData;
+import com.trilead.ssh2.HTTPProxyException;
+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 String httpproxy = null;
+    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().requestPasswordPrompt(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().requestPasswordPrompt(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);
+
+            if (host.getWantX11Forward()) {
+                try {
+                    session.requestX11Forwarding(host.getX11Host(), host.getX11Port(), null, false);
+                }
+                catch (IOException e2) {
+                    Log.e(TAG, "Problem while trying to setup X11 forwarding in finishConnection()", e2);
+                }
+            }
+
+            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);
+        }
+
+        if (httpproxy != null && httpproxy.length() > 0) {
+            Log.d(TAG, "Want HTTP Proxy: " + httpproxy, null);
+
+            try {
+                URL u;
+
+                if (httpproxy.startsWith("http://")) {
+                    u = new URL(httpproxy);
+                }
+                else {
+                    u = new URL("http://" + httpproxy);
+                }
+
+                connection.setProxyData(new HTTPProxyData(
+                                            u.getHost(),
+                                            u.getPort(),
+                                            u.getUserInfo(),
+                                            u.getAuthority()));
+                bridge.outputLine("Connecting via proxy: " + httpproxy);
+            }
+            catch (MalformedURLException e) {
+                Log.e(TAG, "Could not parse proxy " + httpproxy, e);
+                // Display the reason in the text.
+                bridge.outputLine("Bad proxy URL: " + httpproxy);
+                onDisconnect();
+                return;
+            }
+        }
+
+        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 (HTTPProxyException e) {
+            Log.e(TAG, "Failed to connect to HTTP Proxy", e);
+            // Display the reason in the text.
+            bridge.outputLine("Failed to connect to HTTP Proxy.");
+            onDisconnect();
+            return;
+        }
+        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 boolean willBlock() {
+        if (stdout == null) return true;
+        try {
+            return stdout.available() == 0;
+        } catch (Exception e) {
+            return true;
+        }
+    }
+
+    @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));
+
+        if (httpproxy != null)
+            options.put("httpproxy", httpproxy);
+
+        return options;
+    }
+
+    @Override
+    public void setOptions(Map<String, String> options) {
+        if (options.containsKey("compression"))
+            compression = Boolean.parseBoolean(options.get("compression"));
+
+        if (options.containsKey("httpproxy"))
+            httpproxy = options.get("httpproxy");
+    }
+
+    public static String getProtocolName() {
+        return PROTOCOL;
+    }
+
+    @Override
+    public boolean isSessionOpen() {
+        return sessionOpen;
+    }
+
+    @Override
+    public boolean isConnected() {
+        return connected;
+    }
+
+    @Override
+    public boolean isAuthenticated() {
+        return authenticated;
+    }
+
+    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 boolean canTransferFiles() {
+        return true;
+    }
+
+    @Override
+    public boolean downloadFile(String remoteFile, String localFolder) {
+        try {
+            SCPClient client = new SCPClient(connection);
+
+            if (localFolder == null || localFolder == "")
+                localFolder = Environment.getExternalStorageDirectory().getAbsolutePath();
+
+            File dir = new File(localFolder);
+            dir.mkdirs();
+            client.get(remoteFile, localFolder);
+            return true;
+        }
+        catch (IOException e) {
+            Log.e(TAG, "Could not download remote file", e);
+            return false;
+        }
+    }
+
+    @Override
+    public boolean uploadFile(String localFile, String remoteFile,
+                              String remoteFolder, String mode) {
+        try {
+            SCPClient client = new SCPClient(connection);
+
+            if (remoteFolder == null)
+                remoteFolder = "";
+
+            if (remoteFile == null || remoteFile == "")
+                client.put(localFile, remoteFolder, mode);
+            else
+                client.put(localFile, remoteFile, remoteFolder, mode);
+
+            return true;
+        }
+        catch (IOException e) {
+            Log.e(TAG, "Could not upload local file", e);
+            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.requestPasswordPrompt(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;
+    }
+
+    @Override
+    public void setHttpproxy(String httpproxy) {
+        this.httpproxy = httpproxy;
+    }
+
+    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 com.five_ten_sg.connectbot.transport.AbsTransport#usesNetwork()
+     */
+    @Override
+    public boolean usesNetwork() {
+        return true;
+    }
+}