/**
* This file is part of "BBSSH" (c) 2010 Marc A. Paradise Portions of this file Copyright (C) 2004 Karl von Randow as
* part of midpssh. --LICENSE NOTICE-- 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 2 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, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
* --LICENSE NOTICE--
*/
package org.bbssh.net.session;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Vector;
import javax.microedition.io.Connector;
import javax.microedition.io.HttpConnection;
import javax.microedition.io.StreamConnection;
import net.rim.device.api.crypto.RandomSource;
import net.rim.device.api.system.ControlledAccessException;
import org.bbssh.i18n.BBSSHResource;
import org.bbssh.model.ConnectionProperties;
import org.bbssh.model.Key;
import org.bbssh.model.KeyManager;
import org.bbssh.net.ConnectionHelper;
import org.bbssh.ssh.kex.KexAgreement;
import org.bbssh.terminal.VT320;
import org.bbssh.terminal.VT320Debug;
import org.bbssh.util.Logger;
import org.bbssh.util.Tools;
public abstract class Session {
private Vector dataListeners = new Vector();
protected VT320 emulator;
private SessionListener listener;
protected SessionIOHandler filter;
// Indicates that this was a client-requested disconnect.
private boolean forceDisconnect;
private StreamConnection connection;
private InputStream in;
private OutputStream out;
private ConnectionProperties properties;
private Thread reader, writer;
private byte[] outputBuffer = new byte[1024]; // this will grow if needed
private final Object writerMutex = new Object();
private static final int BUF_SIZE = 8192;
private String userName, password;
public static final int CONNSTATE_NONE = 0;
public static final int CONNSTATE_CONNECTING = 1;
public static final int CONNSTATE_CONNECTED = 2;
public static final int CONNSTATE_DISCONNECTING = 3;
public static final int CONNSTATE_DISCONNECTED = 4;
private final byte[] empty = new byte[0];
private boolean wifiOverride;
public int getBytesRead() {
return bytesRead;
}
public int getBytesWritten() {
return bytesWritten;
}
/**
* Number of bytes to be written, from output array, because it has fixed lenght.
*/
private int outputCount = 0;
private int bytesWritten = 0;
private int bytesRead = 0;
private int connState;
private int sessionId;
/**
* Use this if you
*
* @param prop
*/
protected Session(ConnectionProperties prop, int sessId, SessionListener listenr, VT320 emulator) {
if (listenr == null)
throw new NullPointerException("SessionListener must be valid.");
connState = CONNSTATE_NONE;
this.listener = listenr;
this.sessionId = sessId;
this.properties = prop;
this.emulator = emulator;
emulator.setScrollbackBufferSize(prop.getScrollbackLines());
emulator.setFunctionKeyMode(prop.getFunctionKeyMode());
if (prop.getAltPrefixesMeta()) {
emulator.enableAltSendsMeta();
}
}
public Session(ConnectionProperties prop, int sessId, SessionListener listenr) {
if (listenr == null)
throw new NullPointerException("SessionListener must be valid.");
connState = CONNSTATE_NONE;
this.listener = listenr;
this.sessionId = sessId;
this.properties = prop;
if (prop.isCaptureEnabled()) {
// @todo - pluggable emulator type based on configuration
emulator = new VT320Debug(properties.getName() + sessId, prop) {
public void sendData(byte[] b, int offset, int length) throws IOException {
filter.handleSendData(b, offset, length);
}
// @todo is this extra layer of indirection useful?
public void beep() {
listener.onSessionRemoteAlert(sessionId);
}
public void resize() {
if (filter != null) {
filter.handleResize();
}
listener.onDisplayInvalid(sessionId);
}
};
} else {
// @todo - pluggable emulator type based on configuration
emulator = new VT320() {
public void sendData(byte[] b, int offset, int length) throws IOException {
filter.handleSendData(b, offset, length);
}
public void beep() {
listener.onSessionRemoteAlert(sessionId);
}
public void resize() {
if (filter != null) {
filter.handleResize();
}
listener.onDisplayInvalid(sessionId);
}
};
}
emulator.setScrollbackBufferSize(prop.getScrollbackLines());
emulator.setFunctionKeyMode(prop.getFunctionKeyMode());
emulator.setTerminalID(prop.getTermType());
if (prop.getAltPrefixesMeta()) {
emulator.enableAltSendsMeta();
}
reader = new Reader();
writer = new Writer();
}
public SessionListener getListener() {
return listener;
}
public ConnectionProperties getProperties() {
return properties;
}
protected void connect(SessionIOHandler filter) {
this.filter = filter;
if (!writer.isAlive()) {
if (connState >= CONNSTATE_CONNECTED) {
writer = new Writer();
reader = new Reader();
}
setConnectionState(CONNSTATE_CONNECTING);
writer.start();
}
}
protected abstract int getDefaultPort();
/*
* (non-Javadoc)
*/
protected void receiveData(byte[] buffer, int offset, int length) throws IOException {
if (buffer != null && length > 0) {
try {
String data = new String(buffer, offset, length);
sendLocalTerminalOutput(data);
for (int x = dataListeners.size() - 1; x > -1; x--) {
((SessionDataListener) dataListeners.elementAt(x)).onDataReceived(sessionId, data);
}
} catch (Throwable e) {
// We can't allow misuse of this data to mess with the
// connection.
}
}
}
protected void sendData(byte[] b, int offset, int length) throws IOException {
synchronized (writerMutex) {
if (outputCount + length > outputBuffer.length) {
byte[] newOutput = new byte[outputCount + length];
System.arraycopy(outputBuffer, 0, newOutput, 0, outputCount);
outputBuffer = newOutput;
}
System.arraycopy(b, offset, outputBuffer, outputCount, length);
outputCount += length;
writerMutex.notify();
}
}
/**
* @return pending output count - note that this is not guaranteed completely accurate as it is unsynchronized.
*/
public int getOutputBufferLength() {
return outputCount;
}
private void attemptConnect(byte connType, String host, int timeout) throws IOException {
StringBuffer conn = new StringBuffer("socket://").append(host);
ConnectionHelper.configureConnectionString(connType, conn, timeout);
Logger.info("Attempting connection: " + conn.toString());
connection = (StreamConnection) Connector.open(conn.toString(), Connector.READ_WRITE, false);
Logger.info("Connection completed.");
}
private boolean setupConnection() throws IOException {
try {
return connectImpl();
} catch (ControlledAccessException e) {
Logger.error("Unable to connect: ControlledAccessException");
throw new IOException(Tools.getStringResource(BBSSHResource.ERROR_NETWORK_PERM_MISSING));
}
}
/**
* Implementations must handle connection setup after socet connection has been made by the base class.
*/
public abstract void connect();
private void sendLocalTerminalOutput(String message) {
emulator.putString(message);
listener.onDisplayDirty(sessionId);
}
private boolean connectImpl() throws IOException {
String host = properties.getHost();
bytesRead = 0;
bytesWritten = 0;
sendLocalTerminalOutput("Connecting to " + host + "... ");
if (host.indexOf(':') == -1) {
host += ":" + getDefaultPort();
}
StringBuffer conn;
String proxyHost = properties.getHttpProxyHost();
int proxyMode = properties.getHttpProxyMode();
String okMsg = "OK\r\n";
if (proxyHost.length() == 0 || proxyMode == ConnectionHelper.PROXY_MODE_NONE) {
if (properties.getUseWifiIfAvailable() && ConnectionHelper.isWifiAvailable()
&& properties.getConnectionType() != ConnectionHelper.CONNECTION_TYPE_WIFI) {
try {
// First do a failable attempt.
wifiOverride = true;
attemptConnect(ConnectionHelper.CONNECTION_TYPE_WIFI, host, 0);
okMsg = "WiFi OK\r\n";
} catch (IOException e) {
Logger.warn("Initial WIFI connection failed, now attempting to use original connection type.");
}
}
if (connection == null) {
wifiOverride = false;
// If this one fails, the exception will be thrown and handled
// normally.
attemptConnect(properties.getConnectionType(), host, properties.getBESTimeout());
}
in = connection.openInputStream();
out = connection.openOutputStream();
} else {
okMsg = "HTTP Proxy OK\r\n";
int id = RandomSource.getInt();
conn = new StringBuffer("http://").append(proxyHost).append('/').append(id).append('/').append(host);
ConnectionHelper
.configureConnectionString(properties.getConnectionType(), conn, properties.getBESTimeout());
if (properties.getHttpProxyMode() == ConnectionHelper.PROXY_MODE_PERSISTENT) {
HttpConnection outbound = (HttpConnection) Connector.open(conn.toString(), Connector.READ_WRITE, false);
outbound.setRequestMethod(HttpConnection.POST);
out = outbound.openOutputStream();
HttpConnection inbound = (HttpConnection) Connector.open(conn.toString(), Connector.READ_WRITE, false);
inbound.setRequestProperty("X-MidpSSH-Persistent", "true");
in = inbound.openInputStream();
} else {
out = new HttpOutboundStream(conn.toString());
in = new HttpInboundStream(conn.toString());
}
}
sendLocalTerminalOutput(okMsg);
Logger.warn("Connected to " + host);
filter.handleConnection();
// Don't notify of connected state until the implementation tells us to
// as the impl knows when we're completed negotiations.
// listener.onSessionConnected(sessionId);
return true;
}
/**
* Continuously read from remote host and display the data on screen.
*/
private void read() throws IOException {
byte[] buf;
buf = new byte[BUF_SIZE];
int available = 0;
int inputSize = 0;
try {
// May need to read 1 to make available check accurate.
// Logger.debug("read: Reading one byte.");
if (in.read(buf, 0, 1) == -1) {
Logger.fatal("read: initial 1b read failed - aborting");
return;
}
bytesRead++;
filter.handleReceiveData(buf, 0, 1);
// Logger.debug("read: processed 1 byte.");
// @todo refactor - "connState < CONNSTATE_DISCONNECTING" is far
// from threadsafe (or particularly smmart).
while (connState < CONNSTATE_DISCONNECTING && (available = in.available()) != -1) {
// Logger.debug("read: attempting to read bytes: " + available);
while (connState < CONNSTATE_DISCONNECTING && available > 0) {
inputSize = in.read(buf, 0, Math.max(1, Math.min(available, BUF_SIZE)));
// Logger.debug("read: obtained bytes: " + inputSize);
if (inputSize > 0) {
bytesRead += inputSize;
filter.handleReceiveData(buf, 0, inputSize);
} else if (inputSize == -1) {
throw new IOException("Connection Terminated [eof]");
}
available -= inputSize;
}
inputSize = 0;
// May need to read 1 to make available check accurate.
// Logger.debug("read: Reading one byte.");
if (in.read(buf, 0, 1) > 0) {
bytesRead++;
filter.handleReceiveData(buf, 0, 1);
// Logger.debug("read: processed one byte.");
} else {
throw new IOException("Connection Terminated [eof]");
}
}
// }
} catch (IOException e) {
if (connState < CONNSTATE_DISCONNECTING) {
Logger.fatal("read: Exception in Session.read", e);
throw e;
} else {
Logger.warn("Received exception in Session.read, but connection shutting down: " + e.getMessage());
}
}
}
private void write() throws IOException {
// @todo - again the redundant conditions - MUST be a cleaner
// implementation possible
synchronized (writerMutex) {
if (outputCount > 0) {
// Logger.debug("write: sending data -> socket : " +
// outputCount);
out.write(outputBuffer, 0, outputCount);
bytesWritten += outputCount;
out.flush();
// Logger.debug("write: end flush");
// Logger.debug("write: output count to be reset is " +
// outputCount);
outputCount = 0;
}
try {
// Wait until our timeout expires, or we have
// data to send. Timeout values of 0 will
// wait indefinitely.
// Logger.debug("write: waiting");
writerMutex.wait(properties.getKeepAliveTime() * 1000);
// writerMutex.wait();
// Logger.debug("write: mutex reacquired, resuming");
if (connState < CONNSTATE_DISCONNECTING && outputCount == 0) {
// No data to send after timeout so send an empty array
// through the filter which will trigger
// the sending of a NOOP (see TelnetSession and SshSession)
// -
// this has the effect of a keepalive
// Logger.debug("write: queueing NOOP");
filter.handleSendData(empty, 0, 0);
// Logger.debug("write: sent NOOP");
}
} catch (InterruptedException e) {
//
Logger.error("write: interrupted");
}
}
}
public synchronized void disconnect() {
forceDisconnect = true;
doDisconnect();
}
private void doDisconnect() {
if (connState >= CONNSTATE_DISCONNECTING) {
return;
}
if (Logger.isFileLoggingEnabled() && Logger.isLevelEnabled(Logger.LOG_LEVEL_INFO)) {
StringBuffer b = new StringBuffer(2048);
b.append("\r\n");
Tools.buildDiagnosticString(b);
Logger.info(b.toString());
}
setConnectionState(CONNSTATE_DISCONNECTING);
synchronized (writerMutex) {
try {
if (in != null) {
in.close();
in = null;
}
} catch (IOException e) {
}
writerMutex.notify();
}
try {
if (out != null) {
out.close();
out = null;
}
} catch (IOException e) {
}
try {
if (connection != null) {
connection.close();
connection = null;
}
} catch (IOException e) {
}
Logger.error("Disconnected from " + properties.getHost());
setConnectionState(CONNSTATE_DISCONNECTED);
emulator.terminate();
}
// @todo addChannelDataListener(listener)
// @todo WHY Are we extending thread instead of Runnable?
private class Reader extends Thread {
public Reader() {
super("Reader");
}
public void run() {
try {
read();
} catch (Exception e) {
if (!forceDisconnect) {
String msg;
if (e.getMessage() == null) {
msg = "Internal Exception - " + e.toString();
} else {
msg = e.getMessage();
}
Logger.error(msg);
listener.onSessionError(sessionId, msg);
}
} finally {
Logger.info("Reader thread terminating.");
doDisconnect();
}
}
}
// @todo create wrapper for listener notifications? Or just implement it
// ourselvs
// and pass it through if defined? Avoid null check everywhere (and chance
// of forgetting null check)
private class Writer extends Thread {
public Writer() {
super("Writer");
}
public void run() {
try {
setupConnection();
reader.start();
while (connState < CONNSTATE_DISCONNECTING) {
write();
}
Logger.info("Writer thread complete w/ no errors.");
} catch (Throwable e) {
if (!forceDisconnect) {
Logger.error(properties.getHost() + " reports: " + e.getMessage());
listener.onSessionError(sessionId, e.getMessage());
}
} finally {
Logger.info("Writer thread terminating.");
doDisconnect();
}
}
}
public VT320 getEmulator() {
return emulator;
}
public KexAgreement getAgreement() {
return null;
}
public synchronized void registerDataListener(SessionDataListener listener) {
// Assumption here that we won't have a huge number of listeners
// registered...
if (dataListeners.contains(listener)) {
return;
}
dataListeners.addElement(listener);
}
public synchronized void unregisterDataListener(SessionDataListener listener) {
dataListeners.removeElement(listener);
}
public boolean isWifiOverrideConnection() {
return wifiOverride;
}
public int getConnectionState() {
return connState;
}
public void setConnectionState(int state) {
// This seems to be the best place to notify when we change the state to
// connected.
if (state == connState)
return;
connState = state;
switch (state) {
case CONNSTATE_CONNECTED:
listener.onSessionConnected(sessionId);
break;
case CONNSTATE_CONNECTING:
break;
case CONNSTATE_DISCONNECTED:
listener.onSessionDisconnected(sessionId, bytesWritten, bytesRead);
break;
case CONNSTATE_DISCONNECTING:
break;
}
}
/**
* @return true if this session has made a connection, and is not in the process of disconnecting.
*/
public boolean isConnected() {
return connState == Session.CONNSTATE_CONNECTED || connState == Session.CONNSTATE_CONNECTING;
}
/**
* @return true if there is a live netowrk connection associated with this session
*/
public boolean isConnectionActive() {
return connState != CONNSTATE_DISCONNECTED && connState != CONNSTATE_NONE;
}
public int getSessionId() {
return sessionId;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getUserName() {
return userName;
}
public void setPassword(String password) {
this.password = password;
}
public String getPassword() {
return password;
}
public Key getKey() {
return KeyManager.getInstance().getKey(properties.getKeyId());
}
}