2118 lines
76 KiB
Java
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) {}
|
|
}
|
|
}
|
|
}
|