/*
* 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 net.danopia.protonet.service;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import net.danopia.protonet.R;
import net.danopia.protonet.TerminalView;
import net.danopia.protonet.bean.ChannelBean;
import net.danopia.protonet.bean.HostBean;
import android.util.Log;
/**
* 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 {
public final static String TAG = "ConnectBot.TerminalBridge";
public Integer[] color;
protected final TerminalManager manager;
public HostBean host;
/* package */ Transport transport;
private Relay relay;
private final int scrollback;
public ArrayList<String> buffer = null;
private TerminalView parent = null;
private boolean disconnected = false;
private boolean awaitingClose = false;
private final List<String> localOutput;
public PromptHelper promptHelper;
protected BridgeDisconnectedListener disconnectListener = null;
/**
* Create a new terminal bridge suitable for unit testing.
*/
public TerminalBridge() {
buffer = new ArrayList<String>();
manager = null;
scrollback = 1;
localOutput = new LinkedList<String>();
transport = null;
}
/**
* 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 IOException {
this.manager = manager;
this.host = host;
scrollback = manager.getScrollback();
// create prompt helper to relay password and hostkey requests up to gui
promptHelper = new PromptHelper(this);
localOutput = new LinkedList<String>();
// create terminal buffer and handle outgoing data
// this is probably status reply information
buffer = new ArrayList<String>();
//buffer.setBufferSize(scrollback);
}
public PromptHelper getPromptHelper() {
return promptHelper;
}
/**
* Spawn thread to open connection and start login process.
*/
protected void startConnection() {
transport = new Transport();
transport.setBridge(this);
transport.setManager(manager);
transport.setHost(host);
if (transport.canChannels()) {
for (ChannelBean portForward : manager.hostdb.getChannelsForHost(host))
transport.addChannel(portForward);
}
outputLine(manager.res.getString(R.string.terminal_connecting, host.getHostname(), host.getPort()));
Thread connectionThread = new Thread(new Runnable() {
public void run() {
transport.connect();
}
});
connectionThread.setName("Connection");
connectionThread.setDaemon(true);
connectionThread.start();
}
/**
* Handle challenges from keyboard-interactive authentication mode.
*/
public String[] replyToChallenge(String name, String instruction, int numPrompts, String[] prompt, boolean[] echo) {
String[] responses = new String[numPrompts];
for(int i = 0; i < numPrompts; i++) {
// request response from user for each prompt
responses[i] = promptHelper.requestStringPrompt(instruction, prompt[i]);
}
return responses;
}
/**
* @return charset in use by bridge
*/
public Charset getCharset() {
return relay.getCharset();
}
/**
* Sets the encoding used by the terminal. If the connection is live,
* then the character set is changed for the next read.
* @param encoding the canonical name of the character encoding
*/
public void setCharset(String encoding) {
if (relay != null)
relay.setCharset(encoding);
}
/**
* Convenience method for writing a line into the underlying MUD buffer.
* Should never be called once the session is established.
*/
public final void outputLine(String line) {
if (transport != null && transport.isSessionOpen())
Log.e(TAG, "Session established, cannot use outputLine!", new IOException("outputLine call traceback"));
synchronized (localOutput) {
final String s = line + "\r\n";
localOutput.add(s);
buffer.add(s);
}
}
/**
* Inject a specific string into this terminal. Used for post-login strings
* and pasting clipboard.
*/
public void injectString(final String string) {
if (string == null || string.length() == 0)
return;
Thread injectStringThread = new Thread(new Runnable() {
public void run() {
try {
transport.write(string.getBytes(host.getEncoding()));
} catch (Exception e) {
Log.e(TAG, "Couldn't inject string to remote host: ", e);
}
}
});
injectStringThread.setName("InjectString");
injectStringThread.start();
}
/**
* Internal method to request actual PTY terminal once we've finished
* authentication. If called before authenticated, it will just fail.
*/
public void onConnected() {
disconnected = false;
buffer.clear();
// We no longer need our local output.
localOutput.clear();
// create thread to relay incoming connection data to buffer
relay = new Relay(this, transport, buffer, host.getEncoding());
Thread relayThread = new Thread(relay);
relayThread.setDaemon(true);
relayThread.setName("Relay");
relayThread.start();
}
/**
* @return whether a session is open or not
*/
public boolean isSessionOpen() {
if (transport != null)
return transport.isSessionOpen();
return false;
}
public void setOnDisconnectedListener(BridgeDisconnectedListener disconnectListener) {
this.disconnectListener = disconnectListener;
}
/**
* Force disconnection of this terminal bridge.
*/
public void dispatchDisconnect(boolean immediate) {
// We don't need to do this multiple times.
synchronized (this) {
if (disconnected && !immediate)
return;
disconnected = true;
}
// Cancel any pending prompts.
promptHelper.cancelPrompt();
// disconnection request hangs if we havent really connected to a host yet
// temporary fix is to just spawn disconnection into a thread
Thread disconnectThread = new Thread(new Runnable() {
public void run() {
if (transport != null && transport.isConnected())
transport.close();
}
});
disconnectThread.setName("Disconnect");
disconnectThread.start();
if (immediate) {
awaitingClose = true;
if (disconnectListener != null)
disconnectListener.onDisconnected(TerminalBridge.this);
} else {
{
final String line = manager.res.getString(R.string.alert_disconnect_msg);
buffer.add("\r\n" + line + "\r\n");
}
if (host.getStayConnected()) {
manager.requestReconnect(this);
return;
}
Thread disconnectPromptThread = new Thread(new Runnable() {
public void run() {
Boolean result = promptHelper.requestBooleanPrompt(null,
manager.res.getString(R.string.prompt_host_disconnected));
if (result == null || result.booleanValue()) {
awaitingClose = true;
// Tell the TerminalManager that we can be destroyed now.
if (disconnectListener != null)
disconnectListener.onDisconnected(TerminalBridge.this);
}
}
});
disconnectPromptThread.setName("DisconnectPrompt");
disconnectPromptThread.setDaemon(true);
disconnectPromptThread.start();
}
}
public synchronized void tryKeyVibrate() {
manager.tryKeyVibrate();
}
/**
* Somehow our parent {@link TerminalView} was destroyed. Now we don't need
* to redraw anywhere, and we can recycle our internal bitmap.
*/
public synchronized void parentDestroyed() {
parent = null;
}
public void redraw() {
if (parent != null)
parent.postInvalidate();
}
/**
* Adds the {@link ChannelBean} to the list.
* @param portForward the port forward bean to add
* @return true on successful addition
*/
public boolean addChannel(ChannelBean portForward) {
return transport.addChannel(portForward);
}
/**
* Removes the {@link ChannelBean} from the list.
* @param portForward the port forward bean to remove
* @return true on successful removal
*/
public boolean removeChannel(ChannelBean portForward) {
return transport.removeChannel(portForward);
}
/**
* @return the list of port forwards
*/
public List<ChannelBean> getChannels() {
return transport.getChannels();
}
/**
* Enables a port forward member. After calling this method, the port forward should
* be operational.
* @param portForward member of our current port forwards list to enable
* @return true on successful port forward setup
*/
public boolean enableChannel(ChannelBean portForward) {
if (!transport.isConnected()) {
Log.i(TAG, "Attempt to enable port forward while not connected");
return false;
}
return transport.enableChannel(portForward);
}
/**
* Disables a port forward member. After calling this method, the port forward should
* be non-functioning.
* @param portForward member of our current port forwards list to enable
* @return true on successful port forward tear-down
*/
public boolean disableChannel(ChannelBean portForward) {
if (!transport.isConnected()) {
Log.i(TAG, "Attempt to disable port forward while not connected");
return false;
}
return transport.disableChannel(portForward);
}
/**
* @return whether the TerminalBridge should close
*/
public boolean isAwaitingClose() {
return awaitingClose;
}
/**
* @return whether this connection had started and subsequently disconnected
*/
public boolean isDisconnected() {
return disconnected;
}
}