More functional changes and UI adaptations from overnight

This commit is contained in:
2026-08-31 09:16:32 -04:00
parent a43969da08
commit a9956298e2
33 changed files with 3817 additions and 667 deletions
@@ -114,4 +114,14 @@ public class ECLPSTest implements ECLConstants {
int count = ps.pasteLineWrap("ABC\nDEF", 1, 80, false);
assertEquals(6, count);
}
@Test
public void testNVTModeAndSendKeys() {
assertFalse(ps.isNVTmode());
ps.setNVTmode(true);
assertTrue(ps.isNVTmode());
ps.setNVTmode(false);
assertFalse(ps.isNVTmode());
}
}
@@ -453,17 +453,15 @@ public class InputProcessorTest {
}
@Test
public void testSendAidWhenGraphicsCursorActiveFraming() {
public void testSendAidAlwaysSendsStandard3270StreamEvenIfGraphicCursorActive() {
screen.erase(false);
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
screen.getCell(1).ec = (byte) 0xC1; // 'A'
screen.setCellFA(5, (byte) (FA_PRINTABLE | FA_PROTECT));
screen.setCursorAddress(2);
haus.nightmare.lib3270j.graphics.GraphicsPlane plane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(800, 600);
haus.nightmare.lib3270j.graphics.GocaDecoder goca = new haus.nightmare.lib3270j.graphics.GocaDecoder(plane);
goca.setGraphicsCursorActive(true);
goca.setGraphicCursorPosition(150, -80);
java.util.concurrent.atomic.AtomicReference<byte[]> sent = new java.util.concurrent.atomic.AtomicReference<>();
InputProcessor input = new InputProcessor(screen, translator, null) {
@@ -478,64 +476,10 @@ public class InputProcessorTest {
byte[] result = sent.get();
assertNotNull(result);
// Total expected length:
// 1 (AID_SF 0x88) + 56 (SF) + 1 (AID_ENTER 0x7D) + 2 (Cursor Addr) + 1 (SBA) + 2 (Field Addr) + 1 (Data 'A') = 64 bytes
assertEquals(64, result.length);
assertEquals((byte) AID_SF, result[0]);
// SF length = 52 (0x00 0x34) per IBM HOD / GOCA specification
assertEquals(0x00, result[1]);
assertEquals(0x34, result[2]);
// SF ID = 0x0F0F
assertEquals(0x0F, result[3]);
assertEquals(0x0F, result[4]);
// Coordinates in SF at index 1 + 24 = 25
int gx = (result[25] << 8) | (result[26] & 0xFF);
int gy = (result[27] << 8) | (result[28] & 0xFF);
assertEquals(150, (short) gx);
assertEquals(-80, (short) gy);
// Keyboard constants at index 1 + 31 = 32 and 1 + 33 = 34
assertEquals(0x07, result[32]);
assertEquals(0x07, result[34]);
assertEquals((byte) 0xFF, result[35]);
assertEquals((byte) AID_ENTER, result[36]);
// Trailing AID at index 57
assertEquals((byte) AID_ENTER, result[57]);
// Trailing SBA at 60
assertEquals((byte) ORDER_SBA, result[60]);
// Trailing field content 'A' at 63
assertEquals((byte) 0xC1, result[63]);
}
@Test
public void testSendAidPAWhenGraphicsCursorActiveFraming() {
screen.erase(false);
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
screen.getCell(1).ec = (byte) 0xC1;
screen.setCursorAddress(2);
haus.nightmare.lib3270j.graphics.GraphicsPlane plane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(800, 600);
haus.nightmare.lib3270j.graphics.GocaDecoder goca = new haus.nightmare.lib3270j.graphics.GocaDecoder(plane);
goca.setGraphicsCursorActive(true);
goca.setGraphicCursorPosition(100, 200);
java.util.concurrent.atomic.AtomicReference<byte[]> sent = new java.util.concurrent.atomic.AtomicReference<>();
InputProcessor input = new InputProcessor(screen, translator, null) {
@Override
protected void sendAidResponse(byte[] data) {
sent.set(data);
}
};
input.setGocaDecoder(goca);
input.sendAid(AID_PA1);
byte[] result = sent.get();
assertNotNull(result);
// 1 (AID_SF 0x88) + 56 (SF) + 1 (AID_PA1 0x6C) + 2 (Cursor Addr) = 60 bytes (no modified field data)
assertEquals(60, result.length);
assertEquals((byte) AID_SF, result[0]);
assertEquals((byte) AID_PA1, result[36]); // Keyboard AID in SF
assertEquals((byte) AID_PA1, result[57]); // Trailing AID
// Standard 3270 stream: AID_ENTER (1) + Cursor Addr (2) + SBA (1) + Field Addr (2) + Data 'A' (1) = 7 bytes
assertEquals(7, result.length);
assertEquals((byte) AID_ENTER, result[0]);
assertEquals((byte) ORDER_SBA, result[3]);
assertEquals((byte) 0xC1, result[6]);
}
}
@@ -138,4 +138,125 @@ public class NvtProcessorTest {
assertEquals('\r', (char) sent[5]);
assertEquals('\n', (char) sent[6]);
}
@Test
public void testCursorPositionReportDSR() {
// Move to row 12, col 34 (0-indexed: row 11, col 33)
byte[] move = "\u001B[12;34H".getBytes();
processor.processNVTData(move, 0, move.length);
output.reset();
// Send DSR Cursor Position Request: ESC [ 6 n
byte[] dsr = "\u001B[6n".getBytes();
processor.processNVTData(dsr, 0, dsr.length);
assertEquals("\u001B[12;34R", output.toString(java.nio.charset.StandardCharsets.US_ASCII));
}
@Test
public void testDeviceAttributesDA() {
output.reset();
// Send DA Request: ESC [ c
byte[] da1 = "\u001B[c".getBytes();
processor.processNVTData(da1, 0, da1.length);
assertEquals("\u001B[?1;2c", output.toString(java.nio.charset.StandardCharsets.US_ASCII));
output.reset();
// Send DA Request: ESC [ 0 c
byte[] da2 = "\u001B[0c".getBytes();
processor.processNVTData(da2, 0, da2.length);
assertEquals("\u001B[?1;2c", output.toString(java.nio.charset.StandardCharsets.US_ASCII));
}
@Test
public void testExtendedAnsiSequences() {
// CHA: Cursor Horizontal Absolute -> ESC [ 20 G (moves to col 20 -> 0-indexed col 19)
byte[] cha = "\u001B[20G".getBytes();
processor.processNVTData(cha, 0, cha.length);
assertEquals(19, screen.getCursorAddress());
// CNL: Cursor Next Line -> ESC [ 2 E (down 2 rows, col 0)
byte[] cnl = "\u001B[2E".getBytes();
processor.processNVTData(cnl, 0, cnl.length);
assertEquals(2 * 80, screen.getCursorAddress());
// CPL: Cursor Previous Line -> ESC [ 1 F (up 1 row, col 0)
byte[] cpl = "\u001B[1F".getBytes();
processor.processNVTData(cpl, 0, cpl.length);
assertEquals(1 * 80, screen.getCursorAddress());
// Write line "ABCDE"
byte[] text = "\u001B[1;1HABCDE".getBytes();
processor.processNVTData(text, 0, text.length);
assertEquals('A', screen.getCell(0).ucs4);
assertEquals('E', screen.getCell(4).ucs4);
// Move to pos 2 ('C') and ECH (Erase Character): ESC [ 2 X
byte[] ech = "\u001B[1;3H\u001B[2X".getBytes();
processor.processNVTData(ech, 0, ech.length);
assertEquals('A', screen.getCell(0).ucs4);
assertEquals('B', screen.getCell(1).ucs4);
assertEquals(' ', screen.getCell(2).ucs4);
assertEquals(' ', screen.getCell(3).ucs4);
assertEquals('E', screen.getCell(4).ucs4);
// DCH: Delete Character at pos 0 -> ESC [ 1;1H ESC [ 1 P
byte[] dch = "\u001B[1;1H\u001B[1P".getBytes();
processor.processNVTData(dch, 0, dch.length);
assertEquals('B', screen.getCell(0).ucs4);
// ICH: Insert Character at pos 0 -> ESC [ 1;1H ESC [ 1 @
byte[] ich = "\u001B[1;1H\u001B[1@".getBytes();
processor.processNVTData(ich, 0, ich.length);
assertEquals(' ', screen.getCell(0).ucs4);
assertEquals('B', screen.getCell(1).ucs4);
}
@Test
public void testScrollingRegionAndSpecialMovement() {
// Set scrolling region lines 2..4 (1-indexed): ESC [ 2 ; 4 r
byte[] decstbm = "\u001B[2;4r".getBytes();
processor.processNVTData(decstbm, 0, decstbm.length);
assertEquals(0, screen.getCursorAddress());
// Move to row 4 (bottom of region), col 1
byte[] line4 = "\u001B[4;1HLine4".getBytes();
processor.processNVTData(line4, 0, line4.length);
assertEquals('L', screen.getCell(3 * 80).ucs4);
// Index (line feed down at bottom of region): ESC D -> scrolls region up
byte[] ind = "\u001BD".getBytes();
processor.processNVTData(ind, 0, ind.length);
assertEquals('L', screen.getCell(2 * 80).ucs4); // Moved to row 3
// Reverse Index (line feed up at top of region): ESC [ 2;1H ESC M -> scrolls region down
byte[] ri = "\u001B[2;1H\u001BM".getBytes();
processor.processNVTData(ri, 0, ri.length);
assertEquals('L', screen.getCell(3 * 80).ucs4); // Moved back down to row 4
}
@Test
public void testCursorVisibilityAndVt100Graphics() {
assertTrue(processor.isCursorVisible());
// Hide cursor: ESC [ ? 25 l
byte[] hide = "\u001B[?25l".getBytes();
processor.processNVTData(hide, 0, hide.length);
assertFalse(processor.isCursorVisible());
// Show cursor: ESC [ ? 25 h
byte[] show = "\u001B[?25h".getBytes();
processor.processNVTData(show, 0, show.length);
assertTrue(processor.isCursorVisible());
// Enable VT100 line drawing on G0: ESC ( 0
byte[] g0 = "\u001B(0q".getBytes();
processor.processNVTData(g0, 0, g0.length);
assertEquals('─', screen.getCell(0).ucs4);
// Reset G0 to ASCII: ESC ( B
byte[] g0Ascii = "\u001B(Bq".getBytes();
processor.processNVTData(g0Ascii, 0, g0Ascii.length);
assertEquals('q', screen.getCell(1).ucs4);
}
}
@@ -0,0 +1,244 @@
package haus.nightmare.lib3270j.telnet;
import org.junit.jupiter.api.Test;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.TerminalModel;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.*;
import static org.junit.jupiter.api.Assertions.*;
public class ProxyConnectionTest {
@Test
public void testParseHostStringWithProxyFlags() {
// HTTP proxy without auth
ConnectionConfig c1 = ConnectionConfig.parseHostString("--proxy=http://proxy.corp.com:8080 mainframe.net:23", 23, TerminalModel.IBM_3279_4);
assertEquals(ConnectionConfig.ProxyType.HTTP, c1.getProxyType());
assertEquals("proxy.corp.com", c1.getProxyHost());
assertEquals(8080, c1.getProxyPort());
assertNull(c1.getProxyUsername());
assertEquals("mainframe.net", c1.getHost());
assertEquals(23, c1.getPort());
// HTTP proxy with auth
ConnectionConfig c2 = ConnectionConfig.parseHostString("--proxy=http://alice:secret123@10.0.0.1:3128 zos.ibm.com:2323", 23, TerminalModel.IBM_3279_4);
assertEquals(ConnectionConfig.ProxyType.HTTP, c2.getProxyType());
assertEquals("10.0.0.1", c2.getProxyHost());
assertEquals(3128, c2.getProxyPort());
assertEquals("alice", c2.getProxyUsername());
assertEquals("secret123", c2.getProxyPassword());
assertEquals("zos.ibm.com", c2.getHost());
assertEquals(2323, c2.getPort());
// SOCKS4 proxy
ConnectionConfig c3 = ConnectionConfig.parseHostString("--proxy=socks4://socks.local:1080 L:secure.mvs.com:992", 23, TerminalModel.IBM_3279_4);
assertEquals(ConnectionConfig.ProxyType.SOCKS4, c3.getProxyType());
assertEquals("socks.local", c3.getProxyHost());
assertEquals(1080, c3.getProxyPort());
assertTrue(c3.isUseTls());
assertEquals("secure.mvs.com", c3.getHost());
assertEquals(992, c3.getPort());
// SOCKS5 proxy with auth
ConnectionConfig c4 = ConnectionConfig.parseHostString("--proxy=socks5://bob:pass55@127.0.0.1:9050 P:vm.host:23", 23, TerminalModel.IBM_3279_4);
assertEquals(ConnectionConfig.ProxyType.SOCKS5, c4.getProxyType());
assertEquals("127.0.0.1", c4.getProxyHost());
assertEquals(9050, c4.getProxyPort());
assertEquals("bob", c4.getProxyUsername());
assertEquals("pass55", c4.getProxyPassword());
assertFalse(c4.isTn3270eEnabled());
assertEquals("vm.host", c4.getHost());
}
@Test
public void testHttpProxyConnectHandshake() throws Exception {
try (ServerSocket proxyServer = new ServerSocket(0)) {
int proxyPort = proxyServer.getLocalPort();
CountDownLatch serverHandshakeDone = new CountDownLatch(1);
CompletableFuture<String> receivedRequest = new CompletableFuture<>();
Thread serverThread = new Thread(() -> {
try (Socket clientSock = proxyServer.accept()) {
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSock.getInputStream(), StandardCharsets.US_ASCII));
StringBuilder req = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) break;
req.append(line).append("\n");
}
receivedRequest.complete(req.toString());
// Respond 200 Connection established
OutputStream out = clientSock.getOutputStream();
out.write("HTTP/1.1 200 Connection established\r\n\r\n".getBytes(StandardCharsets.US_ASCII));
out.flush();
serverHandshakeDone.countDown();
// Echo back test data
int b;
while ((b = clientSock.getInputStream().read()) != -1) {
out.write(b);
out.flush();
}
} catch (Exception e) {
receivedRequest.completeExceptionally(e);
}
});
serverThread.setDaemon(true);
serverThread.start();
ConnectionConfig config = new ConnectionConfig("target.mainframe.org", 23);
config.setProxy(ConnectionConfig.ProxyType.HTTP, "127.0.0.1", proxyPort, "testuser", "testpass");
config.setConnectTimeoutMs(5000);
TelnetFSM fsm = new TelnetFSM(config, new haus.nightmare.lib3270j.screen.ScreenBuffer(TerminalModel.IBM_3279_4, new haus.nightmare.lib3270j.charset.EbcdicTranslator()), null);
TelnetConnection connection = new TelnetConnection(config, fsm);
connection.connect();
assertTrue(connection.isConnected());
String req = receivedRequest.get(3, TimeUnit.SECONDS);
assertTrue(req.startsWith("CONNECT target.mainframe.org:23 HTTP/1.1"), "Expected CONNECT request");
assertTrue(req.contains("Proxy-Authorization: Basic "), "Expected Basic auth in proxy request");
connection.disconnect();
}
}
@Test
public void testSocks4ProxyHandshake() throws Exception {
try (ServerSocket proxyServer = new ServerSocket(0)) {
int proxyPort = proxyServer.getLocalPort();
CountDownLatch handshakeDone = new CountDownLatch(1);
Thread serverThread = new Thread(() -> {
try (Socket clientSock = proxyServer.accept()) {
InputStream in = clientSock.getInputStream();
OutputStream out = clientSock.getOutputStream();
// Read SOCKS4 request header
byte[] req = new byte[8];
in.read(req);
assertEquals(0x04, req[0]); // SOCKS4
assertEquals(0x01, req[1]); // CONNECT
// Read null-terminated username
ByteArrayOutputStream userBuf = new ByteArrayOutputStream();
int b;
while ((b = in.read()) != 0 && b != -1) {
userBuf.write(b);
}
assertEquals("user4", new String(userBuf.toByteArray()));
// Reply 0x00 0x5A (Request granted)
byte[] resp = new byte[] { 0x00, 0x5A, 0x00, 0x17, 127, 0, 0, 1 };
out.write(resp);
out.flush();
handshakeDone.countDown();
while (in.read() != -1) {}
} catch (Exception ignored) {}
});
serverThread.setDaemon(true);
serverThread.start();
ConnectionConfig config = new ConnectionConfig("127.0.0.1", 23);
config.setProxy(ConnectionConfig.ProxyType.SOCKS4, "127.0.0.1", proxyPort, "user4", null);
config.setConnectTimeoutMs(5000);
TelnetFSM fsm = new TelnetFSM(config, new haus.nightmare.lib3270j.screen.ScreenBuffer(TerminalModel.IBM_3279_4, new haus.nightmare.lib3270j.charset.EbcdicTranslator()), null);
TelnetConnection connection = new TelnetConnection(config, fsm);
connection.connect();
assertTrue(connection.isConnected());
assertTrue(handshakeDone.await(3, TimeUnit.SECONDS));
connection.disconnect();
}
}
@Test
public void testSocks5ProxyHandshakeWithAuth() throws Exception {
try (ServerSocket proxyServer = new ServerSocket(0)) {
int proxyPort = proxyServer.getLocalPort();
CountDownLatch handshakeDone = new CountDownLatch(1);
Thread serverThread = new Thread(() -> {
try (Socket clientSock = proxyServer.accept()) {
InputStream in = clientSock.getInputStream();
OutputStream out = clientSock.getOutputStream();
// 1. Read method selection
int ver = in.read();
int nmethods = in.read();
byte[] methods = new byte[nmethods];
in.read(methods);
assertEquals(0x05, ver);
// Select USER_PASS (0x02)
out.write(new byte[] { 0x05, 0x02 });
out.flush();
// 2. Read auth request (RFC 1929)
int authVer = in.read();
int ulen = in.read();
byte[] u = new byte[ulen];
in.read(u);
int plen = in.read();
byte[] p = new byte[plen];
in.read(p);
assertEquals(1, authVer);
assertEquals("admin", new String(u));
assertEquals("pass123", new String(p));
// Auth success: 0x01 0x00
out.write(new byte[] { 0x01, 0x00 });
out.flush();
// 3. Read connect command
byte[] cmd = new byte[4];
in.read(cmd);
assertEquals(0x05, cmd[0]);
assertEquals(0x01, cmd[1]); // CONNECT
int atyp = cmd[3];
if (atyp == 0x03) { // Domain name
int dlen = in.read();
byte[] d = new byte[dlen];
in.read(d);
} else if (atyp == 0x01) { // IPv4
in.read(new byte[4]);
}
in.read(new byte[2]); // Port
// Reply success: 0x05 0x00 0x00 0x01 127.0.0.1:port
out.write(new byte[] { 0x05, 0x00, 0x00, 0x01, 127, 0, 0, 1, 0, 23 });
out.flush();
handshakeDone.countDown();
while (in.read() != -1) {}
} catch (Exception ignored) {}
});
serverThread.setDaemon(true);
serverThread.start();
ConnectionConfig config = new ConnectionConfig("mvs.corp.local", 23);
config.setProxy(ConnectionConfig.ProxyType.SOCKS5, "127.0.0.1", proxyPort, "admin", "pass123");
config.setConnectTimeoutMs(5000);
TelnetFSM fsm = new TelnetFSM(config, new haus.nightmare.lib3270j.screen.ScreenBuffer(TerminalModel.IBM_3279_4, new haus.nightmare.lib3270j.charset.EbcdicTranslator()), null);
TelnetConnection connection = new TelnetConnection(config, fsm);
connection.connect();
assertTrue(connection.isConnected());
assertTrue(handshakeDone.await(3, TimeUnit.SECONDS));
connection.disconnect();
}
}
}
@@ -0,0 +1,346 @@
package haus.nightmare.lib3270j.telnet;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.listener.SCSInboundListener;
import haus.nightmare.lib3270j.protocol.TelnetConstants;
import haus.nightmare.lib3270j.protocol.TN3270EConstants;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import static org.junit.jupiter.api.Assertions.*;
import static haus.nightmare.lib3270j.protocol.TelnetConstants.*;
import static haus.nightmare.lib3270j.protocol.TN3270EConstants.*;
public class TelnetFSMPhase1FullTest {
private ConnectionConfig config;
private ScreenBuffer screenBuffer;
private DataStreamProcessor dsProcessor;
private InputProcessor inputProcessor;
private TelnetFSM fsm;
private MockConnection connection;
private EbcdicTranslator translator;
private static class MockConnection extends TelnetConnection {
final List<byte[]> sentData = new ArrayList<>();
boolean tlsUpgraded = false;
MockConnection(ConnectionConfig config, TelnetFSM fsm) {
super(config, fsm);
}
@Override
public synchronized void sendRaw(byte[] data) {
sentData.add(data.clone());
}
@Override
public synchronized void sendRaw(byte[] data, int offset, int length) {
byte[] b = new byte[length];
System.arraycopy(data, offset, b, 0, length);
sentData.add(b);
}
@Override
public synchronized void upgradeToTls() throws IOException {
tlsUpgraded = true;
}
}
@BeforeEach
public void setup() {
translator = new EbcdicTranslator();
config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
screenBuffer = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
dsProcessor = new DataStreamProcessor(screenBuffer, translator);
fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
dsProcessor.setInputProcessor(inputProcessor);
connection = new MockConnection(config, fsm);
fsm.setConnection(connection);
}
private void feedBytes(int... bytes) {
for (int b : bytes) {
fsm.feedByte(b & 0xFF);
}
}
@Test
public void testSendTn3270eResponseAndHoD5ByteSignature() {
connection.sentData.clear();
// 1. Standard 6-byte RFC 2355 response
fsm.sendTn3270eResponse((byte) RSF_POSITIVE_RESPONSE, (byte) POS_DEVICE_END, 0x1234);
assertEquals(1, connection.sentData.size());
byte[] resp6 = connection.sentData.get(0);
assertEquals(6 + 2, resp6.length); // 6 bytes header/trailer + IAC EOR
assertEquals(DT_RESPONSE, resp6[0] & 0xFF);
assertEquals(0, resp6[1] & 0xFF);
assertEquals(RSF_POSITIVE_RESPONSE, resp6[2] & 0xFF);
assertEquals(0x12, resp6[3] & 0xFF);
assertEquals(0x34, resp6[4] & 0xFF);
assertEquals(POS_DEVICE_END, resp6[5] & 0xFF);
assertEquals(IAC, resp6[6] & 0xFF);
assertEquals(EOR, resp6[7] & 0xFF);
// 2. HoD 5-byte send_response
connection.sentData.clear();
fsm.send_response((short) 0, (short) RSF_POSITIVE_RESPONSE, 0x5678);
assertEquals(1, connection.sentData.size());
byte[] resp5 = connection.sentData.get(0);
assertEquals(5 + 2, resp5.length); // 5 bytes + IAC EOR
assertEquals(DT_RESPONSE, resp5[0] & 0xFF);
assertEquals(0, resp5[1] & 0xFF);
assertEquals(RSF_POSITIVE_RESPONSE, resp5[2] & 0xFF);
assertEquals(0x56, resp5[3] & 0xFF);
assertEquals(0x78, resp5[4] & 0xFF);
}
@Test
public void testStartTlsOption46NegotiationAndElevation() {
config.setStartTlsEnabled(true);
fsm.onConnected();
connection.sentData.clear();
// Server sends DO STARTTLS (Option 46)
feedBytes(IAC, DO, TELOPT_STARTTLS);
// Client must reply WILL STARTTLS
assertEquals(1, connection.sentData.size());
assertArrayEquals(new byte[] { (byte) IAC, (byte) WILL, (byte) TELOPT_STARTTLS }, connection.sentData.get(0));
assertTrue(fsm.getMyOpts()[TELOPT_STARTTLS]);
connection.sentData.clear();
// Server sends SB STARTTLS 1 (TLS_FOLLOWS) SE
feedBytes(IAC, SB, TELOPT_STARTTLS, TLS_FOLLOWS, IAC, SE);
// Connection must have been upgraded to TLS
assertTrue(connection.tlsUpgraded, "Expected upgradeToTls() to be invoked on STARTTLS_FOLLOWS");
}
@Test
public void testRfc1572NewEnvironEmptySendReturnsAllVariables() {
config.setEnvironmentVariable("USER", "MAINFRAME_USER");
config.setEnvironmentVariable("SYSTEMTYPE", "MVS");
config.setUserVariable("IBM_EXPRESS_LOGON", "CERT_AUTH_ENABLED");
fsm.onConnected();
connection.sentData.clear();
// Server sends DO NEW_ENVIRON
feedBytes(IAC, DO, TELOPT_NEW_ENVIRON);
assertEquals(1, connection.sentData.size());
assertArrayEquals(new byte[] { (byte) IAC, (byte) WILL, (byte) TELOPT_NEW_ENVIRON }, connection.sentData.get(0));
connection.sentData.clear();
// Server sends SB NEW-ENVIRON SEND SE (empty query -> all variables)
feedBytes(IAC, SB, TELOPT_NEW_ENVIRON, TELQUAL_SEND, IAC, SE);
assertEquals(1, connection.sentData.size());
byte[] pkt = connection.sentData.get(0);
assertTrue(pkt.length >= 6);
assertEquals(IAC, pkt[0] & 0xFF);
assertEquals(SB, pkt[1] & 0xFF);
assertEquals(TELOPT_NEW_ENVIRON, pkt[2] & 0xFF);
assertEquals(TELQUAL_IS, pkt[3] & 0xFF);
String pktStr = new String(pkt);
assertTrue(pktStr.contains("USER"));
assertTrue(pktStr.contains("MAINFRAME_USER"));
assertTrue(pktStr.contains("SYSTEMTYPE"));
assertTrue(pktStr.contains("MVS"));
assertTrue(pktStr.contains("IBM_EXPRESS_LOGON"));
assertTrue(pktStr.contains("CERT_AUTH_ENABLED"));
}
@Test
public void testRfc1572NewEnvironSelectiveSend() throws Exception {
config.setEnvironmentVariable("USER", "ALICE");
config.setEnvironmentVariable("JOB", "DAILY_REPORT");
config.setUserVariable("SEC_TOKEN", "XYZ123");
fsm.onConnected();
connection.sentData.clear();
// Server sends SB NEW-ENVIRON SEND VAR "USER" USERVAR "SEC_TOKEN" SE
ByteArrayOutputStream query = new ByteArrayOutputStream();
query.write(IAC);
query.write(SB);
query.write(TELOPT_NEW_ENVIRON);
query.write(TELQUAL_SEND);
query.write(TELOBJ_VAR);
query.write("USER".getBytes());
query.write(TELOBJ_USERVAR);
query.write("SEC_TOKEN".getBytes());
query.write(IAC);
query.write(SE);
for (byte b : query.toByteArray()) {
fsm.feedByte(b & 0xFF);
}
assertEquals(1, connection.sentData.size());
String respStr = new String(connection.sentData.get(0));
assertTrue(respStr.contains("USER"));
assertTrue(respStr.contains("ALICE"));
assertTrue(respStr.contains("SEC_TOKEN"));
assertTrue(respStr.contains("XYZ123"));
assertFalse(respStr.contains("JOB"), "Unrequested variable JOB should not be sent");
}
@Test
public void testSendNewEnvironmentVariablesFromProperties() {
Properties props = new Properties();
props.setProperty("VAR_TEST", "VAL1");
props.setProperty("USERVAR_TOKEN", "VAL2");
connection.sentData.clear();
fsm.sendNewEnvironmentVariables(props);
assertEquals(1, connection.sentData.size());
byte[] pkt = connection.sentData.get(0);
assertEquals(IAC, pkt[0] & 0xFF);
assertEquals(SB, pkt[1] & 0xFF);
assertEquals(TELOPT_NEW_ENVIRON, pkt[2] & 0xFF);
assertEquals(TELQUAL_IS, pkt[3] & 0xFF);
String pktStr = new String(pkt);
assertTrue(pktStr.contains("VAR_TEST"));
assertTrue(pktStr.contains("VAL1"));
assertTrue(pktStr.contains("USERVAR_TOKEN"));
assertTrue(pktStr.contains("VAL2"));
}
@Test
public void testInboundSCSDataHandlingAndAutoPositiveResponse() throws Exception {
config.setTn3270eEnabled(true);
fsm.onConnected();
// Negotiate TN3270E with RESPONSES and SCS_CTL_CODES
feedBytes(IAC, DO, TELOPT_TN3270E);
feedBytes(IAC, SB, TELOPT_TN3270E, OP_SEND, OP_DEVICE_TYPE, IAC, SE);
byte[] devTypeIs = new byte[]{
(byte) IAC, (byte) SB, (byte) TELOPT_TN3270E,
0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '8', '7', '-', '1',
(byte) IAC, (byte) SE
};
for (byte b : devTypeIs) fsm.feedByte(b & 0xFF);
byte[] funcsReq = new byte[]{
(byte) IAC, (byte) SB, (byte) TELOPT_TN3270E,
(byte) OP_FUNCTIONS, (byte) OP_REQUEST,
(byte) FUNC_BIND_IMAGE, (byte) FUNC_RESPONSES, (byte) FUNC_SCS_CTL_CODES,
(byte) IAC, (byte) SE
};
for (byte b : funcsReq) fsm.feedByte(b & 0xFF);
List<byte[]> scsReceived = new ArrayList<>();
fsm.addSCSInboundListener(new SCSInboundListener() {
@Override
public void onSCSDataReceived(byte[] data, int offset, int length) {
byte[] copy = new byte[length];
System.arraycopy(data, offset, copy, 0, length);
scsReceived.add(copy);
}
});
connection.sentData.clear();
// Send DT_SCS_DATA (0x01) record with ALWAYS_RESPONSE
int seq = 0x00A1;
byte[] scsPayload = new byte[] { 0x15, 0x2B, (byte) 0xD2, 0x04, 0x15 }; // SCS orders
ByteArrayOutputStream scsRecord = new ByteArrayOutputStream();
scsRecord.write(DT_SCS_DATA);
scsRecord.write(0x00); // Request flag
scsRecord.write(RSF_ALWAYS_RESPONSE);
scsRecord.write((seq >> 8) & 0xFF);
scsRecord.write(seq & 0xFF);
scsRecord.write(scsPayload);
scsRecord.write(IAC);
scsRecord.write(EOR);
for (byte b : scsRecord.toByteArray()) {
fsm.feedByte(b & 0xFF);
}
// Verify SCS listener was notified with exact payload
assertEquals(1, scsReceived.size());
assertArrayEquals(scsPayload, scsReceived.get(0));
// Verify positive response was sent
boolean foundPositive = false;
for (byte[] pkt : connection.sentData) {
if (pkt.length >= 8 &&
(pkt[0] & 0xFF) == DT_RESPONSE &&
(pkt[2] & 0xFF) == RSF_POSITIVE_RESPONSE &&
((((pkt[3] & 0xFF) << 8) | (pkt[4] & 0xFF)) == seq) &&
(pkt[5] & 0xFF) == POS_DEVICE_END) {
foundPositive = true;
}
}
assertTrue(foundPositive, "Expected positive response for DT_SCS_DATA with RSF_ALWAYS_RESPONSE");
}
@Test
public void testProcessSysReqMethod() {
config.setTn3270eEnabled(true);
fsm.onConnected();
// Negotiate TN3270E
feedBytes(IAC, DO, TELOPT_TN3270E);
feedBytes(IAC, SB, TELOPT_TN3270E, OP_SEND, OP_DEVICE_TYPE, IAC, SE);
byte[] devTypeIs = new byte[]{
(byte) IAC, (byte) SB, (byte) TELOPT_TN3270E,
0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
(byte) IAC, (byte) SE
};
for (byte b : devTypeIs) fsm.feedByte(b & 0xFF);
byte[] funcsReq = new byte[]{
(byte) IAC, (byte) SB, (byte) TELOPT_TN3270E,
(byte) OP_FUNCTIONS, (byte) OP_REQUEST,
(byte) FUNC_BIND_IMAGE, (byte) FUNC_RESPONSES, (byte) FUNC_SYSREQ,
(byte) IAC, (byte) SE
};
for (byte b : funcsReq) fsm.feedByte(b & 0xFF);
// Bind session
byte[] bindPacket = new byte[EH_SIZE + 35];
bindPacket[0] = DT_BIND_IMAGE;
bindPacket[1] = 0; bindPacket[2] = 0; bindPacket[3] = 0; bindPacket[4] = 1;
bindPacket[EH_SIZE + 24] = 0x02;
ByteArrayOutputStream bStream = new ByteArrayOutputStream();
bStream.write(bindPacket, 0, bindPacket.length);
bStream.write(IAC);
bStream.write(EOR);
for (byte b : bStream.toByteArray()) fsm.feedByte(b & 0xFF);
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState());
connection.sentData.clear();
// Call processSysReq()
fsm.processSysReq();
// State changes to CONNECTED_SSCP and IAC AO sent
assertEquals(ConnectionState.CONNECTED_SSCP, fsm.getConnectionState());
boolean foundAo = false;
for (byte[] pkt : connection.sentData) {
if (pkt.length == 2 && (pkt[0] & 0xFF) == IAC && (pkt[1] & 0xFF) == AO) {
foundAo = true;
}
}
assertTrue(foundAo, "Expected IAC AO out-of-band telnet abort");
}
}
@@ -50,6 +50,9 @@ public class TelnetFSMTest {
screenBuffer = new ScreenBuffer(TerminalModel.IBM_3279_4, new EbcdicTranslator());
dsProcessor = new DataStreamProcessor(screenBuffer, new EbcdicTranslator());
fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
haus.nightmare.lib3270j.input.InputProcessor inputProcessor =
new haus.nightmare.lib3270j.input.InputProcessor(screenBuffer, new EbcdicTranslator(), fsm);
dsProcessor.setInputProcessor(inputProcessor);
connection = new MockConnection(config, fsm);
fsm.setConnection(connection);
}
@@ -294,4 +297,212 @@ public class TelnetFSMTest {
String resp3 = new String(lastPkt, 4, lastPkt.length - 6);
assertEquals("IBM-3278-4-E", resp3);
}
@Test
public void testTn3270eNvtInboundAndOutbound() throws Exception {
fsm.onConnected();
connection.sentData.clear();
// Negotiate TN3270E without BIND-IMAGE
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_TN3270E);
// Host sends DEVICE-TYPE SEND
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TN3270E,
0x08, 0x01, TelnetConstants.IAC, TelnetConstants.SE);
// Host sends DEVICE-TYPE IS IBM-3279-4-E
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TN3270E,
0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
TelnetConstants.IAC, TelnetConstants.SE);
// Host sends FUNCTIONS IS (no BIND-IMAGE)
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TN3270E,
0x03, 0x04, TelnetConstants.IAC, TelnetConstants.SE);
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState());
// Host sends DT_NVT_DATA (0x05) record with text "PROMPT: "
byte[] nvtRecord = {
0x05, // DT_NVT_DATA
0x00, // request flag
0x00, // response flag
0x00, 0x01, // seq 1
'P', 'R', 'O', 'M', 'P', 'T', ':', ' ',
(byte) TelnetConstants.IAC, (byte) TelnetConstants.EOR
};
for (byte b : nvtRecord) {
fsm.feedByte(b & 0xFF);
}
// FSM must transition to CONNECTED_E_NVT
assertEquals(ConnectionState.CONNECTED_E_NVT, fsm.getConnectionState());
assertTrue(fsm.getConnectionState().isNvt());
// Verify screen buffer contains the NVT text
assertEquals('P', screenBuffer.getCell(0).ucs4);
assertEquals('R', screenBuffer.getCell(1).ucs4);
assertEquals('O', screenBuffer.getCell(2).ucs4);
assertEquals('M', screenBuffer.getCell(3).ucs4);
assertEquals('P', screenBuffer.getCell(4).ucs4);
assertEquals('T', screenBuffer.getCell(5).ucs4);
assertEquals(':', screenBuffer.getCell(6).ucs4);
assertEquals(' ', screenBuffer.getCell(7).ucs4);
assertEquals(8, screenBuffer.getCursorAddress());
// Test Outbound NVT sending in TN3270E mode
connection.sentData.clear();
fsm.sendNVTString("OK\n");
assertFalse(connection.sentData.isEmpty());
byte[] sentPkt = connection.sentData.get(connection.sentData.size() - 1);
// Sent packet should be TN3270E record: 5-byte header + "OK\r\n" + IAC EOR
assertEquals(5 + 4 + 2, sentPkt.length);
assertEquals(0x05, sentPkt[0]); // DT_NVT_DATA
assertEquals('O', sentPkt[5]);
assertEquals('K', sentPkt[6]);
assertEquals('\r', sentPkt[7]);
assertEquals('\n', sentPkt[8]);
assertEquals((byte) TelnetConstants.IAC, sentPkt[9]);
assertEquals((byte) TelnetConstants.EOR, sentPkt[10]);
}
@Test
public void testPlainTelnetNvtStreamingAndLocalEcho() throws Exception {
fsm.onConnected();
config.setNvtLocalEcho(true);
// Receiving raw bytes in TELNET_PENDING automatically transitions to CONNECTED_NVT
feedBytes('L', 'O', 'G', 'I', 'N', '>');
assertEquals(ConnectionState.CONNECTED_NVT, fsm.getConnectionState());
assertEquals('L', screenBuffer.getCell(0).ucs4);
assertEquals('>', screenBuffer.getCell(5).ucs4);
// Sending NVT data with local echo enabled
connection.sentData.clear();
fsm.sendNVTString("TEST");
// Raw bytes sent over TCP
assertEquals(1, connection.sentData.size());
byte[] sent = connection.sentData.get(0);
assertArrayEquals("TEST".getBytes(), sent);
// Local echo placed "TEST" on screen starting at address 6
assertEquals('T', screenBuffer.getCell(6).ucs4);
assertEquals('E', screenBuffer.getCell(7).ucs4);
assertEquals('S', screenBuffer.getCell(8).ucs4);
assertEquals('T', screenBuffer.getCell(9).ucs4);
assertEquals(10, screenBuffer.getCursorAddress());
}
@Test
public void testVmConmode3270TransitionTn3270e() throws Exception {
fsm.onConnected();
connection.sentData.clear();
// 1. Negotiate TN3270E
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_TN3270E);
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TN3270E,
0x08, 0x01, TelnetConstants.IAC, TelnetConstants.SE);
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TN3270E,
0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
TelnetConstants.IAC, TelnetConstants.SE);
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TN3270E,
0x03, 0x04, TelnetConstants.IAC, TelnetConstants.SE);
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState());
// 2. VM console in line-mode sends DT_NVT_DATA (e.g. CP prompt)
byte[] nvtRecord = {
0x05, 0x00, 0x00, 0x00, 0x01,
'C', 'P', ' ', 'R', 'E', 'A', 'D', 'Y', '\r', '\n',
(byte) TelnetConstants.IAC, (byte) TelnetConstants.EOR
};
for (byte b : nvtRecord) fsm.feedByte(b & 0xFF);
assertEquals(ConnectionState.CONNECTED_E_NVT, fsm.getConnectionState());
assertEquals(TelnetFSM.TN3270ESubmode.E_NVT, fsm.getTn3270eSubmode());
// 3. VM executes "cp term conmode 3270" and sends 3270 full screen (DT_3270_DATA)
EbcdicTranslator trans = new EbcdicTranslator();
String banner = "z/VM 3270 FULLSCREEN";
byte[] stream3270 = new byte[5 + 5 + banner.length() + 2];
// 5-byte TN3270E header
stream3270[0] = 0x00; // DT_3270_DATA
stream3270[1] = 0x00;
stream3270[2] = 0x00;
stream3270[3] = 0x00; stream3270[4] = 0x02; // seq 2
// 3270 Orders
stream3270[5] = (byte) 0xF5; // EraseWrite
stream3270[6] = (byte) 0xC3; // WCC
stream3270[7] = 0x11; // SBA
stream3270[8] = 0x40; stream3270[9] = 0x40; // pos 0
for (int i = 0; i < banner.length(); i++) {
stream3270[10 + i] = (byte) trans.unicodeToEbcdic(banner.charAt(i));
}
stream3270[stream3270.length - 2] = (byte) TelnetConstants.IAC;
stream3270[stream3270.length - 1] = (byte) TelnetConstants.EOR;
for (byte b : stream3270) fsm.feedByte(b & 0xFF);
// Verify transition from E_NVT to E_3270 (CONNECTED_TN3270E)
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState());
assertEquals(TelnetFSM.TN3270ESubmode.E_3270, fsm.getTn3270eSubmode());
assertFalse(fsm.getConnectionState().isNvt());
// Verify screen displays 3270 content
assertEquals('z', trans.ebcdicToUnicode(screenBuffer.getCellEC(0)));
assertEquals('/', trans.ebcdicToUnicode(screenBuffer.getCellEC(1)));
assertEquals('V', trans.ebcdicToUnicode(screenBuffer.getCellEC(2)));
assertEquals('M', trans.ebcdicToUnicode(screenBuffer.getCellEC(3)));
// 4. Outbound 3270 AID transmission should now use DT_3270_DATA
connection.sentData.clear();
dsProcessor.getInputProcessor().sendAid(0x7D);
assertFalse(connection.sentData.isEmpty());
byte[] sentAid = connection.sentData.get(connection.sentData.size() - 1);
assertEquals(0x00, sentAid[0]); // DT_3270_DATA header
assertEquals((byte) 0x7D, sentAid[5]); // Enter AID code
}
@Test
public void testVmConmode3270TransitionPlainTelnet() throws Exception {
fsm.onConnected();
config.setTn3270eEnabled(false);
// 1. Connected in line-mode (NVT)
feedBytes('C', 'P', '>');
assertEquals(ConnectionState.CONNECTED_NVT, fsm.getConnectionState());
// 2. VM sends BINARY and EOR options upon CONMODE 3270
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_BINARY);
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_BINARY);
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_EOR);
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_EOR);
// Verify transition to CONNECTED_3270
assertEquals(ConnectionState.CONNECTED_3270, fsm.getConnectionState());
// 3. VM sends 3270 data stream
EbcdicTranslator trans = new EbcdicTranslator();
String msg = "VM 3270";
byte[] rawStream = new byte[5 + msg.length() + 2];
rawStream[0] = (byte) 0xF5; // EraseWrite
rawStream[1] = (byte) 0xC3; // WCC
rawStream[2] = 0x11; // SBA
rawStream[3] = 0x40; rawStream[4] = 0x40;
for (int i = 0; i < msg.length(); i++) {
rawStream[5 + i] = (byte) trans.unicodeToEbcdic(msg.charAt(i));
}
rawStream[rawStream.length - 2] = (byte) TelnetConstants.IAC;
rawStream[rawStream.length - 1] = (byte) TelnetConstants.EOR;
for (byte b : rawStream) fsm.feedByte(b & 0xFF);
assertEquals('V', trans.ebcdicToUnicode(screenBuffer.getCellEC(0)));
assertEquals('M', trans.ebcdicToUnicode(screenBuffer.getCellEC(1)));
}
}