/*
 * Copyright 2001-2003, Danger, Inc.  All Rights Reserved.
 * This file is subject to the Danger, Inc. Sample Code License,
 * which is provided in the file SAMPLE_CODE_LICENSE.
 * Copies are also available from http://developer.danger.com/
 */

package com.danger.terminal;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import danger.app.Application;
import danger.app.Event;
import danger.app.Listener;
import danger.storage.StorageManager;
import danger.ui.CheckBox;
import danger.ui.DialogWindow;
import danger.ui.Menu;
import danger.ui.MenuItem;
import danger.ui.PopupMenu;
import danger.ui.StaticText;
import danger.ui.StaticTextBox;
import danger.ui.TextField;
import danger.ui.View;
import danger.util.ByteArray;
import danger.util.format.StringFormat;

import net.lag.jaramiko.Channel;
import net.lag.jaramiko.ChannelError;
import net.lag.jaramiko.ChannelException;
import net.lag.jaramiko.ClientTransport;
import net.lag.jaramiko.PasswordRequiredException;
import net.lag.jaramiko.PKey;
import net.lag.jaramiko.SecurityOptions;
import net.lag.jaramiko.SSHException;


public class JaramikoSession
    extends Listener
    implements Session, TerminalResources, TerminalEvents
{
    private static class KeyFile
    {
        public
        KeyFile (File file, boolean needsPassword)
        {
            mFile = file;
            mNeedsPassword = needsPassword;
        }
        
        public File mFile;
        public boolean mNeedsPassword;
    }
    
    
    class JaramikoInputStream
        extends InputStream
    {
        public int read()
            throws IOException
        {
            byte[] b = new byte[1];
            read(b);
            return b[0];
        }

        public int
        read (byte[] buffer, int offset, int length)
            throws IOException
        {
            while (true) {
                boolean haveChannel = true;
                synchronized (JaramikoSession.this) {
                    if (mChannelInputStream == null) {
                        haveChannel = false;
                    }
                }

                if (haveChannel) {
                    if (mChannel.isClosed()) {
                        throw new IOException("Disconnected");
                    }
                    
                    // jaramiko will notify() us when new data is ready
                    synchronized (this) {
                        try {
                            this.wait(1000);
                        } catch (InterruptedException x) { }
                    }
                                            
                    if (mChannel.isClosed()) {
                        throw new IOException("Disconnected");
                    }
                    
                    if (mChannelInputStream.available() > 0) {
                        return mChannelInputStream.read(buffer, offset, length);
                    }
                }

                synchronized (JaramikoSession.this) {
                    if (mDisconnected) {
                        /* do this after the above output buffer checks,
                         * so the console will read anything buffered first.
                         */
                        throw new IOException("Disconnected");
                    }
                }
                
                if (! haveChannel) {
                    // avoid spinning the cpu while waiting for the channel to emerge
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException x) {
                        // pass
                    }
                }
            }
        }
        
        public int
        read (byte[] buffer)
            throws IOException
        {
            return read(buffer, 0, buffer.length);
        }
    }
    
    
    class JaramikoOutputStream
        extends OutputStream
    {
        public void
        write (int value)
            throws IOException
        {
            synchronized (JaramikoSession.this) {
                if (mChannelOutputStream != null) {
                    mChannelOutputStream.write(value);
                }
            }
        }

        public void
        write (byte[] stuff, int offset, int length)
            throws IOException
        {
            synchronized (JaramikoSession.this) {
                if (mChannelOutputStream != null) {
                    mChannelOutputStream.write(stuff, offset, length);
                }
            }
        }

        public void
        write (byte[] stuff)
            throws IOException
        {
            synchronized (JaramikoSession.this) {
                if (mChannelOutputStream != null) {
                    mChannelOutputStream.write(stuff);
                }
            }
        }
    }
    
    
    public
    JaramikoSession (SSHServerInfo info, Socket socket, int width, int height, String terminalType)
        throws IOException
    {
        mInfo = info;
        mRawSocket = socket;
        mColumns = width;
        mRows = height;
        mTerminalType = terminalType;
        
        mTransport = null;
        mChannelInputStream = null;
        mChannelOutputStream = null;
        mInputStream = new JaramikoInputStream();
        mOutputStream = new JaramikoOutputStream();
        mErrorReporter = null;
    }

    public void
    setErrorReporter (TerminalConsole.ErrorReporter reporter)
    {
        mErrorReporter = reporter;
    }
    
    public void
    refreshShow ()
    {
        synchronized (this) {
            // bottom to top
            if (mHostKeyWindow != null) {
                mHostKeyWindow.show();
            }
            if (mLoginWindow != null) {
                mLoginWindow.show();
            }
            if (mKeyWindow != null) {
                mKeyWindow.show();
            }
        }        
    }
    
    public void
    resize (final int cols, final int rows)
    {
        mColumns = cols;
        mRows = rows;

        if (mLoggedIn) {
            try {
                // don't wait for a reply
                mChannel.resizePTY(cols, rows, 0);
            } catch (IOException x) {
                // pass
            }
        }
    }
    
    public InputStream
    getInputStream ()
    {
        return mInputStream;
    }
    
    public OutputStream
    getOutputStream ()
    {
        return mOutputStream;
    }
    
    private void
    saveHostKey ()
    {
        try {
            PKey hostKey = mTransport.getRemoteServerKey();
            if (hostKey.getSSHName().equals("ssh-rsa")) {
                mInfo.setRSAHostKey(hostKey.toByteArray());
            } else {
                mInfo.setDSSHostKey(hostKey.toByteArray());
            }
        } catch (SSHException x) {
            System.err.println("Unable to save host key! " + x);
        }
    }
    
    public boolean
    receiveEvent (Event e)
    {
        System.err.println("event: " + e);
        switch (e.type) {
        case EVENT_AUTHENTICATE:
            synchronized (this) {
                mLoginWindow = null;
                DialogWindow dialog = (DialogWindow) e.argument;
                mLogin = ((TextField) dialog.getDescendantWithID(ID_LOGIN_USERNAME)).toString();
                mPassword = ((TextField) dialog.getDescendantWithID(ID_LOGIN_PASSWORD)).toString();
                mKeyFile = null;
                mKey = null;
                int n = ((PopupMenu) dialog.getDescendantWithID(ID_LOGIN_KEY)).getValue();
                if ((n > 0) && (n <= mKeyList.size())) {
                    mPassword = null;
                    mKeyFile = ((KeyFile) mKeyList.get(n - 1)).mFile;
                }
                mInfo.setUsername(mLogin);
                mInfo.setRememberPassword(((CheckBox) dialog.getDescendantWithID(ID_LOGIN_REMEMBER)).getValue() != 0);
                mReadyToLogin = true;
                this.notifyAll();
            }
            return true;
            
        case EVENT_DONT_AUTHENTICATE:
            mErrorReporter.reportError(Terminal.instance().getString(STRING_DISCONNECTED_NO_AUTH));
            synchronized (this) {
                mLoginWindow = null;
                disconnect();
            }
            return true;
        
        case EVENT_KEY_AUTHENTICATE:
            synchronized (this) {
                mKeyWindow = null;
                DialogWindow dialog = (DialogWindow) e.argument;
                mKeyPassword = ((TextField) dialog.getDescendantWithID(ID_KEY_PASSWORD)).toString();
                mCacheKey = (((CheckBox) dialog.getDescendantWithID(ID_KEY_REMEMBER)).getValue() != 0);
                this.notifyAll();
            }
            return true;
        
        case EVENT_KEY_DONT_AUTHENTICATE:
            synchronized (this) {
                mReadyToLogin = false;
                mKeyWindow = null;
                mKeyFile = null;
                mKey = null;
                this.notifyAll();
            }
            return true;
            
        case EVENT_SAVE_HOST_KEY:
            saveHostKey();
            // fall through
            
        case EVENT_DISMISS_HOST_KEY:
            synchronized (this) {
                mHostKeyWindow = null;
                mHostKeyOkay = true;
                this.notifyAll();
            }
            return true;
            
        case EVENT_REJECT_HOST_KEY:
            mErrorReporter.reportError(Terminal.instance().getString(STRING_HOST_KEY_REJECTED));
            synchronized (this) {
                mHostKeyWindow.hide();
                mHostKeyWindow = null;
                disconnect();
            }
            return true;
            
        case EVENT_KEY_MENU:
            if (e.what >= 0) {
                int n = ((PopupMenu) mLoginWindow.getDescendantWithID(ID_LOGIN_KEY)).getValue();
                switch (n) {
                case 0:
                    // no key
                    ((StaticText) mLoginWindow.getDescendantWithID(ID_LOGIN_PASSWORD_TEXT)).enable();
                    ((TextField) mLoginWindow.getDescendantWithID(ID_LOGIN_PASSWORD)).enable();
                    break;
                default:
                    ((StaticText) mLoginWindow.getDescendantWithID(ID_LOGIN_PASSWORD_TEXT)).disable();
                    ((TextField) mLoginWindow.getDescendantWithID(ID_LOGIN_PASSWORD)).disable();
                }
            }
            return true;            
        }
        
        return super.receiveEvent(e);
    }

    
    // -----  package

    
    /* package */ void
    startSession ()
    {
        new Thread(new Runnable() {
            public void run () {
                privateRun();
            }
        }, "ssh: " + mInfo.getHostname()).start();
    }
    
    /**
     * If the user asks us to remember their password, the settings screen
     * may call this function to retrieve the password they used.
     */
    /* package */ String
    getPassword ()
    {
        if (mLoggedIn && (mLogin != null) && (mPassword != null)) {
            return mPassword;
        } else {
            return null;
        }
    }
    
    /* package */ String
    getKeyFile ()
    {
        if (mLoggedIn && (mLogin != null) && (mKeyFile != null)) {
            return mKeyFile.getAbsolutePath();
        } else {
            return null;
        }
    }
    
    /**
     * Rescan the removable storage filesystems, looking for private keys.
     */
    /* package */ static void
    rescanMedia ()
    {
        synchronized (JaramikoSession.class) {
            sKeyList.clear();
            sCachedKeyMap.clear();
            
            String[] media = StorageManager.getRemovablePaths();
            for (int i = 0; i < media.length; i++) {
                FolderWalker.Filter keyFilter = new FolderWalker.Filter () {
                    public boolean checkFilter (File f) {
                        String name = f.getName().toLowerCase();
                        return name.endsWith(".key") || name.equals("id_rsa") || name.equals("id_dsa");
                    }
                };
                
                for (Iterator iter = new FolderWalker(new File(media[i]), keyFilter); iter.hasNext(); ) {
                    File f = (File) iter.next();
                    
                    boolean needsPassword = false;
                    try {
                        System.err.println("MonkeyTerm attempting to read file " + f);
                        PKey.readPrivateKeyFromStream(new FileInputStream(f), null);
                    } catch (PasswordRequiredException x) {
                        needsPassword = true;
                    } catch (IOException x) {
                        continue;
                    }
                    
                    sKeyList.add(new KeyFile(f, needsPassword));
                    System.err.println("MonkeyTerm found key file: " + f);
                }
            }
        }
    }
    
    
    // -----  private
    
    
    private void
    disconnect ()
    {
        synchronized (this) {
            mDisconnected = true;
            this.notifyAll();   // wake up people waiting on login info
            if (mLoginWindow != null) {
                mLoginWindow.hide();
                mLoginWindow = null;
            }
            if (mHostKeyWindow != null) {
                mHostKeyWindow.hide();
                mHostKeyWindow = null;
            }
            if (mTransport != null) {
                mTransport.close();
                mTransport = null;
            }
            try {
                mRawSocket.close();
            } catch (IOException x) { }
        }
    }

    private void
    popupLoginDialog (int failures)
    {
        Application app = Terminal.instance();
        
        // get list of available keys
        mKeyList = new ArrayList();
        synchronized (JaramikoSession.class) {
            for (Iterator i = sKeyList.iterator(); i.hasNext(); ) {
                KeyFile f = (KeyFile) i.next();
                mKeyList.add(f);
            }
        }
        
        synchronized (this) {
            mReadyToLogin = false;
            mLoginWindow = app.getDialog(ID_LOGIN_DIALOG, this);

            StaticTextBox text = (StaticTextBox) mLoginWindow.getDescendantWithID(ID_LOGIN_DIALOG_HEADING);

            if (failures > 0) {
                text.setText(StringFormat.fromRsrc(STRING_RETRY_LOGIN, mInfo.getHostname()));
            } else {
                text.setText(StringFormat.withFormat(text.toString(), mInfo.getHostname()));
            }

            mLoginWindow.setCancelButtonEvent(new Event(this, EVENT_DONT_AUTHENTICATE, 0, 0, mLoginWindow));

            String login = mInfo.getUsername();
            String password = null;
            String keyFile = null;
            boolean foundKeyFile = false;
            if (login != null) {
                password = mInfo.getSavedPassword();
                keyFile = mInfo.getSavedKeyFile();
            }

            if (login != null) {
                ((TextField) mLoginWindow.getDescendantWithID(ID_LOGIN_USERNAME)).setText(login);
            }
            if (password != null) {
                ((TextField) mLoginWindow.getDescendantWithID(ID_LOGIN_PASSWORD)).setText(password);
            }

            Menu keyMenu = (Menu) mLoginWindow.getDescendantWithID(ID_LOGIN_KEY);
            keyMenu.setMaxItemID(mKeyList.size() + 1);
            int n = 1;
            
            // lock cuz we're looking in the cached key map
            synchronized (JaramikoSession.class) {
                for (Iterator i = mKeyList.iterator(); i.hasNext(); ) {
                    KeyFile f = (KeyFile) i.next();
                    MenuItem item = new MenuItem(f.mFile.getName(), EVENT_KEY_MENU, 0, 0, null, this);
                    String path = f.mFile.getAbsolutePath();
                    if ((sCachedKeyMap.get(path) == null) && f.mNeedsPassword) {
                        item.setIcon(Terminal.instance().getBitmap(ID_LOCKED_KEY_ICON));
                    }
                    keyMenu.addItem(item);
                    if (path.equals(keyFile)) {
                        keyMenu.setValue(n);
                        foundKeyFile = true;
                    }
                    n++;
                }
            }
            if (! foundKeyFile) {
                keyFile = null;
            }
            
            if (mKeyList.isEmpty()) {
                // hide the key list and reflow the dialog so there's no gap there
                View v = mLoginWindow.getDescendantWithID(ID_LOGIN_KEY_TEXT);
                v.hide();
                keyMenu.disable();
                keyMenu.hide();
                mLoginWindow.reflowChildren(v, -keyMenu.getHeight());
            }
            
            ((CheckBox) mLoginWindow.getDescendantWithID(ID_LOGIN_REMEMBER)).setValue(mInfo.getRememberPassword() ? 1 : 0);

            /* if the user has checked "remember this auth" in the past, and we
             * have all the info we need, don't actually draw the dialog: just
             * send the "finished" event to ourselves.
             */
            if ((failures == 0) && mInfo.getRememberPassword() && (login != null)) {
                if ((password != null) || (keyFile != null)) {
                    new Event(this, EVENT_AUTHENTICATE, 0, 0, mLoginWindow).sendToTarget();
                    return;
                }
            }
            
            mLoginWindow.show();

            /* show resets the focus'd field... */
            if (login != null && login.length() > 0) {
                if (password != null && password.length() > 0) {
                    mLoginWindow.setFocusedDescendant(mLoginWindow.getDescendantWithID(ID_LOGIN_OK));
                } else {
                    mLoginWindow.setFocusedDescendant(mLoginWindow.getDescendantWithID(ID_LOGIN_PASSWORD));
                }
            }
        }
    }
    
    private void
    popupUnlockKeyDialog ()
    {
        Application app = Terminal.instance();
        
        synchronized (this) {
            mKeyWindow = app.getDialog(ID_KEY_PASSWORD_DIALOG, this);
            mKeyWindow.setTitle(StringFormat.withFormat(mKeyWindow.getTitle(), mKeyFile.getName()));
            mKeyWindow.setCancelButtonEvent(new Event(this, EVENT_KEY_DONT_AUTHENTICATE, 0, 0, mKeyWindow));
            mKeyWindow.show();
            mKeyWindow.setFocusedDescendant(mKeyWindow.getDescendantWithID(ID_KEY_PASSWORD));
        }
    }
    
    private void
    checkHostKey ()
        throws IOException
    {
        PKey hostKey = mTransport.getRemoteServerKey();
        byte[] hostKeyBytes = hostKey.toByteArray();
        byte[] savedKey = null;
        String keyName = null;
        if (hostKey.getSSHName().equals("ssh-rsa")) {
            savedKey = mInfo.getRSAHostKey();
            keyName = "RSA";
        } else {
            savedKey = mInfo.getDSSHostKey();
            keyName = "DSS";
        }

        if (savedKey == null) {
            // don't have a saved host key for this host
            synchronized (this) {
                mHostKeyWindow = (HostKeyDialog) Terminal.instance().getDialog(ID_HOST_KEY_DIALOG, this);
                mHostKeyWindow.setTitleFormatText(mInfo.getHostname());
                mHostKeyWindow.setHostKey(keyName, hostKeyBytes);
                mHostKeyWindow.show();
            }
            // wait for result from host-key dialog
            return;
        }
        
        if (! ByteArray.equals(savedKey, hostKeyBytes)) {
            // this is bad: host key has changed
            synchronized (this) {
                mHostKeyWindow = (HostKeyDialog) Terminal.instance().getDialog(ID_CHANGED_HOST_KEY_DIALOG, this);
                // cancel is default
                mHostKeyWindow.setDefaultFrameButton(DialogWindow.TOP_POSITION1);
                mHostKeyWindow.setTitleFormatText(mInfo.getHostname());
                mHostKeyWindow.setHostKey(keyName, hostKeyBytes);
                mHostKeyWindow.show();
            }
            // wait for result from host-key dialog
            return;
        }
        
        // host key is good! let's login!
        mHostKeyOkay = true;
    }
    
    private void
    tryPrivateKeyAuth ()
        throws IOException
    {
        for (int tries = 0; tries < 3; tries++) {
            System.err.println("login with key: " + mKeyFile);
            try {
                synchronized (JaramikoSession.class) {
                    mKey = (PKey) sCachedKeyMap.get(mKeyFile.getAbsolutePath());
                }
                if (mKey == null) {
                    mKey = PKey.readPrivateKeyFromStream(new FileInputStream(mKeyFile), mKeyPassword);
                }
                break;
            } catch (PasswordRequiredException x) {
                // fall thru: ask for private key password
            } catch (SSHException x) {
                // usually means the password was incorrect
                mReadyToLogin = false;
                break;
            }
        
            popupUnlockKeyDialog();
            
            synchronized (this) {
                while ((mKeyPassword == null) && mReadyToLogin) {
                    try {
                        this.wait();
                    } catch (InterruptedException x) { }
                }
                if (! mReadyToLogin) {
                    break;
                }
            }
        }
    }
    
    // return true if we're in; false if we should abort
    private boolean
    tryAuthenticate ()
        throws IOException
    {
        int failed = 0;
        while (true) {
            if (failed >= 5) {
                // if we keep getting back here, eventually just give up.
                mErrorReporter.reportError(Terminal.instance().getString(STRING_AUTH_FAILED));
                disconnect();
                return false;
            }

            synchronized (this) {
                while (! mDisconnected && ! mReadyToLogin) {
                    // wait for a signal that we might've gotten auth info
                    try {
                        this.wait();
                    } catch (InterruptedException x) { }
                }
                if (mDisconnected) {
                    return false;
                }
            }
            
            if (! mHostKeyOkay) {
                // (only ask the first time thru this loop)
                checkHostKey();
                synchronized (this) {
                    while (! mDisconnected && ! mHostKeyOkay) {
                        // wait for a signal that we might've gotten auth info
                        try {
                            this.wait();
                        } catch (InterruptedException x) { }
                    }
                    if (mDisconnected) {
                        return false;
                    }
                }
            }
            
            if (mKeyFile != null) {
                tryPrivateKeyAuth();
            }
            if (! mReadyToLogin) {
                mKeyPassword = null;
                failed++;
                popupLoginDialog(failed);
                continue;
            }
        
            try {
                if (mKey != null) {
                    mTransport.authPrivateKey(mLogin, mKey, 60000);
                } else {
                    mTransport.authPassword(mLogin, mPassword, 60000);      // timeout = 60 sec
                }
                return true;
            } catch (SSHException x) {
                failed++;
                mLogin = null;
                mPassword = null;
                mKey = null;
                if (failed >= 3) {
                    // give up
                    mErrorReporter.reportError(Terminal.instance().getString(STRING_AUTH_FAILED));
                    disconnect();
                    return false;
                }
                popupLoginDialog(failed);
            }
        }
    }
    
    private void
    privateRun ()
    {
        try {
            mErrorReporter.reportStatus(Terminal.instance().getString(STRING_NEGOTIATING_ENCRYPTION));
            
            // probably not necessary to grab the lock, but i'm paranoid.
            synchronized (this) {
                mTransport = new ClientTransport(mRawSocket);
                mReadyToLogin = false;
                mHostKeyOkay = false;
                mKeyPassword = null;
                mKeyFile = null;
                mKey = null;
            }
            
            mTransport.setLog(new ConsoleLog());
            if (mInfo.getPreferDSS()) {
                // re-order the SecurityOptions to put RSA at the end
                SecurityOptions opt = mTransport.getSecurityOptions();
                List keys = opt.getKeys();
                keys.remove("ssh-rsa");
                keys.add("ssh-rsa");
                opt.setKeys(keys);
            }
            //mTransport.setDumpPackets(true);
            // distract the user with a login dialog
            popupLoginDialog(0);
            mTransport.start(null, 60000);    // timeout = 60 sec
            
            if (! tryAuthenticate()) {
                return;
            }
            
            mErrorReporter.reportStatus(Terminal.instance().getString(STRING_AUTHENTICATED));
            mErrorReporter.reportStatus("");
            
            if (mInfo.getRememberPassword()) {
                mInfo.setSavedPassword(mPassword);
                mInfo.setSavedKeyFile((mKeyFile == null) ? null : mKeyFile.getAbsolutePath());
                mInfo.save();
            }
            if (mCacheKey && (mKey != null)) {
                synchronized (JaramikoSession.class) {
                    sCachedKeyMap.put(mKeyFile.getAbsolutePath(), mKey);
                }
            }
            
            mChannel = mTransport.openSession(60000);      // timeout = 60 sec
            mChannel.setCombineStderr(true);
            // these two requests really should always succeed.
            if (! mChannel.getPTY(mTerminalType, mColumns, mRows, 60000)) {
                mErrorReporter.reportError(Terminal.instance().getString(STRING_UNABLE_TO_OPEN_PTY));
                disconnect();
                return;
            }
            if (! mChannel.invokeShell(60000)) {
                mErrorReporter.reportError(Terminal.instance().getString(STRING_UNABLE_TO_OPEN_SHELL));
                disconnect();
                return;
            }
            mChannel.setNotifyObject(mInputStream);
            mLoggedIn = true;
            
            synchronized (this) {
                mChannelInputStream = mChannel.getInputStream();
                mChannelOutputStream = mChannel.getOutputStream();
            }
        } catch (ChannelException x) {
            System.err.println(x);
            synchronized (this) {
                String msg = Terminal.instance().getString(STRING_CONNECTION_FAILED);
                // but override with i18n message:
                switch (x.getChannelError()) {
                case ChannelError.ADMINISTRATIVELY_PROHIBITED:
                    msg = Terminal.instance().getString(STRING_CHAN_PROHIBITED);
                    break;
                case ChannelError.CONNECT_FAILED:
                    msg = Terminal.instance().getString(STRING_CHAN_CONNECT_FAILED);
                    break;
                case ChannelError.UNKNOWN_CHANNEL_TYPE:
                    msg = Terminal.instance().getString(STRING_CHAN_UNKNOWN_TYPE);
                    break;
                case ChannelError.RESOURCE_SHORTAGE:
                    msg = Terminal.instance().getString(STRING_CHAN_RESOURCE_SHORTAGE);
                    break;
                }
                mErrorReporter.reportError(msg);
                disconnect();
            }
        } catch (IOException x) {
            System.err.println(x);
            synchronized (this) {
                mErrorReporter.reportError(Terminal.instance().getString(STRING_CONNECTION_CLOSED));
                disconnect();
            }
        }
    }

    
    static {
        ClientTransport.setCrai(new DangerCRAI());
    }

    
    private SSHServerInfo mInfo;
    private Socket mRawSocket;
    private boolean mDisconnected = false;
    private boolean mLoggedIn = false;

    // current terminal configuration
    private String mTerminalType;
    private int mColumns;
    private int mRows;

    // login dialog
    private DialogWindow mLoginWindow;
    private String mLogin;
    private String mPassword;
    private boolean mReadyToLogin;
    
    // unlock private key dialog
    private DialogWindow mKeyWindow;
    private String mKeyPassword;
    private List mKeyList;
    private File mKeyFile;
    private PKey mKey;
    private boolean mCacheKey;

    // host key dialog
    private HostKeyDialog mHostKeyWindow;
    private boolean mHostKeyOkay;
    
    private ClientTransport mTransport;
    private Channel mChannel;
    private InputStream mChannelInputStream;
    private OutputStream mChannelOutputStream;
    private JaramikoInputStream mInputStream;
    private JaramikoOutputStream mOutputStream;
    private TerminalConsole.ErrorReporter mErrorReporter;
    
    private static List sKeyList = new ArrayList();
    
    // cache of unlocked private keys: Map<String filename, Pkey>
    private static Map sCachedKeyMap = new HashMap();
}
