view src/com/five_ten_sg/connectbot/transport/SSH.java @ 166:9d6b581b4656

add ctrl keys for attn and sysreq
author Carl Byington <carl@five-ten-sg.com>
date Tue, 24 Jun 2014 12:46:23 -0700
parents 77ac18bc1b2f
children 91a31873c42a
line wrap: on
line source

/*
 * 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 {
    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;
            }
        }

    }


    public SSH() {
        super();
    }


    /**
     * @return protocol part of the URI
     */
    public static String getProtocolName() {
        return PROTOCOL;
    }


    public 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;
    }


    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 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 void flush() throws IOException {
        if (stdin != null)
            stdin.flush();
    }


    public void connectionLost(Throwable reason) {
        onDisconnect();
    }


    private void onDisconnect() {
        close();
        bridge.dispatchDisconnect(false);
    }


    @Override
    public void close() {
        connected = false;

        if (session != null) {
            session.close();
            session = null;
        }

        if (connection != null) {
            connection.close();
            connection = null;
        }
    }


    @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 void setOptions(Map<String, String> options) {
        if (options.containsKey("compression"))
            compression = Boolean.parseBoolean(options.get("compression"));

        if (options.containsKey("httpproxy"))
            httpproxy = options.get("httpproxy");
    }


    @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 setCompression(boolean compression) {
        this.compression = compression;
    }


    @Override
    public void setHttpproxy(String httpproxy) {
        this.httpproxy = httpproxy;
    }


    @Override
    public void setUseAuthAgent(String useAuthAgent) {
        this.useAuthAgent = useAuthAgent;
    }

    @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 int getDefaultPort() {
        return DEFAULT_PORT;
    }


    @Override
    public boolean isConnected() {
        return connected;
    }


    @Override
    public boolean isSessionOpen() {
        return sessionOpen;
    }


    @Override
    public boolean isAuthenticated() {
        return authenticated;
    }


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


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


    public 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));
    }


    /**
     * @return do we use the network
     */
    @Override
    public boolean usesNetwork() {
        return true;
    }


    /**
     * 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;
    }

    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;
    }

}