<commit_before>/*
	ConnectBot: simple, powerful, open-source SSH client for Android
	Copyright (C) 2007-2008 <NAME>, <NAME>
	
	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.
	
	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.
	
	You should have received a copy of the GNU General Public License
	along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package org.connectbot.service;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.LinkedList;
import java.util.List;

import org.connectbot.R;
import org.connectbot.TerminalView;
import org.connectbot.bean.HostBean;
import org.connectbot.bean.PortForwardBean;
import org.connectbot.util.HostDatabase;
import org.connectbot.util.PubkeyDatabase;
import org.connectbot.util.PubkeyUtils;

import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.graphics.Bitmap.Config;
import android.graphics.Paint.FontMetricsInt;
import android.util.Log;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnKeyListener;

import com.trilead.ssh2.ChannelCondition;
import com.trilead.ssh2.Connection;
import com.trilead.ssh2.ConnectionMonitor;
import com.trilead.ssh2.DynamicPortForwarder;
import com.trilead.ssh2.InteractiveCallback;
import com.trilead.ssh2.KnownHosts;
import com.trilead.ssh2.LocalPortForwarder;
import com.trilead.ssh2.ServerHostKeyVerifier;
import com.trilead.ssh2.Session;
import com.trilead.ssh2.crypto.PEMDecoder;

import de.mud.terminal.VDUBuffer;
import de.mud.terminal.VDUDisplay;
import de.mud.terminal.vt320;


/**
 * Provides a bridge between a MUD terminal buffer and a possible TerminalView.
 * This separation allows us to keep the TerminalBridge running in a background
 * service. A TerminalView shares down a bitmap that we can use for rendering
 * when available.
 * 
 * This class also provides SSH hostkey verification prompting, and password
 * prompting.
 */
public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCallback, ConnectionMonitor {
	
	public final static String TAG = TerminalBridge.class.toString();
	
	public final static int TERM_WIDTH_CHARS = 80,
		TERM_HEIGHT_CHARS = 24,
		DEFAULT_FONT_SIZE = 10;
	
	public final static String ENCODING = "ASCII";
	public static final String AUTH_PUBLICKEY = "publickey",
		AUTH_PASSWORD = "password",
		AUTH_KEYBOARDINTERACTIVE = "keyboard-interactive";
	
	public final static int AUTH_TRIES = 20;

	private int darken(int color) {
		return Color.argb(0xFF,
			(int)(Color.red(color) * 0.8),
			(int)(Color.green(color) * 0.8),
			(int)(Color.blue(color) * 0.8)
		);
	}
	
	private List<PortForwardBean> portForwards = new LinkedList<PortForwardBean>();
	
	public int color[] = { Color.BLACK, Color.RED, Color.GREEN, Color.YELLOW,
		Color.BLUE, Color.MAGENTA, Color.CYAN, Color.WHITE, };
	
	public int darkerColor[] = new int[color.length];
	
	public final static int COLOR_FG_STD = 7;
	public final static int COLOR_BG_STD = 0;
	
	protected final TerminalManager manager;

	public HostBean host;
	
	public final Connection connection;
	protected Session session;
	
	protected final Paint defaultPaint;

	protected OutputStream stdin;
	protected InputStream stdout;
	
	private InputStream stderr;

	protected Thread relay;
	
	protected final String emulation;
	protected final int scrollback;

	public Bitmap bitmap = null;
	public VDUBuffer buffer = null;
	
	protected TerminalView parent = null;
	protected Canvas canvas = new Canvas();

	private boolean ctrlPressed = false;
	private boolean altPressed = false;
	private boolean shiftPressed = false;

	private boolean pubkeysExhausted = false;
	
	private boolean authenticated = false;
	private boolean sessionOpen = false;
	private boolean disconnected = false;
	private boolean awaitingClose = false;

	private boolean forcedSize = false;
	private int termWidth;
	private int termHeight;
	
	public String keymode = null;

	public KeyCharacterMap keymap = KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD);

	public int charWidth = -1;

	public int charHeight = -1;

	public int charDescent = -1;

	protected float fontSize = -1;

	/**
	 * Flag indicating if we should perform a full-screen redraw during our next
	 * rendering pass.
	 */
	protected boolean fullRedraw = false;

	public long drawTolerance = 100;

	public long lastDraw = 0;

	public PromptHelper promptHelper;

	protected BridgeDisconnectedListener disconnectListener = null;

	public class HostKeyVerifier implements ServerHostKeyVerifier {
		
		public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) throws Exception {

			// read in all known hosts from hostdb
			KnownHosts hosts = manager.hostdb.getKnownHosts();
			Boolean result;
			
			String matchName = String.format("%s:%d", hostname, port);
			
			switch(hosts.verifyHostkey(matchName, serverHostKeyAlgorithm, serverHostKey)) {
			case KnownHosts.HOSTKEY_IS_OK:
				return true;

			case KnownHosts.HOSTKEY_IS_NEW:
				// prompt user
				outputLine(String.format("The authenticity of host '%s' can't be established.", hostname));
				outputLine(String.format("RSA key fingerprint is %s", KnownHosts.createHexFingerprint(serverHostKeyAlgorithm, serverHostKey)));

				result = promptHelper.requestBooleanPrompt("Are you sure you want\nto 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:
				outputLine("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
				outputLine("@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @");
				outputLine("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
				outputLine("IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!");
				outputLine("Someone could be eavesdropping on you right now (man-in-the-middle attack)!");
				outputLine("It is also possible that the RSA host key has just been changed.");
				outputLine(String.format("RSA key fingerprint is %s", KnownHosts.createHexFingerprint(serverHostKeyAlgorithm, serverHostKey)));
				
				// Users have no way to delete keys, so we'll prompt them for now.
				result = promptHelper.requestBooleanPrompt("Are you sure you want\nto 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();				
			}
			
			return false;
			
		}
		
	}
	
	/**
	 * Create new terminal bridge with following parameters. We will immediately
	 * launch thread to start SSH connection and handle any hostkey verification
	 * and password authentication.
	 */
	public TerminalBridge(final TerminalManager manager, final HostBean host) throws Exception {
		
		this.manager = manager;
		this.host = host;
		
		emulation = manager.getEmulation();
		scrollback = manager.getScrollback();

		// create prompt helper to relay password and hostkey requests up to gui
		promptHelper = new PromptHelper(this);
		
		// create our default paint
		defaultPaint = new Paint();
		defaultPaint.setAntiAlias(true);
		defaultPaint.setTypeface(Typeface.MONOSPACE);
		
		setFontSize(DEFAULT_FONT_SIZE);

		// prepare our "darker" colors
		for(int i = 0; i < color.length; i++)
			darkerColor[i] = darken(color[i]);
		
		// create terminal buffer and handle outgoing data
		// this is probably status reply information
		buffer = new vt320() {
			public void write(byte[] b) {
				try {
					stdin.write(b);
				} catch (IOException e) {
					Log.e(TAG, "Problem handling incoming data in vt320() thread", e);
				} catch (NullPointerException npe) {
					// TODO buffer input?
					Log.d(TAG, "Input before we were connected discarded");
				}
			}

			public void sendTelnetCommand(byte cmd) {
			}

			public void setWindowSize(int c, int r) {
			}
		};

		buffer.setBufferSize(scrollback);
		buffer.setDisplay(this);
		buffer.setCursorPosition(0, 0);

		// TODO Change this when hosts are beans as well
		portForwards = manager.hostdb.getPortForwardsForHost(host);
		
		// prepare the ssh connection for opening
		// we perform the actual connection later in startConnection()
		outputLine(String.format("Connecting to %s:%d", host.getHostname(), host.getPort()));
		connection = new Connection(host.getHostname(), host.getPort());
		connection.addConnectionMonitor(this);
		connection.setCompression(host.getCompression());
	}
	
	/**
	 * Spawn thread to open connection and start login process.
	 */
	public void startConnection() {
		new Thread(new Runnable() {
			public void run() {
				try {
					connection.connect(new HostKeyVerifier());
				} catch (IOException e) {
					Log.e(TAG, "Problem in SSH connection thread during authentication", e);
					// TODO report cause to user:
					Log.d(TAG, String.format("Cause is: %s", e.getCause().toString()));
					dispatchDisconnect();
					return;
				}
				
				try {	
					// enter a loop to keep trying until authentication
					int tries = 0;
					while(!connection.isAuthenticationComplete() && tries++ < AUTH_TRIES && !disconnected) {
						handleAuthentication();
						
						// 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);
				}
			} 
		}).start();
	}
	
	/**
	 * Attempt connection with database row pointed to by cursor.
	 * @param cursor
	 * @return true for successful authentication
	 * @throws NoSuchAlgorithmException
	 * @throws InvalidKeySpecException
	 * @throws IOException
	 */
	public boolean tryPublicKey(Cursor c) throws NoSuchAlgorithmException, InvalidKeySpecException, IOException {
		int COL_NICKNAME = c.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_NICKNAME),
			COL_TYPE = c.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_TYPE),
			COL_PRIVATE = c.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_PRIVATE),
			COL_PUBLIC = c.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_PUBLIC),
			COL_ENCRYPTED = c.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_ENCRYPTED);

		String keyNickname = c.getString(COL_NICKNAME);
		int encrypted = c.getInt(COL_ENCRYPTED);
		
		Object trileadKey = null;
		if(manager.isKeyLoaded(keyNickname)) {
			// load this key from memory if its already there
			Log.d(TAG, String.format("Found unlocked key '%s' already in-memory", keyNickname));
			trileadKey = manager.getKey(keyNickname);
			
		} else {
			// otherwise load key from database and prompt for password as needed
			String password = null;
			if (encrypted != 0)
				password = promptHelper.requestStringPrompt(String.format("Password for key '%s'", keyNickname));

			String type = c.getString(COL_TYPE);
			if(PubkeyDatabase.KEY_TYPE_IMPORTED.equals(type)) {
				// load specific key using pem format
				byte[] raw = c.getBlob(COL_PRIVATE);
				trileadKey = PEMDecoder.decode(new String(raw).toCharArray(), password);
				
			} else {
				// load using internal generated format
				PrivateKey privKey;
				try {
					privKey = PubkeyUtils.decodePrivate(c.getBlob(COL_PRIVATE),
							c.getString(COL_TYPE), password);
				} catch (Exception e) {
					String message = String.format("Bad password for key '%s'. Authentication failed.", keyNickname);
					Log.e(TAG, message, e);
					outputLine(message);
					return false;
				}
				
				PublicKey pubKey = PubkeyUtils.decodePublic(c.getBlob(COL_PUBLIC),
						c.getString(COL_TYPE));
				
				// convert key to trilead format
				trileadKey = PubkeyUtils.convertToTrilead(privKey, pubKey);
				Log.d(TAG, "Unlocked key " + PubkeyUtils.formatKey(pubKey));
			}

			Log.d(TAG, String.format("Unlocked key '%s'", keyNickname));

			// save this key in-memory if option enabled
			if(manager.isSavingKeys()) {
				manager.addKey(keyNickname, trileadKey);
			}
		}

		return this.tryPublicKey(host.getUsername(), host.getNickname(), trileadKey);
		
	}
	
	protected boolean tryPublicKey(String username, String keyNickname, Object trileadKey) throws IOException {
		//outputLine(String.format("Attempting 'publickey' with key '%s' [%s]...", keyNickname, trileadKey.toString()));
		boolean success = connection.authenticateWithPublicKey(username, trileadKey);
		if(!success)
			outputLine(String.format("Authentication method 'publickey' with key '%s' failed", keyNickname));
		return success;
	}
	
	public void handleAuthentication() {
		try {
			if (connection.authenticateWithNone(host.getUsername())) {
				finishConnection();
				return;
			}
		} catch(Exception e) {
			Log.d(TAG, "Host does not support 'none' authentication.");
		}
		
		outputLine("Trying to authenticate");
		
		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
					outputLine("Attempting 'publickey' authentication with any in-memory SSH keys");
					for(String nickname : manager.loadedPubkeys.keySet()) {
						Object trileadKey = manager.loadedPubkeys.get(nickname);
						if(this.tryPublicKey(host.getUsername(), nickname, trileadKey)) {
							finishConnection();
							break;
						}
					}
					
				} else {
					outputLine("Attempting 'publickey' authentication with a specific SSH key");
					// use a specific key for this host, as requested
					Cursor cursor = manager.pubkeydb.getPubkey(pubkeyId);
					if (cursor.moveToFirst())
						if (tryPublicKey(cursor))
							finishConnection();
					cursor.close();
					
				}
				
				pubkeysExhausted = true;
			} else if (connection.isAuthMethodAvailable(host.getUsername(), AUTH_PASSWORD)) {
				outputLine("Attempting 'password' authentication");
				String password = promptHelper.requestStringPrompt("Password");
				if(connection.authenticateWithPassword(host.getUsername(), password)) {
					finishConnection();
				} else {
					outputLine("Authentication method 'password' failed");
				}
				
			} else if(connection.isAuthMethodAvailable(host.getUsername(), AUTH_KEYBOARDINTERACTIVE)) {
				// this auth method will talk with us using InteractiveCallback interface
				// it blocks until authentication finishes 
				outputLine("Attempting 'keyboard-interactive' authentication");
				if(connection.authenticateWithKeyboardInteractive(host.getUsername(), TerminalBridge.this)) {
					finishConnection();
				} else {
					outputLine("Authentication method 'keyboard-interactive' failed");
				}
				
			} else {
				outputLine("[Your host doesn't support 'password' or 'keyboard-interactive' authentication.]");
				
			}	
		} catch(Exception e) {
			Log.e(TAG, "Problem during handleAuthentication()", e);
		}
		
	}
	
	/**
	 * Handle challenges from keyboard-interactive authentication mode.
	 */
	public String[] replyToChallenge(String name, String instruction, int numPrompts, String[] prompt, boolean[] echo) throws Exception {
		String[] responses = new String[numPrompts];
		for(int i = 0; i < numPrompts; i++) {
			// request response from user for each prompt
			responses[i] = promptHelper.requestStringPrompt(prompt[i]);
		}
		return responses;
	}

	
	/**
	 * Convenience method for writing a line into the underlying MUD buffer.
	 */
	protected void outputLine(String line) {
		buffer.putString(0, buffer.getCursorRow(), line);
		buffer.setCursorPosition(0, buffer.getCursorRow() + 1);
		redraw();
	}

	/**
	 * Inject a specific string into this terminal. Used for post-login strings
	 * and pasting clipboard.
	 */
	public void injectString(final String string) {
		new Thread(new Runnable() {
			public void run() {
				if(string == null || string.length() == 0) return;
				KeyEvent[] events = keymap.getEvents(string.toCharArray());
				if(events == null || events.length == 0) return;
				for(KeyEvent event : events) {
					onKey(null, event.getKeyCode(), event);
				}
			}
		}).start();
	}
	
	/**
	 * Internal method to request actual PTY terminal once we've finished
	 * authentication. If called before authenticated, it will just fail.
	 */
	protected void finishConnection() {
		setAuthenticated(true);
		
		// Start up predefined port forwards
		for (PortForwardBean pfb : portForwards) {
			try {
				enablePortForward(pfb);
				outputLine(String.format("Enable port forward: %s", pfb.getDescription()));
			} catch (Exception e) {
				Log.e(TAG, "Error setting up port forward during connect", e);
			}
		}
		
		if (!host.getWantSession()) {
			outputLine("Session will not be started due to host preference.");
			return;
		}
		
		try {
			session = connection.openSession();
			buffer.deleteArea(0, 0, buffer.getColumns(), buffer.getRows());
			
			// previously tried vt100 and xterm for emulation modes
			// "screen" works the best for color and escape codes
			// TODO: pull this value from the preferences
			((vt320) buffer).setAnswerBack(emulation);
			session.requestPTY(emulation, termWidth, termHeight, 0, 0, null);
			session.startShell();
			
			// grab stdin/out from newly formed session
			stdin = session.getStdin();
			stdout = session.getStdout();
			stderr = session.getStderr();

			// create thread to relay incoming connection data to buffer
			relay = new Thread(new Runnable() {
				public void run() {
					byte[] b = new byte[4096];
					int n = 0;
					int conditions = ChannelCondition.STDOUT_DATA
							| ChannelCondition.STDERR_DATA
							| ChannelCondition.CLOSED
							| ChannelCondition.EXIT_STATUS;
					int newConditions = 0;
					while((newConditions & ChannelCondition.CLOSED) == 0) {
						try {
							newConditions = session.waitForCondition(conditions, 0);
							if ((newConditions & ChannelCondition.STDOUT_DATA) == ChannelCondition.STDOUT_DATA) {
								n = stdout.read(b);
								if (n > 0) {
									((vt320)buffer).putString(new String(b, 0, n, ENCODING));
									redraw();
								}
							}
							
							if ((newConditions & ChannelCondition.STDERR_DATA) == ChannelCondition.STDERR_DATA) {
								n = stderr.read(b);
								// TODO I don't know.. do we want this? We were ignoring it before
								Log.d(TAG, String.format("Read data from stderr: %s", new String(b, 0, n, ENCODING)));
							}
							
							if ((newConditions & ChannelCondition.EXIT_STATUS) == ChannelCondition.EXIT_STATUS) {
								// The other side closed our channel, so let's disconnect.
								// TODO review whether any tunnel is in use currently.
								dispatchDisconnect();
								break;
							}
						} catch (IOException e) {
							Log.e(TAG, "Problem while handling incoming data in relay thread", e);
							break;
						}
					}<commit_msg>Catch when our remote shell is killed.<commit_after>/*
	ConnectBot: simple, powerful, open-source SSH client for Android
	Copyright (C) 2007-2008 <NAME>, <NAME>
	
	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.
	
	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.
	
	You should have received a copy of the GNU General Public License
	along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package org.connectbot.service;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.LinkedList;
import java.util.List;

import org.connectbot.R;
import org.connectbot.TerminalView;
import org.connectbot.bean.HostBean;
import org.connectbot.bean.PortForwardBean;
import org.connectbot.util.HostDatabase;
import org.connectbot.util.PubkeyDatabase;
import org.connectbot.util.PubkeyUtils;

import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.graphics.Bitmap.Config;
import android.graphics.Paint.FontMetricsInt;
import android.util.Log;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnKeyListener;

import com.trilead.ssh2.ChannelCondition;
import com.trilead.ssh2.Connection;
import com.trilead.ssh2.ConnectionMonitor;
import com.trilead.ssh2.DynamicPortForwarder;
import com.trilead.ssh2.InteractiveCallback;
import com.trilead.ssh2.KnownHosts;
import com.trilead.ssh2.LocalPortForwarder;
import com.trilead.ssh2.ServerHostKeyVerifier;
import com.trilead.ssh2.Session;
import com.trilead.ssh2.crypto.PEMDecoder;

import de.mud.terminal.VDUBuffer;
import de.mud.terminal.VDUDisplay;
import de.mud.terminal.vt320;


/**
 * Provides a bridge between a MUD terminal buffer and a possible TerminalView.
 * This separation allows us to keep the TerminalBridge running in a background
 * service. A TerminalView shares down a bitmap that we can use for rendering
 * when available.
 * 
 * This class also provides SSH hostkey verification prompting, and password
 * prompting.
 */
public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCallback, ConnectionMonitor {
	
	public final static String TAG = TerminalBridge.class.toString();
	
	public final static int TERM_WIDTH_CHARS = 80,
		TERM_HEIGHT_CHARS = 24,
		DEFAULT_FONT_SIZE = 10;
	
	public final static String ENCODING = "ASCII";
	public static final String AUTH_PUBLICKEY = "publickey",
		AUTH_PASSWORD = "password",
		AUTH_KEYBOARDINTERACTIVE = "keyboard-interactive";
	
	public final static int AUTH_TRIES = 20;

	private int darken(int color) {
		return Color.argb(0xFF,
			(int)(Color.red(color) * 0.8),
			(int)(Color.green(color) * 0.8),
			(int)(Color.blue(color) * 0.8)
		);
	}
	
	private List<PortForwardBean> portForwards = new LinkedList<PortForwardBean>();
	
	public int color[] = { Color.BLACK, Color.RED, Color.GREEN, Color.YELLOW,
		Color.BLUE, Color.MAGENTA, Color.CYAN, Color.WHITE, };
	
	public int darkerColor[] = new int[color.length];
	
	public final static int COLOR_FG_STD = 7;
	public final static int COLOR_BG_STD = 0;
	
	protected final TerminalManager manager;

	public HostBean host;
	
	public final Connection connection;
	protected Session session;
	
	protected final Paint defaultPaint;

	protected OutputStream stdin;
	protected InputStream stdout;
	
	private InputStream stderr;

	protected Thread relay;
	
	protected final String emulation;
	protected final int scrollback;

	public Bitmap bitmap = null;
	public VDUBuffer buffer = null;
	
	protected TerminalView parent = null;
	protected Canvas canvas = new Canvas();

	private boolean ctrlPressed = false;
	private boolean altPressed = false;
	private boolean shiftPressed = false;

	private boolean pubkeysExhausted = false;
	
	private boolean authenticated = false;
	private boolean sessionOpen = false;
	private boolean disconnected = false;
	private boolean awaitingClose = false;

	private boolean forcedSize = false;
	private int termWidth;
	private int termHeight;
	
	public String keymode = null;

	public KeyCharacterMap keymap = KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD);

	public int charWidth = -1;

	public int charHeight = -1;

	public int charDescent = -1;

	protected float fontSize = -1;

	/**
	 * Flag indicating if we should perform a full-screen redraw during our next
	 * rendering pass.
	 */
	protected boolean fullRedraw = false;

	public long drawTolerance = 100;

	public long lastDraw = 0;

	public PromptHelper promptHelper;

	protected BridgeDisconnectedListener disconnectListener = null;

	public class HostKeyVerifier implements ServerHostKeyVerifier {
		
		public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) throws Exception {

			// read in all known hosts from hostdb
			KnownHosts hosts = manager.hostdb.getKnownHosts();
			Boolean result;
			
			String matchName = String.format("%s:%d", hostname, port);
			
			switch(hosts.verifyHostkey(matchName, serverHostKeyAlgorithm, serverHostKey)) {
			case KnownHosts.HOSTKEY_IS_OK:
				return true;

			case KnownHosts.HOSTKEY_IS_NEW:
				// prompt user
				outputLine(String.format("The authenticity of host '%s' can't be established.", hostname));
				outputLine(String.format("RSA key fingerprint is %s", KnownHosts.createHexFingerprint(serverHostKeyAlgorithm, serverHostKey)));

				result = promptHelper.requestBooleanPrompt("Are you sure you want\nto 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:
				outputLine("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
				outputLine("@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @");
				outputLine("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
				outputLine("IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!");
				outputLine("Someone could be eavesdropping on you right now (man-in-the-middle attack)!");
				outputLine("It is also possible that the RSA host key has just been changed.");
				outputLine(String.format("RSA key fingerprint is %s", KnownHosts.createHexFingerprint(serverHostKeyAlgorithm, serverHostKey)));
				
				// Users have no way to delete keys, so we'll prompt them for now.
				result = promptHelper.requestBooleanPrompt("Are you sure you want\nto 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();				
			}
			
			return false;
			
		}
		
	}
	
	/**
	 * Create new terminal bridge with following parameters. We will immediately
	 * launch thread to start SSH connection and handle any hostkey verification
	 * and password authentication.
	 */
	public TerminalBridge(final TerminalManager manager, final HostBean host) throws Exception {
		
		this.manager = manager;
		this.host = host;
		
		emulation = manager.getEmulation();
		scrollback = manager.getScrollback();

		// create prompt helper to relay password and hostkey requests up to gui
		promptHelper = new PromptHelper(this);
		
		// create our default paint
		defaultPaint = new Paint();
		defaultPaint.setAntiAlias(true);
		defaultPaint.setTypeface(Typeface.MONOSPACE);
		
		setFontSize(DEFAULT_FONT_SIZE);

		// prepare our "darker" colors
		for(int i = 0; i < color.length; i++)
			darkerColor[i] = darken(color[i]);
		
		// create terminal buffer and handle outgoing data
		// this is probably status reply information
		buffer = new vt320() {
			public void write(byte[] b) {
				try {
					stdin.write(b);
				} catch (IOException e) {
					Log.e(TAG, "Problem handling incoming data in vt320() thread", e);
				} catch (NullPointerException npe) {
					// TODO buffer input?
					Log.d(TAG, "Input before we were connected discarded");
				}
			}

			public void sendTelnetCommand(byte cmd) {
			}

			public void setWindowSize(int c, int r) {
			}
		};

		buffer.setBufferSize(scrollback);
		buffer.setDisplay(this);
		buffer.setCursorPosition(0, 0);

		// TODO Change this when hosts are beans as well
		portForwards = manager.hostdb.getPortForwardsForHost(host);
		
		// prepare the ssh connection for opening
		// we perform the actual connection later in startConnection()
		outputLine(String.format("Connecting to %s:%d", host.getHostname(), host.getPort()));
		connection = new Connection(host.getHostname(), host.getPort());
		connection.addConnectionMonitor(this);
		connection.setCompression(host.getCompression());
	}
	
	/**
	 * Spawn thread to open connection and start login process.
	 */
	public void startConnection() {
		new Thread(new Runnable() {
			public void run() {
				try {
					connection.connect(new HostKeyVerifier());
				} catch (IOException e) {
					Log.e(TAG, "Problem in SSH connection thread during authentication", e);
					// TODO report cause to user:
					Log.d(TAG, String.format("Cause is: %s", e.getCause().toString()));
					dispatchDisconnect();
					return;
				}
				
				try {	
					// enter a loop to keep trying until authentication
					int tries = 0;
					while(!connection.isAuthenticationComplete() && tries++ < AUTH_TRIES && !disconnected) {
						handleAuthentication();
						
						// 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);
				}
			} 
		}).start();
	}
	
	/**
	 * Attempt connection with database row pointed to by cursor.
	 * @param cursor
	 * @return true for successful authentication
	 * @throws NoSuchAlgorithmException
	 * @throws InvalidKeySpecException
	 * @throws IOException
	 */
	public boolean tryPublicKey(Cursor c) throws NoSuchAlgorithmException, InvalidKeySpecException, IOException {
		int COL_NICKNAME = c.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_NICKNAME),
			COL_TYPE = c.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_TYPE),
			COL_PRIVATE = c.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_PRIVATE),
			COL_PUBLIC = c.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_PUBLIC),
			COL_ENCRYPTED = c.getColumnIndexOrThrow(PubkeyDatabase.FIELD_PUBKEY_ENCRYPTED);

		String keyNickname = c.getString(COL_NICKNAME);
		int encrypted = c.getInt(COL_ENCRYPTED);
		
		Object trileadKey = null;
		if(manager.isKeyLoaded(keyNickname)) {
			// load this key from memory if its already there
			Log.d(TAG, String.format("Found unlocked key '%s' already in-memory", keyNickname));
			trileadKey = manager.getKey(keyNickname);
			
		} else {
			// otherwise load key from database and prompt for password as needed
			String password = null;
			if (encrypted != 0)
				password = promptHelper.requestStringPrompt(String.format("Password for key '%s'", keyNickname));

			String type = c.getString(COL_TYPE);
			if(PubkeyDatabase.KEY_TYPE_IMPORTED.equals(type)) {
				// load specific key using pem format
				byte[] raw = c.getBlob(COL_PRIVATE);
				trileadKey = PEMDecoder.decode(new String(raw).toCharArray(), password);
				
			} else {
				// load using internal generated format
				PrivateKey privKey;
				try {
					privKey = PubkeyUtils.decodePrivate(c.getBlob(COL_PRIVATE),
							c.getString(COL_TYPE), password);
				} catch (Exception e) {
					String message = String.format("Bad password for key '%s'. Authentication failed.", keyNickname);
					Log.e(TAG, message, e);
					outputLine(message);
					return false;
				}
				
				PublicKey pubKey = PubkeyUtils.decodePublic(c.getBlob(COL_PUBLIC),
						c.getString(COL_TYPE));
				
				// convert key to trilead format
				trileadKey = PubkeyUtils.convertToTrilead(privKey, pubKey);
				Log.d(TAG, "Unlocked key " + PubkeyUtils.formatKey(pubKey));
			}

			Log.d(TAG, String.format("Unlocked key '%s'", keyNickname));

			// save this key in-memory if option enabled
			if(manager.isSavingKeys()) {
				manager.addKey(keyNickname, trileadKey);
			}
		}

		return this.tryPublicKey(host.getUsername(), host.getNickname(), trileadKey);
		
	}
	
	protected boolean tryPublicKey(String username, String keyNickname, Object trileadKey) throws IOException {
		//outputLine(String.format("Attempting 'publickey' with key '%s' [%s]...", keyNickname, trileadKey.toString()));
		boolean success = connection.authenticateWithPublicKey(username, trileadKey);
		if(!success)
			outputLine(String.format("Authentication method 'publickey' with key '%s' failed", keyNickname));
		return success;
	}
	
	public void handleAuthentication() {
		try {
			if (connection.authenticateWithNone(host.getUsername())) {
				finishConnection();
				return;
			}
		} catch(Exception e) {
			Log.d(TAG, "Host does not support 'none' authentication.");
		}
		
		outputLine("Trying to authenticate");
		
		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
					outputLine("Attempting 'publickey' authentication with any in-memory SSH keys");
					for(String nickname : manager.loadedPubkeys.keySet()) {
						Object trileadKey = manager.loadedPubkeys.get(nickname);
						if(this.tryPublicKey(host.getUsername(), nickname, trileadKey)) {
							finishConnection();
							break;
						}
					}
					
				} else {
					outputLine("Attempting 'publickey' authentication with a specific SSH key");
					// use a specific key for this host, as requested
					Cursor cursor = manager.pubkeydb.getPubkey(pubkeyId);
					if (cursor.moveToFirst())
						if (tryPublicKey(cursor))
							finishConnection();
					cursor.close();
					
				}
				
				pubkeysExhausted = true;
			} else if (connection.isAuthMethodAvailable(host.getUsername(), AUTH_PASSWORD)) {
				outputLine("Attempting 'password' authentication");
				String password = promptHelper.requestStringPrompt("Password");
				if(connection.authenticateWithPassword(host.getUsername(), password)) {
					finishConnection();
				} else {
					outputLine("Authentication method 'password' failed");
				}
				
			} else if(connection.isAuthMethodAvailable(host.getUsername(), AUTH_KEYBOARDINTERACTIVE)) {
				// this auth method will talk with us using InteractiveCallback interface
				// it blocks until authentication finishes 
				outputLine("Attempting 'keyboard-interactive' authentication");
				if(connection.authenticateWithKeyboardInteractive(host.getUsername(), TerminalBridge.this)) {
					finishConnection();
				} else {
					outputLine("Authentication method 'keyboard-interactive' failed");
				}
				
			} else {
				outputLine("[Your host doesn't support 'password' or 'keyboard-interactive' authentication.]");
				
			}	
		} catch(Exception e) {
			Log.e(TAG, "Problem during handleAuthentication()", e);
		}
		
	}
	
	/**
	 * Handle challenges from keyboard-interactive authentication mode.
	 */
	public String[] replyToChallenge(String name, String instruction, int numPrompts, String[] prompt, boolean[] echo) throws Exception {
		String[] responses = new String[numPrompts];
		for(int i = 0; i < numPrompts; i++) {
			// request response from user for each prompt
			responses[i] = promptHelper.requestStringPrompt(prompt[i]);
		}
		return responses;
	}

	
	/**
	 * Convenience method for writing a line into the underlying MUD buffer.
	 */
	protected void outputLine(String line) {
		buffer.putString(0, buffer.getCursorRow(), line);
		buffer.setCursorPosition(0, buffer.getCursorRow() + 1);
		redraw();
	}

	/**
	 * Inject a specific string into this terminal. Used for post-login strings
	 * and pasting clipboard.
	 */
	public void injectString(final String string) {
		new Thread(new Runnable() {
			public void run() {
				if(string == null || string.length() == 0) return;
				KeyEvent[] events = keymap.getEvents(string.toCharArray());
				if(events == null || events.length == 0) return;
				for(KeyEvent event : events) {
					onKey(null, event.getKeyCode(), event);
				}
			}
		}).start();
	}
	
	/**
	 * Internal method to request actual PTY terminal once we've finished
	 * authentication. If called before authenticated, it will just fail.
	 */
	protected void finishConnection() {
		setAuthenticated(true);
		
		// Start up predefined port forwards
		for (PortForwardBean pfb : portForwards) {
			try {
				enablePortForward(pfb);
				outputLine(String.format("Enable port forward: %s", pfb.getDescription()));
			} catch (Exception e) {
				Log.e(TAG, "Error setting up port forward during connect", e);
			}
		}
		
		if (!host.getWantSession()) {
			outputLine("Session will not be started due to host preference.");
			return;
		}
		
		try {
			session = connection.openSession();
			buffer.deleteArea(0, 0, buffer.getColumns(), buffer.getRows());
			
			// previously tried vt100 and xterm for emulation modes
			// "screen" works the best for color and escape codes
			// TODO: pull this value from the preferences
			((vt320) buffer).setAnswerBack(emulation);
			session.requestPTY(emulation, termWidth, termHeight, 0, 0, null);
			session.startShell();
			
			// grab stdin/out from newly formed session
			stdin = session.getStdin();
			stdout = session.getStdout();
			stderr = session.getStderr();

			// create thread to relay incoming connection data to buffer
			relay = new Thread(new Runnable() {
				public void run() {
					byte[] b = new byte[4096];
					int n = 0;
					int conditions = ChannelCondition.STDOUT_DATA
							| ChannelCondition.STDERR_DATA
							| ChannelCondition.CLOSED
							| ChannelCondition.EXIT_STATUS
							| ChannelCondition.EXIT_SIGNAL;
					int newConditions = 0;
					while((newConditions & ChannelCondition.CLOSED) == 0) {
						try {
							newConditions = session.waitForCondition(conditions, 0);
							if ((newConditions & ChannelCondition.STDOUT_DATA) != 0) {
								n = stdout.read(b);
								if (n > 0) {
									((vt320)buffer).putString(new String(b, 0, n, ENCODING));
									redraw();
								}
							}
							
							if ((newConditions & ChannelCondition.STDERR_DATA) != 0) {
								n = stderr.read(b);
								// TODO I don't know.. do we want this? We were ignoring it before
								Log.d(TAG, String.format("Read data from stderr: %s", new String(b, 0, n, ENCODING)));
							}
							
							if ((newConditions & (ChannelCondition.EXIT_STATUS | ChannelCondition.EXIT_SIGNAL)) != 0) {
								// The other side closed our channel, so let's disconnect.
								// TODO review whether any tunnel is in use currently.
								dispatchDisconnect();
								break;
							}
						} catch (IOException e) {
							Log.e(TAG, "Problem while handling incoming data in relay thread", e);
							break;
						}
					}
{'fraction_non_alphanumeric': 0.10647902953267256, 'fraction_numerical': 0.004716148394744143, 'mean_word_length': 3.830896686159844, 'pattern_counts': {'":': 0, '<': 19, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4255777', 'n_tokens_mistral': 13989, 'n_tokens_neox': 11249, 'n_words': 3320}