Files
j3270/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java
T

2118 lines
76 KiB
Java

package haus.nightmare.lib3270j.telnet;
import haus.nightmare.lib3270j.*;
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
import haus.nightmare.lib3270j.protocol.*;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.listener.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger;
import java.util.logging.Level;
import static haus.nightmare.lib3270j.protocol.TelnetConstants.*;
import static haus.nightmare.lib3270j.protocol.TN3270EConstants.*;
/**
* Telnet finite state machine.
* Processes incoming bytes one at a time, handling telnet commands,
* option negotiation, sub-negotiation, and 3270 data stream framing.
*
* Eight states matching telnet.c:
* TNS_DATA, TNS_IAC, TNS_WILL, TNS_WONT, TNS_DO, TNS_DONT, TNS_SB, TNS_SB_IAC
*/
public class TelnetFSM {
private static final Logger log = Logger.getLogger(TelnetFSM.class.getName());
// Telnet FSM states
private static final int TNS_DATA = 0;
private static final int TNS_IAC = 1;
private static final int TNS_WILL = 2;
private static final int TNS_WONT = 3;
private static final int TNS_DO = 4;
private static final int TNS_DONT = 5;
private static final int TNS_SB = 6;
private static final int TNS_SB_IAC = 7;
private int state = TNS_DATA;
// Option state tracking
private final boolean[] myOpts = new boolean[256]; // options we have enabled
private final boolean[] hisOpts = new boolean[256]; // options the host has enabled
// 3270 input buffer (accumulated between telnet framing)
private final haus.nightmare.lib3270j.datastream.FastByteBuffer ibuf = new haus.nightmare.lib3270j.datastream.FastByteBuffer(32768);
// Reusable single-byte scratch buffer for plain NVT data dispatch (Phase 27.5 / T1-C)
private final byte[] nvtSingleByteScratch = new byte[1];
// Sub-negotiation buffer
private final ByteArrayOutputStream sbbuf = new ByteArrayOutputStream(4096);
// TN3270E state
private boolean tn3270eNegotiated;
private TN3270ESubmode tn3270eSubmode = TN3270ESubmode.UNBOUND;
private boolean tn3270eBound;
private final boolean[] eFuncs = new boolean[8]; // Negotiated TN3270E functions
private int eXmitSeq;
private int lastRcvSeq;
private short lastRespType;
private short lastRespCode;
private int responseRequired = RSF_NO_RESPONSE;
private boolean deferredWillTtype;
private boolean tn3270eDeviceTypeSent;
private int ttypeIndex = 0;
private int luIndex = 0;
private final java.util.Map<String, List<String>> devicePools = new java.util.HashMap<>();
private List<String> getCandidateTerminalTypes() {
List<String> list = new ArrayList<>();
String currentLu = null;
List<String> lus = config.getLuNames();
if (lus != null && !lus.isEmpty() && luIndex < lus.size()) {
currentLu = lus.get(luIndex);
} else if (config.getLuName() != null && !config.getLuName().trim().isEmpty()) {
currentLu = config.getLuName().trim();
}
if (config.getTerminalName() != null && !config.getTerminalName().trim().isEmpty()) {
String tName = config.getTerminalName().trim();
if (currentLu != null && !currentLu.isEmpty() && !tName.contains("@")) {
list.add(tName + "@" + currentLu);
list.add(tName);
} else {
list.add(tName);
}
return list;
}
if (config.isDynamicModel()) {
list.add("IBM-DYNAMIC");
list.add("IBM-3279-4-E");
list.add("IBM-3279-4");
list.add("IBM-3279-2-E");
list.add("IBM-3279-2");
list.add("IBM-3278-2");
list.add("UNKNOWN");
} else {
TerminalModel model = config.getModel();
list.add(model.getTerminalType());
list.add(model.getBaseTerminalType());
if (model.isColor()) {
try {
TerminalModel mono = TerminalModel.forModel(model.getModelNumber(), false);
list.add(mono.getTerminalType());
list.add(mono.getBaseTerminalType());
} catch (Exception ignored) {}
}
if (model.getModelNumber() != 2) {
list.add("IBM-3279-2-E");
list.add("IBM-3279-2");
list.add("IBM-3278-2");
}
list.add("UNKNOWN");
}
if (currentLu != null && !currentLu.isEmpty()) {
List<String> luCandidates = new ArrayList<>();
for (String item : list) {
if (!"UNKNOWN".equalsIgnoreCase(item) && !item.contains("@")) {
luCandidates.add(item + "@" + currentLu);
}
}
for (String item : list) {
if (!"UNKNOWN".equalsIgnoreCase(item)) {
luCandidates.add(item);
}
}
luCandidates.add("UNKNOWN");
return luCandidates;
}
return list;
}
// Connection references
private TelnetConnection connection;
private final ConnectionConfig config;
private final ScreenBuffer screenBuffer;
private final DataStreamProcessor dsProcessor;
private final haus.nightmare.lib3270j.nvt.NvtProcessor nvtProcessor;
private volatile ConnectionState connectionState = ConnectionState.NOT_CONNECTED;
// Listeners
private final List<ConnectionListener> connectionListeners = new CopyOnWriteArrayList<>();
private final List<ScreenUpdateListener> screenListeners = new CopyOnWriteArrayList<>();
private final List<SCSInboundListener> scsListeners = new CopyOnWriteArrayList<>();
// Connected LU info
private String connectedLu;
private String connectedType;
// Phase 10: Contention Resolution & Auto-Unlock State
private boolean sdi_flag = false;
private boolean kri_flag = false;
private boolean negotiateContentionResolution = true;
public enum TN3270ESubmode { UNBOUND, E_3270, E_NVT, E_SSCP }
public TelnetFSM(ConnectionConfig config, ScreenBuffer screenBuffer, DataStreamProcessor dsProcessor) {
this.config = config;
this.screenBuffer = screenBuffer;
this.dsProcessor = dsProcessor;
this.nvtProcessor = new haus.nightmare.lib3270j.nvt.NvtProcessor(screenBuffer, new haus.nightmare.lib3270j.charset.EbcdicTranslator());
this.nvtProcessor.setOutputSender(this::sendNvtData);
}
public haus.nightmare.lib3270j.nvt.NvtProcessor getNvtProcessor() {
return nvtProcessor;
}
public void setConnection(TelnetConnection connection) {
this.connection = connection;
}
public void addConnectionListener(ConnectionListener l) { connectionListeners.add(l); }
public void removeConnectionListener(ConnectionListener l) { connectionListeners.remove(l); }
public void addScreenUpdateListener(ScreenUpdateListener l) {
screenListeners.add(l);
nvtProcessor.addScreenUpdateListener(l);
}
public void removeScreenUpdateListener(ScreenUpdateListener l) {
screenListeners.remove(l);
nvtProcessor.removeScreenUpdateListener(l);
}
public void addSCSInboundListener(SCSInboundListener l) {
if (l != null && !scsListeners.contains(l)) {
scsListeners.add(l);
}
}
public void removeSCSInboundListener(SCSInboundListener l) {
scsListeners.remove(l);
}
public ConnectionState getConnectionState() { return connectionState; }
public boolean[] getMyOpts() { return myOpts; }
public boolean[] getHisOpts() { return hisOpts; }
/**
* Called when the TCP connection is established.
* Initialize telnet state and prepare for negotiation.
*/
public void onConnected() {
state = TNS_DATA;
java.util.Arrays.fill(myOpts, false);
java.util.Arrays.fill(hisOpts, false);
java.util.Arrays.fill(eFuncs, false);
tn3270eNegotiated = false;
tn3270eDeviceTypeSent = false;
tn3270eSubmode = TN3270ESubmode.UNBOUND;
tn3270eBound = false;
eXmitSeq = 0;
lastRcvSeq = 0;
deferredWillTtype = false;
ttypeIndex = 0;
luIndex = 0;
ibuf.reset();
sbbuf.reset();
// Initial TN3270E function requests
eFuncs[FUNC_BIND_IMAGE] = true;
eFuncs[FUNC_RESPONSES] = true;
eFuncs[FUNC_SYSREQ] = true;
eFuncs[FUNC_SNA_SENSE] = true;
eFuncs[FUNC_DATA_STREAM_CTL] = true;
eFuncs[FUNC_CONTENTION_RESOLUTION] = negotiateContentionResolution;
statusDisplay(STATUS_CONNECTING, "Connecting to host");
changeState(ConnectionState.TELNET_PENDING);
}
/**
* Feed a bulk buffer of bytes from the network into the FSM.
*/
public void feedBytes(byte[] buf, int offset, int len) {
int end = offset + len;
int i = offset;
while (i < end) {
if (state == TNS_DATA) {
int start = i;
while (i < end && (buf[i] & 0xFF) != IAC) {
i++;
}
if (i > start) {
if (connectionState == ConnectionState.TELNET_PENDING) {
changeState(ConnectionState.CONNECTED_NVT);
}
boolean isPlainNvt = (connectionState == ConnectionState.CONNECTED_NVT || connectionState == ConnectionState.CONNECTED_NVT_CHAR)
&& !(hisOpts[TELOPT_BINARY] && hisOpts[TELOPT_EOR]);
if (isPlainNvt) {
nvtProcessor.processNVTData(buf, start, i - start);
} else if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING || hisOpts[TELOPT_BINARY] || hisOpts[TELOPT_EOR]) {
ibuf.write(buf, start, i - start);
}
}
if (i < end) {
state = TNS_IAC;
i++;
}
} else {
feedByte(buf[i] & 0xFF);
i++;
}
}
}
/**
* Feed a single byte from the network into the FSM.
*/
public void feedByte(int c) {
switch (state) {
case TNS_DATA:
processData(c);
break;
case TNS_IAC:
processIAC(c);
break;
case TNS_WILL:
processWill(c);
state = TNS_DATA;
break;
case TNS_WONT:
processWont(c);
state = TNS_DATA;
break;
case TNS_DO:
processDo(c);
state = TNS_DATA;
break;
case TNS_DONT:
processDont(c);
state = TNS_DATA;
break;
case TNS_SB:
processSB(c);
break;
case TNS_SB_IAC:
processSBIAC(c);
break;
}
}
/** Called at the end of a network read batch. */
public void endOfNetworkData() {
// Opportunity to flush any pending NVT data
}
// ========== TNS_DATA ==========
private void processData(int c) {
if (c == IAC) {
state = TNS_IAC;
return;
}
if (connectionState == ConnectionState.TELNET_PENDING) {
// Got data before any telnet commands — assume NVT mode
changeState(ConnectionState.CONNECTED_NVT);
}
boolean isPlainNvt = (connectionState == ConnectionState.CONNECTED_NVT || connectionState == ConnectionState.CONNECTED_NVT_CHAR)
&& !(hisOpts[TELOPT_BINARY] && hisOpts[TELOPT_EOR]);
if (isPlainNvt) {
nvtSingleByteScratch[0] = (byte) c;
nvtProcessor.processNVTData(nvtSingleByteScratch, 0, 1);
return;
}
// Accumulate data for 3270, TN3270E (including CONNECTED_E_NVT, SSCP-LU and unbound states, and pending)
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING || hisOpts[TELOPT_BINARY] || hisOpts[TELOPT_EOR]) {
ibuf.write(c);
}
}
// ========== TNS_IAC ==========
private void processIAC(int c) {
switch (c) {
case IAC: // Escaped IAC — literal 0xFF
ibuf.write(c);
state = TNS_DATA;
break;
case EOR: // End of record — process accumulated 3270 data
log.fine("RCVD EOR");
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
processEndOfRecord();
}
ibuf.reset();
state = TNS_DATA;
break;
case WILL: state = TNS_WILL; break;
case WONT: state = TNS_WONT; break;
case DO: state = TNS_DO; break;
case DONT: state = TNS_DONT; break;
case SB:
sbbuf.reset();
state = TNS_SB;
break;
case GA:
log.fine("RCVD GA");
state = TNS_DATA;
break;
case AYT:
log.info("RCVD AYT - sending acknowledgment");
sendBytes(new byte[] { (byte) IAC, (byte) NOP });
state = TNS_DATA;
break;
case NOP:
log.fine("RCVD NOP");
state = TNS_DATA;
break;
default:
log.fine("RCVD IAC " + TelnetConstants.commandName(c));
state = TNS_DATA;
break;
}
}
// ========== TNS_WILL (host sends WILL option) ==========
private void processWill(int opt) {
log.info("RCVD WILL " + TelnetConstants.optionName(opt));
switch (opt) {
case TELOPT_SGA:
case TELOPT_BINARY:
case TELOPT_EOR:
case TELOPT_ECHO:
if (!hisOpts[opt]) {
hisOpts[opt] = true;
sendCommand(DO, opt);
}
break;
case TELOPT_TM:
// RFC 860: Timing Mark - reply with DO TM
sendCommand(DO, opt);
break;
case TELOPT_STARTTLS:
if (config.isStartTlsEnabled() && !hisOpts[opt]) {
hisOpts[opt] = true;
sendCommand(DO, opt);
}
break;
case TELOPT_TN3270E:
if (!config.isTn3270eEnabled()) {
sendCommand(DONT, opt);
} else if (!hisOpts[opt]) {
hisOpts[opt] = true;
sendCommand(DO, opt);
}
break;
default:
sendCommand(DONT, opt);
break;
}
checkIn3270();
}
// ========== TNS_WONT ==========
private void processWont(int opt) {
log.info("RCVD WONT " + TelnetConstants.optionName(opt));
if (hisOpts[opt]) {
hisOpts[opt] = false;
sendCommand(DONT, opt);
}
checkIn3270();
}
// ========== TNS_DO (host requests we enable option) ==========
private void processDo(int opt) {
log.info("RCVD DO " + TelnetConstants.optionName(opt));
switch (opt) {
case TELOPT_BINARY:
case TELOPT_EOR:
case TELOPT_SGA:
if (!myOpts[opt]) {
myOpts[opt] = true;
sendCommand(WILL, opt);
}
break;
case TELOPT_TM:
// RFC 860: Timing Mark - reply with WILL TM
sendCommand(WILL, opt);
break;
case TELOPT_STARTTLS:
if (config.isStartTlsEnabled() && !myOpts[opt]) {
myOpts[opt] = true;
sendCommand(WILL, opt);
}
break;
case TELOPT_TTYPE:
if (!myOpts[opt]) {
myOpts[opt] = true;
if (config.isTn3270eEnabled() && hisOpts[TELOPT_TN3270E]) {
// Defer TTYPE response until TN3270E negotiation completes
deferredWillTtype = true;
} else {
sendCommand(WILL, opt);
}
}
break;
case TELOPT_TN3270E:
if (!config.isTn3270eEnabled()) {
sendCommand(WONT, opt);
} else if (!myOpts[opt]) {
myOpts[opt] = true;
sendCommand(WILL, opt);
tn3270eDeviceTypeSent = false;
}
break;
case TELOPT_NAWS:
if (!myOpts[opt]) {
myOpts[opt] = true;
sendCommand(WILL, opt);
}
sendNaws();
break;
case TELOPT_NEW_ENVIRON:
if (!myOpts[opt]) {
myOpts[opt] = true;
sendCommand(WILL, opt);
}
break;
default:
sendCommand(WONT, opt);
break;
}
checkIn3270();
}
// ========== TNS_DONT ==========
private void processDont(int opt) {
log.info("RCVD DONT " + TelnetConstants.optionName(opt));
if (myOpts[opt]) {
myOpts[opt] = false;
sendCommand(WONT, opt);
}
checkIn3270();
}
// ========== TNS_SB (accumulating sub-negotiation data) ==========
private void processSB(int c) {
if (c == IAC) {
state = TNS_SB_IAC;
} else {
sbbuf.write(c);
}
}
// ========== TNS_SB_IAC (IAC seen during sub-negotiation) ==========
private void processSBIAC(int c) {
if (c == SE) {
// Sub-negotiation complete
byte[] sbData = sbbuf.toByteArray();
sbbuf.reset();
processSubNegotiation(sbData);
state = TNS_DATA;
} else if (c == IAC) {
// Escaped IAC within sub-negotiation
sbbuf.write(IAC);
state = TNS_SB;
} else {
// Shouldn't happen, but recover
log.warning("Unexpected byte " + c + " after IAC in SB");
sbbuf.reset();
state = TNS_DATA;
}
}
// ========== Sub-negotiation processing ==========
private void processSubNegotiation(byte[] data) {
if (data.length < 1) return;
int opt = data[0] & 0xFF;
log.info("RCVD SB " + TelnetConstants.optionName(opt) + " (" + data.length + " bytes)");
switch (opt) {
case TELOPT_TTYPE:
handleTTypeSB(data);
break;
case TELOPT_TN3270E:
handleTN3270ESB(data);
break;
case TELOPT_NEW_ENVIRON:
handleNewEnvironSB(data);
break;
case TELOPT_STARTTLS:
handleStartTlsSB(data);
break;
default:
log.info("Ignoring SB for option " + opt);
break;
}
}
// ========== STARTTLS sub-negotiation ==========
private void handleStartTlsSB(byte[] data) {
if (data.length >= 2 && (data[1] & 0xFF) == TLS_FOLLOWS) {
log.info("RCVD SB STARTTLS FOLLOWS (1) - Initiating TLS elevation");
processStartTls();
}
}
public void processStartTls() {
log.info("Elevating active connection to TLS via STARTTLS (Option 46)");
try {
if (connection != null) {
connection.upgradeToTls();
statusDisplay(STATUS_SECURITY, "TLS socket elevated via STARTTLS");
}
} catch (IOException e) {
log.log(Level.SEVERE, "Failed to elevate socket to TLS via STARTTLS", e);
onError("STARTTLS elevation failed: " + e.getMessage());
}
}
// ========== TTYPE sub-negotiation ==========
private void handleTTypeSB(byte[] data) {
if (data.length >= 2 && data[1] == TELQUAL_SEND) {
// Host asks for terminal type — cycle per RFC 1091
List<String> candidates = getCandidateTerminalTypes();
String termType = candidates.get(Math.min(ttypeIndex, candidates.size() - 1));
if (ttypeIndex < candidates.size() - 1) {
ttypeIndex++;
}
log.info("RCVD SB TTYPE SEND - Responding with: " + termType);
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(IAC);
out.write(SB);
out.write(TELOPT_TTYPE);
out.write(TELQUAL_IS);
for (char ch : termType.toCharArray()) {
out.write((byte) ch);
}
out.write(IAC);
out.write(SE);
sendBytes(out.toByteArray());
if (termType.contains("@")) {
connectedLu = termType.substring(termType.indexOf('@') + 1).trim();
}
log.warning(">>> SENT SB TTYPE IS " + termType + " SE");
}
}
// ========== NEW_ENVIRON sub-negotiation (RFC 1572 / RFC 2877) ==========
private void handleNewEnvironSB(byte[] data) {
if (data.length < 2) return;
int qual = data[1] & 0xFF;
if (qual == TELQUAL_SEND) {
log.info("RCVD SB NEW-ENVIRON SEND (" + (data.length - 2) + " bytes)");
if (data.length == 2) {
// Empty SEND: send all configured variables
sendNewEnvironmentVariables(config.getEnvironmentVariables(), config.getUserVariables());
return;
}
// Parse requested variable names
java.util.Map<String, String> respVars = new java.util.LinkedHashMap<>();
java.util.Map<String, String> respUserVars = new java.util.LinkedHashMap<>();
int idx = 2;
while (idx < data.length) {
int objType = data[idx++] & 0xFF;
ByteArrayOutputStream nameBuf = new ByteArrayOutputStream();
boolean escaped = false;
while (idx < data.length) {
int b = data[idx] & 0xFF;
if (!escaped && (b == TELOBJ_VAR || b == TELOBJ_USERVAR)) {
break;
}
idx++;
if (!escaped && b == TELOBJ_ESC) {
escaped = true;
continue;
}
nameBuf.write(b);
escaped = false;
}
String varName = new String(nameBuf.toByteArray(), java.nio.charset.StandardCharsets.US_ASCII);
if (objType == TELOBJ_VAR) {
if (varName.isEmpty()) {
respVars.putAll(config.getEnvironmentVariables());
} else if (config.getEnvironmentVariables().containsKey(varName)) {
respVars.put(varName, config.getEnvironmentVariables().get(varName));
}
} else if (objType == TELOBJ_USERVAR) {
if (varName.isEmpty()) {
respUserVars.putAll(config.getUserVariables());
} else if (config.getUserVariables().containsKey(varName)) {
respUserVars.put(varName, config.getUserVariables().get(varName));
}
}
}
sendNewEnvironmentVariables(respVars, respUserVars);
}
}
public void sendNewEnvironmentVariables(java.util.Properties props) {
java.util.Map<String, String> vars = new java.util.LinkedHashMap<>();
java.util.Map<String, String> uVars = new java.util.LinkedHashMap<>();
if (props != null) {
for (String k : props.stringPropertyNames()) {
if (k.startsWith("USERVAR_") || k.startsWith("USER_")) {
uVars.put(k, props.getProperty(k));
} else {
vars.put(k, props.getProperty(k));
}
}
}
sendNewEnvironmentVariables(vars, uVars);
}
public void sendNewEnvironmentVariables(java.util.Map<String, String> vars, java.util.Map<String, String> userVars) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(IAC);
out.write(SB);
out.write(TELOPT_NEW_ENVIRON);
out.write(TELQUAL_IS);
if (vars != null) {
for (java.util.Map.Entry<String, String> e : vars.entrySet()) {
out.write(TELOBJ_VAR);
writeEscapedEnvironString(out, e.getKey());
out.write(TELOBJ_VALUE);
writeEscapedEnvironString(out, e.getValue());
}
}
if (userVars != null) {
for (java.util.Map.Entry<String, String> e : userVars.entrySet()) {
out.write(TELOBJ_USERVAR);
writeEscapedEnvironString(out, e.getKey());
out.write(TELOBJ_VALUE);
writeEscapedEnvironString(out, e.getValue());
}
}
out.write(IAC);
out.write(SE);
sendBytes(out.toByteArray());
log.info("SENT SB NEW-ENVIRON IS (" + ((vars != null ? vars.size() : 0) + (userVars != null ? userVars.size() : 0)) + " vars) SE");
}
private void writeEscapedEnvironString(ByteArrayOutputStream out, String s) {
if (s == null) return;
for (byte b : s.getBytes(java.nio.charset.StandardCharsets.US_ASCII)) {
int ub = b & 0xFF;
if (ub == TELOBJ_VAR || ub == TELOBJ_VALUE || ub == TELOBJ_ESC || ub == TELOBJ_USERVAR) {
out.write(TELOBJ_ESC);
} else if (ub == IAC) {
out.write(IAC);
}
out.write(ub);
}
}
// ========== TN3270E sub-negotiation ==========
private void handleTN3270ESB(byte[] data) {
if (data.length < 2) return;
int op = data[1] & 0xFF;
log.info("TN3270E SB op=" + op + " (" + tn3270eOpName(op) + ")");
switch (op) {
case OP_SEND:
// Host asks us to send device-type request
if (!tn3270eDeviceTypeSent) {
sendTN3270EDeviceTypeRequest();
tn3270eDeviceTypeSent = true;
}
break;
case OP_DEVICE_TYPE:
handleTN3270EDeviceType(data);
break;
case OP_FUNCTIONS:
handleTN3270EFunctions(data);
break;
case OP_IS:
// Could be device-type IS or functions IS, check context
if (data.length >= 3 && data[2] == OP_DEVICE_TYPE) {
handleTN3270EDeviceType(data);
} else if (data.length >= 3 && data[2] == OP_FUNCTIONS) {
handleTN3270EFunctions(data);
}
break;
default:
log.info("Unhandled TN3270E op: " + op);
break;
}
}
private void sendTN3270EDeviceTypeRequest() {
List<String> candidates = getCandidateTerminalTypes();
String termType = candidates.get(Math.min(ttypeIndex, candidates.size() - 1));
String currentLu = null;
List<String> lus = config.getLuNames();
if (lus != null && !lus.isEmpty() && luIndex < lus.size()) {
currentLu = lus.get(luIndex);
} else if (config.getLuName() != null && !config.getLuName().isEmpty()) {
currentLu = config.getLuName();
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(IAC);
out.write(SB);
out.write(TELOPT_TN3270E);
out.write(OP_DEVICE_TYPE);
out.write(OP_REQUEST);
for (char ch : termType.toCharArray()) {
out.write((byte) ch);
}
// Add LU name if specified
if (currentLu != null && !currentLu.isEmpty()) {
out.write(OP_CONNECT);
for (char ch : currentLu.toCharArray()) {
out.write((byte) ch);
}
}
out.write(IAC);
out.write(SE);
sendBytes(out.toByteArray());
log.warning(">>> SENT SB TN3270E DEVICE-TYPE REQUEST " + termType +
(currentLu != null ? " CONNECT " + currentLu : "") + " SE");
}
private void handleTN3270EDeviceType(byte[] data) {
// Parse: TN3270E DEVICE-TYPE IS <type> CONNECT <name>
// or: TN3270E DEVICE-TYPE REJECT REASON <code>
int pos = 2; // Skip TN3270E and DEVICE-TYPE
if (pos < data.length && (data[pos] & 0xFF) == OP_IS) {
pos++;
}
// Check if REJECT
if (pos < data.length && (data[pos] & 0xFF) == OP_REJECT) {
pos++;
int reason = REASON_UNSUPPORTED_REQ;
if (pos < data.length && (data[pos] & 0xFF) == OP_REASON) {
pos++;
}
if (pos < data.length) {
reason = data[pos] & 0xFF;
}
log.warning("TN3270E device-type rejected: " + TN3270EConstants.reasonName(reason));
// 1. Try next LU in pool if available
List<String> lus = config.getLuNames();
if (lus != null && luIndex + 1 < lus.size()) {
luIndex++;
log.info("Retrying TN3270E device-type with next LU: " + lus.get(luIndex));
sendTN3270EDeviceTypeRequest();
return;
}
// 2. Try fallback terminal type candidate if available
List<String> candidates = getCandidateTerminalTypes();
if (ttypeIndex + 1 < candidates.size() - 1) { // exclude UNKNOWN
ttypeIndex++;
log.info("Retrying TN3270E device-type with next candidate: " + candidates.get(ttypeIndex));
sendTN3270EDeviceTypeRequest();
return;
}
// Fall back to plain TN3270
myOpts[TELOPT_TN3270E] = false;
hisOpts[TELOPT_TN3270E] = false;
sendCommand(WONT, TELOPT_TN3270E);
// Send deferred WILL TTYPE if needed
if (deferredWillTtype) {
sendCommand(WILL, TELOPT_TTYPE);
deferredWillTtype = false;
}
return;
}
// Parse device type name
StringBuilder deviceType = new StringBuilder();
while (pos < data.length && (data[pos] & 0xFF) != OP_CONNECT) {
deviceType.append((char) (data[pos] & 0xFF));
pos++;
}
// Parse device name (after CONNECT)
String deviceName = null;
if (pos < data.length && (data[pos] & 0xFF) == OP_CONNECT) {
pos++;
StringBuilder name = new StringBuilder();
while (pos < data.length) {
name.append((char) (data[pos] & 0xFF));
pos++;
}
deviceName = name.toString();
}
connectedType = deviceType.toString();
connectedLu = deviceName;
log.info("TN3270E device-type IS " + connectedType +
(connectedLu != null ? " CONNECT " + connectedLu : ""));
// Now send functions request
sendTN3270EFunctionsRequest();
}
private void sendTN3270EFunctionsRequest() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(IAC);
out.write(SB);
out.write(TELOPT_TN3270E);
out.write(OP_FUNCTIONS);
out.write(OP_REQUEST);
StringBuilder funcNames = new StringBuilder();
for (int i = 0; i < eFuncs.length; i++) {
if (eFuncs[i]) {
out.write(i);
if (funcNames.length() > 0) funcNames.append(" ");
funcNames.append(TN3270EConstants.functionName(i));
}
}
out.write(IAC);
out.write(SE);
sendBytes(out.toByteArray());
log.info("SENT SB TN3270E FUNCTIONS REQUEST " + funcNames + " SE");
}
private void sendTN3270EFunctionsIs() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(IAC);
out.write(SB);
out.write(TELOPT_TN3270E);
out.write(OP_FUNCTIONS);
out.write(OP_IS);
StringBuilder funcNames = new StringBuilder();
for (int i = 0; i < eFuncs.length; i++) {
if (eFuncs[i]) {
out.write(i);
if (funcNames.length() > 0) funcNames.append(" ");
funcNames.append(TN3270EConstants.functionName(i));
}
}
out.write(IAC);
out.write(SE);
sendBytes(out.toByteArray());
log.info("SENT SB TN3270E FUNCTIONS IS " + funcNames + " SE");
}
private void handleTN3270EFunctions(byte[] data) {
// Parse: TN3270E FUNCTIONS REQUEST [func...] or TN3270E FUNCTIONS IS [func...]
int pos = 1;
boolean isRequest = false;
while (pos < data.length) {
int b = data[pos] & 0xFF;
if (b == OP_FUNCTIONS) {
pos++;
} else if (b == OP_REQUEST) {
isRequest = true;
pos++;
break;
} else if (b == OP_IS) {
isRequest = false;
pos++;
break;
} else {
pos++;
}
}
// The remaining bytes are the proposed/agreed functions
boolean[] hostFuncs = new boolean[8];
while (pos < data.length) {
int func = data[pos] & 0xFF;
if (func <= FUNC_SNA_SENSE) {
hostFuncs[func] = true;
}
pos++;
}
if (isRequest) {
// Host sent FUNCTIONS REQUEST -> We reply with FUNCTIONS IS (intersection of functions)
for (int i = 0; i < eFuncs.length; i++) {
eFuncs[i] = eFuncs[i] && hostFuncs[i];
}
sendTN3270EFunctionsIs();
} else {
// Host sent FUNCTIONS IS -> Accept host's agreed function list
for (int i = 0; i < eFuncs.length; i++) {
eFuncs[i] = hostFuncs[i];
}
}
tn3270eNegotiated = true;
log.info("TN3270E functions negotiated: " + getNegotiatedFunctionNames());
log.info("TN3270E negotiation complete");
if (dsProcessor != null) {
dsProcessor.setContentionResolution(isContentionResolutionNegotiated());
}
// RFC 2355: If BIND-IMAGE function is not negotiated, the emulation session is considered
// to be bound immediately upon completion of the FUNCTIONS negotiation.
if (eFuncs[FUNC_BIND_IMAGE]) {
changeState(ConnectionState.CONNECTED_UNBOUND);
} else {
changeState(ConnectionState.CONNECTED_TN3270E);
tn3270eSubmode = TN3270ESubmode.E_3270;
}
// Notify listeners
for (ConnectionListener l : connectionListeners) {
l.onTN3270ENegotiated(connectedType, connectedLu);
l.onTN3270EFunctionsNegotiated(eFuncs);
}
}
private String getNegotiatedFunctionNames() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < eFuncs.length; i++) {
if (eFuncs[i]) {
if (sb.length() > 0) sb.append(" ");
sb.append(TN3270EConstants.functionName(i));
}
}
return sb.length() > 0 ? sb.toString() : "<none>";
}
// ========== End of Record processing ==========
private void processEndOfRecord() {
int dataLen = ibuf.size();
if (dataLen == 0) return;
byte[] data = ibuf.buffer();
if ((connectionState == ConnectionState.TELNET_PENDING ||
connectionState == ConnectionState.CONNECTED_NVT ||
connectionState == ConnectionState.CONNECTED_NVT_CHAR) && !tn3270eNegotiated) {
log.info("Received EOR during NVT/pending - transitioning to plain TN3270 mode");
changeState(ConnectionState.CONNECTED_3270);
}
if (tn3270eNegotiated) {
// TN3270E mode: data starts with 5-byte header
processTN3270ERecord(data, 0, dataLen);
} else {
// Plain TN3270 mode: data is raw 3270 data stream
if (dsProcessor != null) {
dsProcessor.processRecord(data, 0, dataLen, false);
}
notifyScreenUpdate();
}
ibuf.reset();
// Phase 10: Contention Resolution & AUTO_SYS_UNLOCK handling on EOR
if (dsProcessor != null) {
boolean crActive = isContentionResolutionNegotiated();
if (crActive) {
if (this.sdi_flag && !dsProcessor.isRcvdRead()) {
if (dsProcessor.getInputProcessor() != null) {
dsProcessor.getInputProcessor().setKeyboardLocked(false);
}
this.sdi_flag = false;
dsProcessor.setUnlockPending(false);
if (dsProcessor.isUnlockSysPending() || this.kri_flag) {
if (dsProcessor.getInputProcessor() != null && dsProcessor.getInputProcessor().getOIA() != null) {
dsProcessor.getInputProcessor().getOIA().setInputInhibited(haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NOT_INHIBITED);
}
}
this.kri_flag = false;
dsProcessor.setUnlockSysPending(false);
notifyKeyboardUnlocked();
}
this.kri_flag = false;
} else {
// Contention Resolution is not active
boolean autoSysUnlock = (config != null) ? config.isAutoSysUnlock() : true;
if (autoSysUnlock && !dsProcessor.isRcvdRead()) {
boolean sysLocked = false;
if (dsProcessor.getInputProcessor() != null) {
if (dsProcessor.getInputProcessor().isKeyboardLocked() ||
(dsProcessor.getInputProcessor().getOIA() != null && dsProcessor.getInputProcessor().getOIA().isXSystem())) {
sysLocked = true;
}
}
if (sysLocked) {
if (dsProcessor.getInputProcessor() != null) {
dsProcessor.getInputProcessor().setKeyboardLocked(false);
if (dsProcessor.getInputProcessor().getOIA() != null) {
dsProcessor.getInputProcessor().getOIA().setInputInhibited(haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NOT_INHIBITED);
}
}
dsProcessor.setUnlockSysPending(false);
notifyKeyboardUnlocked();
}
}
}
this.sdi_flag = false;
this.kri_flag = false;
dsProcessor.setRcvdRead(false);
}
}
public void processTn3270eHeader(byte[] data) {
processTN3270ERecord(data, 0, data != null ? data.length : 0);
}
private void processTN3270ERecord(byte[] data) {
processTN3270ERecord(data, 0, data != null ? data.length : 0);
}
private void processTN3270ERecord(byte[] data, int offset, int length) {
if (data == null || length < EH_SIZE) {
log.warning("TN3270E record too short: " + length);
return;
}
int dataType = data[offset] & 0xFF;
int requestFlag = data[offset + 1] & 0xFF;
int responseFlag = data[offset + 2] & 0xFF;
int seqNumber = ((data[offset + 3] & 0xFF) << 8) | (data[offset + 4] & 0xFF);
this.sdi_flag = (requestFlag & 0x01) != 0;
this.kri_flag = (requestFlag & 0x02) != 0;
if (dsProcessor != null) {
dsProcessor.setContentionResolution(isContentionResolutionNegotiated());
}
log.fine("TN3270E header: type=" + TN3270EConstants.dataTypeName(dataType) +
" rqf=" + requestFlag + " rsf=" + responseFlag + " seq=" + seqNumber);
responseRequired = responseFlag;
switch (dataType) {
case DT_3270_DATA:
if (length > EH_SIZE) {
// Transition to 3270 mode from any non-3270 state (E_NVT, UNBOUND, SSCP)
if (connectionState != ConnectionState.CONNECTED_TN3270E) {
// Clear screen on transition to 3270 mode from unbound/SSCP/NVT
// This ensures old SSCP-LU or NVT data doesn't persist
screenBuffer.erase(false);
changeState(ConnectionState.CONNECTED_TN3270E);
tn3270eSubmode = TN3270ESubmode.E_3270;
}
try {
dsProcessor.processRecord(data, offset + EH_SIZE, length - EH_SIZE, false);
notifyScreenUpdate();
// Send positive response if required
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
} catch (Exception e) {
log.log(Level.WARNING, "Error processing 3270 record", e);
if (eFuncs[FUNC_RESPONSES] && (responseFlag == RSF_ALWAYS_RESPONSE || responseFlag == RSF_ERROR_RESPONSE)) {
sendTN3270ENegativeResponse(seqNumber, NEG_OPERATION_CHECK);
}
}
} else {
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
}
break;
case DT_SCS_DATA:
if (length > EH_SIZE) {
try {
processSCSInbound(data, offset + EH_SIZE, length - EH_SIZE);
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
} catch (Exception e) {
log.log(Level.WARNING, "Error processing SCS record", e);
if (eFuncs[FUNC_RESPONSES] && (responseFlag == RSF_ALWAYS_RESPONSE || responseFlag == RSF_ERROR_RESPONSE)) {
sendTN3270ENegativeResponse(seqNumber, NEG_OPERATION_CHECK);
}
}
} else {
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
}
break;
case DT_SSCP_LU_DATA:
if (connectionState != ConnectionState.CONNECTED_SSCP) {
if (connectionState == ConnectionState.CONNECTED_UNBOUND ||
connectionState == ConnectionState.CONNECTED_E_NVT) {
// Clear screen on first SSCP-LU transition to remove stale data
screenBuffer.clear();
}
changeState(ConnectionState.CONNECTED_SSCP);
tn3270eSubmode = TN3270ESubmode.E_SSCP;
}
if (length > EH_SIZE) {
try {
dsProcessor.processSscpLuData(data, offset + EH_SIZE, length - EH_SIZE);
notifyScreenUpdate();
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
} catch (Exception e) {
log.log(Level.WARNING, "Error processing SSCP-LU record", e);
if (eFuncs[FUNC_RESPONSES] && (responseFlag == RSF_ALWAYS_RESPONSE || responseFlag == RSF_ERROR_RESPONSE)) {
sendTN3270ENegativeResponse(seqNumber, NEG_OPERATION_CHECK);
}
}
} else {
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
}
break;
case DT_BIND_IMAGE:
{
byte[] bindData = new byte[length];
System.arraycopy(data, offset, bindData, 0, length);
process_bind(bindData, responseFlag, seqNumber);
}
break;
case DT_UNBIND:
int unbindReason = (length > EH_SIZE) ? (data[offset + EH_SIZE] & 0xFF) : UNBIND_NORMAL;
process_unbind(unbindReason, responseFlag, seqNumber);
break;
case DT_NVT_DATA:
// NVT data in TN3270E mode
if (connectionState != ConnectionState.CONNECTED_E_NVT) {
changeState(ConnectionState.CONNECTED_E_NVT);
tn3270eSubmode = TN3270ESubmode.E_NVT;
}
if (dsProcessor != null && dsProcessor.getInputProcessor() != null) {
dsProcessor.getInputProcessor().setKeyboardLocked(false);
}
if (length > EH_SIZE) {
try {
processNVTData(data, offset + EH_SIZE, length - EH_SIZE);
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
} catch (Exception e) {
log.log(Level.WARNING, "Error processing NVT record", e);
if (eFuncs[FUNC_RESPONSES] && (responseFlag == RSF_ALWAYS_RESPONSE || responseFlag == RSF_ERROR_RESPONSE)) {
sendTN3270ENegativeResponse(seqNumber, NEG_OPERATION_CHECK);
}
}
} else {
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
}
notifyScreenUpdate();
break;
case DT_REQUEST:
log.info("Received DT_REQUEST requestFlag=" + requestFlag);
if ((requestFlag & RQF_KEYBOARD_RESTORE) != 0) {
if (dsProcessor != null && dsProcessor.getInputProcessor() != null) {
dsProcessor.getInputProcessor().setKeyboardLocked(false);
}
}
if ((requestFlag & RQF_SIGNAL) != 0) {
for (ScreenUpdateListener l : screenListeners) {
l.onSoundAlarm();
}
}
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
break;
case DT_BID:
process_BID(responseFlag, seqNumber);
break;
case DT_PRINT_EOJ:
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
break;
case DT_RESPONSE:
lastRcvSeq = seqNumber;
log.fine("Received response, seq=" + seqNumber);
break;
default:
// Check if the host sent a raw 3270 data stream (e.g. 0xF5 EraseWrite, 0x7E EW Alternate, 0xF1 Write, etc.)
// This happens when the server ignores or drops TN3270E framing and speaks plain 3270 data stream.
if (dataType == 0xF5 || dataType == 0x7E || dataType == 0xF1 || dataType == 0x6F ||
dataType == 0x6E || dataType == 0xF2 || dataType == 0xF6 ||
dataType == 0x0D || (length >= 2 && (data[offset] & 0xFF) == 0x11)) {
log.warning("Received plain 3270 command (0x" + Integer.toHexString(dataType) +
") in TN3270E mode — automatically switching to plain TN3270 mode");
tn3270eNegotiated = false;
changeState(ConnectionState.CONNECTED_3270);
dsProcessor.processRecord(data, offset, length, true);
notifyScreenUpdate();
} else {
log.info("Unhandled TN3270E data type: " + dataType);
}
break;
}
}
public void processSCSInbound(byte[] data) {
if (data == null) return;
processSCSInbound(data, 0, data.length);
}
public void processSCSInbound(byte[] data, int offset, int length) {
log.fine("Processing SCS inbound data (" + length + " bytes)");
for (SCSInboundListener l : scsListeners) {
try {
l.onSCSDataReceived(data, offset, length);
} catch (Exception e) {
log.log(Level.WARNING, "Error in SCS inbound listener", e);
}
}
}
/**
* Send 5-byte/6-byte TN3270E response packet (DT_RESPONSE = 0x02).
*/
public void sendTn3270eResponse(byte responseFlag, byte responseData, int seq) {
byte[] resp = new byte[EH_SIZE + 1];
resp[0] = (byte) DT_RESPONSE;
resp[1] = 0;
resp[2] = responseFlag;
resp[3] = (byte) ((seq >> 8) & 0xFF);
resp[4] = (byte) (seq & 0xFF);
resp[5] = responseData;
sendRecord(resp);
}
/**
* HoD 5-byte send_response compatible signature.
*/
public void send_response(short s, short s2, int n) {
byte[] byArray = new byte[5];
byArray[0] = (byte) DT_RESPONSE;
byArray[1] = (byte) s;
byArray[2] = (byte) s2;
byArray[3] = (byte) ((n >> 8) & 0xFF);
byArray[4] = (byte) (n & 0xFF);
sendRecord(byArray);
}
public void sendTN3270EPositiveResponse(int seqNumber) {
sendTn3270eResponse((byte) RSF_POSITIVE_RESPONSE, (byte) POS_DEVICE_END, seqNumber);
}
public void sendTN3270ENegativeResponse(int seqNumber, int negCode) {
sendTn3270eResponse((byte) RSF_NEGATIVE_RESPONSE, (byte) (negCode & 0xFF), seqNumber);
}
public void sendTN3270ESnaSenseResponse(int seqNumber, int sense1, int sense2) {
byte[] resp = new byte[EH_SIZE + 2];
resp[0] = (byte) DT_RESPONSE;
resp[1] = 0;
resp[2] = (byte) RSF_SNA_SENSE;
resp[3] = (byte) ((seqNumber >> 8) & 0xFF);
resp[4] = (byte) (seqNumber & 0xFF);
resp[5] = (byte) (sense1 & 0xFF);
resp[6] = (byte) (sense2 & 0xFF);
sendRecord(resp);
}
/**
* Send TN3270E UNBIND request to release LU session cleanly (RFC 2355 Section 9).
* @param reason SNA UNBIND reason code (e.g. UNBIND_NORMAL = 0x01)
*/
public synchronized void sendUnbind(int reason) {
if (!tn3270eNegotiated || !tn3270eBound) {
return;
}
if (connection == null || !connection.isConnected()) {
return;
}
try {
byte[] unbindData = new byte[EH_SIZE + 1];
unbindData[0] = (byte) DT_UNBIND;
unbindData[1] = 0; // Request flags
unbindData[2] = (byte) RSF_NO_RESPONSE; // Response flags
unbindData[3] = (byte) ((eXmitSeq >> 8) & 0xFF);
unbindData[4] = (byte) (eXmitSeq & 0xFF);
unbindData[5] = (byte) (reason & 0xFF);
eXmitSeq = (eXmitSeq + 1) & 0xFFFF;
log.info("Sending TN3270E UNBIND reason=" + unbindReasonName(reason));
sendRecord(unbindData);
tn3270eBound = false;
} catch (Exception e) {
log.log(Level.FINE, "Failed to send TN3270E UNBIND", e);
}
}
/**
* Send graceful Telnet de-negotiation (UNBIND if bound, followed by WONT/DONT for active options).
*/
public synchronized void sendTelnetDisconnect() {
if (connection == null || !connection.isConnected()) {
return;
}
try {
if (tn3270eNegotiated && tn3270eBound) {
sendUnbind(UNBIND_NORMAL);
}
// De-negotiate TN3270E first if active
if (myOpts[TELOPT_TN3270E]) {
sendCommand(WONT, TELOPT_TN3270E);
myOpts[TELOPT_TN3270E] = false;
}
if (hisOpts[TELOPT_TN3270E]) {
sendCommand(DONT, TELOPT_TN3270E);
hisOpts[TELOPT_TN3270E] = false;
}
// De-negotiate all other active options
for (int opt = 0; opt < 256; opt++) {
if (myOpts[opt]) {
sendCommand(WONT, opt);
myOpts[opt] = false;
}
if (hisOpts[opt]) {
sendCommand(DONT, opt);
hisOpts[opt] = false;
}
}
log.fine("Sent Telnet option de-negotiation");
} catch (Exception e) {
log.log(Level.FINE, "Error sending Telnet de-negotiation", e);
}
}
// ========== Check if we should transition to 3270 mode ==========
private void checkIn3270() {
if (connectionState != ConnectionState.TELNET_PENDING &&
connectionState != ConnectionState.CONNECTED_NVT &&
connectionState != ConnectionState.CONNECTED_NVT_CHAR) {
return;
}
// For TN3270E, we wait for TN3270E negotiation to complete
if (config.isTn3270eEnabled() && myOpts[TELOPT_TN3270E] && hisOpts[TELOPT_TN3270E]) {
return; // TN3270E in progress
}
// For plain TN3270: need BINARY and EOR in both directions
if (myOpts[TELOPT_BINARY] && hisOpts[TELOPT_BINARY] &&
myOpts[TELOPT_EOR] && hisOpts[TELOPT_EOR]) {
log.info("Transitioning to plain TN3270 mode");
if (connectionState != ConnectionState.CONNECTED_3270) {
screenBuffer.erase(false);
changeState(ConnectionState.CONNECTED_3270);
notifyScreenUpdate();
}
}
}
// ========== Sending helpers ==========
private void sendCommand(int cmd, int opt) {
byte[] msg = { (byte) IAC, (byte) cmd, (byte) opt };
sendBytes(msg);
log.info("SENT " + TelnetConstants.commandName(cmd) + " " + TelnetConstants.optionName(opt));
}
private void sendNaws() {
int cols = screenBuffer.getMaxCols();
int rows = screenBuffer.getMaxRows();
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(IAC);
out.write(SB);
out.write(TELOPT_NAWS);
writeNawsValue(out, cols);
writeNawsValue(out, rows);
out.write(IAC);
out.write(SE);
sendBytes(out.toByteArray());
log.info("SENT SB NAWS " + cols + " " + rows + " SE");
}
private void writeNawsValue(ByteArrayOutputStream out, int value) {
int hi = (value >> 8) & 0xFF;
int lo = value & 0xFF;
out.write(hi);
if (hi == IAC) out.write(IAC);
out.write(lo);
if (lo == IAC) out.write(IAC);
}
/**
* Send a 3270 record (with EOR framing, and TN3270E header if applicable).
*/
public void sendRecord(byte[] data) {
ByteArrayOutputStream out = new ByteArrayOutputStream(data.length + 10);
// Escape any IAC bytes in the payload
for (byte b : data) {
out.write(b & 0xFF);
if ((b & 0xFF) == IAC) {
out.write(IAC);
}
}
out.write(IAC);
out.write(EOR);
sendBytes(out.toByteArray());
}
/**
* Send a 3270 data record, with TN3270E header if in TN3270E mode.
*/
public void send3270Data(byte[] data) {
if (data != null && data.length >= 2 && (data[data.length - 2] & 0xFF) == IAC && (data[data.length - 1] & 0xFF) == EOR) {
byte[] trimmed = new byte[data.length - 2];
System.arraycopy(data, 0, trimmed, 0, trimmed.length);
data = trimmed;
}
if (tn3270eNegotiated) {
ByteArrayOutputStream out = new ByteArrayOutputStream(data.length + EH_SIZE);
// TN3270E header
out.write(DT_3270_DATA); // data type
out.write(0); // request flag
out.write(0); // response flag
out.write((eXmitSeq >> 8) & 0xFF); // seq high
out.write(eXmitSeq & 0xFF); // seq low
eXmitSeq = (eXmitSeq + 1) & 0xFFFF;
// 3270 data
for (byte b : data) {
out.write(b & 0xFF);
}
sendRecord(out.toByteArray());
} else {
sendRecord(data);
}
}
/**
* Send SSCP-LU data (in TN3270E SSCP-LU mode).
*/
public void sendSscpLuData(byte[] data) {
if (tn3270eNegotiated) {
ByteArrayOutputStream out = new ByteArrayOutputStream(data.length + EH_SIZE);
out.write(DT_SSCP_LU_DATA);
out.write(0);
out.write(0);
out.write((eXmitSeq >> 8) & 0xFF);
out.write(eXmitSeq & 0xFF);
eXmitSeq = (eXmitSeq + 1) & 0xFFFF;
for (byte b : data) {
out.write(b & 0xFF);
}
sendRecord(out.toByteArray());
}
}
/**
* Send NVT data record (with TN3270E header and EOR framing if in TN3270E mode, or raw bytes).
*/
public void sendNvtData(byte[] data) {
if (data == null || data.length == 0) return;
if (tn3270eNegotiated && (connectionState == ConnectionState.CONNECTED_E_NVT || tn3270eSubmode == TN3270ESubmode.E_NVT)) {
ByteArrayOutputStream out = new ByteArrayOutputStream(data.length + EH_SIZE);
out.write(DT_NVT_DATA);
out.write(0);
out.write(0);
out.write((eXmitSeq >> 8) & 0xFF);
out.write(eXmitSeq & 0xFF);
eXmitSeq = (eXmitSeq + 1) & 0xFFFF;
for (byte b : data) {
out.write(b & 0xFF);
}
sendRecord(out.toByteArray());
} else {
sendBytes(data);
if (config != null && config.isNvtLocalEcho()) {
nvtProcessor.processNVTData(data, 0, data.length);
}
}
}
private void sendBytes(byte[] data) {
if (connection == null) return;
try {
connection.sendRaw(data);
} catch (IOException e) {
log.log(Level.WARNING, "Send error", e);
onError("Send error: " + e.getMessage());
}
}
// ========== State management ==========
private void changeState(ConnectionState newState) {
ConnectionState old = this.connectionState;
if (old == newState) {
return;
}
this.connectionState = newState;
log.info("State: " + old + " -> " + newState);
for (ConnectionListener l : connectionListeners) {
l.onConnectionStateChanged(old, newState);
}
}
public void setConnectionState(ConnectionState newState) {
changeState(newState);
}
public void onDisconnect() {
onDisconnect(false);
}
public void onDisconnect(boolean unexpected) {
if (unexpected && config != null && config.isAutoReconnect()) {
log.info("Unexpected connection loss — transitioning to RECONNECTING");
changeState(ConnectionState.RECONNECTING);
} else {
changeState(ConnectionState.NOT_CONNECTED);
}
}
/**
* Cleanly reset Telnet and TN3270E session state prior to reconnecting.
*/
public synchronized void resetSessionState() {
state = TNS_DATA;
java.util.Arrays.fill(myOpts, false);
java.util.Arrays.fill(hisOpts, false);
ibuf.reset();
sbbuf.reset();
tn3270eNegotiated = false;
tn3270eSubmode = TN3270ESubmode.UNBOUND;
tn3270eBound = false;
java.util.Arrays.fill(eFuncs, false);
eXmitSeq = 0;
lastRcvSeq = 0;
lastRespType = 0;
lastRespCode = 0;
responseRequired = RSF_NO_RESPONSE;
deferredWillTtype = false;
tn3270eDeviceTypeSent = false;
ttypeIndex = 0;
luIndex = 0;
connectedLu = null;
connectedType = null;
sdi_flag = false;
kri_flag = false;
}
public void onError(String message) {
log.warning("Error: " + message);
for (ConnectionListener l : connectionListeners) {
l.onConnectionError(message);
}
}
public void notifyScreenUpdate() {
if (screenBuffer != null) {
screenBuffer.notifyScreenUpdate();
}
for (ScreenUpdateListener l : screenListeners) {
l.onScreenUpdated();
}
}
public boolean isTn3270eNegotiated() { return tn3270eNegotiated; }
public void setTn3270eNegotiated(boolean neg) { this.tn3270eNegotiated = neg; }
public String getConnectedLu() { return connectedLu; }
public String getConnectedType() { return connectedType; }
public TN3270ESubmode getTn3270eSubmode() { return tn3270eSubmode; }
// ========== SNA BIND, UNBIND, BID, DFC Handlers (Phase 2) ==========
public void process_bind(byte[] data, int responseFlag, int seqNumber) {
tn3270eBound = true;
int bindLen = data.length - EH_SIZE;
StringBuilder bindHex = new StringBuilder();
for (int bi = EH_SIZE; bi < data.length && bi < EH_SIZE + 40; bi++) {
bindHex.append(String.format("%02x ", data[bi] & 0xFF));
}
log.info("Received BIND image (" + bindLen + " bytes) responseFlag=" + responseFlag +
" raw: " + bindHex.toString().trim());
final int BIND_OFF_RD = 20, BIND_OFF_CD = 21;
final int BIND_OFF_RA = 22, BIND_OFF_CA = 23;
final int BIND_OFF_SSIZE = 24;
if (bindLen > BIND_OFF_SSIZE) {
int ssize = data[EH_SIZE + BIND_OFF_SSIZE] & 0xFF;
int bindRd = data[EH_SIZE + BIND_OFF_RD] & 0xFF;
int bindCd = data[EH_SIZE + BIND_OFF_CD] & 0xFF;
int bindRa, bindCa;
switch (ssize) {
case 0x00: case 0x02:
bindRd = 24; bindCd = 80;
bindRa = 24; bindCa = 80;
break;
case 0x03:
bindRd = 24; bindCd = 80;
bindRa = config.isDynamicModel() ? config.getDynamicRows() : screenBuffer.getMaxRows();
bindCa = config.isDynamicModel() ? config.getDynamicCols() : screenBuffer.getMaxCols();
break;
case 0x7E:
bindRa = bindRd; bindCa = bindCd;
break;
case 0x7F:
bindRa = data[EH_SIZE + BIND_OFF_RA] & 0xFF;
bindCa = data[EH_SIZE + BIND_OFF_CA] & 0xFF;
break;
default:
bindRa = config.isDynamicModel() ? config.getDynamicRows() : screenBuffer.getMaxRows();
bindCa = config.isDynamicModel() ? config.getDynamicCols() : screenBuffer.getMaxCols();
break;
}
log.info("BIND SSIZE=0x" + String.format("%02x", ssize) +
" default=" + bindRd + "x" + bindCd +
" alt=" + bindRa + "x" + bindCa);
if (bindRa > 0 && bindCa > 0) {
screenBuffer.setAlternateDimensions(bindRa, bindCa);
}
}
screenBuffer.erase(false);
changeState(ConnectionState.CONNECTED_TN3270E);
tn3270eSubmode = TN3270ESubmode.E_3270;
statusDisplay(STATUS_CONNECTED, "Session bound");
notifyScreenUpdate();
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
}
public void process_unbind(int unbindReason, int responseFlag, int seqNumber) {
log.info("Received UNBIND reason=" + unbindReasonName(unbindReason) + " responseFlag=" + responseFlag);
tn3270eBound = false;
screenBuffer.setAlternateDimensions(screenBuffer.getMaxRows(), screenBuffer.getMaxCols());
screenBuffer.clear();
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
changeState(ConnectionState.CONNECTED_UNBOUND);
tn3270eSubmode = TN3270ESubmode.UNBOUND;
statusDisplay(STATUS_BIND_ERROR, "Session unbound: " + unbindReasonName(unbindReason));
notifyScreenUpdate();
}
public void process_BID(int responseFlag, int seqNumber) {
log.info("Received BID contention request");
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
}
public void process_DFC(int order, int responseFlag, int seqNumber) {
log.info("Received SNA DFC command: " + dfcCommandName(order));
switch (order) {
case DFC_CLEAR:
handleSnaClear();
break;
case DFC_SIGNAL:
handleSnaSignal();
break;
case DFC_CANCEL:
handleSnaCancel();
break;
case DFC_RQR:
handleSnaRqr();
break;
case DFC_STSN:
handleSnaStsn();
break;
default:
break;
}
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
}
// ========== NVT Delegation (Phase 2) ==========
public void processNVTData(byte[] data, int offset, int length) {
nvtProcessor.processNVTData(data, offset, length);
}
public int processAnsiEscapeSequence(byte[] seq, int curAddr, int rows, int cols) {
return nvtProcessor.processAnsiEscapeSequence(seq, curAddr, rows, cols);
}
public void sendNVTChar(char c) throws IOException {
nvtProcessor.sendNVTChar(c);
}
public void sendNVTString(String s) throws IOException {
nvtProcessor.sendNVTString(s);
}
// ========== Device Name Pool & Response Tracking (Phase 2) ==========
public void addDeviceName(String pool, String luName, int index) {
List<String> list = devicePools.computeIfAbsent(pool, k -> new ArrayList<>());
if (index >= 0 && index < list.size()) {
list.add(index, luName);
} else {
list.add(luName);
}
}
public void removeDeviceName(String pool, String luName, int index) {
List<String> list = devicePools.get(pool);
if (list != null) {
if (index >= 0 && index < list.size()) {
list.remove(index);
} else if (luName != null) {
list.remove(luName);
}
}
}
public String getDeviceName(String pool, int index) {
List<String> list = devicePools.get(pool);
if (list != null && index >= 0 && index < list.size()) {
return list.get(index);
}
return null;
}
public List<String> getDevicePoolList(String pool) {
return devicePools.getOrDefault(pool, Collections.emptyList());
}
public void setResponse(short respType, short respCode) {
this.lastRespType = respType;
this.lastRespCode = respCode;
}
public void sendRequest(int requestType) {
if (tn3270eNegotiated) {
byte[] req = new byte[EH_SIZE];
req[0] = (byte) DT_REQUEST;
req[1] = (byte) requestType;
req[2] = 0;
req[3] = (byte) ((eXmitSeq >> 8) & 0xFF);
req[4] = (byte) (eXmitSeq & 0xFF);
eXmitSeq = (eXmitSeq + 1) & 0xFFFF;
sendRecord(req);
}
}
public void statusDisplay(int statusNumber, String msg) {
log.info("[HoD Status " + statusNumber + "] " + (msg != null ? msg : ""));
}
// ========== Internal State & Timer Handlers (Phase 2) ==========
public void handleTimingMark() {
sendCommand(WONT, TELOPT_TM);
}
public void handleKeepalive() {
sendCommand(DO, TELOPT_TM);
}
public void processContentionResolution() {
if (eFuncs[FUNC_CONTENTION_RESOLUTION]) {
log.fine("Contention resolution processed");
}
}
public boolean validateSessionState() {
return connectionState != ConnectionState.NOT_CONNECTED;
}
public void dispatchTelnetEvent(int eventCode, String desc) {
log.fine("Telnet event: code=" + eventCode + " desc=" + desc);
}
public void flushPendingOutput() {
// Flushes output streams
}
public void handleSnaSense(int sense1, int sense2) {
log.warning(String.format("SNA Sense Code received: 0x%02X 0x%02X", sense1, sense2));
}
public void handleSnaSignal() {
for (ScreenUpdateListener l : screenListeners) {
l.onSoundAlarm();
}
}
public void handleSnaClear() {
screenBuffer.clear();
notifyScreenUpdate();
}
public void handleSnaCancel() {
log.fine("SNA Cancel handled");
}
public void handleSnaRqr() {
log.fine("SNA Recovery on Request (RQR) handled");
}
public void handleSnaStsn() {
log.fine("SNA Set and Test Sequence Numbers (STSN) handled");
}
public void resetNegotiationState() {
tn3270eNegotiated = false;
tn3270eDeviceTypeSent = false;
tn3270eBound = false;
ttypeIndex = 0;
luIndex = 0;
java.util.Arrays.fill(myOpts, false);
java.util.Arrays.fill(hisOpts, false);
java.util.Arrays.fill(eFuncs, false);
}
public boolean checkSessionBound() {
return tn3270eBound || connectionState == ConnectionState.CONNECTED_3270;
}
public boolean isTn3270eBound() {
return tn3270eBound;
}
public void processSysReq() {
handleSysReq();
}
public void processAttn() {
handleAttn();
}
public void handleAttn() {
if (connection == null || !connection.isConnected()) {
return;
}
if (tn3270eNegotiated && (tn3270eBound || connectionState == ConnectionState.CONNECTED_TN3270E)) {
// RFC 2355 10.1 & HoD parity: Send raw Telnet IAC IP
byte[] ip = new byte[] { (byte) IAC, (byte) IP };
sendBytes(ip);
log.info("SENT Telnet IP (ATTN)");
} else if (connectionState == ConnectionState.CONNECTED_SSCP) {
// In SSCP-LU mode, host does not receive ATTN; clear pending and unlock
if (dsProcessor != null && dsProcessor.getInputProcessor() != null) {
dsProcessor.getInputProcessor().setKeyboardLocked(false);
dsProcessor.getInputProcessor().reset();
}
} else if (connectionState == ConnectionState.CONNECTED_3270) {
// RFC 1576 & HoD parity: Send PA1 record followed by Telnet IAC BREAK
byte[] attnBytes = new byte[] {
(byte) DS3270Constants.AID_PA1,
(byte) IAC, (byte) EOR,
(byte) IAC, (byte) BREAK
};
sendBytes(attnBytes);
log.info("SENT TN3270 ATTN (PA1 + IAC BREAK)");
} else {
byte[] ip = new byte[] { (byte) IAC, (byte) IP };
sendBytes(ip);
log.info("SENT Telnet IP");
}
}
public void handleSysReq() {
if (tn3270eNegotiated) {
byte[] ao = new byte[] { (byte) IAC, (byte) AO };
sendBytes(ao);
screenBuffer.clear();
screenBuffer.setCursorAddress(0);
screenBuffer.markAllChanged();
if (dsProcessor != null && dsProcessor.getInputProcessor() != null) {
dsProcessor.getInputProcessor().reset();
}
if (connectionState != ConnectionState.CONNECTED_SSCP) {
changeState(ConnectionState.CONNECTED_SSCP);
tn3270eSubmode = TN3270ESubmode.E_SSCP;
} else if (tn3270eBound) {
changeState(ConnectionState.CONNECTED_TN3270E);
tn3270eSubmode = TN3270ESubmode.E_3270;
}
notifyScreenUpdate();
}
}
private static String tn3270eOpName(int op) {
switch (op) {
case OP_ASSOCIATE: return "ASSOCIATE";
case OP_CONNECT: return "CONNECT";
case OP_DEVICE_TYPE: return "DEVICE-TYPE";
case OP_FUNCTIONS: return "FUNCTIONS";
case OP_IS: return "IS";
case OP_REASON: return "REASON";
case OP_REJECT: return "REJECT";
case OP_REQUEST: return "REQUEST";
case OP_SEND: return "SEND";
default: return "OP-" + op;
}
}
// ========== Phase 3: HoD Telnet3270E Methods ==========
/**
* Sends a 5-byte TN3270E header matching HoD Telnet3270E.send_TN3270E_header.
*/
public void send_TN3270E_header(byte type, byte req, byte resp) {
if (!tn3270eNegotiated) return;
byte[] header = new byte[EH_SIZE];
header[0] = type;
header[1] = req;
header[2] = resp;
header[3] = (byte) ((eXmitSeq >> 8) & 0xFF);
header[4] = (byte) (eXmitSeq & 0xFF);
eXmitSeq = (eXmitSeq + 1) & 0xFFFF;
sendBytes(header);
}
/**
* Processes outbound data from short buffer matching HoD Telnet3270E.process_outbound.
*/
public void process_outbound(short[] buf, int off, int len) {
if (buf == null || len <= 0) return;
byte[] bdata = new byte[len];
for (int i = 0; i < len; i++) {
bdata[i] = (byte) (buf[off + i] & 0xFF);
}
process_outbound(bdata, 0, len);
}
/**
* Processes outbound data from byte buffer.
*/
public void process_outbound(byte[] buf, int off, int len) {
if (buf == null || len <= 0) return;
byte[] data = new byte[len];
System.arraycopy(buf, off, data, 0, len);
send3270Data(data);
}
/**
* Processes End of Record (EOR) transmission matching HoD Telnet3270E.process_EOR.
*/
public void process_EOR(boolean endOfRecord) {
if (endOfRecord) {
byte[] eor = new byte[] { (byte) IAC, (byte) EOR };
sendBytes(eor);
}
}
/**
* Requests negotiation of TN3270E functions matching HoD Telnet3270E.negotiate_functions.
*/
public void negotiate_functions() {
if (tn3270eNegotiated) {
sendTN3270EFunctionsRequest();
}
}
/**
* Checks if a TN3270E function has been negotiated.
*/
public boolean isFunctionNegotiated(int func) {
return func >= 0 && func < eFuncs.length && eFuncs[func];
}
public boolean isContentionResolutionNegotiated() {
return tn3270eNegotiated && eFuncs[FUNC_CONTENTION_RESOLUTION];
}
public boolean isNegotiateContentionResolution() {
return negotiateContentionResolution;
}
public void setNegotiateContentionResolution(boolean neg) {
this.negotiateContentionResolution = neg;
eFuncs[FUNC_CONTENTION_RESOLUTION] = neg;
}
public void setContentionResolutionNegotiated(boolean cr) {
if (cr) {
this.tn3270eNegotiated = true;
}
eFuncs[FUNC_CONTENTION_RESOLUTION] = cr;
if (dsProcessor != null) {
dsProcessor.setContentionResolution(cr);
}
}
public boolean isSdi_flag() { return sdi_flag; }
public void setSdi_flag(boolean sdi) { this.sdi_flag = sdi; }
public boolean isKri_flag() { return kri_flag; }
public void setKri_flag(boolean kri) { this.kri_flag = kri; }
public void notifyKeyboardUnlocked() {
for (ScreenUpdateListener l : screenListeners) {
try {
l.onKeyboardUnlocked();
} catch (Exception ignored) {}
}
}
}