More functional changes and UI adaptations from overnight
This commit is contained in:
@@ -5,6 +5,10 @@ package haus.nightmare.lib3270j;
|
||||
*/
|
||||
public class ConnectionConfig {
|
||||
|
||||
public enum ProxyType {
|
||||
NONE, HTTP, SOCKS4, SOCKS5
|
||||
}
|
||||
|
||||
private String host;
|
||||
private int port = 23;
|
||||
private TerminalModel model = TerminalModel.IBM_3279_4;
|
||||
@@ -28,6 +32,21 @@ public class ConnectionConfig {
|
||||
private haus.nightmare.lib3270j.graphics.GraphicsMode graphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH;
|
||||
private String codePage = "037";
|
||||
private String associatedPrinterLu = null;
|
||||
private boolean nvtLocalEcho = false;
|
||||
|
||||
// Proxy configuration
|
||||
private ProxyType proxyType = ProxyType.NONE;
|
||||
private String proxyHost = null;
|
||||
private int proxyPort = 0;
|
||||
private String proxyUsername = null;
|
||||
private String proxyPassword = null;
|
||||
|
||||
// STARTTLS (Telnet Option 46) dynamic socket elevation
|
||||
private boolean startTlsEnabled = true;
|
||||
|
||||
// RFC 1572 / RFC 2877 Environment variables (Express Logon)
|
||||
private java.util.Map<String, String> environmentVariables = new java.util.LinkedHashMap<>();
|
||||
private java.util.Map<String, String> userVariables = new java.util.LinkedHashMap<>();
|
||||
|
||||
public ConnectionConfig() {}
|
||||
|
||||
@@ -101,6 +120,9 @@ public class ConnectionConfig {
|
||||
this.codePage = (codePage != null && !codePage.trim().isEmpty()) ? codePage.trim() : "037";
|
||||
}
|
||||
|
||||
public boolean isNvtLocalEcho() { return nvtLocalEcho; }
|
||||
public void setNvtLocalEcho(boolean nvtLocalEcho) { this.nvtLocalEcho = nvtLocalEcho; }
|
||||
|
||||
public String getTerminalName() { return terminalName; }
|
||||
public void setTerminalName(String name) { this.terminalName = name; }
|
||||
|
||||
@@ -134,9 +156,58 @@ public class ConnectionConfig {
|
||||
this.dynamicCols = cols;
|
||||
}
|
||||
|
||||
public ProxyType getProxyType() { return proxyType; }
|
||||
public void setProxyType(ProxyType proxyType) { this.proxyType = proxyType != null ? proxyType : ProxyType.NONE; }
|
||||
|
||||
public String getProxyHost() { return proxyHost; }
|
||||
public void setProxyHost(String proxyHost) { this.proxyHost = proxyHost; }
|
||||
|
||||
public int getProxyPort() { return proxyPort; }
|
||||
public void setProxyPort(int proxyPort) { this.proxyPort = proxyPort; }
|
||||
|
||||
public String getProxyUsername() { return proxyUsername; }
|
||||
public void setProxyUsername(String proxyUsername) { this.proxyUsername = proxyUsername; }
|
||||
|
||||
public String getProxyPassword() { return proxyPassword; }
|
||||
public void setProxyPassword(String proxyPassword) { this.proxyPassword = proxyPassword; }
|
||||
|
||||
public void setProxy(ProxyType type, String host, int port, String username, String password) {
|
||||
this.proxyType = type != null ? type : ProxyType.NONE;
|
||||
this.proxyHost = host;
|
||||
this.proxyPort = port;
|
||||
this.proxyUsername = username;
|
||||
this.proxyPassword = password;
|
||||
}
|
||||
|
||||
public boolean isStartTlsEnabled() { return startTlsEnabled; }
|
||||
public void setStartTlsEnabled(boolean enabled) { this.startTlsEnabled = enabled; }
|
||||
|
||||
public java.util.Map<String, String> getEnvironmentVariables() { return environmentVariables; }
|
||||
public void setEnvironmentVariables(java.util.Map<String, String> vars) {
|
||||
this.environmentVariables = (vars != null) ? new java.util.LinkedHashMap<>(vars) : new java.util.LinkedHashMap<>();
|
||||
}
|
||||
public void setEnvironmentVariable(String name, String value) {
|
||||
if (name != null) {
|
||||
if (value != null) this.environmentVariables.put(name, value);
|
||||
else this.environmentVariables.remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
public java.util.Map<String, String> getUserVariables() { return userVariables; }
|
||||
public void setUserVariables(java.util.Map<String, String> vars) {
|
||||
this.userVariables = (vars != null) ? new java.util.LinkedHashMap<>(vars) : new java.util.LinkedHashMap<>();
|
||||
}
|
||||
public void setUserVariable(String name, String value) {
|
||||
if (name != null) {
|
||||
if (value != null) this.userVariables.put(name, value);
|
||||
else this.userVariables.remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"),
|
||||
* plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), or standard "host:port" formats.
|
||||
* plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), proxy flags (e.g. "--proxy=http://proxy:8080 host:23"),
|
||||
* or standard "host:port" formats.
|
||||
*/
|
||||
public static ConnectionConfig parseHostString(String hostStr, int defaultPort, TerminalModel defaultModel) {
|
||||
if (hostStr == null || hostStr.trim().isEmpty()) {
|
||||
@@ -146,6 +217,50 @@ public class ConnectionConfig {
|
||||
boolean tls = false;
|
||||
boolean tn3270e = true;
|
||||
|
||||
// Parse --proxy=<url> or -proxy=<url> flags
|
||||
ProxyType pType = ProxyType.NONE;
|
||||
String pHost = null;
|
||||
int pPort = 0;
|
||||
String pUser = null;
|
||||
String pPass = null;
|
||||
|
||||
String[] tokens = s.split("\\s+");
|
||||
StringBuilder remaining = new StringBuilder();
|
||||
for (String tok : tokens) {
|
||||
if (tok.startsWith("--proxy=") || tok.startsWith("-proxy=")) {
|
||||
String proxyUrl = tok.substring(tok.indexOf('=') + 1).trim();
|
||||
try {
|
||||
java.net.URI uri = new java.net.URI(proxyUrl);
|
||||
String scheme = uri.getScheme() != null ? uri.getScheme().toLowerCase() : "http";
|
||||
if (scheme.equals("http") || scheme.equals("https")) {
|
||||
pType = ProxyType.HTTP;
|
||||
pPort = uri.getPort() > 0 ? uri.getPort() : 8080;
|
||||
} else if (scheme.equals("socks4") || scheme.equals("socks4a")) {
|
||||
pType = ProxyType.SOCKS4;
|
||||
pPort = uri.getPort() > 0 ? uri.getPort() : 1080;
|
||||
} else if (scheme.equals("socks5") || scheme.equals("socks")) {
|
||||
pType = ProxyType.SOCKS5;
|
||||
pPort = uri.getPort() > 0 ? uri.getPort() : 1080;
|
||||
}
|
||||
pHost = uri.getHost();
|
||||
String userInfo = uri.getUserInfo();
|
||||
if (userInfo != null) {
|
||||
int colon = userInfo.indexOf(':');
|
||||
if (colon >= 0) {
|
||||
pUser = userInfo.substring(0, colon);
|
||||
pPass = userInfo.substring(colon + 1);
|
||||
} else {
|
||||
pUser = userInfo;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
} else {
|
||||
if (remaining.length() > 0) remaining.append(" ");
|
||||
remaining.append(tok);
|
||||
}
|
||||
}
|
||||
s = remaining.toString().trim();
|
||||
|
||||
// Parse chained x3270-style prefixes (e.g. "L:P:host:port" or "P:host:port")
|
||||
boolean prefixFound = true;
|
||||
while (prefixFound) {
|
||||
@@ -200,6 +315,9 @@ public class ConnectionConfig {
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
|
||||
config.setUseTls(tls);
|
||||
config.setTn3270eEnabled(tn3270e);
|
||||
if (pType != ProxyType.NONE && pHost != null) {
|
||||
config.setProxy(pType, pHost, pPort, pUser, pPass);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +118,14 @@ public class Telnet3270Client {
|
||||
dsProcessor.addScreenUpdateListener(l);
|
||||
}
|
||||
|
||||
public void addSCSInboundListener(haus.nightmare.lib3270j.listener.SCSInboundListener l) {
|
||||
fsm.addSCSInboundListener(l);
|
||||
}
|
||||
|
||||
public void removeSCSInboundListener(haus.nightmare.lib3270j.listener.SCSInboundListener l) {
|
||||
fsm.removeSCSInboundListener(l);
|
||||
}
|
||||
|
||||
// ========== Screen access ==========
|
||||
|
||||
/** Get the screen buffer for rendering. */
|
||||
|
||||
@@ -144,6 +144,9 @@ public class DataStreamProcessor {
|
||||
int oldCols = screen.getCols();
|
||||
screen.erase(false);
|
||||
graphicsPlane.clear();
|
||||
if (gocaDecoder != null) {
|
||||
gocaDecoder.setGraphicsCursorActive(false);
|
||||
}
|
||||
processWrite(data, offset, length, true);
|
||||
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
|
||||
notifyScreenSizeChanged();
|
||||
@@ -161,6 +164,9 @@ public class DataStreamProcessor {
|
||||
int oldCols = screen.getCols();
|
||||
screen.erase(true);
|
||||
graphicsPlane.clear();
|
||||
if (gocaDecoder != null) {
|
||||
gocaDecoder.setGraphicsCursorActive(false);
|
||||
}
|
||||
processWrite(data, offset, length, true);
|
||||
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
|
||||
notifyScreenSizeChanged();
|
||||
|
||||
@@ -33,6 +33,19 @@ public class ECLPS implements ECLConstants {
|
||||
return fieldList;
|
||||
}
|
||||
|
||||
private boolean nvtMode = false;
|
||||
|
||||
public boolean isNVTmode() {
|
||||
if (inputProcessor != null && inputProcessor.isNvtMode()) {
|
||||
return true;
|
||||
}
|
||||
return nvtMode;
|
||||
}
|
||||
|
||||
public void setNVTmode(boolean nvt) {
|
||||
this.nvtMode = nvt;
|
||||
}
|
||||
|
||||
public int getSize() { return screen.getRows() * screen.getCols(); }
|
||||
public int getRows() { return screen.getRows(); }
|
||||
public int getCols() { return screen.getCols(); }
|
||||
|
||||
@@ -8,6 +8,7 @@ import haus.nightmare.lib3270j.protocol.TelnetConstants;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
@@ -31,6 +32,14 @@ public class InputProcessor {
|
||||
this.fsm = fsm;
|
||||
}
|
||||
|
||||
public TelnetFSM getFsm() {
|
||||
return fsm;
|
||||
}
|
||||
|
||||
public boolean isNvtMode() {
|
||||
return fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isNvt();
|
||||
}
|
||||
|
||||
private haus.nightmare.lib3270j.graphics.GraphicsPlane graphicsPlane;
|
||||
private haus.nightmare.lib3270j.graphics.GocaDecoder gocaDecoder;
|
||||
|
||||
@@ -95,6 +104,15 @@ public class InputProcessor {
|
||||
public void typeCharacter(char ch) {
|
||||
if (keyboardLocked) return;
|
||||
|
||||
if (isNvtMode()) {
|
||||
try {
|
||||
fsm.sendNVTChar(ch);
|
||||
} catch (IOException e) {
|
||||
log.warning("Failed to send NVT character: " + e.getMessage());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
if (size <= 0) return;
|
||||
int baddr = screen.getCursorAddress();
|
||||
@@ -237,86 +255,6 @@ public class InputProcessor {
|
||||
return;
|
||||
}
|
||||
|
||||
if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive()) {
|
||||
int gx = gocaDecoder.getGraphicCursorX();
|
||||
int gy = gocaDecoder.getGraphicCursorY();
|
||||
int cols = (screen != null && screen.getCols() > 0) ? screen.getCols() : 80;
|
||||
int numRows = (screen != null && screen.getRows() > 0) ? screen.getRows() : 24;
|
||||
int cursorAddr = screen != null ? screen.getCursorAddress() : 0;
|
||||
int row = cursorAddr / cols;
|
||||
int col = cursorAddr % cols;
|
||||
|
||||
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
|
||||
gx, gy, row, col, aidCode, false, false, false
|
||||
);
|
||||
|
||||
StringBuilder sfHex = new StringBuilder();
|
||||
for (byte b : sf) {
|
||||
sfHex.append(String.format("%02X ", b & 0xFF));
|
||||
}
|
||||
log.info(String.format(
|
||||
"sendAid (graphic): goca=(%d, %d) row=%d col=%d cursorAddr=%d aid=0x%02X SF_HEX=[%s]",
|
||||
gx, gy, row, col, cursorAddr, aidCode, sfHex.toString().trim()
|
||||
));
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
|
||||
// Structured Field AID (0x88) + 56-byte Graphic Input SF
|
||||
out.write(AID_SF);
|
||||
try {
|
||||
out.write(sf);
|
||||
} catch (java.io.IOException ignored) {}
|
||||
|
||||
// Trailing AID + cursor address
|
||||
out.write(aidCode);
|
||||
byte[] caddr = encodeAddress(cursorAddr, numRows, cols);
|
||||
out.write(caddr[0] & 0xFF);
|
||||
out.write(caddr[1] & 0xFF);
|
||||
|
||||
if (aidCode == AID_PA1 || aidCode == AID_PA2 || aidCode == AID_PA3) {
|
||||
sendAidResponse(out.toByteArray());
|
||||
return;
|
||||
}
|
||||
|
||||
if (screen.isFormatted()) {
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
for (int i = 0; i < size; i++) {
|
||||
ExtendedAttribute ea = screen.getCell(i);
|
||||
if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) {
|
||||
int fieldStart = (i + 1) % size;
|
||||
|
||||
// Always send SBA and address of first character in field
|
||||
out.write(ORDER_SBA);
|
||||
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
|
||||
out.write(addr[0] & 0xFF);
|
||||
out.write(addr[1] & 0xFF);
|
||||
|
||||
// Send all non-null characters in field (suppressing 0x00)
|
||||
int pos = fieldStart;
|
||||
while (!screen.getCell(pos).isFieldAttribute()) {
|
||||
int b = screen.getCell(pos).ec & 0xFF;
|
||||
if (b != 0x00) {
|
||||
out.write(b);
|
||||
}
|
||||
pos = (pos + 1) % size;
|
||||
if (pos == fieldStart) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
for (int i = 0; i < size; i++) {
|
||||
int b = screen.getCell(i).ec & 0xFF;
|
||||
if (b != 0x00) {
|
||||
out.write(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendAidResponse(out.toByteArray());
|
||||
return;
|
||||
}
|
||||
|
||||
if (aidCode == AID_PA1 || aidCode == AID_PA2 || aidCode == AID_PA3) {
|
||||
// PA keys: send AID + optional PID + cursor address only (no modified data)
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
@@ -863,6 +801,16 @@ public class InputProcessor {
|
||||
* Equivalent to emulate_input() in x3270.
|
||||
*/
|
||||
public void emulateInput(String text) {
|
||||
if (text == null) return;
|
||||
if (isNvtMode()) {
|
||||
try {
|
||||
fsm.sendNVTString(text);
|
||||
} catch (IOException e) {
|
||||
log.warning("Failed to send NVT input: " + e.getMessage());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Type each character
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char ch = text.charAt(i);
|
||||
@@ -967,6 +915,93 @@ public class InputProcessor {
|
||||
}
|
||||
|
||||
private void executeMnemonicToken(String token) {
|
||||
if (isNvtMode()) {
|
||||
try {
|
||||
switch (token) {
|
||||
case "enter":
|
||||
case "return":
|
||||
fsm.sendNVTString("\r\n");
|
||||
break;
|
||||
case "clear":
|
||||
fsm.sendNVTChar('\u000C');
|
||||
break;
|
||||
case "tab":
|
||||
fsm.sendNVTChar('\t');
|
||||
break;
|
||||
case "backtab":
|
||||
case "btab":
|
||||
fsm.sendNVTString("\u001B[Z");
|
||||
break;
|
||||
case "newline":
|
||||
case "nl":
|
||||
fsm.sendNVTString("\r\n");
|
||||
break;
|
||||
case "home":
|
||||
fsm.sendNVTString("\u001B[H");
|
||||
break;
|
||||
case "end":
|
||||
fsm.sendNVTString("\u001B[F");
|
||||
break;
|
||||
case "up":
|
||||
case "curup":
|
||||
fsm.sendNVTString("\u001B[A");
|
||||
break;
|
||||
case "down":
|
||||
case "curdown":
|
||||
fsm.sendNVTString("\u001B[B");
|
||||
break;
|
||||
case "left":
|
||||
case "curleft":
|
||||
fsm.sendNVTString("\u001B[D");
|
||||
break;
|
||||
case "right":
|
||||
case "curright":
|
||||
fsm.sendNVTString("\u001B[C");
|
||||
break;
|
||||
case "pageup":
|
||||
case "pgup":
|
||||
fsm.sendNVTString("\u001B[5~");
|
||||
break;
|
||||
case "pagedown":
|
||||
case "pgdn":
|
||||
fsm.sendNVTString("\u001B[6~");
|
||||
break;
|
||||
case "delete":
|
||||
case "del":
|
||||
fsm.sendNVTString("\u001B[3~");
|
||||
break;
|
||||
case "backspace":
|
||||
case "bs":
|
||||
fsm.sendNVTChar('\b');
|
||||
break;
|
||||
case "attn":
|
||||
case "break":
|
||||
fsm.sendNVTChar('\u0003');
|
||||
break;
|
||||
case "sysreq":
|
||||
case "escape":
|
||||
case "esc":
|
||||
fsm.sendNVTChar('\u001B');
|
||||
break;
|
||||
case "reset":
|
||||
setKeyboardLocked(false);
|
||||
break;
|
||||
default:
|
||||
if (token.startsWith("pf") || token.startsWith("f")) {
|
||||
try {
|
||||
String numStr = token.startsWith("pf") ? token.substring(2) : token.substring(1);
|
||||
int fn = Integer.parseInt(numStr);
|
||||
sendNvtFunctionKey(fn);
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.warning("Failed to send NVT mnemonic token: " + e.getMessage());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (token) {
|
||||
case "enter":
|
||||
case "return":
|
||||
@@ -1068,6 +1103,24 @@ public class InputProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
private void sendNvtFunctionKey(int fn) throws IOException {
|
||||
switch (fn) {
|
||||
case 1: fsm.sendNVTString("\u001BOP"); break;
|
||||
case 2: fsm.sendNVTString("\u001BOQ"); break;
|
||||
case 3: fsm.sendNVTString("\u001BOR"); break;
|
||||
case 4: fsm.sendNVTString("\u001BOS"); break;
|
||||
case 5: fsm.sendNVTString("\u001B[15~"); break;
|
||||
case 6: fsm.sendNVTString("\u001B[17~"); break;
|
||||
case 7: fsm.sendNVTString("\u001B[18~"); break;
|
||||
case 8: fsm.sendNVTString("\u001B[19~"); break;
|
||||
case 9: fsm.sendNVTString("\u001B[20~"); break;
|
||||
case 10: fsm.sendNVTString("\u001B[21~"); break;
|
||||
case 11: fsm.sendNVTString("\u001B[23~"); break;
|
||||
case 12: fsm.sendNVTString("\u001B[24~"); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Jump cursor to next or previous word boundary.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package haus.nightmare.lib3270j.listener;
|
||||
|
||||
/**
|
||||
* Listener interface for receiving inbound SCS (SNA Character String) data streams
|
||||
* transmitted by the host in TN3270E mode (Data Type DT_SCS_DATA = 0x01).
|
||||
*/
|
||||
public interface SCSInboundListener {
|
||||
|
||||
/**
|
||||
* Called when an inbound SCS record is received from the host.
|
||||
*
|
||||
* @param data Raw SCS record payload bytes
|
||||
* @param offset Start offset within buffer
|
||||
* @param length Number of bytes in record
|
||||
*/
|
||||
void onSCSDataReceived(byte[] data, int offset, int length);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.logging.Logger;
|
||||
@@ -17,7 +17,7 @@ import java.util.logging.Logger;
|
||||
/**
|
||||
* Network Virtual Terminal (NVT) processor.
|
||||
* Handles ASCII / ANSI VT100 character stream processing, cursor positioning,
|
||||
* escape sequence decoding, and NVT character/string transmission.
|
||||
* escape sequence decoding, terminal capability reports, and NVT transmission.
|
||||
*/
|
||||
public class NvtProcessor {
|
||||
|
||||
@@ -28,9 +28,10 @@ public class NvtProcessor {
|
||||
private final List<ScreenUpdateListener> screenListeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
// Escape sequence parser states
|
||||
private static final int STATE_NORMAL = 0;
|
||||
private static final int STATE_ESC = 1;
|
||||
private static final int STATE_CSI = 2;
|
||||
private static final int STATE_NORMAL = 0;
|
||||
private static final int STATE_ESC = 1;
|
||||
private static final int STATE_CSI = 2;
|
||||
private static final int STATE_CHARSET = 3;
|
||||
|
||||
private int parseState = STATE_NORMAL;
|
||||
private final ByteArrayOutputStream escBuffer = new ByteArrayOutputStream();
|
||||
@@ -47,6 +48,21 @@ public class NvtProcessor {
|
||||
private int savedCursorRow = 0;
|
||||
private int savedCursorCol = 0;
|
||||
|
||||
// Scrolling margins (0-indexed, inclusive)
|
||||
private int scrollTop = 0;
|
||||
private int scrollBottom = -1; // -1 means default (rows - 1)
|
||||
|
||||
// Tab stops
|
||||
private boolean[] tabStops;
|
||||
|
||||
// Cursor visibility
|
||||
private boolean cursorVisible = true;
|
||||
|
||||
// Line drawing mode
|
||||
private boolean lineDrawingG0 = false;
|
||||
private boolean lineDrawingG1 = false;
|
||||
private boolean activeCharsetG1 = false;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface OutputSender {
|
||||
void sendRaw(byte[] data) throws IOException;
|
||||
@@ -55,6 +71,15 @@ public class NvtProcessor {
|
||||
public NvtProcessor(ScreenBuffer screenBuffer, EbcdicTranslator translator) {
|
||||
this.screenBuffer = screenBuffer;
|
||||
this.translator = translator;
|
||||
initTabStops();
|
||||
}
|
||||
|
||||
private void initTabStops() {
|
||||
int cols = screenBuffer.getCols();
|
||||
tabStops = new boolean[cols];
|
||||
for (int i = 0; i < cols; i++) {
|
||||
tabStops[i] = (i % 8 == 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void setOutputSender(OutputSender outputSender) {
|
||||
@@ -65,6 +90,14 @@ public class NvtProcessor {
|
||||
screenListeners.add(l);
|
||||
}
|
||||
|
||||
public void removeScreenUpdateListener(ScreenUpdateListener l) {
|
||||
screenListeners.remove(l);
|
||||
}
|
||||
|
||||
public boolean isCursorVisible() {
|
||||
return cursorVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process incoming ASCII NVT data bytes.
|
||||
*/
|
||||
@@ -73,6 +106,12 @@ public class NvtProcessor {
|
||||
|
||||
int rows = screenBuffer.getRows();
|
||||
int cols = screenBuffer.getCols();
|
||||
if (tabStops == null || tabStops.length != cols) {
|
||||
initTabStops();
|
||||
}
|
||||
int effectiveScrollBottom = (scrollBottom >= 0 && scrollBottom < rows) ? scrollBottom : rows - 1;
|
||||
int effectiveScrollTop = Math.max(0, Math.min(scrollTop, effectiveScrollBottom));
|
||||
|
||||
int size = rows * cols;
|
||||
int curAddr = screenBuffer.getCursorAddress();
|
||||
|
||||
@@ -90,27 +129,60 @@ public class NvtProcessor {
|
||||
} else if (b == 0x0A) { // LF
|
||||
int r = curAddr / cols;
|
||||
int c = curAddr % cols;
|
||||
r++;
|
||||
if (r >= rows) {
|
||||
scrollUp();
|
||||
r = rows - 1;
|
||||
if (r == effectiveScrollBottom) {
|
||||
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
|
||||
} else if (r < rows - 1) {
|
||||
r++;
|
||||
}
|
||||
curAddr = r * cols + c;
|
||||
} else if (b == 0x08 || b == 0x7F) { // BS or DEL
|
||||
} else if (b == 0x08) { // BS
|
||||
int c = curAddr % cols;
|
||||
if (c > 0) {
|
||||
curAddr--;
|
||||
}
|
||||
} else if (b == 0x7F) { // DEL
|
||||
// Ignore or backspace per NVT convention
|
||||
} else if (b == 0x09) { // TAB
|
||||
int r = curAddr / cols;
|
||||
int c = curAddr % cols;
|
||||
int nextTab = ((c / 8) + 1) * 8;
|
||||
if (nextTab >= cols) nextTab = cols - 1;
|
||||
curAddr = (curAddr / cols) * cols + nextTab;
|
||||
int nextTab = cols - 1;
|
||||
for (int tc = c + 1; tc < cols; tc++) {
|
||||
if (tc < tabStops.length && tabStops[tc]) {
|
||||
nextTab = tc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
curAddr = r * cols + nextTab;
|
||||
} else if (b == 0x0C) { // FF
|
||||
screenBuffer.clear();
|
||||
curAddr = 0;
|
||||
} else if (b >= 0x20 && b < 0xFF) { // Printable ASCII
|
||||
} else if (b == 0x07) { // BEL
|
||||
for (ScreenUpdateListener l : screenListeners) {
|
||||
l.onSoundAlarm();
|
||||
}
|
||||
} else if (b == 0x0E) { // SO (Select G1 charset)
|
||||
activeCharsetG1 = true;
|
||||
} else if (b == 0x0F) { // SI (Select G0 charset)
|
||||
activeCharsetG1 = false;
|
||||
} else if (b >= 0x20 && b <= 0xFF) { // Printable character
|
||||
char ch = (char) b;
|
||||
if (activeCharsetG1 ? lineDrawingG1 : lineDrawingG0) {
|
||||
ch = mapVt100SpecialGraphics(ch);
|
||||
}
|
||||
|
||||
int r = curAddr / cols;
|
||||
int c = curAddr % cols;
|
||||
|
||||
if (c >= cols) {
|
||||
c = 0;
|
||||
if (r == effectiveScrollBottom) {
|
||||
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
|
||||
} else if (r < rows - 1) {
|
||||
r++;
|
||||
}
|
||||
curAddr = r * cols + c;
|
||||
}
|
||||
|
||||
int ebc = translator.unicodeToEbcdic(ch);
|
||||
ExtendedAttribute cell = screenBuffer.getCell(curAddr);
|
||||
cell.clear();
|
||||
@@ -120,37 +192,98 @@ public class NvtProcessor {
|
||||
cell.bg = currentBg;
|
||||
cell.gr = currentGr;
|
||||
|
||||
curAddr++;
|
||||
if (curAddr >= size) {
|
||||
scrollUp();
|
||||
curAddr = (rows - 1) * cols;
|
||||
c++;
|
||||
if (c >= cols) {
|
||||
if (r < rows - 1) {
|
||||
if (r == effectiveScrollBottom) {
|
||||
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
|
||||
c = 0;
|
||||
} else {
|
||||
r++;
|
||||
c = 0;
|
||||
}
|
||||
} else {
|
||||
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
|
||||
c = 0;
|
||||
}
|
||||
}
|
||||
curAddr = r * cols + c;
|
||||
}
|
||||
} else if (parseState == STATE_ESC) {
|
||||
escBuffer.write(b);
|
||||
if (b == '[') {
|
||||
parseState = STATE_CSI;
|
||||
} else if (b == '7') { // Save cursor
|
||||
} else if (b == '(' || b == ')') {
|
||||
parseState = STATE_CHARSET;
|
||||
} else if (b == '7') { // DECSC - Save cursor
|
||||
savedCursorRow = curAddr / cols;
|
||||
savedCursorCol = curAddr % cols;
|
||||
parseState = STATE_NORMAL;
|
||||
} else if (b == '8') { // Restore cursor
|
||||
} else if (b == '8') { // DECRC - Restore cursor
|
||||
curAddr = Math.min(rows - 1, savedCursorRow) * cols + Math.min(cols - 1, savedCursorCol);
|
||||
parseState = STATE_NORMAL;
|
||||
} else if (b == 'D') { // IND - Index (down 1 line)
|
||||
int r = curAddr / cols;
|
||||
int c = curAddr % cols;
|
||||
if (r == effectiveScrollBottom) {
|
||||
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
|
||||
} else if (r < rows - 1) {
|
||||
r++;
|
||||
}
|
||||
curAddr = r * cols + c;
|
||||
parseState = STATE_NORMAL;
|
||||
} else if (b == 'M') { // RI - Reverse Index (up 1 line)
|
||||
int r = curAddr / cols;
|
||||
int c = curAddr % cols;
|
||||
if (r == effectiveScrollTop) {
|
||||
scrollDownRegion(effectiveScrollTop, effectiveScrollBottom);
|
||||
} else if (r > 0) {
|
||||
r--;
|
||||
}
|
||||
curAddr = r * cols + c;
|
||||
parseState = STATE_NORMAL;
|
||||
} else if (b == 'E') { // NEL - Next Line (CR + LF)
|
||||
int r = curAddr / cols;
|
||||
if (r == effectiveScrollBottom) {
|
||||
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
|
||||
} else if (r < rows - 1) {
|
||||
r++;
|
||||
}
|
||||
curAddr = r * cols;
|
||||
parseState = STATE_NORMAL;
|
||||
} else if (b == 'H') { // HTS - Horizontal Tab Set
|
||||
int c = curAddr % cols;
|
||||
if (c < tabStops.length) {
|
||||
tabStops[c] = true;
|
||||
}
|
||||
parseState = STATE_NORMAL;
|
||||
} else if (b == 'c') { // RIS - Reset to Initial State
|
||||
screenBuffer.clear();
|
||||
curAddr = 0;
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
scrollTop = 0;
|
||||
scrollBottom = rows - 1;
|
||||
cursorVisible = true;
|
||||
initTabStops();
|
||||
parseState = STATE_NORMAL;
|
||||
} else {
|
||||
// Unknown 2-byte escape, finish
|
||||
// Unknown 2-byte escape, return to normal
|
||||
parseState = STATE_NORMAL;
|
||||
}
|
||||
} else if (parseState == STATE_CHARSET) {
|
||||
byte[] seq = escBuffer.toByteArray();
|
||||
if (seq.length >= 2) {
|
||||
boolean isG1 = (seq[1] == ')');
|
||||
boolean isLineDraw = (b == '0');
|
||||
if (isG1) lineDrawingG1 = isLineDraw;
|
||||
else lineDrawingG0 = isLineDraw;
|
||||
}
|
||||
parseState = STATE_NORMAL;
|
||||
} else if (parseState == STATE_CSI) {
|
||||
escBuffer.write(b);
|
||||
// CSI parameter/intermediate bytes: 0x20..0x3F, final bytes: 0x40..0x7E
|
||||
// CSI final bytes are in the range 0x40..0x7E
|
||||
if (b >= 0x40 && b <= 0x7E) {
|
||||
byte[] seq = escBuffer.toByteArray();
|
||||
curAddr = processAnsiEscapeSequence(seq, curAddr, rows, cols);
|
||||
@@ -159,7 +292,7 @@ public class NvtProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
screenBuffer.setCursorAddress(curAddr);
|
||||
screenBuffer.setCursorAddress(Math.max(0, Math.min(size - 1, curAddr)));
|
||||
screenBuffer.markAllChanged();
|
||||
screenBuffer.updateDisplaySnapshot();
|
||||
notifyScreenUpdated();
|
||||
@@ -175,6 +308,9 @@ public class NvtProcessor {
|
||||
String paramStr = new String(seq, 2, seq.length - 3, StandardCharsets.US_ASCII);
|
||||
String[] params = paramStr.split(";");
|
||||
|
||||
int effectiveScrollBottom = (scrollBottom >= 0 && scrollBottom < rows) ? scrollBottom : rows - 1;
|
||||
int effectiveScrollTop = Math.max(0, Math.min(scrollTop, effectiveScrollBottom));
|
||||
|
||||
int r = curAddr / cols;
|
||||
int c = curAddr % cols;
|
||||
|
||||
@@ -212,6 +348,31 @@ public class NvtProcessor {
|
||||
c = Math.max(0, c - count);
|
||||
return r * cols + c;
|
||||
}
|
||||
case 'E': // CNL - Cursor Next Line
|
||||
{
|
||||
int count = parseParam(params, 0, 1);
|
||||
r = Math.min(rows - 1, r + count);
|
||||
return r * cols; // column 0
|
||||
}
|
||||
case 'F': // CPL - Cursor Previous Line
|
||||
{
|
||||
int count = parseParam(params, 0, 1);
|
||||
r = Math.max(0, r - count);
|
||||
return r * cols; // column 0
|
||||
}
|
||||
case 'G': // CHA - Cursor Horizontal Absolute
|
||||
case '`': // HPA - Horizontal Position Absolute
|
||||
{
|
||||
int p = parseParam(params, 0, 1) - 1;
|
||||
c = Math.max(0, Math.min(cols - 1, p));
|
||||
return r * cols + c;
|
||||
}
|
||||
case 'd': // VPA - Vertical Position Absolute
|
||||
{
|
||||
int p = parseParam(params, 0, 1) - 1;
|
||||
r = Math.max(0, Math.min(rows - 1, p));
|
||||
return r * cols + c;
|
||||
}
|
||||
case 'J': // ED - Erase in Display
|
||||
{
|
||||
int mode = parseParam(params, 0, 0);
|
||||
@@ -239,6 +400,84 @@ public class NvtProcessor {
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 'L': // IL - Insert Line
|
||||
{
|
||||
int count = parseParam(params, 0, 1);
|
||||
for (int n = 0; n < count; n++) {
|
||||
scrollDownRegion(r, effectiveScrollBottom);
|
||||
}
|
||||
return r * cols;
|
||||
}
|
||||
case 'M': // DL - Delete Line
|
||||
{
|
||||
int count = parseParam(params, 0, 1);
|
||||
for (int n = 0; n < count; n++) {
|
||||
scrollUpRegion(r, effectiveScrollBottom);
|
||||
}
|
||||
return r * cols;
|
||||
}
|
||||
case '@': // ICH - Insert Character
|
||||
{
|
||||
int count = parseParam(params, 0, 1);
|
||||
int lineStart = r * cols;
|
||||
for (int col = cols - 1; col >= c + count; col--) {
|
||||
screenBuffer.getCell(lineStart + col).copyFrom(screenBuffer.getCell(lineStart + col - count));
|
||||
}
|
||||
for (int col = c; col < Math.min(cols, c + count); col++) {
|
||||
clearCell(lineStart + col);
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 'P': // DCH - Delete Character
|
||||
{
|
||||
int count = parseParam(params, 0, 1);
|
||||
int lineStart = r * cols;
|
||||
for (int col = c; col < cols - count; col++) {
|
||||
screenBuffer.getCell(lineStart + col).copyFrom(screenBuffer.getCell(lineStart + col + count));
|
||||
}
|
||||
for (int col = cols - count; col < cols; col++) {
|
||||
clearCell(lineStart + col);
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 'X': // ECH - Erase Character
|
||||
{
|
||||
int count = parseParam(params, 0, 1);
|
||||
int end = Math.min((r + 1) * cols, curAddr + count);
|
||||
for (int i = curAddr; i < end; i++) {
|
||||
clearCell(i);
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 'S': // SU - Scroll Up
|
||||
{
|
||||
int count = parseParam(params, 0, 1);
|
||||
for (int n = 0; n < count; n++) {
|
||||
scrollUpRegion(effectiveScrollTop, effectiveScrollBottom);
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 'T': // SD - Scroll Down
|
||||
{
|
||||
int count = parseParam(params, 0, 1);
|
||||
for (int n = 0; n < count; n++) {
|
||||
scrollDownRegion(effectiveScrollTop, effectiveScrollBottom);
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 'r': // DECSTBM - Set Top and Bottom Margins (Scrolling Region)
|
||||
{
|
||||
int top = parseParam(params, 0, 1) - 1;
|
||||
int bottom = parseParam(params, 1, rows) - 1;
|
||||
if (top >= 0 && bottom < rows && top < bottom) {
|
||||
scrollTop = top;
|
||||
scrollBottom = bottom;
|
||||
} else {
|
||||
scrollTop = 0;
|
||||
scrollBottom = rows - 1;
|
||||
}
|
||||
return 0; // Move cursor to home
|
||||
}
|
||||
case 'm': // SGR - Select Graphic Rendition
|
||||
{
|
||||
if (params.length == 0 || (params.length == 1 && params[0].isEmpty())) {
|
||||
@@ -256,6 +495,57 @@ public class NvtProcessor {
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 'n': // DSR - Device Status Report
|
||||
{
|
||||
int code = parseParam(params, 0, 0);
|
||||
if (code == 6) { // Cursor position request
|
||||
// Reply: ESC [ <row> ; <col> R (1-indexed)
|
||||
String response = String.format("\u001B[%d;%dR", r + 1, c + 1);
|
||||
sendResponseString(response);
|
||||
} else if (code == 5) { // Status report request
|
||||
sendResponseString("\u001B[0n"); // OK
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 'c': // DA - Device Attributes
|
||||
{
|
||||
int code = parseParam(params, 0, 0);
|
||||
if (code == 0) {
|
||||
// Identify as standard VT100 with Advanced Video Option
|
||||
sendResponseString("\u001B[?1;2c");
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 'g': // TBC - Tab Clear
|
||||
{
|
||||
int mode = parseParam(params, 0, 0);
|
||||
if (mode == 0) {
|
||||
if (c < tabStops.length) tabStops[c] = false;
|
||||
} else if (mode == 3) {
|
||||
Arrays.fill(tabStops, false);
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 'h': // Set Mode / Private Mode
|
||||
{
|
||||
if (paramStr.startsWith("?")) {
|
||||
String sub = paramStr.substring(1).trim();
|
||||
if ("25".equals(sub)) {
|
||||
cursorVisible = true;
|
||||
}
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 'l': // Reset Mode / Private Mode
|
||||
{
|
||||
if (paramStr.startsWith("?")) {
|
||||
String sub = paramStr.substring(1).trim();
|
||||
if ("25".equals(sub)) {
|
||||
cursorVisible = false;
|
||||
}
|
||||
}
|
||||
return curAddr;
|
||||
}
|
||||
case 's': // Save cursor
|
||||
savedCursorRow = r;
|
||||
savedCursorCol = c;
|
||||
@@ -269,10 +559,22 @@ public class NvtProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
private void sendResponseString(String s) {
|
||||
if (outputSender != null) {
|
||||
try {
|
||||
outputSender.sendRaw(s.getBytes(StandardCharsets.US_ASCII));
|
||||
} catch (IOException e) {
|
||||
log.warning("Failed to send ANSI response: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int parseParam(String[] params, int idx, int defaultVal) {
|
||||
if (params != null && idx < params.length && !params[idx].trim().isEmpty()) {
|
||||
try {
|
||||
return Integer.parseInt(params[idx].trim());
|
||||
String val = params[idx].trim();
|
||||
if (val.startsWith("?")) val = val.substring(1);
|
||||
return Integer.parseInt(val);
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
return defaultVal;
|
||||
@@ -281,27 +583,73 @@ public class NvtProcessor {
|
||||
private void clearCell(int addr) {
|
||||
ExtendedAttribute cell = screenBuffer.getCell(addr);
|
||||
cell.clear();
|
||||
cell.ec = 0;
|
||||
cell.ec = (byte) 0x40; // EBCDIC space
|
||||
cell.ucs4 = ' ';
|
||||
cell.fg = 0;
|
||||
cell.bg = 0;
|
||||
cell.gr = 0;
|
||||
}
|
||||
|
||||
private void scrollUp() {
|
||||
private void scrollUpRegion(int top, int bottom) {
|
||||
int rows = screenBuffer.getRows();
|
||||
int cols = screenBuffer.getCols();
|
||||
for (int r = 0; r < rows - 1; r++) {
|
||||
top = Math.max(0, Math.min(rows - 1, top));
|
||||
bottom = Math.max(top, Math.min(rows - 1, bottom));
|
||||
|
||||
for (int r = top; r < bottom; r++) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
int dst = r * cols + c;
|
||||
int src = (r + 1) * cols + c;
|
||||
screenBuffer.getCell(dst).copyFrom(screenBuffer.getCell(src));
|
||||
}
|
||||
}
|
||||
// Clear last line
|
||||
int lastRowStart = (rows - 1) * cols;
|
||||
int lastRowStart = bottom * cols;
|
||||
for (int c = 0; c < cols; c++) {
|
||||
clearCell(lastRowStart + c);
|
||||
}
|
||||
}
|
||||
|
||||
private void scrollDownRegion(int top, int bottom) {
|
||||
int rows = screenBuffer.getRows();
|
||||
int cols = screenBuffer.getCols();
|
||||
top = Math.max(0, Math.min(rows - 1, top));
|
||||
bottom = Math.max(top, Math.min(rows - 1, bottom));
|
||||
|
||||
for (int r = bottom; r > top; r--) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
int dst = r * cols + c;
|
||||
int src = (r - 1) * cols + c;
|
||||
screenBuffer.getCell(dst).copyFrom(screenBuffer.getCell(src));
|
||||
}
|
||||
}
|
||||
int topRowStart = top * cols;
|
||||
for (int c = 0; c < cols; c++) {
|
||||
clearCell(topRowStart + c);
|
||||
}
|
||||
}
|
||||
|
||||
private char mapVt100SpecialGraphics(char c) {
|
||||
switch (c) {
|
||||
case 'j': return '┘';
|
||||
case 'k': return '┐';
|
||||
case 'l': return '┌';
|
||||
case 'm': return '└';
|
||||
case 'n': return '┼';
|
||||
case 'q': return '─';
|
||||
case 't': return '├';
|
||||
case 'u': return '┤';
|
||||
case 'v': return '┴';
|
||||
case 'w': return '┬';
|
||||
case 'x': return '│';
|
||||
case '`': return '◆';
|
||||
case 'a': return '▒';
|
||||
case 'f': return '°';
|
||||
case 'g': return '±';
|
||||
case '~': return '•';
|
||||
default: return c;
|
||||
}
|
||||
}
|
||||
|
||||
private void applySgr(int code) {
|
||||
switch (code) {
|
||||
case 0: // Reset
|
||||
|
||||
@@ -115,4 +115,23 @@ public final class TelnetConstants {
|
||||
default: return "CMD-" + cmd;
|
||||
}
|
||||
}
|
||||
public static String qualifierName(int qual) {
|
||||
switch (qual) {
|
||||
case TELQUAL_IS: return "IS";
|
||||
case TELQUAL_SEND: return "SEND";
|
||||
case TELQUAL_INFO: return "INFO";
|
||||
default: return "QUAL-" + qual;
|
||||
}
|
||||
}
|
||||
|
||||
/** NEW-ENVIRON object name lookup. */
|
||||
public static String environObjectName(int obj) {
|
||||
switch (obj) {
|
||||
case TELOBJ_VAR: return "VAR";
|
||||
case TELOBJ_VALUE: return "VALUE";
|
||||
case TELOBJ_ESC: return "ESC";
|
||||
case TELOBJ_USERVAR: return "USERVAR";
|
||||
default: return "OBJ-" + obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,44 +40,71 @@ public class TelnetConnection {
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the host. Blocks until connection is established or fails.
|
||||
* Connect to the host (optionally through a proxy). Blocks until connection is established or fails.
|
||||
*/
|
||||
public void connect() throws IOException {
|
||||
ConnectionConfig.ProxyType proxyType = config.getProxyType();
|
||||
boolean hasProxy = proxyType != null && proxyType != ConnectionConfig.ProxyType.NONE &&
|
||||
config.getProxyHost() != null && !config.getProxyHost().trim().isEmpty();
|
||||
|
||||
String connectHost = hasProxy ? config.getProxyHost().trim() : config.getHost();
|
||||
int connectPort = hasProxy ? (config.getProxyPort() > 0 ? config.getProxyPort() : (proxyType == ConnectionConfig.ProxyType.HTTP ? 8080 : 1080)) : config.getPort();
|
||||
|
||||
log.info("Connecting TCP socket to " + connectHost + ":" + connectPort +
|
||||
(hasProxy ? " (via " + proxyType + " proxy for " + config.getHost() + ":" + config.getPort() + ")" : "") +
|
||||
(config.isUseTls() ? " with TLS" : ""));
|
||||
|
||||
Socket rawSocket = new Socket();
|
||||
rawSocket.setKeepAlive(config.isSoKeepAlive());
|
||||
rawSocket.setOOBInline(true);
|
||||
rawSocket.setTcpNoDelay(config.isTcpNoDelay());
|
||||
if (config.getSoTimeoutMs() > 0) {
|
||||
rawSocket.setSoTimeout(config.getSoTimeoutMs());
|
||||
}
|
||||
rawSocket.connect(new InetSocketAddress(connectHost, connectPort), config.getConnectTimeoutMs());
|
||||
|
||||
// Perform proxy handshake if configured
|
||||
if (hasProxy) {
|
||||
switch (proxyType) {
|
||||
case HTTP:
|
||||
establishHttpProxy(rawSocket, config.getHost(), config.getPort(), config.getProxyUsername(), config.getProxyPassword());
|
||||
break;
|
||||
case SOCKS4:
|
||||
establishSocks4Proxy(rawSocket, config.getHost(), config.getPort(), config.getProxyUsername());
|
||||
break;
|
||||
case SOCKS5:
|
||||
establishSocks5Proxy(rawSocket, config.getHost(), config.getPort(), config.getProxyUsername(), config.getProxyPassword());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (config.isUseTls()) {
|
||||
log.info("Connecting with TLS to " + config.getHost() + ":" + config.getPort() +
|
||||
log.info("Performing TLS handshake with " + config.getHost() + ":" + config.getPort() +
|
||||
" (verifyCert=" + config.isTlsVerifyCert() + ")");
|
||||
try {
|
||||
javax.net.ssl.SSLContext sslContext = haus.nightmare.lib3270j.tls.TlsTrustManager.createSSLContext(config);
|
||||
javax.net.ssl.SSLSocketFactory ssf = sslContext.getSocketFactory();
|
||||
javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket();
|
||||
javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket(
|
||||
rawSocket, config.getHost(), config.getPort(), true);
|
||||
sslSocket.setKeepAlive(config.isSoKeepAlive());
|
||||
sslSocket.setTcpNoDelay(config.isTcpNoDelay());
|
||||
if (config.getSoTimeoutMs() > 0) {
|
||||
sslSocket.setSoTimeout(config.getSoTimeoutMs());
|
||||
}
|
||||
sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
|
||||
config.getConnectTimeoutMs());
|
||||
sslSocket.startHandshake();
|
||||
socket = sslSocket;
|
||||
sslSession = sslSocket.getSession();
|
||||
log.info("TLS session active: protocol=" + sslSession.getProtocol() +
|
||||
" cipher=" + sslSession.getCipherSuite());
|
||||
" cipher=" + sslSession.getCipherSuite());
|
||||
} catch (IOException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IOException("TLS setup failure: " + e.getMessage(), e);
|
||||
}
|
||||
} else {
|
||||
log.info("Connecting to " + config.getHost() + ":" + config.getPort());
|
||||
socket = new Socket();
|
||||
socket.setKeepAlive(config.isSoKeepAlive());
|
||||
socket.setOOBInline(true);
|
||||
socket.setTcpNoDelay(config.isTcpNoDelay());
|
||||
if (config.getSoTimeoutMs() > 0) {
|
||||
socket.setSoTimeout(config.getSoTimeoutMs());
|
||||
}
|
||||
socket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
|
||||
config.getConnectTimeoutMs());
|
||||
socket = rawSocket;
|
||||
}
|
||||
|
||||
inputStream = new BufferedInputStream(socket.getInputStream(), READ_BUFFER_SIZE);
|
||||
@@ -91,6 +118,235 @@ public class TelnetConnection {
|
||||
readerThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically elevate active socket to TLS in-band (STARTTLS / Option 46).
|
||||
*/
|
||||
public synchronized void upgradeToTls() throws IOException {
|
||||
if (socket == null || !socket.isConnected() || socket.isClosed()) {
|
||||
throw new IOException("Cannot upgrade disconnected socket to TLS");
|
||||
}
|
||||
log.info("Elevating active connection to TLS via STARTTLS");
|
||||
try {
|
||||
javax.net.ssl.SSLContext sslContext = haus.nightmare.lib3270j.tls.TlsTrustManager.createSSLContext(config);
|
||||
javax.net.ssl.SSLSocketFactory ssf = sslContext.getSocketFactory();
|
||||
javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket(
|
||||
socket, config.getHost(), config.getPort(), true);
|
||||
sslSocket.setKeepAlive(config.isSoKeepAlive());
|
||||
sslSocket.setTcpNoDelay(config.isTcpNoDelay());
|
||||
if (config.getSoTimeoutMs() > 0) {
|
||||
sslSocket.setSoTimeout(config.getSoTimeoutMs());
|
||||
}
|
||||
sslSocket.startHandshake();
|
||||
this.socket = sslSocket;
|
||||
this.sslSession = sslSocket.getSession();
|
||||
this.inputStream = new BufferedInputStream(sslSocket.getInputStream(), READ_BUFFER_SIZE);
|
||||
this.outputStream = new BufferedOutputStream(sslSocket.getOutputStream());
|
||||
log.info("STARTTLS session active: protocol=" + sslSession.getProtocol() +
|
||||
" cipher=" + sslSession.getCipherSuite());
|
||||
} catch (IOException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IOException("STARTTLS setup failure: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void establishHttpProxy(Socket s, String targetHost, int targetPort, String user, String pass) throws IOException {
|
||||
OutputStream out = s.getOutputStream();
|
||||
InputStream in = s.getInputStream();
|
||||
|
||||
StringBuilder req = new StringBuilder();
|
||||
req.append("CONNECT ").append(targetHost).append(":").append(targetPort).append(" HTTP/1.1\r\n");
|
||||
req.append("Host: ").append(targetHost).append(":").append(targetPort).append("\r\n");
|
||||
if (user != null && !user.isEmpty()) {
|
||||
String auth = user + ":" + (pass != null ? pass : "");
|
||||
String encoded = java.util.Base64.getEncoder().encodeToString(auth.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
req.append("Proxy-Authorization: Basic ").append(encoded).append("\r\n");
|
||||
}
|
||||
req.append("Proxy-Connection: Keep-Alive\r\n\r\n");
|
||||
out.write(req.toString().getBytes(java.nio.charset.StandardCharsets.US_ASCII));
|
||||
out.flush();
|
||||
|
||||
// Read HTTP status line
|
||||
ByteArrayOutputStream lineBuf = new ByteArrayOutputStream();
|
||||
int b;
|
||||
while ((b = in.read()) != -1) {
|
||||
if (b == '\n') break;
|
||||
if (b != '\r') lineBuf.write(b);
|
||||
}
|
||||
String statusLine = new String(lineBuf.toByteArray(), java.nio.charset.StandardCharsets.US_ASCII);
|
||||
if (!statusLine.contains(" 200")) {
|
||||
throw new IOException("HTTP proxy connection failed: " + statusLine);
|
||||
}
|
||||
|
||||
// Consume remaining response headers until empty line
|
||||
while (true) {
|
||||
lineBuf.reset();
|
||||
while ((b = in.read()) != -1) {
|
||||
if (b == '\n') break;
|
||||
if (b != '\r') lineBuf.write(b);
|
||||
}
|
||||
if (lineBuf.size() == 0) break; // empty line terminates headers
|
||||
}
|
||||
log.info("HTTP proxy tunnel established to " + targetHost + ":" + targetPort);
|
||||
}
|
||||
|
||||
private void establishSocks4Proxy(Socket s, String targetHost, int targetPort, String user) throws IOException {
|
||||
OutputStream out = s.getOutputStream();
|
||||
InputStream in = s.getInputStream();
|
||||
|
||||
byte[] ip = new byte[4];
|
||||
boolean isSocks4a = false;
|
||||
try {
|
||||
InetAddress addr = InetAddress.getByName(targetHost);
|
||||
if (addr instanceof Inet4Address) {
|
||||
ip = addr.getAddress();
|
||||
} else {
|
||||
isSocks4a = true;
|
||||
ip = new byte[] { 0, 0, 0, 1 };
|
||||
}
|
||||
} catch (Exception e) {
|
||||
isSocks4a = true;
|
||||
ip = new byte[] { 0, 0, 0, 1 };
|
||||
}
|
||||
|
||||
ByteArrayOutputStream req = new ByteArrayOutputStream();
|
||||
req.write(0x04); // SOCKS version 4
|
||||
req.write(0x01); // CONNECT command
|
||||
req.write((targetPort >> 8) & 0xFF);
|
||||
req.write(targetPort & 0xFF);
|
||||
req.write(ip);
|
||||
if (user != null && !user.isEmpty()) {
|
||||
req.write(user.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1));
|
||||
}
|
||||
req.write(0x00); // Null terminator for userid
|
||||
if (isSocks4a) {
|
||||
req.write(targetHost.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1));
|
||||
req.write(0x00); // Null terminator for domain name
|
||||
}
|
||||
|
||||
out.write(req.toByteArray());
|
||||
out.flush();
|
||||
|
||||
byte[] resp = new byte[8];
|
||||
int read = 0;
|
||||
while (read < 8) {
|
||||
int n = in.read(resp, read, 8 - read);
|
||||
if (n < 0) throw new IOException("Unexpected EOF reading SOCKS4 response");
|
||||
read += n;
|
||||
}
|
||||
|
||||
int status = resp[1] & 0xFF;
|
||||
if (status != 0x5A) {
|
||||
throw new IOException("SOCKS4 proxy request rejected, status=0x" + Integer.toHexString(status));
|
||||
}
|
||||
log.info("SOCKS4 proxy tunnel established to " + targetHost + ":" + targetPort);
|
||||
}
|
||||
|
||||
private void establishSocks5Proxy(Socket s, String targetHost, int targetPort, String user, String pass) throws IOException {
|
||||
OutputStream out = s.getOutputStream();
|
||||
InputStream in = s.getInputStream();
|
||||
|
||||
boolean hasAuth = user != null && !user.isEmpty();
|
||||
if (hasAuth) {
|
||||
out.write(new byte[] { 0x05, 0x02, 0x00, 0x02 }); // SOCKS5, 2 methods: NO_AUTH(0x00), USER_PASS(0x02)
|
||||
} else {
|
||||
out.write(new byte[] { 0x05, 0x01, 0x00 }); // SOCKS5, 1 method: NO_AUTH(0x00)
|
||||
}
|
||||
out.flush();
|
||||
|
||||
byte[] methodResp = new byte[2];
|
||||
readFully(in, methodResp);
|
||||
if ((methodResp[0] & 0xFF) != 0x05) {
|
||||
throw new IOException("Invalid SOCKS5 version response: " + (methodResp[0] & 0xFF));
|
||||
}
|
||||
|
||||
int authMethod = methodResp[1] & 0xFF;
|
||||
if (authMethod == 0x02) {
|
||||
// RFC 1929 Username/Password Authentication
|
||||
byte[] uBytes = user.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
byte[] pBytes = (pass != null ? pass : "").getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
ByteArrayOutputStream authReq = new ByteArrayOutputStream();
|
||||
authReq.write(0x01); // Auth subnegotiation version
|
||||
authReq.write(uBytes.length);
|
||||
authReq.write(uBytes);
|
||||
authReq.write(pBytes.length);
|
||||
authReq.write(pBytes);
|
||||
out.write(authReq.toByteArray());
|
||||
out.flush();
|
||||
|
||||
byte[] authResp = new byte[2];
|
||||
readFully(in, authResp);
|
||||
if (authResp[1] != 0x00) {
|
||||
throw new IOException("SOCKS5 username/password authentication failed");
|
||||
}
|
||||
} else if (authMethod != 0x00) {
|
||||
throw new IOException("SOCKS5 proxy authentication method rejected: 0x" + Integer.toHexString(authMethod));
|
||||
}
|
||||
|
||||
// Send CONNECT request
|
||||
ByteArrayOutputStream connReq = new ByteArrayOutputStream();
|
||||
connReq.write(0x05); // SOCKS5
|
||||
connReq.write(0x01); // CONNECT
|
||||
connReq.write(0x00); // Reserved
|
||||
|
||||
try {
|
||||
InetAddress addr = InetAddress.getByName(targetHost);
|
||||
if (addr instanceof Inet4Address) {
|
||||
connReq.write(0x01); // ATYP IPv4
|
||||
connReq.write(addr.getAddress());
|
||||
} else if (addr instanceof Inet6Address) {
|
||||
connReq.write(0x04); // ATYP IPv6
|
||||
connReq.write(addr.getAddress());
|
||||
} else {
|
||||
byte[] dBytes = targetHost.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
|
||||
connReq.write(0x03); // ATYP Domain
|
||||
connReq.write(dBytes.length);
|
||||
connReq.write(dBytes);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
byte[] dBytes = targetHost.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
|
||||
connReq.write(0x03); // ATYP Domain
|
||||
connReq.write(dBytes.length);
|
||||
connReq.write(dBytes);
|
||||
}
|
||||
|
||||
connReq.write((targetPort >> 8) & 0xFF);
|
||||
connReq.write(targetPort & 0xFF);
|
||||
out.write(connReq.toByteArray());
|
||||
out.flush();
|
||||
|
||||
byte[] connResp = new byte[4];
|
||||
readFully(in, connResp);
|
||||
int rep = connResp[1] & 0xFF;
|
||||
if (rep != 0x00) {
|
||||
throw new IOException("SOCKS5 connect command failed, rep=0x" + Integer.toHexString(rep));
|
||||
}
|
||||
|
||||
int atyp = connResp[3] & 0xFF;
|
||||
if (atyp == 0x01) {
|
||||
byte[] bnd = new byte[4 + 2]; // IPv4 + Port
|
||||
readFully(in, bnd);
|
||||
} else if (atyp == 0x03) {
|
||||
int len = in.read();
|
||||
if (len < 0) throw new IOException("Unexpected EOF in SOCKS5 domain response");
|
||||
byte[] bnd = new byte[len + 2]; // Domain + Port
|
||||
readFully(in, bnd);
|
||||
} else if (atyp == 0x04) {
|
||||
byte[] bnd = new byte[16 + 2]; // IPv6 + Port
|
||||
readFully(in, bnd);
|
||||
}
|
||||
log.info("SOCKS5 proxy tunnel established to " + targetHost + ":" + targetPort);
|
||||
}
|
||||
|
||||
private static void readFully(InputStream in, byte[] buf) throws IOException {
|
||||
int read = 0;
|
||||
while (read < buf.length) {
|
||||
int n = in.read(buf, read, buf.length - read);
|
||||
if (n < 0) throw new IOException("Unexpected EOF reading proxy response");
|
||||
read += n;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send raw bytes to the host.
|
||||
*/
|
||||
|
||||
@@ -107,19 +107,20 @@ public class TelnetFSM {
|
||||
// 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;
|
||||
|
||||
enum TN3270ESubmode { UNBOUND, E_3270, E_NVT, E_SSCP }
|
||||
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::sendBytes);
|
||||
this.nvtProcessor.setOutputSender(this::sendNvtData);
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.nvt.NvtProcessor getNvtProcessor() {
|
||||
@@ -131,10 +132,25 @@ public class TelnetFSM {
|
||||
}
|
||||
|
||||
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; }
|
||||
@@ -189,9 +205,11 @@ public class TelnetFSM {
|
||||
if (connectionState == ConnectionState.TELNET_PENDING) {
|
||||
changeState(ConnectionState.CONNECTED_NVT);
|
||||
}
|
||||
if (connectionState.isNvt()) {
|
||||
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) {
|
||||
} else if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING || hisOpts[TELOPT_BINARY] || hisOpts[TELOPT_EOR]) {
|
||||
ibuf.write(buf, start, i - start);
|
||||
}
|
||||
}
|
||||
@@ -260,13 +278,15 @@ public class TelnetFSM {
|
||||
changeState(ConnectionState.CONNECTED_NVT);
|
||||
}
|
||||
|
||||
if (connectionState.isNvt()) {
|
||||
boolean isPlainNvt = (connectionState == ConnectionState.CONNECTED_NVT || connectionState == ConnectionState.CONNECTED_NVT_CHAR)
|
||||
&& !(hisOpts[TELOPT_BINARY] && hisOpts[TELOPT_EOR]);
|
||||
if (isPlainNvt) {
|
||||
nvtProcessor.processNVTData(new byte[] { (byte) c }, 0, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Accumulate data for 3270, TN3270E (including SSCP-LU and unbound states, and pending)
|
||||
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -336,6 +356,13 @@ public class TelnetFSM {
|
||||
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);
|
||||
@@ -384,6 +411,13 @@ public class TelnetFSM {
|
||||
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;
|
||||
@@ -489,12 +523,37 @@ public class TelnetFSM {
|
||||
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) {
|
||||
@@ -522,15 +581,117 @@ public class TelnetFSM {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== NEW_ENVIRON sub-negotiation ==========
|
||||
// ========== NEW_ENVIRON sub-negotiation (RFC 1572 / RFC 2877) ==========
|
||||
|
||||
private void handleNewEnvironSB(byte[] data) {
|
||||
if (data.length >= 2 && data[1] == TELQUAL_SEND) {
|
||||
log.info("RCVD SB NEW-ENVIRON SEND - Responding with empty IS");
|
||||
byte[] response = { (byte) IAC, (byte) SB, (byte) TELOPT_NEW_ENVIRON,
|
||||
(byte) TELQUAL_IS, (byte) IAC, (byte) SE };
|
||||
sendBytes(response);
|
||||
log.info("SENT SB NEW-ENVIRON IS SE");
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -816,8 +977,10 @@ public class TelnetFSM {
|
||||
ibuf.reset();
|
||||
if (data.length == 0) return;
|
||||
|
||||
if (connectionState == ConnectionState.TELNET_PENDING && !tn3270eNegotiated) {
|
||||
log.info("Received EOR during TELNET_PENDING - transitioning to plain TN3270 mode");
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -831,6 +994,10 @@ public class TelnetFSM {
|
||||
}
|
||||
}
|
||||
|
||||
public void processTn3270eHeader(byte[] data) {
|
||||
processTN3270ERecord(data);
|
||||
}
|
||||
|
||||
private void processTN3270ERecord(byte[] data) {
|
||||
if (data.length < EH_SIZE) {
|
||||
log.warning("TN3270E record too short: " + data.length);
|
||||
@@ -850,11 +1017,10 @@ public class TelnetFSM {
|
||||
switch (dataType) {
|
||||
case DT_3270_DATA:
|
||||
if (data.length > EH_SIZE) {
|
||||
// Transition to 3270 mode
|
||||
if (connectionState == ConnectionState.CONNECTED_UNBOUND ||
|
||||
connectionState == ConnectionState.CONNECTED_SSCP) {
|
||||
// Clear screen on transition to 3270 mode from unbound/SSCP
|
||||
// This ensures old SSCP-LU data or stale content doesn't persist
|
||||
// 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;
|
||||
@@ -879,9 +1045,30 @@ public class TelnetFSM {
|
||||
}
|
||||
break;
|
||||
|
||||
case DT_SCS_DATA:
|
||||
if (data.length > EH_SIZE) {
|
||||
try {
|
||||
processSCSInbound(data, EH_SIZE, data.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) {
|
||||
if (connectionState == ConnectionState.CONNECTED_UNBOUND ||
|
||||
connectionState == ConnectionState.CONNECTED_E_NVT) {
|
||||
// Clear screen on first SSCP-LU transition to remove stale data
|
||||
screenBuffer.clear();
|
||||
}
|
||||
@@ -919,8 +1106,13 @@ public class TelnetFSM {
|
||||
|
||||
case DT_NVT_DATA:
|
||||
// NVT data in TN3270E mode
|
||||
changeState(ConnectionState.CONNECTED_E_NVT);
|
||||
tn3270eSubmode = TN3270ESubmode.E_NVT;
|
||||
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 (data.length > EH_SIZE) {
|
||||
try {
|
||||
processNVTData(data, EH_SIZE, data.length - EH_SIZE);
|
||||
@@ -938,6 +1130,7 @@ public class TelnetFSM {
|
||||
sendTN3270EPositiveResponse(seqNumber);
|
||||
}
|
||||
}
|
||||
notifyScreenUpdate();
|
||||
break;
|
||||
|
||||
case DT_REQUEST:
|
||||
@@ -957,6 +1150,16 @@ public class TelnetFSM {
|
||||
}
|
||||
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);
|
||||
@@ -966,8 +1169,8 @@ public class TelnetFSM {
|
||||
// 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 == 0x05 ||
|
||||
dataType == 0x0D || dataType == 0x01 || (data.length >= 2 && (data[0] & 0xFF) == 0x11)) {
|
||||
dataType == 0x6E || dataType == 0xF2 || dataType == 0xF6 ||
|
||||
dataType == 0x0D || (data.length >= 2 && (data[0] & 0xFF) == 0x11)) {
|
||||
log.warning("Received plain 3270 command (0x" + Integer.toHexString(dataType) +
|
||||
") in TN3270E mode — automatically switching to plain TN3270 mode");
|
||||
tn3270eNegotiated = false;
|
||||
@@ -981,28 +1184,56 @@ public class TelnetFSM {
|
||||
}
|
||||
}
|
||||
|
||||
public void sendTN3270EPositiveResponse(int seqNumber) {
|
||||
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] = (byte) RSF_POSITIVE_RESPONSE;
|
||||
resp[3] = (byte) ((seqNumber >> 8) & 0xFF);
|
||||
resp[4] = (byte) (seqNumber & 0xFF);
|
||||
resp[5] = (byte) POS_DEVICE_END;
|
||||
resp[2] = responseFlag;
|
||||
resp[3] = (byte) ((seq >> 8) & 0xFF);
|
||||
resp[4] = (byte) (seq & 0xFF);
|
||||
resp[5] = responseData;
|
||||
|
||||
sendRecord(resp);
|
||||
}
|
||||
|
||||
public void sendTN3270ENegativeResponse(int seqNumber, int negCode) {
|
||||
byte[] resp = new byte[EH_SIZE + 1];
|
||||
resp[0] = (byte) DT_RESPONSE;
|
||||
resp[1] = 0;
|
||||
resp[2] = (byte) RSF_NEGATIVE_RESPONSE;
|
||||
resp[3] = (byte) ((seqNumber >> 8) & 0xFF);
|
||||
resp[4] = (byte) (seqNumber & 0xFF);
|
||||
resp[5] = (byte) (negCode & 0xFF);
|
||||
/**
|
||||
* HoD 5-byte send_response compatible signature (com.ibm.eNetwork.ECL.tn3270.Telnet3270E).
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
sendRecord(resp);
|
||||
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) {
|
||||
@@ -1021,7 +1252,11 @@ public class TelnetFSM {
|
||||
// ========== Check if we should transition to 3270 mode ==========
|
||||
|
||||
private void checkIn3270() {
|
||||
if (connectionState != ConnectionState.TELNET_PENDING) return;
|
||||
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]) {
|
||||
@@ -1032,7 +1267,11 @@ public class TelnetFSM {
|
||||
if (myOpts[TELOPT_BINARY] && hisOpts[TELOPT_BINARY] &&
|
||||
myOpts[TELOPT_EOR] && hisOpts[TELOPT_EOR]) {
|
||||
log.info("Transitioning to plain TN3270 mode");
|
||||
changeState(ConnectionState.CONNECTED_3270);
|
||||
if (connectionState != ConnectionState.CONNECTED_3270) {
|
||||
screenBuffer.erase(false);
|
||||
changeState(ConnectionState.CONNECTED_3270);
|
||||
notifyScreenUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1130,6 +1369,32 @@ public class TelnetFSM {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
try {
|
||||
connection.sendRaw(data);
|
||||
@@ -1170,6 +1435,7 @@ public class TelnetFSM {
|
||||
public boolean isTn3270eNegotiated() { return tn3270eNegotiated; }
|
||||
public String getConnectedLu() { return connectedLu; }
|
||||
public String getConnectedType() { return connectedType; }
|
||||
public TN3270ESubmode getTn3270eSubmode() { return tn3270eSubmode; }
|
||||
|
||||
// ========== SNA BIND, UNBIND, BID, DFC Handlers (Phase 2) ==========
|
||||
|
||||
@@ -1434,6 +1700,10 @@ public class TelnetFSM {
|
||||
return tn3270eBound;
|
||||
}
|
||||
|
||||
public void processSysReq() {
|
||||
handleSysReq();
|
||||
}
|
||||
|
||||
public void handleSysReq() {
|
||||
if (tn3270eNegotiated) {
|
||||
byte[] ao = new byte[] { (byte) IAC, (byte) AO };
|
||||
|
||||
Reference in New Issue
Block a user