com.ascert.open.term.core.RWTelnet Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of openterm Show documentation
Show all versions of openterm Show documentation
An open source emulator supporting 3270 and potentially later 5250 terminal types.
The newest version!
/*
* Copyright (c) 2016, 2017 Ascert, LLC.
* www.ascert.com
*
* Based on original code from FreeHost3270, copyright for derivations from original works remain:
* Copyright (C) 1998, 2001 Art Gillespie
* Copyright (2) 2005 the http://FreeHost3270.Sourceforge.net
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.ascert.open.term.core;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Logger;
import java.util.logging.Level;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
/**
* TODO: - look into TN3270E protocol, SSCP LU device name, options etc https://tools.ietf.org/html/rfc1647
*
*
* @since 0.1
*/
public class RWTelnet implements Runnable
{
private static final Logger log = Logger.getLogger(RWTelnet.class.getName());
/* TELNET protocol constants */
/* found on page 14 of RFC 845 */
public static final short SE = 240; //End of subnegotiation parameters
public static final short NOP = 241; //No Operation
public static final short DATA_MARK = 242; //data mark Operation
public static final short BREAK = 243; //break Operation
public static final short SB = 250; //Begin subnegotiation
public static final short WILL = 251; //Will perform an indicated option
public static final short WONT = 252; //Won't perform an indicated option
public static final short DO = 253; //Please perform an indicated option
public static final short DONT = 254; //Please don't perform an indicated option
public static final short IAC = 255; //The following bytes are a telnet command
public static final short EOR = 239; //End of record
/* TELNET OPTIONS */
public static final short OPT_BINARY = 0; //Use 8-bit data path
public static final short OPT_ECHO = 1; //Echo
public static final short OPT_SUPPRESS_GA = 3;
public static final short TIMING_MARK = 6;
public static final short OPT_TERMINAL_TYPE = 24; //
public static final short OPT_EOR = 25; //End of Record
public static final short OPT_WINDOW = 31; //Window size
public static final short OPT_LINE_MODE = 34;
public static final short OPTION_IS = 0; //option is
public static final short OPTION_SEND = 1; //send option
/* TELNET STATES (internal) */
public static final short TN_DEFAULT = 0; //default incoming data
public static final short TN_IAC = 1; //next is command
public static final short TN_CMD = 2; //incoming command
public static final short TN_SUB_CMD = 3; //incoming sub-command
/* CUSTOM CODES */
/**
* Custom byte for flagging the next bytes as broadcast message.
*/
public static final short BROADCAST = 245;
public static byte[] shortArrayToByte(short[] buff, int offset, int len)
{
return shortArrayToByte(buff, offset, len, null);
}
public static byte[] shortArrayToByte(short[] buff, int offset, int len, byte[] dest)
{
if (dest == null)
{
dest = new byte[len];
}
for (int i = 0; i < len; i++)
{
// Cast our results from the byte array to unsigned shorts so none are negative
dest[i] = (byte) buff[i + offset];
}
return dest;
}
public static short[] byteArrayToShort(byte[] buff)
{
return byteArrayToShort(buff, buff.length);
}
public static short[] byteArrayToShort(byte[] buff, int len)
{
short[] sBuf = new short[len];
for (int i = 0; i < len; i++)
{
// Cast our results from the byte array to unsigned shorts so none are negative
sBuf[i] = (short) Byte.toUnsignedInt(buff[i]);
}
return sBuf;
}
private InputStream is;
private OutputStream os;
protected TnStreamParser tnParser;
private Socket tnSocket;
private SSLSocket tnSocketSSL;
private volatile Thread sessionThread;
protected short[] bufferTerm; //put the 3270 bytes in here
protected int bufferTermLen;
private boolean[] doHistory;
private short[] inBuf; //put raw data from the inputstream here
private byte[] key;
private short[] subOptionBuffer;
private boolean[] willHistory;
private boolean encryption;
private int connectionTimeout;
private int inBufLen;
private int keyCounter;
private int subOptionBufferLen;
private int tnState;
private short tnCommand;
// Optional Tn commands - respond positively to a DO or WILL
private Set optTnCmds = new HashSet<>();
// Required Tn commands - an error if we get a DONT or WONT
private Set reqdTnCmds = new HashSet<>();
// Currently negotiated commands - used to prevent loops
private Set negTnCmds = new HashSet<>();
// Determines whether optional and required commands are sent initially
private final boolean sendInitialCmds;
// Line mode options - if not null, will be used at startup
protected byte[] lineModeOpts = null;
protected TraceHandler traceHandler = null;
// For keep Alive handling
private long lastSendTS;
private int keepAliveMillis;
/**
* DOCUMENT ME!
*
* @param rw the Parser for the incoming data stream.
* @param tn3270Model the tn3270 model number corresponding to this session.
*/
public RWTelnet(TnStreamParser tnParser)
{
this(tnParser, new Short[]
{
OPT_TERMINAL_TYPE
}, new Short[]
{
OPT_BINARY, OPT_EOR
}, false);
}
public RWTelnet(TnStreamParser tnParser, Short[] reqdCmds, Short[] optCmds, boolean sendInitialCmds)
{
this.tnParser = tnParser;
this.sendInitialCmds = sendInitialCmds;
bufferTerm = new short[50000];
bufferTermLen = 0;
subOptionBuffer = new short[50];
subOptionBufferLen = 0;
tnState = TN_DEFAULT;
keyCounter = 0;
connectionTimeout = 60;
doHistory = new boolean[3];
willHistory = new boolean[3];
// Temp for now - allow caller to supply
reqdTnCmds.addAll(Arrays.asList(reqdCmds));
optTnCmds.addAll(Arrays.asList(optCmds));
// A Telnet client should never negotiate or refuse echo - so we add it all here in
// case, and make sure not set as required
optTnCmds.add(OPT_ECHO);
reqdTnCmds.remove(OPT_ECHO);
}
/**
* The 'thread code' for this class. Consumers need to invoke this method to begin the communications process. It will run indefinitely
* until the socket read returns -1 (Host disconnected) or disconnect is called. Usage Thread t = new Thread(RWTelnet
* instance);
t.run;
Any problems encountered (IOException, Host Disconnect) will be transmitted back to the consumer via
* the TnAction interface.
*/
public void run()
{
int n = 0;
log.finer("started the telnet thread");
try
{
sendInitialCommands();
startKeepAlive();
while (true)
{
if ((inBufLen = readSocket()) == -1)
{
log.finer("telnet socket is empty, disconnecting");
tnParser.status(TnAction.DISCONNECTED_BY_REMOTE_HOST);
break;
}
synchronized (this)
{
parseData();
}
}
//rw.connectionStatus(TnAction.DISCONNECTED_BY_REMOTE_HOST);
}
catch (IOException e)
{
if (sessionThread == null)
{
log.log(Level.FINER, "Telnet thread loop terminated", e);
}
else
{
log.log(Level.SEVERE, "Failure in telnet thread loop", e);
}
tnParser.status(TnAction.DISCONNECTED_BY_REMOTE_HOST);
}
finally
{
disconnect();
}
}
/**
* Performs a direct connection to a host terminal server.
*
* @param host destination server host name.
* @param port destination terminal server port number.
*
* @throws UnknownHostException DOCUMENT ME!
* @throws IOException DOCUMENT ME!
*/
protected void connect(String host, int port)
throws UnknownHostException, IOException
{
log.fine("connecting to " + host + ":" + port);
if (encryption)
{
log.fine("encrypted connection");
SSLSocketFactory sslFact = (SSLSocketFactory) SSLSocketFactory.getDefault();
tnSocketSSL = (SSLSocket) sslFact.createSocket();
tnSocketSSL.connect(new InetSocketAddress(host, port), connectionTimeout * 1000);
connect(tnSocketSSL.getInputStream(), tnSocketSSL.getOutputStream());
}
else
{
tnSocket = new Socket();
tnSocket.connect(new InetSocketAddress(host, port), connectionTimeout * 1000);
connect(tnSocket.getInputStream(), tnSocket.getOutputStream());
}
}
protected void connect(InputStream is, OutputStream os)
{
this.is = is;
this.os = os;
negTnCmds.clear();
tnState = TN_DEFAULT;
sessionThread = new Thread(this);
sessionThread.start();
}
/**
* Disconnects the current session.
*/
protected void disconnect()
{
if (sessionThread == null || tnSocket == null && tnSocketSSL == null)
{
log.fine("socket is null, not connected");
return;
}
try
{
Thread sessionNotify = sessionThread;
sessionThread = null;
sessionNotify.interrupt();
silentClose(is);
silentClose(os);
silentClose(encryption ? tnSocketSSL : tnSocket);
willHistory = new boolean[3];
doHistory = new boolean[3];
log.fine("disconnected");
}
catch (Exception e)
{
log.log(Level.SEVERE, "Error in disconnecting", e);
}
finally
{
is = null;
os = null;
tnSocketSSL = null;
tnSocket = null;
}
}
public boolean isConnected()
{
if (tnSocket != null)
{
return tnSocket.isConnected();
}
else if (tnSocketSSL != null)
{
return tnSocketSSL.isConnected();
}
else if (is != null && os != null)
{
// we have streams, so must be a direct connection
return true;
}
return false;
}
protected void silentClose(Closeable cls)
{
if (cls != null)
{
try
{
cls.close();
}
catch (Exception e)
{
log.finest("Closeable exception: " + e);
}
}
}
/**
* Processes broadcast message. Is called when a broadcast message is received.
*
* @param netBuf DOCUMENT ME!
*/
protected void receiveMessage(short[] netBuf)
{
log.fine("received broadcast message");
char[] msg = new char[netBuf.length];
for (int i = 2; i < netBuf.length; i++)
{
msg[i - 2] = (char) netBuf[i];
}
tnParser.broadcastMessage(new String(msg).trim());
}
public boolean using(short opt)
{
return negTnCmds.contains(opt);
}
/**
* This method provides outbound communication to the Telnet host.
*
* @param out an array of shorts, representing the data to be sent to the host.
* @param outLen the number of valid bytes in the out array
*
* @throws IOException DOCUMENT ME!
*/
public void sendData(short[] out, int outLen) throws IOException
{
sendData(shortArrayToByte(out, 0, outLen, new byte[outLen + 2]), outLen);
}
public void sendData(byte[] out) throws IOException
{
sendData(out, out.length);
}
public void sendData(byte[] out, int outLen) throws IOException
{
if (os == null)
{
log.warning("attempt to send when telnet not connected, discarding");
return;
}
if (using(OPT_EOR))
{
//add the is a command telnet command
out[outLen++] = (byte) IAC;
//add the end of record telnet command
out[outLen++] = (byte) EOR;
}
send(out, 0, outLen);
//System.out.println("Sent " + tmpByteBuf.length + " bytes");
//for(int i = 0; i < tmpByteBuf.length; i++)
//System.out.print(Integer.toHexString(tmpByteBuf[i]) + " ");
}
// Precautionary sync so we can't get overlapping calls to put data into the
// output buffer.
public synchronized void send(byte[] out, int off, int outLen) throws IOException
{
lastSendTS = System.currentTimeMillis();
//write the data out to the EncryptedOutputStream
os.write(out, off, outLen);
os.flush();
if (traceHandler != null)
{
// possible we'd want this waited/queued to some async handler thread
traceHandler.outgoingData(out, off, outLen);
}
}
public void sendBreak() throws IOException
{
sendData(new byte[]
{
(byte) IAC, (byte) BREAK
});
}
public void sendNOP() throws IOException
{
sendData(new byte[]
{
(byte) IAC, (byte) NOP
});
}
/**
* Turns the encryption on and off.
*
* @param encryption True = on False = off
*/
protected void setEncryption(boolean encryption)
{
this.encryption = encryption;
}
/**
* Sets the connection timeout for the connect(String,int) method.
*
* @param timeout integer value in seconds
*/
protected void setConnectionTimeout(int timeout)
{
this.connectionTimeout = timeout;
}
public synchronized void setSessionData(String key, String value)
{
log.fine("SessionData Key: " + key + " Value: " + value);
byte[] keyByte = charToByte(key.toCharArray());
byte[] valueByte = charToByte(value.toCharArray());
byte[] outData = new byte[keyByte.length + valueByte.length + 4];
outData[0] = (byte) 0xCC;
outData[1] = (byte) 0xCC;
System.arraycopy(keyByte, 0, outData, 2, keyByte.length);
outData[keyByte.length + 2] = (byte) 0xCC;
System.arraycopy(valueByte, 0, outData, keyByte.length + 3,
valueByte.length);
outData[keyByte.length + valueByte.length + 3] = (byte) 0xCC;
try
{
os.write(outData, 0, outData.length);
log.fine("SessionData sent to server");
}
catch (IOException e)
{
// Seems somewhat bogus to swallow this?
log.log(Level.SEVERE, "Error setting session data "+key+" to "+value, e);
}
}
private byte[] charToByte(char[] c)
{
byte[] ret = new byte[c.length];
for (int i = 0; i < c.length; i++)
{
ret[i] = (byte) c[i];
}
return ret;
}
/**
* This method sends TELNET specific commands to the telnet host. Primarily this would be used for protocol feature negotiation.
*
* @param tnCmd DOCUMENT ME!
* @param tnOption DOCUMENT ME!
*/
private void sendCommand(short tnCmd, short tnOption)
throws IOException
{
log.finer(String.format("sending command: %s - %s", decodeCmd(tnCmd), decodeOpt(tnOption)));
byte[] tmpBuffer = new byte[3];
tmpBuffer[0] = (byte) IAC;
tmpBuffer[1] = (byte) tnCmd;
tmpBuffer[2] = (byte) tnOption;
send(tmpBuffer, 0, 3);
}
/**
* This is a special instance of sendCommand where the client specifies that it is a 3270 client.
*
* @throws IOException DOCUMENT ME!
*/
public void sendTerminalType() throws IOException
{
String type = tnParser.getTermType();
log.fine("sending terminal type: " + type);
byte[] bTyp = (type != null) ? type.getBytes() : "NO-TYPE".getBytes();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write(new byte[]
{
(byte) IAC, (byte) SB
});
baos.write(new byte[]
{
(byte) OPT_TERMINAL_TYPE, (byte) OPTION_IS
});
baos.write(bTyp);
baos.write(new byte[]
{
(byte) IAC, (byte) SE
});
if (getLineModeOpts() != null)
{
log.finer("sending TN SUB OPTS : " + decodeSubOpts(byteArrayToShort(getLineModeOpts()), getLineModeOpts().length));
// strictly, line mode opts should be sent afte a DO LINE_MODE
// for current usaes, sending after we've sent terminal type is workable
baos.write(new byte[]
{
(byte) IAC, (byte) SB
});
baos.write(this.getLineModeOpts());
baos.write(new byte[]
{
(byte) IAC, (byte) SE
});
}
send(baos.toByteArray(), 0, baos.size());
}
// Override if needed
protected void doIsTerminalType(String termType)
{
}
//Probably should use lookup maps or Enums - quick hack for now or better tracing
private String decodeOpt(short opt)
{
switch (opt)
{
case OPT_BINARY:
return "BINARY";
case OPT_EOR:
return "OPT_EOR";
case OPT_TERMINAL_TYPE:
return "TERMINAL_TYPE";
case OPT_ECHO:
return "ECHO";
case OPT_WINDOW:
return "WINDOW";
case OPT_LINE_MODE:
return "LINE_MODE";
case OPT_SUPPRESS_GA:
return "SUPPRESS_GA";
default:
return String.format("0x%x", opt);
}
}
private String decodeCmd(short tnCmd)
{
switch (tnCmd)
{
case WILL:
return "WILL";
case DO:
return "DO";
case WONT:
return "WONT";
case DONT:
return "DONT";
default:
return String.format("0x%x", tnCmd);
}
}
public String decodeSubOpts(short[] subOptBuf, int len)
{
StringBuffer dBuf = new StringBuffer();
dBuf.append(decodeOpt(subOptBuf[0]));
dBuf.append(" - ");
for (int ix = 1; ix < len; ix++)
{
dBuf.append(String.format("0x%x ", subOptBuf[ix]));
}
return dBuf.toString();
}
/**
* This method handles incoming TELNET-specific commands. specifically, WILL, WONT, DO, DONT.
*
* @param tnCmd the incoming telnet command
* @param tnOption the option for which the command is being sent
*
* @throws IOException DOCUMENT ME!
*/
private void handleTnCommand(short tnCmd, short tnOption)
throws IOException
{
log.finer(String.format("received telnet command: %s - %s", decodeCmd(tnCmd), decodeOpt(tnOption)));
short cmd;
switch (tnCmd)
{
case WILL:
case DO:
if (optTnCmds.contains(tnOption) || reqdTnCmds.contains(tnOption))
{
cmd = (tnCmd == WILL) ? DO : WILL;
if (tnCmd == DO && !negTnCmds.add(tnOption))
{
// Prevent endless loop negotiation as per RFC854
log.finer("tn option already present: " + tnOption);
}
else
{
log.finer("tn option not already present: " + tnOption);
sendCommand(cmd, tnOption);
}
}
else
{
cmd = (tnCmd == WILL) ? DONT : WONT;
negTnCmds.remove(tnOption);
sendCommand(cmd, tnOption);
}
break;
case WONT:
case DONT:
if (reqdTnCmds.contains(tnOption))
{
// Nasty, but what else can we do??
throw new IOException("Telnet WONT/DONT received for required option: " + decodeOpt(tnOption));
}
else
{
cmd = (tnCmd == WONT) ? DONT : WONT;
// Record option, and also prevent endless loop negotiation as per RFC854
if (tnCmd == DONT && !negTnCmds.remove(tnOption))
{
log.finer("tn option was not present: " + tnOption);
}
else
{
log.finer("tn option was present: " + tnOption);
sendCommand(cmd, tnOption);
}
sendCommand(cmd, tnOption);
}
}
}
protected void processTermBuffer() throws IOException
{
tnParser.parse(bufferTerm, bufferTermLen);
bufferTermLen = 0;
}
private void handleUnframedData() throws IOException
{
if (!using(OPT_EOR) && bufferTermLen > 0)
{
processTermBuffer();
}
}
/**
* Checks the input stream for commands and routes the stream appropriately. Standard data is stored in the bufferTerm
* array and passed to the RWTelnetAction
interface's refresh(buf, int)
method when an EOR (End-of-record)
* byte is detected. Other telnet commands (WILL WONT DO DONT IAC) are handled in accordance to RFC 845
*
* @throws IOException DOCUMENT ME!
*/
private void parseData() throws IOException
{
short curr_byte;
log.finer("parsing data");
//this if clause traps the inputStream if it is a broadcast message
if ((inBuf[0] == IAC) && (inBuf[1] == BROADCAST))
{
receiveMessage(inBuf);
inBufLen = 0;
return;
}
for (int i = 0; i < inBufLen; i++)
{
curr_byte = inBuf[i];
switch (tnState)
{
case TN_DEFAULT:
if (curr_byte == IAC)
{
tnState = TN_IAC;
}
else
{
try
{
bufferTerm[bufferTermLen++] = curr_byte;
}
catch (ArrayIndexOutOfBoundsException ee)
{
log.fine("telnet buffer size: " + bufferTerm.length + " len: " + bufferTermLen);
return;
}
}
break;
case TN_IAC:
switch (curr_byte)
{
case IAC:
//Two IACs in a row means this is really a single occurrence
//of byte 255 (0xFF). (255 is its own escape character)
bufferTerm[bufferTermLen++] = curr_byte;
//Since it wasn't really an IAC, reset the tnState to default
tnState = TN_DEFAULT;
break;
case EOR:
//Done with this data record, send to Implementation via TnAction interface
log.finer("TN EOR data len: " + bufferTermLen);
processTermBuffer();
tnState = TN_DEFAULT;
break;
case WILL:
case WONT:
case DO:
case DONT:
// Check for and handle any unframed data that has been buffered as it
// might be important to do this before processing any command
handleUnframedData();
tnCommand = curr_byte;
tnState = TN_CMD;
break;
case SB:
// Check for and handle any unframed data that has been buffered as it
// might be important to do this before processing any command
handleUnframedData();
//System.err.println("Sub-option: " + subOptionBufferLen);
subOptionBufferLen = 0;
tnState = TN_SUB_CMD;
break;
}
break;
case TN_CMD:
//System.out.println("CMD...");
handleTnCommand(tnCommand, curr_byte);
//System.out.println("did command...");
tnState = TN_DEFAULT;
break;
case TN_SUB_CMD:
if (curr_byte != SE)
{
// buffer until we reach end of negotiation
subOptionBuffer[subOptionBufferLen++] = curr_byte;
break;
}
handleSubOptions();
tnState = TN_DEFAULT;
break;
}
}
// Handle any leftover unframed data
handleUnframedData();
}
public void handleSubOptions() throws IOException
{
log.finer("> received TN SUB OPTS : " + decodeSubOpts(subOptionBuffer, subOptionBufferLen - 1));
switch (subOptionBuffer[0])
{
case OPT_TERMINAL_TYPE:
switch (subOptionBuffer[1])
{
case OPTION_SEND:
sendTerminalType();
break;
case OPTION_IS:
byte[] bType = RWTelnet.shortArrayToByte(subOptionBuffer, 2, subOptionBufferLen - 3);
doIsTerminalType(new String(bType));
break;
default:
log.severe("Invalid Terminal Type command sub-option:: " + subOptionBuffer[1]);
}
break;
default:
this.tnParser.telnetSubOpts(this.subOptionBuffer, this.subOptionBufferLen - 1);
}
}
private int readSocket() throws IOException
{
inBufLen = 2048;
// Since inputstreams require a byte array as a parameter (and
// not a short[]), we have to use this temporary buffer to
// store the results.
byte[] tmpByteBuf = new byte[inBufLen];
int bytes_read = is.read(tmpByteBuf, 0, inBufLen);
if (bytes_read > -1)
{
// System.out.println("Done... ");
if (traceHandler != null)
{
// possible we'd want this waited/queued to some async handler thread
traceHandler.incomingData(tmpByteBuf, 0, bytes_read);
}
inBuf = byteArrayToShort(tmpByteBuf, bytes_read);
}
//System.out.println("Bytes read: " + bytes_read);
return bytes_read;
}
public void processDataIn(byte[] buff)
throws IOException
{
processDataIn(buff, buff.length);
}
public void processDataIn(byte[] buff, int len)
throws IOException
{
inBuf = byteArrayToShort(buff, len);
inBufLen = len;
parseData();
}
public void setOutputStream(OutputStream os)
{
this.os = os;
}
public void setTraceHandler(TraceHandler traceHandler)
{
this.traceHandler = traceHandler;
}
public void sendTnCommand(short tnCmd, short tnOption)
throws IOException
{
switch (tnCmd)
{
case WILL:
case DO:
reqdTnCmds.add(tnOption);
//negTnCmds.add(tnOption);
break;
case WONT:
case DONT:
reqdTnCmds.remove(tnOption);
//negTnCmds.remove(tnOption);
break;
}
sendCommand(tnCmd, tnOption);
}
public void sendTnSubOpts(short[] optVals)
throws IOException
{
log.finer("sending TN SUB OPTS : " + decodeSubOpts(optVals, optVals.length));
byte[] tmpBuffer = new byte[4 + optVals.length];
tmpBuffer[0] = (byte) IAC;
tmpBuffer[1] = (byte) SB;
for (int ix = 0; ix < optVals.length; ix++)
{
tmpBuffer[2 + ix] = (byte) optVals[ix];
}
tmpBuffer[tmpBuffer.length - 2] = (byte) IAC;
tmpBuffer[tmpBuffer.length - 1] = (byte) SE;
send(tmpBuffer, 0, tmpBuffer.length);
}
private void sendInitialCommands()
throws IOException
{
if (!this.sendInitialCmds)
{
return;
}
Set cmds = new HashSet<>();
cmds.addAll(this.reqdTnCmds);
cmds.addAll(this.optTnCmds);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
for (short cmd : cmds)
{
// All except ECHO are fair game for client to say they will support.
if (cmd != OPT_ECHO)
{
log.finer(String.format("sending initial command: %s - %s", decodeCmd(WILL), decodeOpt(cmd)));
baos.write((byte) IAC);
baos.write((byte) WILL);
baos.write((byte) cmd);
//negTnCmds.add(cmd);
}
}
send(baos.toByteArray(), 0, baos.size());
}
/**
* @return the lineModeOpts
*/
public byte[] getLineModeOpts()
{
return lineModeOpts;
}
/**
* @param lineModeOpts the lineModeOpts to set
*/
public void setLineModeOpts(byte[] lineModeOpts)
{
this.lineModeOpts = lineModeOpts;
}
void setKeepAliveTimeout(int keepAliveTimeout)
{
this.keepAliveMillis = keepAliveTimeout * 1000;
log.fine("Keep alive timeout: " + this.keepAliveMillis);
}
void startKeepAlive()
{
if (keepAliveMillis > 0)
{
log.fine("Starting keep alive thread");
Thread kaThread = new Thread(new KeepAliveRunner());
kaThread.start();
}
}
private class KeepAliveRunner implements Runnable
{
@Override
public void run()
{
while (true)
{
try
{
Thread.sleep(keepAliveMillis);
if (lastSendTS < (System.currentTimeMillis() - keepAliveMillis))
{
log.fine("Sending keep alive ...");
sendNOP();
}
}
catch (IOException ex)
{
// Will probably get caught and handled elsewhere anyway - warn for now, disconnect on multiple maybe
log.warning("IO Exception on KeepAlive: " + ex);
}
catch (InterruptedException ex)
{
// Nothing we need to do here really
}
}
}
}
}