More functional changes and UI adaptations from overnight
This commit is contained in:
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user